Core lesson
Static Site With CloudFront And S3
A globally fast HTTPS static website with CloudFront while S3 stores the files privately behind origin access control.
After this, you will understand
Learn why a public website does not require a public bucket, and how caching changes both request flow and deployment strategy.
Article guideprerequisites, mental models, and concepts
Article overview
Three useful mental models
Keep the website files in a private S3 bucket and make CloudFront the only public entrance.
Learners confuse an S3 website endpoint with a private S3 origin, leave a direct path around CloudFront, or overwrite cached files without planning how browsers receive the new version.
Use an S3 bucket origin with OAC, deliver it through CloudFront, map the domain with Route 53, attach an ACM certificate, and deploy cache-safe file versions.
Think before reading
If everyone can view the website, why should the S3 bucket remain private?
The content is public through CloudFront, but a private bucket removes the direct S3 side door and makes CloudFront the controlled delivery layer.
Connected learning
These lessons add useful context to the current core lesson.Concepts Covered
- What makes a website static
- S3 as private origin storage
- CloudFront as the public delivery layer
- S3 bucket origins versus S3 website endpoints
- Origin access control (OAC)
- CloudFront default root object
- Cache hits and cache misses
- Route 53 alias records and ACM certificates
- Cache-safe deployments with versioned assets
- AWS Solutions Architect Associate exam (SAA-C03) recognition patterns and traps
1. Situation
Your team has built a product website from HTML, CSS, JavaScript, images, and fonts. A build process creates files such as these:
index.html
styles.18c4.css
app.a91f.js
logo.webp
The browser downloads those files and runs the JavaScript. AWS does not need to execute server-side code for each page request. That is what static site means here.
Static does not mean the site never changes. It means each request returns an already-created file rather than asking an application server to generate the page.
The website needs to:
- load quickly for users around the world;
- use
https://example.com; - run without web servers to patch or scale;
- prevent users from bypassing the public entry point and reading S3 directly; and
- show new releases without mixing old and new files.
The central problem is therefore not merely, "Where do we put the files?"
It is:
How do we store static files cheaply, deliver them globally, and keep the storage layer private?
2. Naive Design
Naive option 1: run a web server
flowchart LR User["Global users"] --> EC2["EC2 web server<br/>static files"]
This can work, but the server adds operating system patching, capacity planning, scaling, and instance recovery. None of those tasks make an HTML file more static.
Use a server when the request needs server-side computation. Do not introduce one merely to return files that already exist.
Naive option 2: make an S3 website endpoint public
flowchart LR User["Users"] --> Website["Public S3<br/>website endpoint"]
S3 website hosting can serve a simple site, but it creates a different architecture from the private-origin design in this lesson.
An S3 website endpoint must be reachable as a website origin. It does not support HTTPS itself, and CloudFront treats it as a custom origin. You cannot use OAC to protect that website endpoint.
That makes this the wrong starting point when the requirement says:
- keep the bucket private;
- allow access only through CloudFront; or
- prevent direct S3 access.
Naive option 3: add CloudFront but leave S3 public
flowchart LR User["Users"] --> CF["CloudFront"] --> S3["Public S3 bucket"] User -.->|"direct side door"| S3
CloudFront now caches the files, but users can still bypass it if they know the S3 URL. The delivery layer is optional rather than enforced.
3. What Breaks
Follow four concrete failure stories.
Failure 1: distant users repeatedly cross the world for the same file
An S3 bucket lives in one AWS Region. Without an edge cache, a user far from that Region repeatedly fetches common files over a long network path.
The architecture needs a global cache close to users.
Failure 2: the public origin becomes a second entrance
The team adds CloudFront but leaves S3 public. Security rules, HTTPS redirects, logging choices, or future WAF rules attached to CloudFront do not apply when a user accesses S3 directly.
The architecture needs one intended public entrance.
Failure 3: a deployment mixes file generations
The team overwrites app.js, but an edge cache or browser still holds the old copy. Meanwhile, index.html expects the new JavaScript. Some users receive a page assembled from two releases.
The architecture needs a cache-aware deployment strategy, not just an upload command.
Failure 4: a single-page application returns an error on refresh
The app handles /orders/42 inside the browser. When a user refreshes that URL, CloudFront asks S3 for an object at that path. If no such object exists, S3 returns an error instead of index.html.
This is a routing requirement, not an S3 availability problem. The team must deliberately configure suitable error or rewrite behavior if it is building a client-side single-page application.
4. AWS Architecture
Build the solution one responsibility at a time.
Step 1: store the build output in S3
S3 is object storage. Each generated site file becomes an object in a bucket.
flowchart LR Build["Site build output"] --> S3["S3 bucket<br/>HTML, CSS, JS, images"]
For this design, choose the regular S3 bucket origin in CloudFront. Do not enable S3 static website hosting. The bucket stores files; it is not the public website endpoint.
That distinction unlocks private origin access later.
Step 2: make CloudFront the public delivery layer
Create a CloudFront distribution and configure the S3 bucket as its origin. An origin is simply the place CloudFront goes when it does not already have the requested file cached.
flowchart LR User["User"] -->|"HTTPS request"| CF["CloudFront distribution"] CF -->|"cache miss"| S3["S3 bucket origin"]
CloudFront can return cached files from edge locations closer to users. S3 remains the durable source of the files, while CloudFront becomes the public front door.
Set the distribution's default root object to index.html. Then a request for / retrieves the home page instead of asking S3 for an object with an empty name. This setting does not automatically rewrite every nested client-side route, which is why single-page application routing still needs separate attention.
Need a deeper refresher on distributions, origins, and cache behaviors? Review Amazon CloudFront.
Step 3: lock the origin with OAC
Now enable Origin Access Control (OAC) for the S3 origin.
OAC lets CloudFront sign its requests to S3. The S3 bucket policy grants the intended CloudFront distribution permission to read the site objects. S3 Block Public Access remains enabled.
flowchart LR User["User"] --> CF["Public CloudFront distribution"] CF -->|"signed origin request with OAC"| S3["Private S3 bucket<br/>Block Public Access on"] User -.->|"direct request denied"| S3
OAC is not another network hop. It is the trust mechanism on the CloudFront-to-S3 request.
Step 4: add the name and HTTPS identity
CloudFront provides a domain name, but users expect a name such as example.com.
Request or import an ACM certificate that covers the custom domain and attach it to the CloudFront distribution. For CloudFront, the ACM certificate must be in us-east-1.
Create a Route 53 alias record that points the domain to the distribution. Route 53 answers the DNS question, "Where should example.com connect?" It does not carry every HTTP request through itself.
flowchart LR DNS["Route 53 alias<br/>example.com"] -.->|"DNS answer"| User["User"] User -->|"HTTPS"| CF["CloudFront distribution<br/>ACM certificate"] CF -->|"signed request with OAC"| S3["Private S3 bucket"]
Step 5: plan deployments around the cache
Use content-hashed or otherwise versioned names for assets that can be cached for a long time:
app.a91f.js -> old release
app.42bd.js -> new release
The new index.html points to the new asset name. CloudFront sees a new path and fetches the new object instead of confusing it with the old cached file.
Keep index.html on a shorter cache lifetime, or invalidate that small entry file when a release must appear immediately. Avoid invalidating every asset on every deployment when unique asset names already solve the problem.
The completed architecture is:
flowchart LR Pipeline["Build and deployment pipeline"] -->|"uploads versioned files"| S3["Private S3 bucket<br/>origin storage"] DNS["Route 53 alias"] -.->|"resolves domain"| User["Global user"] User -->|"HTTPS"| CF["CloudFront<br/>public delivery and cache"] CF -->|"OAC-signed cache miss"| S3 ACM["ACM certificate<br/>us-east-1"] -.->|"TLS identity"| CF
5. Request Or Data Flow
Learn three short flows: a cache hit, a cache miss, and a deployment.
Flow 1: cache hit
flowchart LR User["User requests<br/>/logo.webp"] --> CF["CloudFront edge cache"] CF -->|"fresh object found"| User
- Route 53 has already resolved the domain to CloudFront.
- The user sends an HTTPS request to CloudFront.
- CloudFront finds a fresh copy under the request's cache key.
- CloudFront returns it without contacting S3.
This is the fast path.
Flow 2: cache miss
flowchart LR
User["User"] --> CF["CloudFront"]
CF --> Decision{"Fresh cached copy?"}
Decision -->|"yes"| Response["Return cached object"]
Decision -->|"no"| S3["Private S3 origin"]
S3 -->|"object"| Store["Cache and return"]
Store --> User
- CloudFront cannot use a cached copy because the object is absent or expired.
- CloudFront signs the origin request through OAC.
- The bucket policy permits the configured distribution to read the object.
- S3 returns the file.
- CloudFront caches it according to the cache policy and response headers, then returns it to the user.
Flow 3: deployment
- The build creates new asset names such as
app.42bd.js. - The deployment role uploads the new files to S3.
- The new
index.htmlreferences those new names. - The pipeline invalidates
index.htmlif the release cannot wait for its cache lifetime. - Users receive one coherent release while old versioned assets remain available during the transition.
6. Security Controls
Keep the bucket private
Enable S3 Block Public Access and use a bucket policy that allows the intended CloudFront distribution to read the required objects. Do not grant anonymous public reads merely because the website itself is public.
The public/private boundary becomes:
public: user -> CloudFront
private: CloudFront -> S3 through OAC
denied: user -> S3 directly
Require HTTPS
Attach a certificate that covers the custom domain and configure CloudFront to redirect HTTP viewers to HTTPS or require HTTPS, depending on the requirement.
Limit deployment permissions
Give the deployment role only the actions and resources it needs, such as writing to the site prefix and creating an invalidation when the workflow requires one. It should not be an account administrator.
Never place secrets in frontend files
Anything downloaded by a browser can be inspected by a user. Do not embed database passwords, private API keys, or long-lived AWS credentials in JavaScript bundles or configuration files.
Add WAF only when the threat requirement calls for it
AWS WAF can attach to CloudFront when the scenario needs request filtering, rate-based rules, or managed web protections. It is not required merely to store and deliver a static file. Review CloudFront WAF Protected Web Edge for that decision.
7. Resilience Controls
S3 provides durable regional object storage, and CloudFront distributes cached copies across its edge network. That removes the need to maintain a fleet of web servers for the static content.
Caching can reduce dependence on the origin for objects that are already fresh at an edge. Do not treat the cache as a backup, however: whether CloudFront can serve an object during an origin problem depends on cache state, expiration, and error-handling configuration.
Versioned asset names make releases easier to roll forward and back. A rollback can point index.html back to the previous asset names while those objects still exist.
Consider S3 Versioning when accidental overwrite or deletion recovery matters. Use lifecycle rules to expire old releases only after the rollback window has passed.
For a single-page application, test direct navigation to nested routes. Configure a deliberate rewrite or error-response strategy rather than discovering after launch that only the home page loads.
8. Performance Controls
Treat files according to how they change.
| File type | Useful caching approach | Why |
|---|---|---|
| Hashed JavaScript, CSS, images, and fonts | Long cache lifetime; often marked immutable | A content change creates a new filename |
index.html | Shorter lifetime or targeted invalidation | It tells browsers which versioned assets to load |
| Rare unversioned files | Deliberate lifetime plus targeted invalidation when needed | Reusing the same path can otherwise serve stale content |
Enable compression for compressible content such as HTML, CSS, and JavaScript. Optimize image size and format before delivery; a CDN moves an oversized image quickly, but the user still downloads an oversized image.
Keep the cache key small. Forward cookies, headers, or query strings only when they genuinely change the response. Unnecessary variation creates separate cache entries for the same file and lowers the cache-hit ratio.
9. Cost Controls
This architecture replaces always-running web servers with usage-based storage and delivery services. The important cost drivers include:
- S3 storage and requests;
- CloudFront requests and data transfer;
- Route 53 hosted-zone and DNS usage; and
- retained build versions and logs.
A higher cache-hit ratio means fewer origin requests. Long lifetimes work especially well for uniquely named assets because a new release creates new names rather than overwriting cached content.
Use lifecycle policies to remove old build artifacts after the rollback period. Scope logs and retention to the operational need.
Prefer versioned filenames for frequently updated assets. Invalidations are useful when an existing path must disappear or refresh before expiry, but broad, frequent invalidations add work and can add cost.
10. Exam Variants
| If the question says... | Think... | Why |
|---|---|---|
| "Static website," "global users," or "low latency" | CloudFront with an S3 origin | S3 stores the objects; CloudFront caches them near users |
| "Prevent direct access to the bucket" | Private S3 bucket origin + OAC + bucket policy | CloudFront becomes the enforced public entrance |
| "Custom domain over HTTPS" | CloudFront + ACM certificate in us-east-1 + Route 53 alias | The certificate proves the domain identity; DNS points the name to the distribution |
| "Users receive stale files after deployment" | Versioned filenames, correct cache headers, or targeted invalidation | Deployment must account for edge and browser caches |
| "Lowest operational overhead" | S3 and CloudFront, not an EC2 web-server fleet | Static files do not require request-time server compute |
| "OAC with an S3 website endpoint" | Change to a regular S3 bucket origin | Website endpoints are custom origins and do not support OAC |
| "Server-side code must run for each request" | Static hosting alone is insufficient | Add an application or API compute layer for the dynamic work |
The recognition pattern to remember is:
global static delivery -> CloudFront
durable object storage -> S3
no direct bucket access -> OAC + private bucket
friendly DNS name -> Route 53 alias
HTTPS for the custom domain -> ACM certificate for CloudFront
safe cache updates -> versioned assets + deliberate HTML caching
11. Common Traps
| Trap | Better reasoning |
|---|---|
| "The site is public, so the bucket must be public." | The content can be public through CloudFront while the origin stays private. |
| "S3 website hosting is required for every S3-backed site." | A private CloudFront design uses the regular S3 bucket origin, not the website endpoint. |
Drawing user -> Route 53 -> CloudFront as the HTTP flow | Route 53 answers DNS; the browser then connects to CloudFront. |
| Treating OAC as a proxy server | OAC signs and authorizes CloudFront's origin request; it is not a separate hop. |
| Putting the ACM certificate in the S3 bucket's Region | A certificate attached to CloudFront must be requested or imported in us-east-1. |
Overwriting app.js and assuming every cache updates immediately | Give changing assets new names or deliberately invalidate reused paths. |
| Forwarding every cookie, header, and query string | Forward only values that change the response so static files share cache entries. |
| Expecting a CDN to hide huge assets | Optimize the files as well as their delivery path. |
Final Mental Model: One-Minute Review
Final Architecture Map
The public website ends at CloudFront. The S3 bucket remains a private origin, while versioned assets allow long cache lifetimes without stale deployments.
| Term | Exact job | Remember it as |
|---|---|---|
| Route 53 alias | Answers DNS so the custom domain resolves to CloudFront | The address book |
| ACM certificate | Proves the site's identity and enables HTTPS at CloudFront | The storefront identity certificate |
| CloudFront | Publicly delivers and caches website files close to users | The global storefront |
| OAC | Signs CloudFront requests authorized by the private S3 bucket policy | The storefront's warehouse badge |
| Private S3 bucket origin | Durably stores the built HTML, CSS, JavaScript, images, and fonts | The locked warehouse |
| Versioned assets | Give changed files new names so caches can keep long lifetimes safely | New labels for each product version |
index.html cache strategy | Controls how quickly browsers discover the newest asset names | The storefront's current catalog |
flowchart LR DNS["Route 53 alias"] -.->|"Resolves the domain"| User["User"] User -->|"HTTPS"| CF["CloudFront<br/>public delivery and cache"] ACM["ACM certificate<br/>us-east-1"] -.->|"TLS identity"| CF CF -->|"OAC-signed cache miss"| S3["Private S3 bucket<br/>origin files"] Pipeline["Deployment pipeline"] -->|"Versioned assets + updated index.html"| S3
The website is public through CloudFront, not through S3. Route 53 supplies the name, ACM supplies the HTTPS identity, OAC protects the origin relationship, and versioned assets keep deployments compatible with caching.
12. Related Topics
Use Amazon CloudFront for deeper coverage of distributions, origins, behaviors, cache keys, and invalidations. Review Amazon S3 for object storage and bucket controls, Amazon Route 53 for DNS routing, and Identity Policies vs Resource Policies for the bucket-policy mental model.
Continue to Serverless API With Lambda And DynamoDB when the browser needs to call a dynamic backend rather than download only static files.
Official AWS references:
Finished reading?
Your reading history is saved in this browser so you can continue later.
Recommended Next
Serverless API With Lambda And DynamoDBAWS Architecture Scenarios16 min readThis applies the foundation mental models to a real architecture decision instead of a service inventory.
Optional exploration
These links add context, but they do not replace the recommended next lesson.
More Links
Additional references connected to this page.
Arcflow Plus is coming — review drills, research breakdowns, more AI. Get one email at launch.