aws_lambda: Lambda version resource id changes on each synth without any changes
- Dominant language
- TypeScript
- Stars
- 12.9k
- Forks
- 4.6k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 83
Description
### Describe the bug
When running the `cdk synth` command, the resource id of the AWS::LAMBDA::VERSION always changes on each run.
### Regression Issue
- [ ] Select this option if this issue appears to be a regression.
### Last Known Working CDK Version
_No response_
### Expected Behavior
The version resource id should only change when the function actually changed or if we force a change.
### Current Behavior
The resource id changes each and every time the `synth` command is executed both locally and on CI/CD pipelines.
### Reproduction Steps
The following stack reproduces the exact issue that I have in my project now.
```typescript
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { RemovalPolicy } from "aws-cdk-lib";
import { ManagedPolicy } from "aws-cdk-lib/aws-iam";
import type { FunctionProps, IVersion } from "aws-cdk-lib/aws-lambda";
import {
Alias,
Architecture,
Code,
Function,
LayerVersion,
Runtime,
} from "aws-cdk-lib/aws-lambda";
import { LogGroup, RetentionDays } from "aws-cdk-lib/aws-logs";
import { Provider } from "aws-cdk-lib/custom-resources";
/// Available architectures found here:
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Lambda-Insights-extension-versions.html
const getLambdaInsightsLayerArn = (architectureName: string): string => {
if (architectureName === Architecture.ARM_64.name) {
return "arn:aws:lambda:eu-west-1:580247275435:layer:LambdaInsightsExtension-Arm64:18";
}
if (architectureName === Architecture.X86_64.name) {
return "arn:aws:lambda:eu-west-1:580247275435:layer:LambdaInsightsExtension:51";
}
throw new Error(`Unsupported architecture found: [${architectureName}]`);
};
export class ICLambda extends Function {
alias: Alias;
constructor(scope: Construct, id: string, functionProperties: FunctionProps) {
super(scope, id, functionProperties);
const layerArn = getLambdaInsightsLayerArn(this.architecture.name);
const lambdaInsightsLayer = LayerVersion.fromLayerVersionArn(
this,
`LambdaInsightsLayer`,
layerArn
);
this.addLayers(lambdaInsightsLayer);
this.role?.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName(
"CloudWatchLambdaInsightsExecutionRolePolicy"
)
);
this.currentVersion.applyRemovalPolicy(
RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE
);
new LogGroup(this, "LogGroup", {
logGroupName: `/aws/lambda/${this.functionName}`,
retention: RetentionDays.TEN_YEARS,
removalPolicy: RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE,
});
this.alias = new Alias(this, `Alias`, {
aliasName: "v0",
version: this.currentVersion as IVersion,
});
}
}
export class LambdaIssueStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const justALambda = new ICLambda(this, "JustALambda", {
runtime: Runtime.NODEJS_20_X,
handler: "index.handler",
architecture: Architecture.X86_64,
code: Code.fromInline(
'exports.handler = () => { console.log("Hello, world!"); };'
),
});
const myCustomResourceLambda = new ICLambda(this, "MigrateToLatestLambda", {
runtime: Runtime.NODEJS_20_X,
handler: "index.handler",
architecture: Architecture.X86_64,
code: Code.fromInline(
'exports.handler = () => { console.log("Hello, world!"); };'
),
});
const migrateToLatestProvider = new Provider(
this,
"MigrateToLatestProvider",
{
onEventHandler: myCustomResourceLambda,
}
);
new cdk.CustomResource(this, "Custom::DbSchemaMigration", {
serviceToken: migrateToLatestProvider.serviceToken,
resourceType: "Custom::DbSchemaMigration",
properties: {
migrationDirectoryHash: "",
},
});
}
}
```
What I already have discovered is that it only breaks when BOTH the `.addLayers` is called AND the `Provider` class is initialized. If one of both is removed, the issue no longer happens. But we need the `Provider` class as we want to add a custom resource in our cloudformation stack.
Another important note is that it doesn't matter if the `ICLambda` construct is initialized inside the same stack or another stack under `bin/stack.ts`. It also affects other stacks for some reason.
Simply run `cdk synth` mutiple times and you'll see that there is a diff in the resourceId of the lambda version on each run.
### Possible Solution
_No response_
### Additional Information/Context
As described in the repro steps. Important to know is that it happens when both `addLayers` is called on a `Function` construct AND `Provider` class is initialized somewhere. It doesn't matter if the `Function` instance is provided to the `Provider` or not.
I have been digging and it's always the `calculateFunctionHash` in `packages/aws-cdk-lib/aws-lambda/lib/function-hash.ts` that returns a different hash on each synth.
### CDK CLI Version
2.157.0 (build 7315a59)
### Framework Version
2.157.0
### Node.js Version
v20.13.1
### OS
MacOS
### Language
TypeScript
### Language Version
5.1.6
### Other information
_No response_
Contributor guide
Research direction
Start with packages/aws-cdk-lib/aws-lambda/lib/function-hash.ts and the calculateFunctionHash entry point. Reproduce the issue by running cdk synth multiple times with the provided TypeScript stack, then trace why addLayers and Provider together alter the hash. Done means repeated synths produce the same Lambda version resource id when the function is unchanged, while genuine function changes still update it.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, typescript
- Domain
- cloud, infrastructure
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100