CDKのバージョンを上げただけなのに "A version for this Lambda function exists"

CDKのバージョンを上げただけなのに "A version for this Lambda function exists"

aws-cdk-lib を上げただけでコードは何も変えていないのに、Lambda のデプロイが "A version for this Lambda function exists" で失敗しました。合成テンプレートは同一なのに Version のハッシュだけが動く現象を検証で切り分け、根本原因と対策まで書きます。
2026.07.23

はじめに

こんにちは、じゅんきちです。

aws-cdk-lib を上げて cdk deploy したところ、以下のエラーに遭遇しました。

CREATE_FAILED  AWS::Lambda::Version
A version for this Lambda function exists ( 1 ). Modify the function to create a new version.
(HandlerErrorCode: AlreadyExists)

Lambda 関数のコードもスタックの設定も変えておらず、変えたのは aws-cdk-lib のバージョンだけです。それだけで AWS::Lambda::Version の作成が失敗し、スタックはロールバックされました。

結論

「合成されるテンプレートは同一なのに AWS::Lambda::Version のハッシュ(論理ID)だけが変わる」問題は、CDK で以前から知られています(Issue #26739 など)。今回のエラーもその一例でした。

具体的には、aws-cdk-lib を 2.258.1 から 2.259.0 に上げたときに、Runtime.NODEJS_LATEST という可変エイリアスの指す先が nodejs22.x から nodejs24.x に変わった(PR #38031)ことが、Lambda Insights レイヤー(ARN 参照の imported layer)の compatibleRuntimes を経由して Version のハッシュに混入したのが原因です。関数自身の runtimeNODEJS_24_X に固定していても起きます。

対策は、関数の descriptionaws-cdk-lib のバージョンを埋め込むことです。

description: `built-with-aws-cdk-lib@${require('aws-cdk-lib/package.json').version}`,

エラー「A version for this Lambda function exists」とは

このエラーの正体を理解するには、Lambda 側のバージョンの仕様と、CloudFormation / CDK 側のリソースの扱いの両方を見る必要があります。

Lambda バージョンの仕様

Lambda はバージョンを発行するたびに単調増加する番号(1, 2, 3, …)を振り、削除しても番号を再利用しません。そして新しいバージョンを発行するのは、 $LATEST が前回発行したバージョンから変わっているときだけです。コードも設定も変わっていなければ、PublishVersion を呼んでも新しいバージョンは発行されません。つまりこのエラーは「同じ番号を指定したから」出るのではなく、「関数に変更がないのに新しいバージョンを発行しようとした」ときに出ます。エラー文の Modify the function to create a new versionがそれを表しています。

CloudFormation/CDK で Lambda バージョンを扱う場合

CloudFormation では Lambda のバージョンは AWS::Lambda::Version という独立したリソースタイプで、リソースは論理IDで識別されます。論理IDが変われば「別のリソース」とみなされ、新規作成(と古いリソースの削除)が走ります。CDK はこの Version の論理IDを、関数のコードと version-locked なプロパティ(RuntimeDescriptionEnvironment など、変更すると新バージョンの発行が必要になる項目)から計算したハッシュで組み立てます。つまり CDK から見ると、「関数の設定が変わった → ハッシュが変わった → 論理IDが変わった → 新しい Version リソースを作る」という流れになります。

エラーが発生する理由

CDK 側は「関数に変更があった」と判定して新しい論理IDの AWS::Lambda::Version を作ろうとするのに、Lambda の PublishVersion 側は「関数に変更はない」と判定して新バージョンの発行を拒否する。この判定のズレが A version for this Lambda function existsHandlerErrorCode: AlreadyExists)です。したがって原因を突き止めるには、「なぜ CDK は変更があったと判定したのか」=「何が論理IDのハッシュ入力を動かしたのか」を追えばよいことになります。

CDKによるハッシュ計算に起因した不具合は、前例が存在しています。(#26739 / #14428)。CDK 公式の README にも次のようにあります。

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.

参考: Lambda 関数のバージョン管理 / AWS CDK aws_lambda README(Versions)

事象が起きた構成

最小構成で再現します。NodejsFunction(arm64 / nodejs24.x 固定)に Lambda Insights レイヤーを付け、currentVersion を Alias から参照するだけのスタックです。

const fn = new NodejsFunction(this, 'Fn', {
  runtime: Runtime.NODEJS_24_X, // 関数自身のランタイムは固定している
  architecture: Architecture.ARM_64,
  entry: path.join(__dirname, '..', 'lambda', 'handler.ts'),
  handler: 'handler',
  // これが唯一のトリガー。外すと 2.258.1 -> 2.259.0 で論理IDが変わらなくなる。
  insightsVersion: LambdaInsightsVersion.VERSION_1_0_498_0,
  bundling: {
    forceDockerBundling: false,
  },
});

new Alias(this, 'LiveAlias', {
  aliasName: 'live',
  version: fn.currentVersion,
});

検証

検証は以下の環境で行いました。

  • aws-cdk CLI 2.1132.0(比較中は固定し、aws-cdk-lib のみ入れ替え)
  • 関数ランタイム: NODEJS_24_X 固定
  • ビルド: NodejsFunction + esbuild のみ(forceDockerBundlingを指定しなくてもEsbuildになるが一応明示)

ローカル synth で論理IDを比較する

Insights の有無 × バージョンで cdk synth し、AWS::Lambda::Version の論理ID末尾のハッシュを比較しました。

aws-cdk-lib insights あり insights なし
2.258.1 …be4938273272… …7960d9a4b981…
2.259.0 …0e85dd15c09c…(変化) …7960d9a4b981…(不変)

Insights を付けているときだけ 2.258.1 → 2.259.0 で論理IDが変わり、外すと両バージョンで一致します。Lambda Insights レイヤーが唯一のトリガーであることが、この対照実験で分かります。

このとき、AWS::Lambda::Version の論理IDと CDKMetadata を除けば、合成されるテンプレートはバージョン間でバイト単位で同一です。関数リソースそのものは何も変わっていません。

実際にデプロイして失敗を再現する

ローカルで論理IDが変わることは分かったので、検証用の AWS アカウントに実デプロイして挙動を確認しました。

  1. 2.258.1 でデプロイ → 成功。AWS::Lambda::Version のバージョン 1 が発行され、alias live が 1 を指す。
  2. コードを一切変えず 2.259.0 に上げて cdk diff:
  3. 2.259.0 でデプロイ → 失敗
CREATE_FAILED  AWS::Lambda::Version
A version for this Lambda function exists ( 1 ). Modify the function to create a new version.
(HandlerErrorCode: AlreadyExists)

新しい論理IDの Version を作ろうとするものの、関数の実体はバージョン 1 と同一のため、Lambda が新バージョンの発行を拒否します。

根本原因

synth プロセスの中で calculateFunctionHash に渡る入力を捕捉して比較したところ、バージョン間で違う唯一の入力は、Insights レイヤーの compatibleRuntimes に含まれる NODEJS_LATEST の項(nodejs22.xnodejs24.x)だけでした。ソースをたどりました。

まず NODEJS_LATEST は「時間とともに変わる」可変エイリアスとして定義されています(aws-lambda/lib/runtime.ts)。

// isVariable: true = 時間とともに指す先が変わる可変エイリアス。
// v2.259.0 では 'nodejs24.x'(2.258.1 では 'nodejs22.x' だった。PR #38031 での変更)。
public static readonly NODEJS_LATEST =
  new Runtime('nodejs24.x', RuntimeFamily.NODEJS, { supportsInlineCode: true, isVariable: true });

// すべての Runtime を集めた配列。各 Runtime は生成時(コンストラクタ)にここへ push される。
public static readonly ALL = new Array<Runtime>();

Runtime.ALL は全 Runtime を集めた配列で、可変エイリアスである NODEJS_LATEST も含みます。そして ARN からレイヤーを import すると、その compatibleRuntimesRuntime.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 には Runtime.ALL が付く
  });
}

insightsVersion はこの fromLayerVersionArn で追加されるため、Insights レイヤーの compatibleRuntimesRuntime.ALL になります。

そして CDK は、Version のハッシュを計算する calculateFunctionHashaws-lambda/lib/function-hash.ts)の中で、feature flag recognizeLayerVersion が有効(v2 既定)なら calculateLayersHash の結果をハッシュ入力に足し込みます。

// L11-27: 関数のプロパティに加えて、条件付きでレイヤーのハッシュを混ぜている。
export function calculateFunctionHash(fn: LambdaFunction, additional: string = '') {
  // ...関数プロパティを stringifiedConfig にまとめる処理は省略...
  if (FeatureFlags.of(fn).isEnabled(LAMBDA_RECOGNIZE_LAYER_VERSION)) {
    stringifiedConfig = stringifiedConfig + calculateLayersHash([...fn._layers].sort());
  }
  return md5hash(stringifiedConfig + additional);
}

その calculateLayersHash該当箇所)が、imported layer(layerResource === undefined の分岐)から compatibleRuntimes をそのままハッシュ入力に採用している箇所です。

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; // ← ここ
      } else {
        layerConfig[layer.node.id] = {
          arn: stack.resolve(layer.layerVersionArn),
          runtimes: layer.compatibleRuntimes?.map(r => r.name),
        };
      }
      continue;
    }
    // owned なレイヤー(CfnLayerVersion を持つ)はこちらの経路で、
    // compatibleRuntimes を直接は見ない。
    const { properties } = resolveSingleResourceProperties(stack, layerResource);
    layerConfig[layer.node.id] = sortLayerVersionProperties(properties);
  }
  return md5hash(JSON.stringify(layerConfig));
}

論理IDが変わるのは以下の理由となります。

  1. NODEJS_LATEST の名前が nodejs22.xnodejs24.x に変わる(2.259.0 / PR #38031)。
  2. Runtime.ALL はその NODEJS_LATEST を含む。
  3. imported layer の compatibleRuntimesRuntime.ALL
  4. recognizeLayerVersion(v2 既定で有効)だと、calculateLayersHashlayer.compatibleRuntimes をそのままハッシュ入力に採用する。
  5. Runtime.ALL 内の NODEJS_LATEST の名前が 22→24 で動く → レイヤーのハッシュが変わる → Version のハッシュが変わる。
  6. 新しい論理IDの AWS::Lambda::Version を作ろうとするが、関数の実体は同一 → AlreadyExists でデプロイ失敗。

ここに、Lambda の API と CDK のハッシュ計算ロジックのズレがあります。Lambda のバージョン発行がレイヤーについて見るのは、関数にアタッチされたレイヤーバージョンの ARN だけです。レイヤーバージョンは公開後不変で同じ ARN は常に同じコードを指すため、この ARN が今回のように 2.258.1 と 2.259.0 で同一なら、サービスから見れば本来「同じレイヤーが付いた同じ関数」のはずです。ところが CDK は、imported layer が持つ compatibleRuntimesRuntime.ALL)まで論理IDハッシュの入力に含めています。

Lambda Insights 固有ではないことを確認する

原因が fromLayerVersionArncompatibleRuntimes: Runtime.ALL にあるなら、Lambda Insights でなくても、ARN で参照する自作レイヤーなら同じことが起きるはずです。架空 ARN の自作レイヤーを、与え方だけ変えて 2.258.1 と 2.259.0 で cdk synth し、Version 論理IDを比較しました。

レイヤーの与え方 compatibleRuntimes 2.258.1 vs 2.259.0
LayerVersion.fromLayerVersionArn(arn) Runtime.ALLNODEJS_LATEST を含む・自動注入) 論理IDが変わる(…57c4b3a6……fdc5af42…
LayerVersion.fromLayerVersionAttributes({ arn, compatibleRuntimes: [NODEJS_24_X] }) 明示(可変エイリアスなし) 変わらない
new LayerVersion(...)(owned) リソースプロパティ経路 変わらない

Lambda Insights 固有ではなく、fromLayerVersionArn で ARN 参照する自作レイヤーでも同じく論理IDが変わりました。一方、同じ ARN 参照でも fromLayerVersionAttributescompatibleRuntimes を明示して可変エイリアスを含めなければ変わりません。owned なレイヤーも変わりません。

したがって、fromLayerVersionArn で入るレイヤー全般が該当します。

対策

関数の descriptionaws-cdk-lib のバージョンを埋め込みます。

description: `built-with-aws-cdk-lib@${require('aws-cdk-lib/package.json').version}`,

Description は version-locked なプロパティ、つまりハッシュの対象であり、かつ「変えれば新バージョンを発行できる」項目です。CDK を上げると Version の論理IDが変わりますが、そのとき description に入っている CDK バージョンも必ず変わるので、関数の実体(設定)が実際に変化します。すると Lambda は新バージョンを発行でき、A version for this Lambda function exists の失敗が起きません。require で自動取得するため手動でのバージョン更新は不要です。

実機でも確認しました。

デプロイ 関数 Description 結果
2.258.1(description なし)→ 2.259.0 なし 失敗(AlreadyExists
2.258.1(description あり) built-with-aws-cdk-lib@2.258.1 成功(version 2)
2.259.0(description あり) built-with-aws-cdk-lib@2.259.0 成功(version 3、alias → 3)

feature flag @aws-cdk/aws-lambda:recognizeLayerVersionfalse にする方法も対策として機能しますが、レイヤーの実バージョン変更も Version ハッシュに反映しなくなるため、レイヤー更新を検知したいケースでは副作用になります。

description 方式はCDK を上げるたびに、ハッシュが実際に変わらなくても常に新バージョンが 1 つ発行されるという副作用がありますが、比較的導入しやすいのではと考えます。

まとめ

  • 今回のエラーの原因は、2.259.0 で NODEJS_LATEST が nodejs22.x→24.x に変わり、それが imported layer の compatibleRuntimesRuntime.ALL)を通じて Version ハッシュに混入したこと。
  • Lambda Insights 固有ではなく、fromLayerVersionArn で ARN 参照するレイヤー全般(ADOT / Parameters and Secrets Extension 等)で再現する。
  • 対策は関数の descriptionbuilt-with-aws-cdk-lib@${require('aws-cdk-lib/package.json').version} を入れること。CDK を上げると description が必ず変わるので、Lambda が確実に新バージョンを発行できるようになる。

どなたかの参考になれば幸いです。

この記事をシェアする

関連記事