Stuck Updating Lambda@Edge in AWS CDK Cross-Region Stacks

A story about getting stuck updating Lambda@Edge due to Export values between cross-region stacks (us-east-1 and ap-northeast-1) when building an S3 + CloudFront + cognito-at-edge architecture with AWS CDK, and the solution using SSM Parameter Store.


When I built an S3 + CloudFront + cognito-at-edge architecture with AWS CDK across us-east-1 and ap-northeast-1 cross-region stacks, I got stuck with an Exports cannot be updated error when trying to update Lambda@Edge. Here is a summary of the cause and the solution.

Background

I needed to quickly build a prototype for an AI Agent / AgentCore backend.

  • Streamlit is subtle for fine interactions.
  • Next.js is too heavy (the AgentCore environment was already running experimentally).

So, I decided to build a minimal SPA with React + Vite. I used Cognito for authentication and CloudFront + S3 for static hosting. Since it's a prototype environment, I codified the infrastructure with AWS CDK so I could "spin it up / tear it down quickly."

What is cognito-at-edge?

When integrating Cognito authentication into a static site on CloudFront + S3, instead of having the login flow on the SPA side, it's convenient to intercept requests with Lambda@Edge and perform Cognito authentication checks. cognito-at-edge is an npm package that wraps this implementation.

By attaching Lambda@Edge to CloudFront's viewer request event, you can check the authentication state before accessing the content in S3.

Architecture Image: CloudFront → Lambda@Edge (cognito-at-edge) → S3

Stack Splitting Strategy

There is a restriction that Lambda@Edge and CloudFront ACM certificates can only be deployed in us-east-1.

Lambda@Edge functions must be created in the US East (N. Virginia) Region. — Restrictions on Edge Functions

On the other hand, there are cases where you want to place S3 or Cognito in ap-northeast-1 due to the location of app data (data residency, etc.) (although I did this without thinking too deeply this time).

As a result, it became the following 3-stack architecture.

1EdgeStack      → us-east-1 (Lambda@Edge, WAF WebACL)
2DomainStack    → us-east-1 (ACM Certificate, Route 53 Hosted Zone)
3AppStack       → ap-northeast-1 (S3, Cognito, CloudFront distribution)
1// bin/app.ts (Outline)
2
3// 1. Deploy Lambda@Edge to 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. Deploy Certificate / Hosted Zone to us-east-1
10//    (ACM for CloudFront must be in 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. Deploy S3 / Cognito / CloudFront to 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});

By setting crossRegionReferences: true, CDK automatically generates cross-region references (cdk-exports-* stacks) internally via SSM Parameter Store.


What Happened

After running it for a while, I needed to update the processing script of cognito-at-edge. When I changed the Lambda function code and ran cdk deploy, the deployment failed with the following error.

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

Or, on the CloudFormation console, an error message like this:

1Export EdgeStack:ExportsOutputFnGetAtt... cannot be updated as it is
2in use by stack ClientAppStack

Why Does This Happen?

To realize inter-stack references with crossRegionReferences: true, CDK automatically generates an intermediate stack (cdk-exports-*) with export values. As a CloudFormation specification, an export value imported by another stack cannot be updated or deleted.

You can't modify or remove an output value that is referenced by another stack. — AWS CloudFormation Documentation

When attaching Lambda@Edge to CloudFront, you must specify the version ARN ($LATEST is not allowed).

You must specify a version of the Lambda@Edge function. You can't use $LATEST. — Restrictions on Edge Functions

Every time you update a Lambda function, a new version is published, and its ARN changes as an export value. However, as long as AppStack imports that value, the export value cannot be changed. It falls into a deadlock state every time you deploy.

Error Flowchart: EdgeStack Update → Export ARN Cannot Be Changed → Deployment Failed


Solution: Break the Export Dependency by Pinning with Context

The root cause is that "AppStack directly references the output of EdgeStack via CloudFormation Export." You just need to break this.

Approach: Pin the ARN with CDK Context

Allow the Lambda version ARN to be passed from the outside via CDK context (cdk.json or --context flag), removing the direct reference between stacks.

1// bin/app.ts
2
3// Get fixed ARN from context (fallback to dynamic reference if it doesn't exist)
4const pinnedEdgeLambdaVersionArn = app.node.tryGetContext(
5  "edgeLambdaVersionArn",
6) as string | undefined;
7
8new AppStack(app, "ClientAppStack", {
9  env: config.env,
10  crossRegionReferences: true,
11  // If there is a pinned ARN, use it and don't have an Export dependency on EdgeStack
12  edgeLambdaVersionArn:
13    pinnedEdgeLambdaVersionArn || edgeStack.authFunctionVersionArn,
14  // ...
15});

When pinning in cdk.json:

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

Or specify directly during cdk deploy:

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

Deployment Procedure (When Updating):

  1. Deploy EdgeStack and check the new version ARN.
  2. Set that ARN in the context and deploy AppStack (ClientAppStack).

With this order, you can switch the Import dependency to the context value before deploying the stack that imports the Export value first.


Alternative: Cross-Region Coordination Using SSM Parameter Store

There is also a pattern of passing values using SSM Parameter Store without relying on CDK's auto-generated stacks with crossRegionReferences: true.

1// EdgeStack side: Write ARN to us-east-1
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 side: Read SSM in us-east-1 from ap-northeast-1
2// * Since cross-region SSM references are not directly supported in CDK,
3//   execute aws ssm get-parameter --region us-east-1 in the deployment script
4//   and pass it via context, or use a custom resource.
5
6// Custom resource example (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");

However, reading SSM via a custom resource also uses Lambda internally, so there is overhead. For prototype purposes, simply pinning the context should be sufficient.


Retrospective

I Shouldn't Have Made It Cross-Region in the First Place

For a prototype architecture, if I had unified all stacks in us-east-1, this problem wouldn't have occurred. This time, I vaguely decided to "put the app data in ap-northeast-1," but there was no need to separate regions at a stage where there were no data residency requirements.

CDK's crossRegionReferences is Convenient but Has Pitfalls

crossRegionReferences: true easily realizes cross-region references, but you will get stuck if you use it for resources whose Export values can change (like Lambda version ARNs). I should have considered externalizing frequently changing values to context or parameter store from the beginning.

Reference Links