AWS CDK で S3 + CloudFront + cognito-at-edge の構成を us-east-1 / ap-northeast-1 のクロスリージョンスタックで組んだら,Lambda@Edge の更新時に Exports cannot be updated エラーが出て詰まった。その原因と対処をまとめる。
背景
AI Agent / AgentCore バックエンドのプロトタイプを手早く作りたいという状況があった。
- 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 パッケージである。
- npm: cognito-at-edge
- GitHub: awslabs/cognito-at-edge
CloudFront のビューアリクエストイベントに Lambda@Edge をアタッチすることで,S3 のコンテンツへのアクセス前に認証状態を確認できる。

スタック分割の方針
Lambda@Edge と CloudFront の ACM 証明書は us-east-1 にのみデプロイできる制約がある。
Lambda@Edge 関数は,バージニア北部 (us-east-1) リージョンで作成する必要があります。 — CloudFront の Lambda@Edge に関する制限事項
一方,アプリデータの置き場所(データレジデンシー等の都合)で S3 や Cognito を ap-northeast-1 に置きたいケースがある(今回は特に深く考えずそうしてしまったが)。
結果として次の 3 スタック構成になった。
1EdgeStack → us-east-1 (Lambda@Edge, WAF WebACL) 2DomainStack → us-east-1 (ACM 証明書, Route 53 Hosted Zone) 3AppStack → ap-northeast-1 (S3, Cognito, CloudFront distribution)
1// bin/app.ts(概略) 2 3// 1. Lambda@Edge を us-east-1 にデプロイ 4const edgeStack = new EdgeStack(app, "EdgeStack", { 5 env: { region: "us-east-1", account: config.env.account }, 6 crossRegionReferences: true, 7}); 8 9// 2. 証明書・ Hosted Zone を us-east-1 にデプロイ 10// (CloudFront 用の ACM は us-east-1 必須) 11const domainStack = new DomainStack(app, "DomainStack", { 12 env: { region: "us-east-1", account: config.env.account }, 13 config, 14 crossRegionReferences: true, 15}); 16 17// 3. S3 / Cognito / CloudFront を ap-northeast-1 にデプロイ 18new AppStack(app, "ClientAppStack", { 19 env: config.env, // ap-northeast-1 20 crossRegionReferences: true, 21 edgeLambdaVersionArn: 22 pinnedEdgeLambdaVersionArn || edgeStack.authFunctionVersionArn, 23 legacyEdgeLambdaVersionArn: edgeStack.legacyAuthFunctionVersionArn, 24 webAclArn: edgeStack.webAclArn, 25 config, 26 domainStack, 27});
crossRegionReferences: true を設定することで,CDK が内部的に SSM Parameter Store 経由のクロスリージョン参照(cdk-exports-* スタック)を自動生成する。
何が起きたか
しばらく運用してから,cognito-at-edge の処理スクリプトを更新する必要が生じた。Lambda 関数コードを変更して cdk deploy を走らせると,次のエラーでデプロイが失敗した。
1❌ EdgeStack failed: Error: The stack named EdgeStack failed creation, 2 it may need to be manually deleted from the AWS console: 3 ROLLBACK_COMPLETE: Export EdgeStack:ExportsOutputFnGetAttXXXXXXXX 4 cannot be updated as it is in use by ClientAppStack
あるいは CloudFormation コンソール上では以下のようなエラーメッセージ。
1Export EdgeStack:ExportsOutputFnGetAtt... cannot be updated as it is 2in 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@Edge は CloudFront にアタッチする際に バージョン ARN($LATEST 不可)を指定しなければならない。
Lambda@Edge 関数のバージョンを指定する必要があります。$LATEST は使用できません。 — Lambda@Edge に関する制限事項
Lambda 関数を更新するたびに新しいバージョンが発行され,その ARN がエクスポート値として変わる。ところが AppStack がその値を Import している限り,エクスポート値は変更できない。デプロイのたびにデッドロック状態に陥る。

解決策: Context ピン留めで Export 依存を断ち切る
根本原因は「AppStack が EdgeStack の出力を CloudFormation Export 経由で直接参照している」こと。これを断ち切ればよい。
アプローチ: CDK context で ARN をピン留めする
Lambda のバージョン ARN を CDK context(cdk.json または --context フラグ)で外から渡せるようにし,スタック間の直接参照を取り除く。
1// bin/app.ts 2 3// context から固定 ARN を取得 (存在しなければ動的参照にフォールバック) 4const pinnedEdgeLambdaVersionArn = app.node.tryGetContext( 5 "edgeLambdaVersionArn", 6) as string | undefined; 7 8new AppStack(app, "ClientAppStack", { 9 env: config.env, 10 crossRegionReferences: true, 11 // ピン留め ARN があればそれを使い,EdgeStack への Export 依存を持たない 12 edgeLambdaVersionArn: 13 pinnedEdgeLambdaVersionArn || edgeStack.authFunctionVersionArn, 14 // ... 15});
cdk.json にピン留めする場合:
1{ 2 "context": { 3 "edgeLambdaVersionArn": "arn:aws:lambda:us-east-1:123456789012:function:AuthFunction:42" 4 } 5}
または cdk deploy 時に直接指定:
1cdk deploy ClientAppStack \ 2 --context edgeLambdaVersionArn=arn:aws:lambda:us-east-1:123456789012:function:AuthFunction:42
デプロイ手順(更新時):
EdgeStackをデプロイして新しいバージョン ARN を確認する- その ARN を context に設定して
AppStack(ClientAppStack)をデプロイする
この順序であれば,Export 値を Import しているスタックを先にデプロイする前に,Import 依存を context 値に切り替えてしまえる。
代替案: SSM Parameter Store を使ったリージョン間連携
crossRegionReferences: true の CDK 自動生成スタックに頼らず,SSM Parameter Store を使って値を受け渡すパターンもある。
1// EdgeStack 側: us-east-1 に ARN を書き込む 2import * as ssm from "aws-cdk-lib/aws-ssm"; 3 4new ssm.StringParameter(this, "EdgeLambdaVersionArnParam", { 5 parameterName: "/myapp/edge-lambda-version-arn", 6 stringValue: authFunctionVersion.functionArn, 7});
1// AppStack 側: ap-northeast-1 から us-east-1 の SSM を読む 2// ※ クロスリージョン SSM 参照は CDK では直接サポートされないため, 3// デプロイスクリプトで aws ssm get-parameter --region us-east-1 を実行して 4// context 経由で渡すか,カスタムリソースを使う 5 6// カスタムリソース例 (AwsCustomResource) 7import { 8 AwsCustomResource, 9 AwsCustomResourcePolicy, 10 PhysicalResourceId, 11} from "aws-cdk-lib/custom-resources"; 12 13const getParam = new AwsCustomResource(this, "GetEdgeLambdaArn", { 14 onUpdate: { 15 service: "SSM", 16 action: "getParameter", 17 parameters: { Name: "/myapp/edge-lambda-version-arn" }, 18 region: "us-east-1", 19 physicalResourceId: PhysicalResourceId.of(Date.now().toString()), 20 }, 21 policy: AwsCustomResourcePolicy.fromSdkCalls({ 22 resources: AwsCustomResourcePolicy.ANY_RESOURCE, 23 }), 24}); 25 26const versionArn = getParam.getResponseField("Parameter.Value");
ただし,カスタムリソース経由の SSM 読み取りも内部的に Lambda を使うためオーバーヘッドがある。プロト用途であれば,シンプルに context ピン留めで十分だろう。
振り返り
そもそもクロスリージョンにしなければよかった
プロト構成なら,全スタックを us-east-1 に統一してしまえばこの問題は起きなかった。今回は「アプリデータは ap-northeast-1 にしよう」と何となくそうしたが,データレジデンシーの要件がない段階でリージョンを分ける必要はなかった。
CDK の crossRegionReferences は便利だが落とし穴がある
crossRegionReferences: true は手軽にクロスリージョン参照を実現してくれるが,Export 値が変わり得るリソース(Lambda バージョン ARN など)に使うと詰まる。変更頻度が高い値は context やパラメータストアで外出しにすることを最初から検討すべきだった。