← Back to posts

The CloudFormation Export Lock: How CDK's Simplest Feature Creates Your Hardest Outage

· 7 min read

Your CDK stacks pass certificates, hosted zones, and database tables between each other with a single line of code. Under the hood, each of those lines creates an invisible lock that will block you at the worst possible moment.


The Outage That Could Have Been Worse

At 3am on a Thursday, our monitoring lit up. HTTPS was broken across staging. The ACM certificates had expired.

The root cause was simple: our certificates used email validation. AWS sends renewal emails 45 days before expiry. Nobody approved them. The certs expired. Services went down.

The fix was also simple: approve the emails, wait for renewal. We were back up within the hour.

But we got lucky. If our domain’s MX records had changed, or the approval mailbox had been decommissioned, or this had happened during a domain migration - we would have been stuck. Email validation is a human-in-the-loop process for infrastructure that needs to be always-on.

The permanent fix was obvious: switch to DNS validation. AWS creates a CNAME record in Route 53, verifies it automatically, and renews certificates forever without human intervention.

We had no idea what we were about to walk into.


”Cannot Update Export”

We created new DNS-validated certificates alongside the old ones. Each certificate’s ARN is stored in SSM Parameter Store so our CDK stacks can find it. The plan:

  1. Create new DNS-validated certs (new SSM params at /certificate/v2/ paths)
  2. Update the original SSM params to point to the new cert ARNs
  3. Redeploy CDK stacks - they pick up the new certs
  4. Delete old certificate stacks

Step 2 succeeded. Step 3 did not.

UPDATE_ROLLBACK_IN_PROGRESS | DomainStack
Cannot update export DomainStack:ExportsOutputRefCertificateArnParamParameter4277DB65
as it is in use by WebhooksStack.

CloudFormation refused to deploy. The certificate ARN had changed in SSM, which changed a cross-stack export value, which CloudFormation won’t allow while any other stack imports it.

We hadn’t created this export explicitly. CDK created it for us.


The Line of Code That Locks Your Infrastructure

In our CDK app, DomainStack reads the certificate ARN from SSM and exposes it as a property:

// DomainStack
this.certificate = Certificate.fromCertificateArn(this, "Certificate", certArn);

Other stacks receive it as a prop:

// In the CDK app
const webhooksStack = new WebhooksStack(app, "WebhooksStack", {
  domainResources: {
    certificate: domainStack.certificate,  // This line creates the lock
    hostedZone: domainStack.hostedZone,
  },
});

This looks like passing a variable between two TypeScript objects. It’s not. CDK translates this into:

DomainStack template:

Outputs:
  ExportsOutputRefCertificateArnParam4277DB65:
    Value: !Ref CertificateArnParam
    Export:
      Name: DomainStack:ExportsOutputRefCertificateArnParam4277DB65

WebhooksStack template:

Resources:
  ApiDomain:
    Properties:
      CertificateArn:
        Fn::ImportValue: DomainStack:ExportsOutputRefCertificateArnParam4277DB65

CloudFormation’s rule is absolute: you cannot change an exported value while any stack imports it. You can’t update the producer. You can’t update the consumer in the same deployment. You’re locked.

The CDK documentation doesn’t warn you about this. The TypeScript compiler doesn’t warn you about this. You find out when you need to rotate a certificate in production.


The Fix: SSM as a Decoupling Layer

The solution is to have each stack read from SSM Parameter Store directly, instead of receiving values as cross-stack props:

// Before: cross-stack prop (creates CloudFormation export/import lock)
const webhooksStack = new WebhooksStack(app, "WebhooksStack", {
  domainResources: {
    certificate: domainStack.certificate,
  },
});

// After: each stack reads SSM independently (no cross-stack dependency)
export class WebhooksStack extends cdk.Stack {
  constructor(scope, id, props) {
    super(scope, id, props);
    const certArn = getCertificateArn(this);  // Reads from SSM
    const certificate = Certificate.fromCertificateArn(this, "Certificate", certArn);
  }
}

With this pattern, changing the SSM parameter value and redeploying just works. No exports, no imports, no locks.

Ironically, some of our other CDK apps already used this pattern. They read from SSM directly and were never locked. Only the shared-core stacks used cross-stack props - and they were the ones that broke.


What We Didn’t Expect

The refactor should have been straightforward. It wasn’t.

Deploying the fix requires deploying in the wrong order

To break a cross-stack dependency, you need to:

  1. Deploy consumers first (so they stop importing the export)
  2. Deploy the producer second (so it can remove the export)

But CDK’s --all flag deploys in dependency order - producer first, consumer second. The exact opposite of what you need.

We had to use cdk deploy AuthStack WebhooksStack --exclusively to skip the dependency chain, then deploy DomainStack separately.

CloudFormation cares about template encoding, not just values

When we changed hostedZoneName from a cross-stack import to a local variable, the template changed from:

{ "Fn::ImportValue": "DomainStack:HostedZoneName" }

to:

"staging.example.co.uk"

Same value. Different encoding. CloudFormation treated this as a change to every resource that referenced it - including Cognito UserPoolDomain, which rejects in-place updates. The deployment failed, the stack rolled back, and the rollback failed too.

The fix: keep hostedZoneName as a cross-stack prop (it’s a stable string that never changes), and only decouple the values that actually rotate (certificate ARN, hosted zone ID).

Ghost dependencies from past deployments

When AuthStack’s rollback failed, we discovered it couldn’t roll back because other stacks in other CDK apps were importing its exports. These imports were from old deployments - the current CDK code had long since switched to SSM reads. But the deployed CloudFormation templates still had Fn::ImportValue references.

We had to redeploy those stacks (with current code that doesn’t import) before the rollback could complete.

Orphaned resources block new ones

When we updated our infrastructure-as-code to write SSM parameters at the original paths (instead of /v2/ paths), CloudFormation refused: “Resource of type AWS::SSM::Parameter with identifier /certificate/local/arn already exists.” The parameter existed from a stack that had been deleted months earlier. CloudFormation treats a renamed parameter as a create, not an update.

We had to manually delete the orphaned parameters before the deployment could proceed.


The Pattern: When to Use Props vs SSM

Not all cross-stack references are dangerous. The test is simple:

“Will I ever need to change this value without redeploying the producing stack?”

ValueChanges?Use
Certificate ARNYes (rotation, migration)SSM
Hosted zone IDRarely, but possibleSSM
Domain name stringNeverProps are fine
DynamoDB table ARNNever (table is permanent)Props are fine
S3 bucket nameNeverProps are fine

If a value might change independently of the stack that creates it - certificates, secrets, feature flags, configuration - read it from SSM. If it’s truly permanent and defined by the stack itself - table ARNs, bucket names, queue URLs - cross-stack props are fine.


The Uncomfortable Question

Open your CDK app. Search for patterns like:

someStack.someProperty  // passed to another stack's props

Each one is a CloudFormation export. Each export is a lock. Ask yourself:

  • What happens when this certificate expires and I need to replace it?
  • What happens when I need to migrate this database?
  • What happens when this hosted zone changes?

If the answer is “I’ll just update the value and redeploy” - you can’t. CloudFormation won’t let you. You’ll discover this at 3am, in production, during an incident.

We got lucky. Our outage was a warning. Yours might not be.


This post is based on a real incident involving ACM certificate migration across a multi-account AWS organization using CDK. The infrastructure patterns described - both the problems and the solutions - are common in any CDK codebase with multiple stacks.