aws / aws/aws-cdk

aws_lambda: Lambda version resource id changes on each synth without any changes

Open
#31,427 5 comments 6 reactions 0 assignees View on GitHub
@aws-cdk/aws-lambda bug p3
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.