TL;DR: AWS China doesn’t support ACM for CloudFront. Certificates must be uploaded to IAM manually. I automated the entire renewal process with certbot, a deploy hook script, and a daily cron job. No more 3am certificate expiry panics.


The China Problem

AWS China is a region many DevOps engineers will never touch. I wasn’t so lucky.

The region exists as a separate partition from global AWS, operated by local partners (Sinnet in Beijing, NWCD in Ningxia) to comply with Chinese regulations. This means some services work differently - or don’t exist at all.

For a business with significant Chinese users, the performance benefits are real. Direct access without traversing the Great Firewall means faster load times and a better user experience. But the operational overhead? That’s where things get interesting.

What Broke

We had an EKS cluster in AWS China running an application with heavy static assets. To improve performance, we put CloudFront in front of it - standard CDN pattern.

Everything worked until we needed HTTPS.

In global AWS, you’d use ACM (AWS Certificate Manager) to provision and auto-renew certificates. CloudFront integrates seamlessly. Set it and forget it.

In AWS China, ACM certificates don’t work directly with CloudFront.

Note (2025 update): AWS now offers exportable ACM public certificates in China regions. You could potentially use these instead of Let’s Encrypt, but you’d still need to upload them to IAM for CloudFront. The automation approach below works with either certificate source.

flowchart LR
    subgraph "Global AWS"
        ACM[ACM Certificate] --> CF1[CloudFront]
        CF1 --> |"Auto-renews"| ACM
    end

    subgraph "AWS China"
        IAM[IAM Server Certificate] --> CF2[CloudFront]
        CF2 --> |"Manual renewal"| MANUAL[Manual Upload]
    end

    style ACM fill:#4ecdc4,color:#000
    style IAM fill:#ff6b6b,color:#fff
    style MANUAL fill:#ff6b6b,color:#fff

Instead, you must:

  1. Generate certificates externally (Let’s Encrypt, commercial CA, etc.)
  2. Upload them to IAM as server certificates
  3. Attach them to CloudFront distributions
  4. Repeat every 90 days (for Let’s Encrypt) or yearly

This is the kind of manual process that works fine until someone forgets, goes on vacation, or changes teams. Then your CDN goes down at 3am.

The Constraints

RequirementWhy
Must use IAM server certificatesACM not supported for CloudFront in China
Certificates live in /cloudfront/ pathRequired for CloudFront to see them
Need automated renewalLet’s Encrypt certs expire every 90 days (45 days by 2028)
DNS validation preferredNo need to expose HTTP endpoints for validation
Zero-downtime rotationCan’t have gaps in certificate coverage

The Solution

A bastion server we used for EKS access became our automation box. The approach:

  1. Certbot handles certificate generation via DNS-01 challenge (Route 53)
  2. Deploy hook script uploads new cert to IAM and updates CloudFront
  3. Daily cron ensures renewal attempts happen before expiry
flowchart TD
    CRON[Daily Cron] --> CERTBOT[Certbot Renew]
    CERTBOT --> |"Certificate renewed?"| CHECK{New cert?}
    CHECK --> |No| DONE([Done])
    CHECK --> |Yes| HOOK[Deploy Hook Script]

    HOOK --> UPLOAD[Upload to IAM<br/>as domain-new]
    UPLOAD --> FETCH[Fetch CloudFront config]
    FETCH --> UPDATE[Swap certificate IDs]
    UPDATE --> APPLY[Update CloudFront distribution]
    APPLY --> CLEANUP[Delete old cert<br/>Rename new → domain]
    CLEANUP --> DONE

    style CRON fill:#ffd93d,color:#000
    style HOOK fill:#4ecdc4,color:#000
    style DONE fill:#9f9,stroke:#333

The Deploy Hook Script

This script runs automatically when certbot successfully renews a certificate:

#!/bin/bash
set -euo pipefail

DOMAIN=$1

# Upload new certificate to IAM (in /cloudfront/ path for CloudFront access)
aws iam upload-server-certificate \
    --server-certificate-name "${DOMAIN}-new" \
    --certificate-body "file:///etc/letsencrypt/live/${DOMAIN}/cert.pem" \
    --private-key "file:///etc/letsencrypt/live/${DOMAIN}/privkey.pem" \
    --certificate-chain "file:///etc/letsencrypt/live/${DOMAIN}/chain.pem" \
    --path /cloudfront/

# Find the CloudFront distribution using this domain
DISTRIBUTION=$(aws cloudfront list-distributions \
    --query "DistributionList.Items[?Aliases.Items[0]=='${DOMAIN}'].Id" \
    --output text)

# Get current distribution config
aws cloudfront get-distribution-config \
    --id "$DISTRIBUTION" \
    --output json | jq '.DistributionConfig' > "${DOMAIN}.json"

# Get old and new certificate IDs
OLDCERT=$(aws iam list-server-certificates \
    --query "ServerCertificateMetadataList[?ServerCertificateName=='${DOMAIN}'].ServerCertificateId" \
    --output text)

NEWCERT=$(aws iam list-server-certificates \
    --query "ServerCertificateMetadataList[?ServerCertificateName=='${DOMAIN}-new'].ServerCertificateId" \
    --output text)

# Swap certificate ID in config
sed -i "s/${OLDCERT}/${NEWCERT}/" "${DOMAIN}.json"

# Get current ETag for optimistic locking
ETAG=$(aws cloudfront get-distribution-config \
    --id "$DISTRIBUTION" \
    --query "ETag" \
    --output text)

# Update the distribution
aws cloudfront update-distribution \
    --id "$DISTRIBUTION" \
    --if-match "$ETAG" \
    --distribution-config "file://${DOMAIN}.json"

# Cleanup: delete old cert, rename new cert to standard name
aws iam delete-server-certificate \
    --server-certificate-name "${DOMAIN}"

aws iam update-server-certificate \
    --server-certificate-name "${DOMAIN}-new" \
    --new-server-certificate-name "${DOMAIN}"

echo "Certificate renewed for ${DOMAIN}"

The Cron Job

Certbot’s renew command only acts when certificates are within 30 days of expiry, so running daily is safe:

@daily certbot renew --dns-route53 --deploy-hook "/home/ubuntu/.scripts/renew.sh cdn.example.com && /home/ubuntu/.scripts/renew.sh assets.example.com"

Why This Works

StepWhat HappensWhy It’s Safe
Upload as domain-newNew cert exists alongside oldNo gap in coverage
Swap IDs in configCloudFront points to new certAtomic update
Delete old, rename newClean state for next renewalPredictable naming

The key insight: always upload the new certificate before updating CloudFront, and only delete the old one after the distribution is updated. This ensures there’s never a moment without a valid certificate attached.

Additional Safeguards

I also set up monitoring to alert if a certificate is within 14 days of expiry. If the automation fails silently, I’ll know before it becomes an outage.

You could extend this with:

  • SNS notifications on successful renewal
  • CloudWatch alarms on certificate age
  • Slack/PagerDuty integration for failures

The Process Overview

flowchart LR
    subgraph "Daily (Automated)"
        A[Cron triggers certbot] --> B{Within 30 days<br/>of expiry?}
        B --> |No| C([Skip])
        B --> |Yes| D[Renew via DNS-01]
        D --> E[Deploy hook runs]
        E --> F[CloudFront updated]
    end

    subgraph "Monitoring"
        G[Certificate age check] --> H{< 14 days left?}
        H --> |Yes| I[Alert]
        H --> |No| J([OK])
    end

    style D fill:#4ecdc4,color:#000
    style F fill:#4ecdc4,color:#000
    style I fill:#ff6b6b,color:#fff

Takeaways

  1. AWS China is a different beast. Don’t assume global AWS patterns will work. Always verify service availability and feature parity.

  2. IAM server certificates still work. They’re the legacy approach, but for China CloudFront, they’re your only option.

  3. Automate certificate renewal from day one. Manual processes fail eventually. The question is when, not if.

  4. Upload before you swap. Never leave CloudFront without a valid certificate attached.

  5. Monitor independently. Automation can fail silently. External monitoring is your safety net.

The irony of AWS China is that it pushes you toward better automation practices. When you can’t rely on managed services, you build more resilient systems yourself.