AWS CDK クロスリージョンスタックで Lambda@Edge の更新に詰まった話

AWS CDK で S3 + CloudFront + cognito-at-edge 構成を組んだ際に,us-east-1 と ap-northeast-1 のクロスリージョンスタック間の Export 値が更新できず Lambda@Edge のデプロイが詰まった話と,SSM Parameter Store を使った解決策について。


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 パッケージである。

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

構成イメージ: CloudFront → Lambda@Edge (cognito-at-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 している限り,エクスポート値は変更できない。デプロイのたびにデッドロック状態に陥る。

エラーの流れ図: EdgeStack 更新 → Export ARN 変更不可 → デプロイ失敗


解決策: 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

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

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

この順序であれば,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 やパラメータストアで外出しにすることを最初から検討すべきだった。

参考リンク