I Just Upgraded the CDK Version, But Got "A version for this Lambda function exists"
This page has been translated by machine translation. View original
Introduction
Hello, this is Junkichi.
After upgrading aws-cdk-lib and running cdk deploy, I encountered the following error.
CREATE_FAILED AWS::Lambda::Version
A version for this Lambda function exists ( 1 ). Modify the function to create a new version.
(HandlerErrorCode: AlreadyExists)
I had not changed the Lambda function code or the stack settings. The only thing I changed was the version of aws-cdk-lib. That alone caused the creation of AWS::Lambda::Version to fail, and the stack was rolled back.
Conclusion
The problem where "the synthesized template is identical, but only the hash (logical ID) of AWS::Lambda::Version changes" has long been known in CDK (e.g. Issue #26739). This error was one instance of it.
Specifically, when upgrading aws-cdk-lib from 2.258.1 to 2.259.0, the target of the mutable alias Runtime.NODEJS_LATEST changed from nodejs22.x to nodejs24.x (PR #38031). This leaked into the Version hash via the compatibleRuntimes of the Lambda Insights layer (an imported layer referenced by ARN), which was the cause. It happens even if the function's own runtime is pinned to NODEJS_24_X.
The workaround is to embed the aws-cdk-lib version in the function's description.
description: `built-with-aws-cdk-lib@${require('aws-cdk-lib/package.json').version}`,
What the error "A version for this Lambda function exists" means
To understand the real cause of this error, we need to look at both the Lambda-side version specification and how CloudFormation / CDK handle the resource.
Lambda version specification
Each time Lambda publishes a version, it assigns a monotonically increasing number (1, 2, 3, ...), and never reuses a number even after deletion. A new version is published only when $LATEST differs from the previously published version. If neither the code nor the configuration has changed, calling PublishVersion will not publish a new version. In other words, this error does not occur "because the same number was specified", but when "you tried to publish a new version even though the function has not changed". The message Modify the function to create a new version says exactly that.
Handling Lambda versions in CloudFormation/CDK
In CloudFormation, a Lambda version is an independent resource type called AWS::Lambda::Version, and resources are identified by their logical ID. If the logical ID changes, it is regarded as a "different resource", and creation of a new one (and deletion of the old one) runs. CDK builds the logical ID of this Version from a hash calculated from the function's code and its version-locked properties (items such as Runtime, Description, and Environment, which require publishing a new version when changed). So from CDK's perspective, the flow is: "the function's configuration changed → the hash changed → the logical ID changed → create a new Version resource".
Why the error occurs
CDK decides that "the function has changed" and tries to create an AWS::Lambda::Version with a new logical ID, but Lambda's PublishVersion decides that "the function has not changed" and refuses to publish a new version. This mismatch in judgment is A version for this Lambda function exists (HandlerErrorCode: AlreadyExists). Therefore, to find the cause, we only need to trace "why CDK judged that there was a change", that is, "what moved the hash input of the logical ID".
There is precedent for issues caused by CDK's hash calculation (#26739 / #14428). The official CDK README also says the following.
a bug was introduced in this calculation that caused the logical id to change when it was not required (...) This caused the deployment to fail since the Lambda service does not allow creating duplicate versions.
References: Lambda function versions / AWS CDK aws_lambda README (Versions)
The configuration where the issue occurs
I reproduce it with a minimal configuration. It is a stack that attaches a Lambda Insights layer to a NodejsFunction (pinned to arm64 / nodejs24.x) and simply references currentVersion from an Alias.
const fn = new NodejsFunction(this, 'Fn', {
runtime: Runtime.NODEJS_24_X, // The function's own runtime is pinned
architecture: Architecture.ARM_64,
entry: path.join(__dirname, '..', 'lambda', 'handler.ts'),
handler: 'handler',
// This is the only trigger. If removed, the logical ID no longer changes between 2.258.1 -> 2.259.0.
insightsVersion: LambdaInsightsVersion.VERSION_1_0_498_0,
bundling: {
forceDockerBundling: false,
},
});
new Alias(this, 'LiveAlias', {
aliasName: 'live',
version: fn.currentVersion,
});
Verification
Verification was performed in the following environment.
aws-cdkCLI2.1132.0(pinned during comparison; onlyaws-cdk-libwas swapped)- Function runtime: pinned to
NODEJS_24_X - Build:
NodejsFunction+ esbuild only (esbuild is used even without specifying forceDockerBundling, but I specified it explicitly just in case)
Comparing logical IDs with a local synth
I ran cdk synth for each combination of Insights on/off × version, and compared the hash at the end of the AWS::Lambda::Version logical ID.
| aws-cdk-lib | with insights | without insights |
|---|---|---|
| 2.258.1 | …be4938273272… |
…7960d9a4b981… |
| 2.259.0 | …0e85dd15c09c… (changed) |
…7960d9a4b981… (unchanged) |
The logical ID changes from 2.258.1 to 2.259.0 only when Insights is attached; when it is removed, both versions match. This controlled experiment shows that the Lambda Insights layer is the only trigger.
Also, apart from the logical ID of AWS::Lambda::Version and CDKMetadata, the synthesized template is byte-for-byte identical between versions. The function resource itself has not changed at all.
Actually deploying to reproduce the failure
Since I confirmed locally that the logical ID changes, I did a real deployment to a verification AWS account to check the behavior.
- Deploy with 2.258.1 → success. Version 1 of
AWS::Lambda::Versionis published, and the aliaslivepoints to 1. - Upgrade to 2.259.0 without changing any code and run
cdk diff: - Deploy with 2.259.0 → failure
CREATE_FAILED AWS::Lambda::Version
A version for this Lambda function exists ( 1 ). Modify the function to create a new version.
(HandlerErrorCode: AlreadyExists)
It tries to create a Version with a new logical ID, but because the function itself is identical to version 1, Lambda refuses to publish a new version.
Root cause
I captured and compared the inputs passed to calculateFunctionHash during the synth process. The only input that differed between versions was the NODEJS_LATEST entry (nodejs22.x ↔ nodejs24.x) in the Insights layer's compatibleRuntimes. I traced the source.
First, NODEJS_LATEST is defined as a mutable alias that "changes over time" (aws-lambda/lib/runtime.ts).
// isVariable: true = a mutable alias whose target changes over time.
// In v2.259.0 it is 'nodejs24.x' (it was 'nodejs22.x' in 2.258.1; changed in PR #38031).
public static readonly NODEJS_LATEST =
new Runtime('nodejs24.x', RuntimeFamily.NODEJS, { supportsInlineCode: true, isVariable: true });
// An array collecting all Runtimes. Each Runtime is pushed here when created (in the constructor).
public static readonly ALL = new Array<Runtime>();
Runtime.ALL is an array collecting all Runtimes, and it includes NODEJS_LATEST, which is a mutable alias. When a layer is imported from an ARN, Runtime.ALL is set wholesale as its compatibleRuntimes (aws-lambda/lib/layers.ts).
public static fromLayerVersionArn(scope: Construct, id: string, layerVersionArn: string): ILayerVersion {
return LayerVersion.fromLayerVersionAttributes(scope, id, {
layerVersionArn,
compatibleRuntimes: Runtime.ALL, // ← Runtime.ALL is attached to an imported layer
});
}
Since insightsVersion is added through this fromLayerVersionArn, the Insights layer's compatibleRuntimes becomes Runtime.ALL.
Then, in calculateFunctionHash (aws-lambda/lib/function-hash.ts), which computes the Version hash, CDK adds the result of calculateLayersHash to the hash input if the feature flag recognizeLayerVersion is enabled (the v2 default).
// L11-27: In addition to the function's properties, the layer hash is conditionally mixed in.
export function calculateFunctionHash(fn: LambdaFunction, additional: string = '') {
// ...the code that gathers function properties into stringifiedConfig is omitted...
if (FeatureFlags.of(fn).isEnabled(LAMBDA_RECOGNIZE_LAYER_VERSION)) {
stringifiedConfig = stringifiedConfig + calculateLayersHash([...fn._layers].sort());
}
return md5hash(stringifiedConfig + additional);
}
That calculateLayersHash (relevant location) is where, for an imported layer (the layerResource === undefined branch), compatibleRuntimes is used as-is as hash input.
function calculateLayersHash(layers: ILayerVersion[]): string {
const layerConfig: {[key: string]: any } = {};
for (const layer of layers) {
const layerResource = layer.node.defaultChild as CfnResource;
// if there is no layer resource, then the layer was imported
// and we will include the layer arn and runtimes in the hash
if (layerResource === undefined) {
if (!Token.isUnresolved(layer.layerVersionArn)) {
layerConfig[layer.layerVersionArn] = layer.compatibleRuntimes; // ← here
} else {
layerConfig[layer.node.id] = {
arn: stack.resolve(layer.layerVersionArn),
runtimes: layer.compatibleRuntimes?.map(r => r.name),
};
}
continue;
}
// Owned layers (those with a CfnLayerVersion) take this path,
// which does not look at compatibleRuntimes directly.
const { properties } = resolveSingleResourceProperties(stack, layerResource);
layerConfig[layer.node.id] = sortLayerVersionProperties(properties);
}
return md5hash(JSON.stringify(layerConfig));
}
The logical ID changes for the following reasons.
- The name of
NODEJS_LATESTchanges fromnodejs22.x→nodejs24.x(2.259.0 / PR #38031). Runtime.ALLincludes thatNODEJS_LATEST.- The
compatibleRuntimesof an imported layer isRuntime.ALL. - With
recognizeLayerVersion(enabled by default in v2),calculateLayersHashuseslayer.compatibleRuntimesas-is as hash input. - The name of
NODEJS_LATESTwithinRuntime.ALLmoves from 22→24 → the layer hash changes → the Version hash changes. - It tries to create an
AWS::Lambda::Versionwith a new logical ID, but the function itself is identical → deployment fails withAlreadyExists.
Here lies the mismatch between the Lambda API and CDK's hash calculation logic. The only thing Lambda's version publishing looks at regarding layers is the ARNs of the layer versions attached to the function. Layer versions are immutable after publication, and the same ARN always points to the same code, so if the ARN is identical between 2.258.1 and 2.259.0 as in this case, then from the service's point of view it should be "the same function with the same layers attached". However, CDK includes even the compatibleRuntimes (Runtime.ALL) held by the imported layer in the input to the logical ID hash.
Confirming it is not specific to Lambda Insights
If the cause is compatibleRuntimes: Runtime.ALL in fromLayerVersionArn, the same thing should happen with a custom layer referenced by ARN, not just Lambda Insights. I took a custom layer with a fictitious ARN, varied only how it is provided, ran cdk synth with 2.258.1 and 2.259.0, and compared the Version logical IDs.
| How the layer is provided | compatibleRuntimes |
2.258.1 vs 2.259.0 |
|---|---|---|
LayerVersion.fromLayerVersionArn(arn) |
Runtime.ALL (includes NODEJS_LATEST, injected automatically) |
Logical ID changes (…57c4b3a6… → …fdc5af42…) |
LayerVersion.fromLayerVersionAttributes({ arn, compatibleRuntimes: [NODEJS_24_X] }) |
Explicit (no mutable alias) | Unchanged |
new LayerVersion(...) (owned) |
Resource property path | Unchanged |
It is not specific to Lambda Insights: a custom layer referenced by ARN via fromLayerVersionArn also had its logical ID change. On the other hand, even with the same ARN reference, it does not change if you specify compatibleRuntimes explicitly with fromLayerVersionAttributes without including the mutable alias. Owned layers also do not change.
Therefore, all layers that come in through fromLayerVersionArn are affected.
Workaround
Embed the aws-cdk-lib version in the function's description.
description: `built-with-aws-cdk-lib@${require('aws-cdk-lib/package.json').version}`,
Description is a version-locked property, meaning it is part of the hash and is an item where "changing it allows a new version to be published". When you upgrade CDK, the Version logical ID changes, but the CDK version in the description necessarily changes at the same time, so the function itself (its configuration) actually changes. Lambda can then publish a new version, and the A version for this Lambda function exists failure does not occur. Since it is obtained automatically with require, no manual version updates are needed.
I also confirmed this on a real environment.
| Deployment | Function Description | Result |
|---|---|---|
| 2.258.1 (no description) → 2.259.0 | None | Failure (AlreadyExists) |
| 2.258.1 (with description) | built-with-aws-cdk-lib@2.258.1 |
Success (version 2) |
| 2.259.0 (with description) | built-with-aws-cdk-lib@2.259.0 |
Success (version 3, alias → 3) |
Setting the feature flag @aws-cdk/aws-lambda:recognizeLayerVersion to false also works as a workaround, but since actual layer version changes would then no longer be reflected in the Version hash either, it becomes a side effect in cases where you want to detect layer updates.
The description approach has the side effect that every time you upgrade CDK, a new version is always published even if the hash would not actually have changed, but I think it is relatively easy to adopt.
Summary
- The cause of this error was that in 2.259.0
NODEJS_LATESTchanged from nodejs22.x to 24.x, and this leaked into the Version hash through the imported layer'scompatibleRuntimes(Runtime.ALL). - It is not specific to Lambda Insights; it reproduces with all layers referenced by ARN via
fromLayerVersionArn(ADOT / Parameters and Secrets Extension, etc.). - The workaround is to put
built-with-aws-cdk-lib@${require('aws-cdk-lib/package.json').version}in the function'sdescription. Since the description necessarily changes when you upgrade CDK, Lambda can reliably publish a new version.
I hope this is helpful to someone.
