AWS CloudFront skill for Codex
High hit ratios without stale deploys. Codex puts a private S3 bucket or API behind CloudFront with Origin Access Control, splits cache policies from origin request policies, and keeps the cache key down to what actually changes a response. Hashed assets get year-long immutable caching while HTML revalidates, so deploys stop depending on full invalidations, and SPA routing moves into a tested CloudFront Function.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- aws-cloudfront Installs to ~/.agents/skills/aws-cloudfront/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install aws-cloudfront The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project aws-cloudfront Installs to .agents/skills/aws-cloudfront/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/aws-cloudfront.zipunzip -o aws-cloudfront.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) aws-cloudfront PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $aws-cloudfront, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Configures Amazon CloudFront distributions - private S3 origins with Origin Access Control, cache policies vs origin request policies, Cache-Control strategy for hashed assets and HTML, CloudFront Functions vs Lambda@Edge, SPA and index.html rewrites, invalidations, response headers policies, signed URLs and ACM certificates in us-east-1. Use when putting a CDN in front of S3 or an API, the cache hit ratio is low, users see stale content after a deploy, S3 returns 403 through CloudFront, or adding redirects, security headers or edge auth.
Use it when
- Putting a CDN in front of S3, an ALB or API Gateway
- Cache hit ratio is low or users see stale content
- S3 returns 403 through CloudFront
- Adding redirects, rewrites, security headers or signed URLs at the edge
Use something else when
- You need provider-neutral CDN cache design across vendors: use cdn-caching
- The site is hosted on Cloudflare Pages: use cloudflare-pages
How to check the result
Repeated curl requests show x-cache Hit from cloudfront with the expected Cache-Control, direct S3 requests return 403, client-side routes return HTML 200 and API 404s stay 404.
The mistake it prevents
Mapping 403 and 404 to /index.html with custom error responses, which turns API errors and missing assets into HTML 200 responses.
Done well means: the origin is reachable only through CloudFront, the cache key contains
only what changes the response, immutable assets are cached for a year while HTML
revalidates quickly, deploys never need a full /* invalidation to look correct, and
the hit ratio is measured rather than assumed.
Before changing anything
- Read
AGENTS.mdand the IaC for the distribution; note how the site is built and deployed (which files are content-hashed, which command uploads them). - Inspect the live distribution (needs network and credentials):
aws cloudfront get-distribution-config --id E123EXAMPLE > dist.json
jq '.DistributionConfig | {aliases:.Aliases.Items, origins:[.Origins.Items[]|{id:.Id,domain:.DomainName,oac:.OriginAccessControlId}], default:.DefaultCacheBehavior|{cache:.CachePolicyId,originReq:.OriginRequestPolicyId,fns:.FunctionAssociations,viewer:.ViewerProtocolPolicy}, behaviors:[.CacheBehaviors.Items[]?|{path:.PathPattern,cache:.CachePolicyId}]}' dist.json
aws cloudfront list-cache-policies --type managed --query 'CachePolicyList.Items[].CachePolicy.[Id,CachePolicyConfig.Name]' --output table
Updating with the raw API requires the current ETag via --if-match; prefer IaC.
S3 origin with Origin Access Control
Keep the bucket private (Block Public Access on) and use OAC, not the legacy Origin Access Identity and not the S3 website endpoint.
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowCloudFrontOAC",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::acme-site-prod/*",
"Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::111111111111:distribution/E123EXAMPLE" } }
}]
}
- Without
s3:ListBucket, missing objects return 403, not 404. That is usually what you want (no key enumeration), but map 403 deliberately in error responses. - SSE-KMS buckets also need the key policy to allow
cloudfront.amazonaws.comtokms:Decryptwith the sameAWS:SourceArncondition. - In CDK,
origins.S3BucketOrigin.withOriginAccessControl(bucket)writes both.
Cache key and forwarding
Two separate policies per behavior:
- Cache policy: what goes into the cache key (headers, cookies, query strings) and TTLs.
- Origin request policy: what is forwarded to the origin without being part of the key.
| Behavior | Cache policy | Origin request policy |
|---|---|---|
Static assets from S3 (/assets/*) | Managed-CachingOptimized (658327ea-f89d-4fab-a63d-7e88639e58f6) | none, or Managed-CORS-S3Origin if CORS is used |
Dynamic API (/api/*) | Managed-CachingDisabled (4135ea2d-6df8-44a3-9df3-4b5a84be39ad) | Managed-AllViewerExceptHostHeader (b689b0a8-53d0-40ab-baf2-68738e2966ac) for API Gateway/Lambda URL origins |
| Cacheable API responses varying by a header or query param | Custom cache policy with only those keys | Forward what the origin needs |
Confirm managed IDs with the list-cache-policies command above before hardcoding them.
Every header, cookie or query string in the cache key multiplies cache entries and cuts
the hit ratio. Never put Authorization, all cookies or User-Agent into the key of a
cacheable behavior. Forwarding the Host header to API Gateway or a Lambda function URL
breaks TLS/routing; that is why AllViewerExceptHostHeader exists.
Cache-Control strategy
CloudFront honours origin Cache-Control within the cache policy’s min/max TTL.
| Content | Header at upload | Deploy action |
|---|---|---|
Content-hashed assets (app.3f9c1a.js) | public, max-age=31536000, immutable | Upload before HTML; never invalidate |
HTML, index.html, unhashed manifest files | public, max-age=0, s-maxage=60 (or no-cache) | Upload last; optionally invalidate /index.html |
| Downloadable files that change in place | Short max-age, or version the URL | Prefer new URLs over invalidation |
aws s3 sync dist/assets s3://acme-site-prod/assets \
--cache-control "public, max-age=31536000, immutable"
aws s3 sync dist s3://acme-site-prod --exclude "assets/*" \
--cache-control "public, max-age=0, s-maxage=60"
Deploying to production and deleting stale files (--delete) need the user’s
confirmation; old hashed assets should stay for a while so cached HTML still works.
Invalidations: the first 1,000 paths per month are free, then charged per path; a wildcard
counts as one path. A routine /* invalidation on every deploy hides a broken
Cache-Control strategy and briefly sends all traffic to the origin.
aws cloudfront create-invalidation --distribution-id E123EXAMPLE --paths "/index.html" "/"
Edge code: Functions vs Lambda@Edge
| Need | Use |
|---|---|
| URL rewrites, redirects, header manipulation, simple token or cookie checks, A/B bucketing | CloudFront Functions (viewer request/response, sub-millisecond, very cheap) |
| Network calls, request body access, origin-request/response events, heavier libraries | Lambda@Edge (Node.js/Python, deployed in us-east-1, versioned ARN) |
| Key-value lookups at the edge (redirect maps, feature flags) | CloudFront Functions with a KeyValueStore |
SPA and directory-index rewrite (viewer request, runtime cloudfront-js-2.0):
function handler(event) {
var request = event.request;
var uri = request.uri;
if (uri.startsWith('/api/')) return request;
if (uri.endsWith('/')) {
request.uri = uri + 'index.html';
} else if (!uri.split('/').pop().includes('.')) {
request.uri = '/index.html'; // SPA client-side route
}
return request;
}
Prefer this over custom error responses that map 403/404 to /index.html with 200: those
apply to every behavior, so API 404s turn into HTML 200s, and real missing assets are
masked.
Test before publishing:
aws cloudfront test-function --name spa-rewrite --if-match $ETAG \
--stage DEVELOPMENT --event-object fileb://test-event.json
aws cloudfront publish-function --name spa-rewrite --if-match $ETAG
Publishing pushes to the LIVE stage for every associated distribution; confirm first.
Security and TLS
ViewerProtocolPolicy: redirect-to-https, minimum protocolTLSv1.2_2021.- ACM certificates for CloudFront must be in us-east-1, whatever the origin region.
- Add a response headers policy for HSTS,
X-Content-Type-Options,Referrer-Policy,X-Frame-Optionsor CSP rather than setting them in edge code. - Private content: signed URLs or signed cookies with a trusted key group (public key in CloudFront, private key in Secrets Manager). Never the root account key pair method.
- Associate AWS WAF for rate limiting and managed rule groups on public apps.
- Custom origins (ALB): restrict the ALB security group to the CloudFront managed prefix list and require a secret custom header, or use VPC origins for private ALBs.
Gotchas
- Changes take a few minutes to deploy to all edge locations; wait for
Deployedstatus (aws cloudfront wait distribution-deployed --id E123EXAMPLE) before testing. DefaultRootObjectonly applies to/, not subdirectories; use the rewrite function.- Behaviors match in order of precedence;
/*style defaults catch everything not matched earlier. Put specific patterns first. - Query strings are not forwarded or cached by default with
CachingOptimized; an app relying on?v=2cache busting will get the old file. - Lambda@Edge replicas take time to delete after disassociation; deleting the function fails until they are gone.
- Origin response timeout defaults to 30 seconds; long API calls need a higher value or an async design.
Verification
curl -sI https://www.acme.com/assets/app.3f9c1a.js | grep -iE 'x-cache|age|cache-control'
curl -sI https://www.acme.com/assets/app.3f9c1a.js | grep -i x-cache # second call: Hit from cloudfront
curl -sI https://acme-site-prod.s3.eu-west-1.amazonaws.com/index.html # direct S3: 403
curl -sI https://www.acme.com/some/client/route # 200, HTML
Check the cache hit ratio in the CloudFront console reports or the CacheHitRate metric
(requires additional metrics enabled) over a day of traffic; static-heavy sites should be
well above 90%.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "AWS CloudFront"
short_description: "High hit ratios without stale deploys"
default_prompt: "$aws-cloudfront Serve our React build from a private S3 bucket through CloudFront with a custom domain" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.