title: “Deploy a Custom Domain to CloudFront with Route 53 + ACM” created: 2026-10-28 updated: 2026-10-28 type: runbook tags:
- aws
- certification
- devops
- route53
- acm
- cloudfront
- dns
- tls
- deployment
Deploy a Custom Domain to CloudFront with Route 53 + ACM
Purpose
Register the domain mimifasol.com in AWS Route 53, issue a public TLS
certificate for it in ACM (us-east-1), attach the domain to the existing
CloudFront distribution as an alternate domain name, and point the apex at the
distribution with Alias A + AAAA records. End state: the Phase 1 private
S3 + CloudFront + OAC site is reachable at https://mimifasol.com.
This is Phase 2 (custom domain) of the bhu-docs public site. Phase 1
(private bucket, CloudFront, OAC, default *.cloudfront.net URL) is described in
[[deploy-static-site-s3-cloudfront-oac]]. The DNS theory behind the apex Alias
requirement is in [[route53-alias-vs-cname]] and [[route53-dns-records]].
This runbook is console-first (each screen is described, for certification study), with equivalent/verification CLI commands next to each step.
Prerequisites
- Active SSO session (
aws login) — credentials last 12h, us-east-1 - Phase 1 deployed: CloudFront distribution serving the site on its
*.cloudfront.netURL (seepublic/.deploy-statefor the live IDs) - Correct account + region
us-east-1(account/resource IDs live in private config —bhu-skillssteering and the gitignoredpublic/.deploy-state— never in this published note) - AWS CLI v2 +
jq(for the verification commands) - Budget awareness:
.comregistration is $16/year (STANDARD price, billed immediately, non-refundable); a Route 53 hosted zone is $0.50/month; ACM public certs are free
Key Facts (read before starting)
route53domains(registration) is us-east-1 only — the registrar API and console live there regardless of your default region.- ACM cert for CloudFront must be in us-east-1 — CloudFront only reads certs from N. Virginia. If you issue it anywhere else, it will not appear in the distribution’s certificate picker.
- Apex needs an Alias, not a CNAME —
mimifasol.comalready has NS + SOA, so a CNAME there is illegal (RFC 1034). Route 53 Alias A/AAAA is the fix. See [[route53-alias-vs-cname]]. - Registering a domain auto-creates a hosted zone with the same name and its NS/SOA records. Use that zone for everything else.
Procedure
Step 1 — Register mimifasol.com in Route 53
Console: Route 53 → Domains → Registered domains → Register domains.
- Search box: type
mimifasol.com, click Check. Confirm it shows Available. Add to cart, Continue. - Contact details: fill registrant / admin / tech contact (one block, reusable for all three). Real data is required by ICANN.
- Privacy protection: leave Enabled (default) — hides personal contact data in public WHOIS, free with Route 53.
- Auto-renew: leave Enabled so the domain does not silently expire.
- Review, accept the ICANN agreement, Complete order.
Registration is asynchronous. Status goes from the order being submitted to the domain appearing under Registered domains.
.comis usually a few minutes but can take up to ~1 hour (occasionally longer for other TLDs).
Billing / payment — there is no separate card entry in the register flow. Clicking Complete order charges the payment method already on file for the AWS account (the same one used for S3/CloudFront). The $16 appears as a Route 53 domain-registration line item on the regular AWS bill, charged ~on order (not deferred to month-end). To pre-check: Billing and Cost Management → Payment methods → confirm a valid, non-expired card is present (changing the payment method itself is typically a root-user action; using the existing one is not). With auto-renew ON, next year’s fee is charged automatically to the same method ~1 month before expiry — disable auto-renew to let the domain lapse (see Rollback). See [[aws-budgets-config]].
CLI check / verify:
# Availability (registrar API is us-east-1 only)
aws route53domains check-domain-availability \
--domain-name mimifasol.com --region us-east-1
# -> "Availability": "AVAILABLE"
# After ordering, track operation + registration
aws route53domains list-operations --region us-east-1
aws route53domains list-domains --region us-east-1
Registration automatically creates a public hosted zone for
mimifasol.com. Grab its ID (used in later steps):
aws route53 list-hosted-zones-by-name \
--dns-name mimifasol.com \
--query 'HostedZones[0].Id' --output text
# -> /hostedzone/ZXXXXXXXXXXXXX
This deployment (2026-10-28): registration operation
REGISTER_DOMAINreturnedSUCCESSFULafter ~11 min. A public hosted zone was auto-created (2 records = NS + SOA), expiry 2027-10-28, auto-renew ON. The site’s existing CloudFront distribution was reused; its assigned*.cloudfront.netdomain is the Alias target in Step 5 (note it is not derived from the distribution ID — read it withget-distribution). Concrete IDs are kept in private config (bhu-skillssteering + gitignoredpublic/.deploy-state), not here.
Step 2 — Request the ACM certificate (us-east-1)
Console: Certificate Manager → Request certificate (confirm the region selector top-right reads N. Virginia / us-east-1 first).
- Choose Request a public certificate, Next.
- Fully qualified domain name:
mimifasol.com. - Add another name:
*.mimifasol.com(wildcard — coversbenoit.,docs.,ohmb., … in one cert). - Validation method: DNS validation (recommended — renews automatically as long as the DNS record stays in place).
- Key algorithm: leave RSA 2048 (default).
- Request. The cert appears with status Pending validation.
CLI equivalent:
aws acm request-certificate \
--domain-name mimifasol.com \
--subject-alternative-names '*.mimifasol.com' \
--validation-method DNS \
--region us-east-1
# -> "CertificateArn": "arn:aws:acm:us-east-1:<ACCOUNT_ID>:certificate/<CERT_ID>"
Step 3 — Validate the certificate via Route 53 DNS
Console: open the certificate → Domains section → button Create records in Route 53.
- ACM shows one CNAME validation record per distinct name. Because both names
share the same base domain, ACM typically shows one
_<hash>.mimifasol.comCNAME covering the pair. - Click Create records in Route 53 → ACM writes the CNAME(s) straight into
the
mimifasol.comhosted zone. Create records. - Wait. Status flips Pending validation → Issued, usually a few minutes (can be up to ~30 min).
These
_<hash>CNAMEs are validation-only records; leave them in the zone forever so ACM can auto-renew the cert.
This deployment (2026-10-28): the cert was issued in
us-east-1. As expected, both names (mimifasol.com+*.mimifasol.com) shared a single validation CNAME (_<hash>.mimifasol.com→_<hash>.acm-validations.aws). The concrete cert ARN and validation hashes are kept in private config, not in this published note.
CLI equivalent (only if not using the console button):
# Inspect the validation records ACM wants
aws acm describe-certificate --certificate-arn "$CERT_ARN" \
--region us-east-1 \
--query 'Certificate.DomainValidationOptions[].ResourceRecord'
# Block until issued
aws acm wait certificate-validated \
--certificate-arn "$CERT_ARN" --region us-east-1
Step 4 — Attach the domain to the CloudFront distribution
Console: CloudFront → Distributions → (the bhu-docs distribution) → General → Settings → Edit.
- Alternate domain names (CNAMEs): Add item →
mimifasol.com, then Add item again →docs.mimifasol.com. Both are served by this distribution: the apex as a temporary landing (umbrella not built yet) anddocs.as the real home of the bhu-docs site. Both are already covered by the cert (mimifasol.com+ the*.mimifasol.comwildcard). - Custom SSL certificate: switch from Default CloudFront certificate to
Custom, then pick the
mimifasol.comACM cert from the dropdown.- If it is not in the list, the cert is not Issued or not in us-east-1 — recheck Step 2/3.
- Leave Security policy at the recommended default (TLSv1.2_2021).
- Save changes. The distribution status goes Deploying; wait for it to finish (~5–10 min) before testing.
CLI verify:
DIST_ID=$(grep DIST_ID ~/work/skills/bhu-docs/public/.deploy-state | cut -d= -f2)
aws cloudfront get-distribution --id "$DIST_ID" \
--query 'Distribution.{Status:Status,Aliases:DistributionConfig.Aliases.Items,Cert:DistributionConfig.ViewerCertificate.ACMCertificateArn}'
aws cloudfront wait distribution-deployed --id "$DIST_ID"
Step 5 — Create the Alias records (apex + docs., A + AAAA)
Console: Route 53 → Hosted zones → mimifasol.com → Create record.
Create four records total: A + AAAA for the apex, A + AAAA for docs..
Apex (mimifasol.com):
- Record name: leave empty (= apex
mimifasol.com). - Record type: A – IPv4 address.
- Alias: toggle ON.
- Route traffic to: Alias to CloudFront distribution → region shows
automatically → pick the distribution
*.cloudfront.netdomain. - Create record.
- Repeat 1–5 with Record type = AAAA (IPv6) pointing at the same distribution. Both A and AAAA are needed so the site resolves on IPv4 and IPv6.
Sub-domain (docs.mimifasol.com):
- Create record again, Record name:
docs. - Record type A, Alias ON, Alias to CloudFront distribution, same distribution. Create record.
- Repeat with Record type AAAA for
docs.
Apex = Alias, mandatory. A CNAME at the apex is rejected because of the existing NS/SOA records. A sub-domain like
docs.could use a CNAME, but we use Alias there too for consistency and free resolution. See [[route53-alias-vs-cname]].
CLI equivalent (change batch):
ZONE_ID=$(aws route53 list-hosted-zones-by-name --dns-name mimifasol.com \
--query 'HostedZones[0].Id' --output text)
CF_DOMAIN=$(aws cloudfront get-distribution --id "$DIST_ID" \
--query 'Distribution.DomainName' --output text)
# CloudFront's fixed Alias hosted-zone ID is always Z2FDTNDATAQYW2
cat > /tmp/apex-alias.json <<JSON
{"Changes":[
{"Action":"UPSERT","ResourceRecordSet":{"Name":"mimifasol.com","Type":"A",
"AliasTarget":{"HostedZoneId":"Z2FDTNDATAQYW2","DNSName":"$CF_DOMAIN","EvaluateTargetHealth":false}}},
{"Action":"UPSERT","ResourceRecordSet":{"Name":"mimifasol.com","Type":"AAAA",
"AliasTarget":{"HostedZoneId":"Z2FDTNDATAQYW2","DNSName":"$CF_DOMAIN","EvaluateTargetHealth":false}}},
{"Action":"UPSERT","ResourceRecordSet":{"Name":"docs.mimifasol.com","Type":"A",
"AliasTarget":{"HostedZoneId":"Z2FDTNDATAQYW2","DNSName":"$CF_DOMAIN","EvaluateTargetHealth":false}}},
{"Action":"UPSERT","ResourceRecordSet":{"Name":"docs.mimifasol.com","Type":"AAAA",
"AliasTarget":{"HostedZoneId":"Z2FDTNDATAQYW2","DNSName":"$CF_DOMAIN","EvaluateTargetHealth":false}}}
]}
JSON
aws route53 change-resource-record-sets \
--hosted-zone-id "$ZONE_ID" --change-batch file:///tmp/apex-alias.json
Verification
# DNS resolves both names to CloudFront edge IPs (A + AAAA).
# NOTE: `dig` is NOT installed by default on Ubuntu 24.04 (it ships in the
# `bind9-dnsutils` package: `sudo apt install bind9-dnsutils`). If `dig` is
# missing, use `getent` (always present) or `nslookup`, or just rely on the
# curl check below — a 200 proves DNS resolved.
dig +short A mimifasol.com # or: getent ahosts mimifasol.com
dig +short AAAA mimifasol.com
dig +short A docs.mimifasol.com # or: getent ahosts docs.mimifasol.com
dig +short AAAA docs.mimifasol.com
# TLS + HTTP: HTTP should 301 -> HTTPS; HTTPS should 200 (both names)
curl -sI http://mimifasol.com | head -n1 # 301
curl -sI https://mimifasol.com | head -n1 # HTTP/2 200
curl -sI https://docs.mimifasol.com | head -n1 # HTTP/2 200
# Certificate CN/SAN served matches the domain
echo | openssl s_client -connect mimifasol.com:443 -servername mimifasol.com 2>/dev/null \
| openssl x509 -noout -subject -ext subjectAltName
Open https://mimifasol.com and https://docs.mimifasol.com in a browser:
valid padlock, no name-mismatch warning, and the mdBook site loads.
This deployment (2026-10-28) — all checks passed:
- DNS: both names resolve to the same CloudFront edges — IPv4
18.66.112.{2,6,14,37}, IPv62600:9000:223f:...(A + AAAA).http://mimifasol.com→301;https://mimifasol.comandhttps://docs.mimifasol.com→HTTP/2 200.- Cert served:
subject=CN=mimifasol.com, SANDNS:mimifasol.com, DNS:*.mimifasol.com.- Tooling note:
dig/nslookupwere absent; usedgetent ahostsandnc -vz <host> 443instead — both work out of the box on this Ubuntu.
Rollback
- Detach domain from CloudFront: edit the distribution, remove the alternate
domain names (
mimifasol.com,docs.mimifasol.com), switch the certificate back to Default CloudFront certificate, save. The site returns to the*.cloudfront.netURL. - Remove DNS records: delete the apex and
docs.A + AAAA alias records in the hosted zone. (Keep the ACM_<hash>validation CNAMEs if you may reattach later.) - Certificate: an unused ACM cert costs nothing; delete it only if you are sure. A cert in use by CloudFront cannot be deleted until detached.
- Domain registration is NOT easily reversible —
.comis billed on registration and is non-refundable. Do not register unless committed. You can disable auto-renew to let it lapse at year end.
Notes
- Timing: domain registration (minutes to ~1h), ACM validation (minutes to ~30 min), CloudFront redeploy (~5–10 min). The apex Alias itself resolves almost immediately once the distribution is deployed.
- Cost:
.com~$13/yr (non-refundable), hosted zone $0.50/mo, ACM public cert free, Alias queries to AWS targets free. See [[aws-budgets-config]]. - Wildcard cert covers one level (
*.mimifasol.com), not nested (*.*.mimifasol.com). One cert now serves every planned sub-domain (benoit.,docs.,ohmb., …). - Why not CNAME at apex: NS + SOA already live there; RFC 1034 forbids a CNAME coexisting with other records. Route 53 Alias sidesteps this by looking like an A record. See [[route53-dns-records]].
- Sub-domains later: each new sub-domain = one Alias (or CNAME) record in the zone + adding it as a CloudFront alternate domain name (already covered by the wildcard cert).
See Also
- [[route53-acm-cloudfront-custom-domain-cheat-sheet]]
- [[deploy-static-site-s3-cloudfront-oac]]
- [[cloudfront-s3-cli-cheat-sheet]]
- [[cloudfront-custom-domain-vs-default-url]]
- [[route53-alias-vs-cname]]
- [[route53-dns-records]]
- [[umbrella-domain-musical-name]]
- [[adr-001-s3-cloudfront-personal-site-hosting]]