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 the mdBook Static Site to S3 + CloudFront + OAC” created: 2026-10-26 updated: 2026-10-28 type: runbook tags:

  • aws
  • certification
  • devops
  • s3
  • cloudfront
  • oac
  • deployment

Deploy the mdBook Static Site to S3 + CloudFront + OAC

Purpose

Build the public mdBook site (bhu-docs/public/book/) and deploy it to AWS as a private S3 bucket fronted by CloudFront, where CloudFront is the only reader (authorized via Origin Access Control). HTTPS is served on the default *.cloudfront.net URL. This is Phase 1 (AWS CLI, no custom domain, no ACM).

Phase 2 (custom domain) is done — mimifasol.com / docs.mimifasol.com now front this same distribution via Route 53 + ACM. See [[deploy-custom-domain-route53-acm-cloudfront]].

See the design rationale in [[adr-001-s3-cloudfront-personal-site-hosting]] and the domain/URL discussion in [[cloudfront-custom-domain-vs-default-url]].

Prerequisites

  • AWS CLI v2 installed (aws --version)
  • Active SSO session (aws login) — credentials last 12h
  • jq installed (jq --version) — the scripts parse JSON with it
  • Region us-east-1, correct account (the IDs live in private config, not here)
  • Site built: bhu-docs/public/book/index.html exists

Architecture

Visitor ──HTTPS──> CloudFront (edge, e.g. FRA56 Frankfurt)
                        │  cache miss
                        │  SigV4-signed origin request (OAC)
                        ▼
                   S3 bucket (PRIVATE, Block Public Access ON)
                   bucket policy: allow ONLY this distribution ARN
ResourcePurpose
S3 bucket (private)Origin — stores the HTML/CSS/JS. Never public.
OACLets only this CloudFront distribution read the bucket (SigV4).
CloudFront distributionCDN + HTTPS + cache; the single reader of the bucket.
Bucket policyGrants s3:GetObject to the CloudFront service principal, scoped to the distribution ARN.

Procedure

The whole flow is two scripts in bhu-docs/scripts/. Both are idempotent (deploy) / safe (destroy with typed confirmation).

Step 1 — Build the static site

bash ~/work/skills/bhu-docs/scripts/build-public.sh

Expected output:

✓ Build public terminé -> .../public/book/

Step 2 — Deploy to AWS

bash ~/work/skills/bhu-docs/scripts/deploy-public.sh

What it does (6 steps): create/ensure private bucket → ensure OAC → ensure distribution → apply bucket policy → s3 sync → invalidate cache. It prints the site URL and writes resource IDs to public/.deploy-state (gitignored).

Expected tail:

Site URL : https://<id>.cloudfront.net
Dist ID  : <DIST_ID>

Step 3 — Update content later (re-deploy)

Just re-run build + deploy. The script reuses the existing bucket, OAC, and distribution (found by stable name/comment) and only syncs changed files + invalidates the cache. No duplicate resources.

bash ~/work/skills/bhu-docs/scripts/build-public.sh
bash ~/work/skills/bhu-docs/scripts/deploy-public.sh

Verification

DIST_ID=$(grep DIST_ID ~/work/skills/bhu-docs/public/.deploy-state | cut -d= -f2)

# Distribution status: InProgress -> Deployed (~5-10 min on first create)
aws cloudfront get-distribution --id "$DIST_ID" \
  --query 'Distribution.Status' --output text

# Block until fully deployed (built-in waiter)
aws cloudfront wait distribution-deployed --id "$DIST_ID"

# HTTP should 301 -> HTTPS; HTTPS should 200
DOMAIN=$(grep DIST_DOMAIN ~/work/skills/bhu-docs/public/.deploy-state | cut -d= -f2)
curl -sI "http://$DOMAIN"  | head -n1    # HTTP/1.1 301 Moved Permanently
curl -sI "https://$DOMAIN" | head -n1    # HTTP/2 200

A 200 with headers x-cache: Hit from cloudfront and x-amz-cf-pop: FRA56-* confirms the edge (Frankfurt) is serving cached content from the us-east-1 origin — the CDN working as intended.

Rollback / Cleanup

Tear down every resource to avoid residual cost. Destructive and irreversible; the site goes offline.

bash ~/work/skills/bhu-docs/scripts/destroy-public.sh   # type: destroy

Order matters (and the script handles it): disable distribution → wait for Deployed → delete distribution → delete OAC → empty + delete bucket. A CloudFront distribution must be disabled and fully deployed before it can be deleted (the wait can take several minutes).

Notes

  • First-create timing: a new distribution takes ~5–10 min to reach Deployed. Until then, the *.cloudfront.net host may not resolve (curl: (6) Could not resolve host) — this is expected, not an error.
  • Cost: effectively ~$0 at this scale (CloudFront free tier 1 TB/10M req per month; S3 a few cents; invalidations 1,000 paths/month free). See [[aws-budgets-cost-controls]].
  • OAC vs OAI: OAC is the current recommended mechanism; OAI is legacy. See the cheat-sheet [[cloudfront-s3-cli-cheat-sheet]] and [[s3-access-without-public]].
  • Idempotency: the deploy script discovers resources by deterministic name (bucket), OAC name, and distribution comment — re-running updates instead of duplicating.
  • Known content wart: udemy-clf-c02-progress.md has unclosed <section> / <domain> HTML tags that mdBook warns about. Site still builds; worth escaping later.

See Also

  • [[cloudfront-s3-cli-cheat-sheet]]
  • [[adr-001-s3-cloudfront-personal-site-hosting]]
  • [[cloudfront-custom-domain-vs-default-url]]
  • [[s3-access-without-public]]
  • [[s3-block-public-access]]
  • [[aws-cli-s3-commands]]