AWS CDK の Lambda@Edge の更新時に Cross-Region References により詰まる

Category:Tech BlogTags:
#AWS#CDK#Lambda@Edge#CloudFront#Cognito#TypeScript
Published: 2025 - 12 - 29

AWS CDK で S3 + CloudFront + cognito-at-edge の構成を us-east-1 / ap-northeast-1 のクロスリージョンスタックを利用し,crossRegionReferencesを用いると Lambda@Edge 更新時に Exports cannot be updated エラーが出て詰まったのでメモ (自分用)

APIバックエンドが別で存在するシナリオでそのフロントエンドアプリケーションのプロトタイプを手早く作りたいという状況

  • Streamlit は細かいインタラクションが微妙
  • Next.js は重すぎる(既に AgentCore 環境が実験的に稼働済み)

ということで,React + Vite でミニマムな SPA を構築する。 認証は Cognito を使い,静的ホスティングは CloudFront + S3 の構成。 すぐ立てる / すぐ消すができるよう,AWS CDK で諸々のスタックを構築した。

cognito-at-edge

CloudFront + S3 の静的サイトに Cognito 認証を組み込む場合,SPA 側でログインフローを持つのではなく,Lambda@Edge でリクエストをインターセプトして Cognito の認証チェックを行う方法が便利。cognito-at-edge はその実装をラップした npm パッケージ。

CloudFront のViewer Request イベントに Lambda@Edge をアタッチすることで,S3 のコンテンツへのアクセス前に認証状態を確認できる。

スタック分割

architecture

Lambda@Edge と CloudFront 用 ACM はグローバルサービスなので us-east-1 のみの制約。

Lambda@Edge 関数は,バージニア北部 (us-east-1) リージョンで作成する必要があります。 — CloudFront の Lambda@Edge に関する制限事項

一方,アプリデータの置き場所(データレジデンシー等の都合)で S3 や Cognito を ap-northeast-1 に置きたいケースがある(今回は特に深く考えずそうしてしまった)

結果として次の 3 スタック構成になった。

  • EdgeStack: us-east-1 (Lambda@Edge, WAF WebACL)
  • DomainStack: us-east-1 (ACM 証明書, Route 53 Hosted Zone)
  • AppStack: ap-northeast-1 (S3, Cognito, CloudFront distribution)
// bin/app.ts(概略)

// 1. Lambda@Edge を us-east-1 にデプロイ
const edgeStack = new EdgeStack(app, "EdgeStack", {
  env: { region: "us-east-1", account: config.env.account },
  crossRegionReferences: true,
});

// 2. 証明書・ Hosted Zone を us-east-1 にデプロイ
//    (CloudFront 用の ACM は us-east-1 必須)
const domainStack = new DomainStack(app, "DomainStack", {
  env: { region: "us-east-1", account: config.env.account },
  config,
  crossRegionReferences: true,
});

// 3. S3 / Cognito / CloudFront を ap-northeast-1 にデプロイ
new AppStack(app, "ClientAppStack", {
  env: config.env, // ap-northeast-1
  crossRegionReferences: true,
  edgeLambdaVersionArn:
    pinnedEdgeLambdaVersionArn || edgeStack.authFunctionVersionArn,
  webAclArn: edgeStack.webAclArn,
  config,
  domainStack,
});

crossRegionReferences: true を設定することで,CDK が内部的に SSM Parameter Store 経由のクロスリージョン参照(cdk-exports-* スタック)を自動生成する。


問題

開発用環境を立ててから,cognito-at-edge の処理スクリプトを更新する必要が生じた。

このときLambda@Edge の CloudFront へのアタッチ時は バージョン ARN$LATEST 不可)を指定する必要があり

Lambda@Edge 関数のバージョンを指定する必要があります。$LATEST は使用できません。 — Lambda@Edge に関する制限事項

更新バージョンを export して参照させようとしたところ次のエラーでデプロイが失敗した。

❌  EdgeStack failed: Error: The stack named EdgeStack failed creation,
    it may need to be manually deleted from the AWS console:
    ROLLBACK_COMPLETE: Export EdgeStack:ExportsOutputFnGetAttXXXXXXXX
    cannot be updated as it is in use by ClientAppStack
Export EdgeStack:ExportsOutputFnGetAtt... cannot be updated as it is in use by stack ClientAppStack

これはCDK が crossRegionReferences: true のスタック間参照を実現するために,エクスポート値を持つ中間スタック(cdk-exports-*)を自動生成する。CloudFormation の仕様として,別スタックが Import しているエクスポート値は更新も削除もできない

You can't modify or remove an output value that is referenced by another stack. — AWS CloudFormation ドキュメント

Lambda 関数を更新するたびに新しいバージョンが発行され,その ARN がエクスポート値として変わる。ところが AppStack がその値を Import している限り,エクスポート値は変更できない。デプロイのたびにデッドロック状態になってしまった。

様々調べて似た問題があった

根本原因は「AppStack が EdgeStack の出力を CloudFormation Export 経由で直接参照している」点なので,Context ピン留めで Export 依存を一旦なくす。

CDK
context
ARN
をピン留めする

Lambda のバージョン ARN を CDK context(cdk.json または --context フラグ)で外から渡せるようにし,スタック間の直接参照を取り除く。

// bin/app.ts

// context から固定 ARN を取得 (存在しなければ動的参照にフォールバック)
const pinnedEdgeLambdaVersionArn = app.node.tryGetContext(
  "edgeLambdaVersionArn",
) as string | undefined;

new AppStack(app, "ClientAppStack", {
  env: config.env,
  crossRegionReferences: true,
  // ピン留め ARN があればそれを使い,EdgeStack への Export 依存を持たない
  edgeLambdaVersionArn:
    pinnedEdgeLambdaVersionArn || edgeStack.authFunctionVersionArn,
  // ...
});

cdk.json にピン留めする場合:

{
  "context": {
    "edgeLambdaVersionArn": "arn:aws:lambda:us-east-1:123456789012:function:AuthFunction:42"
  }
}

または cdk deploy 時に直接指定:

cdk deploy ClientAppStack \
  --context edgeLambdaVersionArn=arn:aws:lambda:us-east-1:123456789012:function:AuthFunction:42

デプロイ手順(更新時):

  1. EdgeStack をデプロイして新しいバージョン ARN を確認する
  2. その ARN を context に設定して AppStackClientAppStack)をデプロイする

この順序であれば,Export 値を Import しているスタックを先にデプロイする前に,Import 依存を context 値に切り替えてしまえる。

crossRegionReferences: true の CDK 自動生成スタックに頼らず,自前でSSM Parameter Store を使って値を受け渡すパターンもある。

// EdgeStack 側: us-east-1 に ARN を書き込む
import * as ssm from "aws-cdk-lib/aws-ssm";

new ssm.StringParameter(this, "EdgeLambdaVersionArnParam", {
  parameterName: "/myapp/edge-lambda-version-arn",
  stringValue: authFunctionVersion.functionArn,
});
// AppStack 側: ap-northeast-1 から us-east-1 の SSM を読む
// ※ クロスリージョン SSM 参照は CDK では直接サポートされないため,
//    デプロイスクリプトで aws ssm get-parameter --region us-east-1 を実行して
//    context 経由で渡すか,カスタムリソースを使う

// カスタムリソース例 (AwsCustomResource)
import {
  AwsCustomResource,
  AwsCustomResourcePolicy,
  PhysicalResourceId,
} from "aws-cdk-lib/custom-resources";

const getParam = new AwsCustomResource(this, "GetEdgeLambdaArn", {
  onUpdate: {
    service: "SSM",
    action: "getParameter",
    parameters: { Name: "/myapp/edge-lambda-version-arn" },
    region: "us-east-1",
    physicalResourceId: PhysicalResourceId.of(Date.now().toString()),
  },
  policy: AwsCustomResourcePolicy.fromSdkCalls({
    resources: AwsCustomResourcePolicy.ANY_RESOURCE,
  }),
});

const versionArn = getParam.getResponseField("Parameter.Value");

ただし,カスタムリソース経由の SSM 読み取りも内部的に Lambda を使うためオーバーヘッドがある。プロト用途であれば,シンプルに context ピン留めで十分。

そもそもクロスリージョンにしなければよかった

プロト構成なら,全スタックを us-east-1 に統一してしまえばこの問題は起きなかった。 今回は「アプリデータは ap-northeast-1 にしよう」と何となくそうしたが,データレジデンシー等諸々の要件がない段階でリージョンを分ける必要はなかった。

CDK
crossRegionReferences
は便利だが落とし穴がある

crossRegionReferences: true は手軽にクロスリージョン参照を実現してくれるが,Export 値が変わり得るリソース(Lambda バージョン ARN など)に使うと詰まる。変更頻度が高い値は context やパラメータストアで外出しにすることを最初から検討すべきだった。

参考リンク

他の記事を読む