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:
- Generate certificates externally (Let’s Encrypt, commercial CA, etc.)
- Upload them to IAM as server certificates
- Attach them to CloudFront distributions
- 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
| Requirement | Why |
|---|---|
| Must use IAM server certificates | ACM not supported for CloudFront in China |
Certificates live in /cloudfront/ path | Required for CloudFront to see them |
| Need automated renewal | Let’s Encrypt certs expire every 90 days (45 days by 2028) |
| DNS validation preferred | No need to expose HTTP endpoints for validation |
| Zero-downtime rotation | Can’t have gaps in certificate coverage |
The Solution
A bastion server we used for EKS access became our automation box. The approach:
- Certbot handles certificate generation via DNS-01 challenge (Route 53)
- Deploy hook script uploads new cert to IAM and updates CloudFront
- 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
| Step | What Happens | Why It’s Safe |
|---|---|---|
Upload as domain-new | New cert exists alongside old | No gap in coverage |
| Swap IDs in config | CloudFront points to new cert | Atomic update |
| Delete old, rename new | Clean state for next renewal | Predictable 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
-
AWS China is a different beast. Don’t assume global AWS patterns will work. Always verify service availability and feature parity.
-
IAM server certificates still work. They’re the legacy approach, but for China CloudFront, they’re your only option.
-
Automate certificate renewal from day one. Manual processes fail eventually. The question is when, not if.
-
Upload before you swap. Never leave CloudFront without a valid certificate attached.
-
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.