Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


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.net URL (see public/.deploy-state for the live IDs)
  • Correct account + region us-east-1 (account/resource IDs live in private config — bhu-skills steering and the gitignored public/.deploy-state — never in this published note)
  • AWS CLI v2 + jq (for the verification commands)
  • Budget awareness: .com registration 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.com already 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.

  1. Search box: type mimifasol.com, click Check. Confirm it shows Available. Add to cart, Continue.
  2. Contact details: fill registrant / admin / tech contact (one block, reusable for all three). Real data is required by ICANN.
  3. Privacy protection: leave Enabled (default) — hides personal contact data in public WHOIS, free with Route 53.
  4. Auto-renew: leave Enabled so the domain does not silently expire.
  5. Review, accept the ICANN agreement, Complete order.

Registration is asynchronous. Status goes from the order being submitted to the domain appearing under Registered domains. .com is 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_DOMAIN returned SUCCESSFUL after ~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.net domain is the Alias target in Step 5 (note it is not derived from the distribution ID — read it with get-distribution). Concrete IDs are kept in private config (bhu-skills steering + gitignored public/.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).

  1. Choose Request a public certificate, Next.
  2. Fully qualified domain name: mimifasol.com.
  3. Add another name: *.mimifasol.com (wildcard — covers benoit., docs., ohmb., … in one cert).
  4. Validation method: DNS validation (recommended — renews automatically as long as the DNS record stays in place).
  5. Key algorithm: leave RSA 2048 (default).
  6. 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.

  1. ACM shows one CNAME validation record per distinct name. Because both names share the same base domain, ACM typically shows one _<hash>.mimifasol.com CNAME covering the pair.
  2. Click Create records in Route 53 → ACM writes the CNAME(s) straight into the mimifasol.com hosted zone. Create records.
  3. 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.

  1. 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) and docs. as the real home of the bhu-docs site. Both are already covered by the cert (mimifasol.com + the *.mimifasol.com wildcard).
  2. Custom SSL certificate: switch from Default CloudFront certificate to Custom, then pick the mimifasol.com ACM 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.
  3. Leave Security policy at the recommended default (TLSv1.2_2021).
  4. 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):

  1. Record name: leave empty (= apex mimifasol.com).
  2. Record type: A – IPv4 address.
  3. Alias: toggle ON.
  4. Route traffic to: Alias to CloudFront distribution → region shows automatically → pick the distribution *.cloudfront.net domain.
  5. Create record.
  6. 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):

  1. Create record again, Record name: docs.
  2. Record type A, Alias ON, Alias to CloudFront distribution, same distribution. Create record.
  3. 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}, IPv6 2600:9000:223f:... (A + AAAA).
  • http://mimifasol.com → 301; https://mimifasol.com and https://docs.mimifasol.com → HTTP/2 200.
  • Cert served: subject=CN=mimifasol.com, SAN DNS:mimifasol.com, DNS:*.mimifasol.com.
  • Tooling note: dig/nslookup were absent; used getent ahosts and nc -vz <host> 443 instead — 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.net URL.
  • 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 — .com is 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]]