Stuck Updating Lambda@Edge using Cross-Region References in AWS CDK

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

This is a personal note on getting stuck with an Exports cannot be updated error when trying to update Lambda@Edge. It happened when I built an S3 + CloudFront + cognito-at-edge architecture using cross-region stacks (us-east-1 / ap-northeast-1) and crossRegionReferences: true in AWS CDK.

I was in a situation where a separate API backend already existed, and I wanted to quickly build a prototype frontend application for it.

  • Streamlit is subtle for fine interactions.
  • Next.js is too heavy (an 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. To be able to spin it up and tear it down quickly, I codified the various stacks with AWS CDK.

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.

Stack
Splitting

architecture

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

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:

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

// 1. Deploy Lambda@Edge to us-east-1
const edgeStack = new EdgeStack(app, "EdgeStack", {
  env: { region: "us-east-1", account: config.env.account },
  crossRegionReferences: true,
});

// 2. Deploy Certificate / Hosted Zone to us-east-1
//    (ACM for CloudFront must be in us-east-1)
const domainStack = new DomainStack(app, "DomainStack", {
  env: { region: "us-east-1", account: config.env.account },
  config,
  crossRegionReferences: true,
});

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

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


The
Problem

After running the development environment, I needed to update the processing script of cognito-at-edge.

At this time, 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

When I tried to export the updated version and reference it, the deployment failed with the following error.

❌  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

This occurs because CDK automatically generates an intermediate stack (cdk-exports-*) with export values to realize inter-stack references for crossRegionReferences: true. 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

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.

After some research, I found similar issues reported:

The root cause is that "AppStack directly references the output of EdgeStack via CloudFormation Export." Breaking this dependency by pinning with CDK Context solves the issue temporarily.

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.

// bin/app.ts

// Get fixed ARN from context (fallback to dynamic reference if it doesn't exist)
const pinnedEdgeLambdaVersionArn = app.node.tryGetContext(
  "edgeLambdaVersionArn",
) as string | undefined;

new AppStack(app, "ClientAppStack", {
  env: config.env,
  crossRegionReferences: true,
  // If there is a pinned ARN, use it and don't have an Export dependency on EdgeStack
  edgeLambdaVersionArn:
    pinnedEdgeLambdaVersionArn || edgeStack.authFunctionVersionArn,
  // ...
});

When pinning in cdk.json:

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

Or specify directly during cdk deploy:

cdk deploy ClientAppStack \
  --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.

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

// EdgeStack side: Write ARN to us-east-1
import * as ssm from "aws-cdk-lib/aws-ssm";

new ssm.StringParameter(this, "EdgeLambdaVersionArnParam", {
  parameterName: "/myapp/edge-lambda-version-arn",
  stringValue: authFunctionVersion.functionArn,
});
// AppStack side: Read SSM in us-east-1 from ap-northeast-1
// * Since cross-region SSM references are not directly supported in CDK,
//   execute `aws ssm get-parameter --region us-east-1` in the deployment script
//   and pass it via context, or use a custom resource.

// Custom resource example (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");

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.

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 strict 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 frequently (like Lambda version ARNs). I should have considered externalizing frequently changing values to context or parameter store from the beginning.

Reference
Links

Read more articles