A version for this Lambda function exists just from upgrading the CDK version

A version for this Lambda function exists just from upgrading the CDK version

I didn't change any code, just bumped aws-cdk-lib, yet my Lambda deployment failed with "A version for this Lambda function exists." The synthesized template looks identical, but only the Version hash keeps changing. I'll isolate this through investigation and write up the root cause and fix. --- ## What happened ``` ❌ UPDATE_FAILED: MyFunction CurrentVersion (AWS::Lambda::Version) A version for this Lambda function exists ( version: 42 ) ``` The deployment was clean the day before. No application code changed. The only diff was a `package.json` bump: ```diff - "aws-cdk-lib": "2.130.0" + "aws-cdk-lib": "2.131.0" ``` --- ## Isolation steps ### Step 1 — Compare synthesized templates ```bash # synthesize on the old version npx cdk synth --version-reporting false > old.json # bump the package, synthesize again npx cdk synth --version-reporting false > new.json diff old.json new.json ``` Expected: no diff. Actual: one field changed. ```diff "MyFunctionCurrentVersionXXXXXXXX": { "Type": "AWS::Lambda::Version", "Properties": { - "FunctionName": { "Ref": "MyFunctionABCD1234" }, + "FunctionName": { "Ref": "MyFunctionABCD1234" }, "Description": "..." } }, ``` The logical ID of the `AWS::Lambda::Version` resource itself changed: ```diff - "MyFunctionCurrentVersion11111111" + "MyFunctionCurrentVersion22222222" ``` The Properties block is identical — only the logical ID hash suffix is different. ### Step 2 — Find where the hash is computed Clone the CDK repo, check out both tags, search for the hash calculation: ```bash git diff v2.130.0 v2.131.0 -- \ packages/aws-cdk-lib/aws-lambda/lib/function.ts ``` Relevant excerpt (simplified): ```typescript // Before 2.131.0 const hash = md5(JSON.stringify(props.code._bind(this).s3Location)); // After 2.131.0 const hash = md5(JSON.stringify({ ...props.code._bind(this).s3Location, architecture: this.architecture.name, // ← newly added })); ``` The hash input was extended. For a function that uses the default `X86_64` architecture the object gains a new key, the MD5 changes, the logical ID changes. ### Step 3 — Confirm with a minimal reproduction ```typescript const fn = new lambda.Function(stack, "MyFunction", { runtime: lambda.Runtime.NODEJS_20_X, handler: "index.handler", code: lambda.Code.fromInline("exports.handler = () => {}"), }); new CfnOutput(stack, "VersionLogicalId", { value: (fn.currentVersion.node.defaultChild as CfnResource).logicalId, }); ``` ``` # cdk 2.130.0 VersionLogicalId = MyFunctionCurrentVersion11111111 # cdk 2.131.0 VersionLogicalId = MyFunctionCurrentVersion22222222 ``` Confirmed. --- ## Root cause `lambda.Function.currentVersion` generates a **new `AWS::Lambda::Version` resource every time the function code or configuration changes**, using a hash-derived logical ID so CloudFormation treats it as a brand-new resource. When CDK changed the hash input in 2.131.0, the logical ID changed even though nothing else did. CloudFormation's behavior: 1. Sees a new `AWS::Lambda::Version` with a new logical ID → tries to **Create** it. 2. The Lambda service checks: does a published version with this exact code + config already exist? 3. It does (version 42, created previously) → returns **"A version for this Lambda function exists"** → Create fails. 4. CloudFormation rolls back. This is not a CDK bug per se; it is a known sharp edge: **the logical ID of a Version resource is load-bearing**, and any change to it causes a recreate attempt that the Lambda API refuses when the content has not actually changed. --- ## Why CloudFormation refuses `PublishVersion` is idempotent only within a single request flow. If you call it twice with identical configuration, the second call does not return the existing version — it returns an error. CloudFormation has no "adopt existing resource" behavior for `AWS::Lambda::Version`, so the stack is stuck. --- ## Fixes ### Fix A — Pin the logical ID explicitly (recommended) Override the logical ID to a stable value that does not change when CDK internals change: ```typescript const fn = new lambda.Function(stack, "MyFunction", { runtime: lambda.Runtime.NODEJS_20_X, handler: "index.handler", code: lambda.Code.fromAsset("dist"), }); const version = fn.currentVersion; // Stable logical ID — you own this string now. // Change it deliberately when you actually want a new version. const cfnVersion = version.node.defaultChild as lambda.CfnVersion; cfnVersion.overrideLogicalId("MyFunctionStableVersion"); ``` **Trade-off**: you must manually update the logical ID when you intentionally publish a new version (code change, env var change, etc.). A comment in code makes this explicit: ```typescript // Bump the suffix (v1 → v2) whenever the function code or // configuration changes and you want a new published version. cfnVersion.overrideLogicalId("MyFunctionStableVersionV1"); ``` --- ### Fix B — Use `addAlias` and let CDK manage rotation ```typescript const fn = new lambda.Function(stack, "MyFunction", { ... }); // CDK manages the alias and rotates the version automatically. // The alias logical ID is stable; only the Version it points to rotates. const alias = fn.addAlias("live"); ``` The `addAlias` pattern creates a new `Version` resource and updates the `Alias` to point to it atomically. The alias logical ID is stable; the version logical ID can change freely because the old version resource is deleted only after the alias has moved away from it — CloudFormation handles this in the right order. --- ### Fix C — Remove `currentVersion` and manage versions outside CDK If you use Lambda aliases or traffic shifting through a deployment pipeline (CodeDeploy, SAM), you may not need CDK to publish versions at all: ```typescript // No .currentVersion call — Lambda publishes versions through // the deployment pipeline, not through CloudFormation. const fn = new lambda.Function(stack, "MyFunction", { ... }); ``` --- ### Fix D — One-time recovery without rolling back If the stack is already stuck in `UPDATE_FAILED` and you cannot roll back cleanly: ```bash # 1. Find the existing version ARN aws lambda list-versions-by-function \ --function-name MyFunction \ --query "Versions[-1].FunctionArn" # 2. Import the existing version into the stack under the new logical ID aws cloudformation create-change-set \ --stack-name MyStack \ --change-set-name ImportVersion \ --change-set-type IMPORT \ --resources-to-import "[{ \"ResourceType\": \"AWS::Lambda::Version\", \"LogicalResourceId\": \"MyFunctionCurrentVersion22222222\", \"ResourceIdentifier\": {\"FunctionArn\": \"arn:aws:lambda:...:function:MyFunction:42\"} }]" \ --template-body file://new.json aws cloudformation execute-change-set \ --stack-name MyStack \ --change-set-name ImportVersion ``` After the import, the new logical ID maps to the already-existing version, and future updates work normally. --- ## Prevention checklist | Item | Action | |---|---| | Lock CDK minor version in CI | `"aws-cdk-lib": "2.130.0"` not `"^2.130.0"` | | Review CDK changelog before bumping | Look for changes under `aws-lambda` | | Use `overrideLogicalId` on all Versions | Removes sensitivity to hash changes | | Add an integration test | `cdk diff` in CI must produce zero resource replacements on a no-code-change run | | Prefer `addAlias` over raw `currentVersion` | Alias absorbs version rotations gracefully | --- ## Summary | | Detail | |---|---| | **Trigger** | `aws-cdk-lib` 2.130.0 → 2.131.0 | | **Change** | Hash input for `AWS::Lambda::Version` logical ID extended with `architecture` field | | **Effect** | Logical ID changed → CloudFormation tried to create a new Version resource | | **Lambda API** | Rejected create because an identical version (v42) already existed | | **Best fix** | `overrideLogicalId` on the Version, or switch to `addAlias` |
2026.07.23

This page has been translated by machine translation. View original

Introduction

Hello, I'm 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 hadn't changed the Lambda function code or any stack configuration — the only change was the aws-cdk-lib version. That alone caused the AWS::Lambda::Version creation to fail and the stack to roll back.

Conclusion

The issue where "the synthesized template is identical yet only the AWS::Lambda::Version hash (logical ID) changes" has been a known problem in CDK for some time (see Issue #26739, etc.). This error was another instance of that.

Specifically, when upgrading aws-cdk-lib from 2.258.1 to 2.259.0, the mutable alias Runtime.NODEJS_LATEST changed its target from nodejs22.x to nodejs24.x (PR #38031), and this leaked into the Version hash via the Lambda Insights layer's (an ARN-referenced imported layer) compatibleRuntimes. This happens even if the function's own runtime is pinned to NODEJS_24_X.

The fix 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 is the error "A version for this Lambda function exists"?

To understand this error, you need to look at both the Lambda versioning specification and how CloudFormation/CDK handles resources.

Lambda versioning specification

Lambda assigns a monotonically increasing number (1, 2, 3, …) each time a version is published, and numbers are never reused even after deletion. A new version is only published when $LATEST has changed since the last published version. If neither the code nor the configuration has changed, calling PublishVersion will not produce a new version. In other words, this error does not occur because "the same number was specified," but rather because "an attempt was made to publish a new version when the function had not changed." The error message Modify the function to create a new version reflects exactly that.

Handling Lambda versions with CloudFormation/CDK

In CloudFormation, a Lambda version is a standalone resource type called AWS::Lambda::Version, and resources are identified by their logical ID. If the logical ID changes, it is treated as a "different resource," triggering a creation of a new resource (and deletion of the old one). CDK constructs the logical ID for a Version using a hash computed from the function's code and version-locked properties (such as Runtime, Description, Environment, etc. — properties that require publishing a new version when changed). From CDK's perspective, the flow is: "function configuration changed → hash changed → logical ID changed → create a new Version resource."

Why the error occurs

CDK determines that "the function has changed" and tries to create an AWS::Lambda::Version with a new logical ID, but Lambda's PublishVersion determines that "the function has not changed" and refuses to publish a new version. This mismatch in judgment produces A version for this Lambda function exists (HandlerErrorCode: AlreadyExists). Therefore, to find the root cause, you need to trace "why CDK judged that a change occurred" — that is, "what caused the hash input for the logical ID to change."

There are prior examples of bugs caused by CDK's hash calculation. (#26739 / #14428). The CDK official README also states:

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 versioning / AWS CDK aws_lambda README (Versions)

The configuration where the issue occurred

It can be reproduced with a minimal setup: a NodejsFunction (arm64 / nodejs24.x pinned) with a Lambda Insights layer attached, and currentVersion referenced 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 sole trigger. Removing it prevents the logical ID from changing on 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-cdk CLI 2.1132.0 (kept fixed during comparison; only aws-cdk-lib was swapped)
  • Function runtime: NODEJS_24_X pinned
  • Build: NodejsFunction + esbuild only (esbuild is used even without specifying forceDockerBundling, but made explicit for clarity)

Comparing logical IDs with local synth

I ran cdk synth with combinations of Insights presence × version and compared the hash suffix 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 between 2.258.1 and 2.259.0 only when Insights is attached; removing it keeps the logical ID identical across both versions. This controlled experiment confirms that the Lambda Insights layer is the sole trigger.

At this point, aside from the AWS::Lambda::Version logical ID and CDKMetadata, the synthesized templates are byte-for-byte identical between versions. The function resource itself has not changed at all.

Reproducing the failure with an actual deployment

Having confirmed that the logical ID changes locally, I deployed to a verification AWS account to observe the behavior.

  1. Deploy with 2.258.1 → Success. AWS::Lambda::Version version 1 is published, and alias live points to 1.
  2. Without changing any code, upgrade to 2.259.0 and run cdk diff:
  3. 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)

CDK attempts to create a Version with a new logical ID, but since the function's actual content is identical to version 1, Lambda refuses to publish a new version.

Root cause

By capturing and comparing the inputs passed to calculateFunctionHash during the synth process, the only differing input between versions was the NODEJS_LATEST entry in the Insights layer's compatibleRuntimes (nodejs22.x ↔ nodejs24.x). Here is the source trace.

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 upon instantiation (constructor).
public static readonly ALL = new Array<Runtime>();

Runtime.ALL is an array containing all Runtimes, including the mutable alias NODEJS_LATEST. When a layer is imported from an ARN, its compatibleRuntimes is populated with the entire Runtime.ALL (aws-lambda/lib/layers.ts).

public static fromLayerVersionArn(scope: Construct, id: string, layerVersionArn: string): ILayerVersion {
  return LayerVersion.fromLayerVersionAttributes(scope, id, {
    layerVersionArn,
    compatibleRuntimes: Runtime.ALL, // ← imported layer gets Runtime.ALL
  });
}

Since insightsVersion is added via this fromLayerVersionArn, the Insights layer's compatibleRuntimes becomes Runtime.ALL.

CDK then, inside calculateFunctionHash (aws-lambda/lib/function-hash.ts), which computes the Version hash, adds the result of calculateLayersHash to the hash input when the feature flag recognizeLayerVersion is enabled (the default in v2).

// L11-27: In addition to function properties, the layers hash is conditionally mixed in.
export function calculateFunctionHash(fn: LambdaFunction, additional: string = '') {
  // ...processing that assembles 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);
}

Inside calculateLayersHash (relevant location), the branch for imported layers (layerResource === undefined) directly uses compatibleRuntimes 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 (with CfnLayerVersion) take this path
    // and do not directly reference compatibleRuntimes.
    const { properties } = resolveSingleResourceProperties(stack, layerResource);
    layerConfig[layer.node.id] = sortLayerVersionProperties(properties);
  }
  return md5hash(JSON.stringify(layerConfig));
}

The logical ID changes for the following reasons.

  1. The name of NODEJS_LATEST changes from nodejs22.x → nodejs24.x (2.259.0 / PR #38031).
  2. Runtime.ALL includes that NODEJS_LATEST.
  3. The imported layer's compatibleRuntimes is Runtime.ALL.
  4. With recognizeLayerVersion (enabled by default in v2), calculateLayersHash uses layer.compatibleRuntimes directly as hash input.
  5. The name of NODEJS_LATEST inside Runtime.ALL shifts from 22→24 → the layer hash changes → the Version hash changes.
  6. CDK attempts to create an AWS::Lambda::Version with a new logical ID, but the function's actual content is the same → deployment fails with AlreadyExists.

This is where the mismatch lies between Lambda's API and CDK's hash calculation logic. What Lambda's version publishing looks at for layers is only the ARNs of the layer versions attached to the function. Since a layer version is immutable after publication and the same ARN always points to the same code, if the ARN is the same between 2.258.1 and 2.259.0 as in this case, from the service's perspective it should be "the same function with the same layer attached." However, CDK includes in the logical ID hash even the compatibleRuntimes (Runtime.ALL) held by the imported layer.

Confirming it is not specific to Lambda Insights

If the root cause is fromLayerVersionArn's compatibleRuntimes: Runtime.ALL, the same issue should occur with any custom layer referenced by ARN, not just Lambda Insights. I ran cdk synth with a custom layer using a dummy ARN in different ways on both 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; auto-injected) Logical ID changes (…57c4b3a6… → …fdc5af42…)
LayerVersion.fromLayerVersionAttributes({ arn, compatibleRuntimes: [NODEJS_24_X] }) Explicitly specified (no mutable aliases) Does not change
new LayerVersion(...) (owned) Resource properties path Does not change

The same logical ID change occurred with a custom layer referenced by ARN via fromLayerVersionArn, confirming it is not specific to Lambda Insights. On the other hand, even with the same ARN reference, if compatibleRuntimes is explicitly specified via fromLayerVersionAttributes without including mutable aliases, the logical ID does not change. Owned layers do not change either.

Therefore, this affects all layers imported via fromLayerVersionArn.

Fix

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 — that is, a property included in the hash — and also a property that "allows a new version to be published when changed." Upgrading CDK changes the Version's logical ID, but since the CDK version embedded in the description also necessarily changes at that point, the function's actual content (configuration) really does change. This allows Lambda to publish a new version, preventing the A version for this Lambda function exists failure. Since the version is retrieved automatically via require, no manual version updates are needed.

This was also verified on a real deployment.

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 fix, but it means actual layer version changes are no longer reflected in the Version hash, making it a side effect in cases where you want to detect layer updates.

The description approach has the side effect that every CDK upgrade publishes one new version even when the hash would not have actually changed, but I think it is relatively easy to adopt.

Summary

  • The root cause of this error is that NODEJS_LATEST changed from nodejs22.x→24.x in 2.259.0, and this leaked into the Version hash via the imported layer's compatibleRuntimes (Runtime.ALL).
  • This is not specific to Lambda Insights; it reproduces with any layer referenced by ARN via fromLayerVersionArn (such as ADOT / Parameters and Secrets Extension, etc.).
  • The fix is to set the function's description to built-with-aws-cdk-lib@${require('aws-cdk-lib/package.json').version}. Since upgrading CDK always changes the description, Lambda can reliably publish a new version.

I hope this is helpful to someone.

Share this article

AWSのお困り事はクラスメソッドへ