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.comnow 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 -
jqinstalled (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.htmlexists
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
| Resource | Purpose |
|---|---|
| S3 bucket (private) | Origin — stores the HTML/CSS/JS. Never public. |
| OAC | Lets only this CloudFront distribution read the bucket (SigV4). |
| CloudFront distribution | CDN + HTTPS + cache; the single reader of the bucket. |
| Bucket policy | Grants 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.nethost 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.mdhas 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]]