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?
Think of S3 as a locked warehouse that holds the website files. CloudFront is the public storefront close to customers. OAC is the delivery badge that lets the storefront collect files from the warehouse. Route 53 is the address book, and the AWS Certificate Manager (ACM) certificate proves that the storefront really owns the website name.
2. Naive Design
Naive option 1: run a web server
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
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 is the public web-hosting interface of a bucket. The endpoint itself supports only HTTP, and CloudFront treats it as a custom origin. Most importantly for this scenario, OAC cannot protect a website endpoint, so it cannot provide the private CloudFront-to-S3 relationship we need.
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
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
An SPA usually ships one HTML entry point, such as index.html, plus its JavaScript files. After that JavaScript loads, a client-side router can understand a URL such as /orders/42 even though the S3 bucket contains no object named orders/42. The route exists inside the SPA, but it does not exist as an S3 object.
Normal client-side navigation works because the browser has already loaded index.html and started the JavaScript application. When the user navigates to /orders/42, the client-side router renders that view inside the browser. S3 is not asked for an /orders/42 object.
A refresh or direct request is different. The browser starts by sending GET /orders/42 to CloudFront. CloudFront asks the S3 origin for that path, but no matching object exists, so S3 returns a 403 or 404. The JavaScript router never gets a chance to handle the route because the application has not loaded yet.
The distribution therefore needs deliberate SPA fallback behavior. A common approach is a CloudFront Custom Error Response that maps the relevant S3 403 or 404 to /index.html and returns HTTP 200. Once index.html loads, the client-side router reads /orders/42 from the browser URL and renders the correct view. A CloudFront Function that rewrites application routes to /index.html is another approach.
Exam trap: setting CloudFront's Default Root Object to index.html handles a request for /. It does not automatically rewrite /orders/42 or other nested SPA routes.
4. AWS Architecture
Build the solution one responsibility at a time.
Step 1: store the build output and choose how CloudFront reaches S3
S3 is object storage. Each generated site file becomes an object in a bucket.
One S3 bucket can be reached through two different endpoints. The regular S3 bucket endpoint is the normal object-storage interface. If you enable Static Website Hosting, S3 also provides an S3 website endpoint. When you configure the CloudFront origin—the place CloudFront fetches files from—you are choosing which of these two S3 interfaces CloudFront will contact. The files remain objects in the same kind of bucket either way.
With the regular S3 bucket endpoint, CloudFront requests objects such as index.html directly from S3. CloudFront treats this as a native S3 bucket origin, can connect over HTTPS, and can use Origin Access Control (OAC). OAC allows the intended CloudFront distribution to read the objects while the bucket remains private and direct user access stays blocked.
With the S3 website endpoint, S3 behaves more like a basic web server and provides website-specific features such as index documents, error documents, and redirects. CloudFront treats this endpoint as a custom origin, not as a private S3 origin. The website endpoint is HTTP-only and does not support OAC, so it cannot provide the private-origin relationship required here. Compare the two choices directly:
| Question | Regular S3 bucket origin | S3 website endpoint |
|---|---|---|
| Which S3 interface CloudFront calls | S3 REST API endpoint; treated as a native S3 origin | S3 static website endpoint; treated as a custom HTTP origin |
| Can the bucket stay private with OAC? | Yes | No; OAC does not support website endpoints. |
| Can CloudFront use HTTPS to the S3 endpoint? | Yes; CloudFront uses the S3 REST endpoint. | No; the website endpoint is HTTP-only. |
| Best fit for this scenario | Yes | No |
For this scenario, choose the regular S3 bucket origin and leave S3 Static Website Hosting disabled. The intended path is User → CloudFront → OAC-authorized request → private S3 bucket. The phrase “host a static site on S3” is not enough to choose an origin—the deciding requirement is that CloudFront must be the only public entrance while the S3 bucket remains private. Takeaway: private S3 behind CloudFront means a regular S3 bucket origin plus OAC; an S3 website endpoint provides website-hosting behavior but cannot use OAC.
Step 2: put CloudFront in front of S3
S3 stores the website files. CloudFront sits in front of S3 and delivers those files to users. The CloudFront setup for this website is called a distribution. In that distribution, configure the private S3 bucket as the origin—the place CloudFront goes to get a file when it does not already have a cached copy.
For each request, CloudFront checks whether it already has a fresh copy of the file. A cache hit means the file is available in CloudFront's cache, so CloudFront returns it directly and does not contact S3. A cache miss means the file is not cached, so CloudFront fetches it from the S3 origin and can save a copy for future requests. On that origin request, Origin Access Control (OAC) gives CloudFront permission to read the private bucket while direct user access to S3 remains blocked.
Set the distribution's Default Root Object to index.html. This tells CloudFront to return index.html when a user requests /. It does not automatically rewrite a nested SPA route such as /orders/42; that still needs the separate fallback behavior explained earlier.
Need a deeper refresher on distributions, origins, and cache behaviors? Review Amazon CloudFront.
Step 3: keep S3 private with OAC
Now enforce the relationship introduced above: users may reach the website through CloudFront, but they may not read the S3 bucket directly.
OAC makes CloudFront sign its requests to S3. The bucket policy allows the intended CloudFront distribution to read the required website objects, while S3 Block Public Access keeps anonymous public access disabled. A user who tries to bypass CloudFront and request the bucket directly is denied.
OAC is a permission mechanism on the CloudFront-to-S3 request. It is not another server or network hop.
Use a regular S3 bucket origin with OAC when the bucket must remain private. An S3 website endpoint is configured as a custom origin and cannot use OAC.
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.
Step 5: plan deployments around the cache
Use content-hashed or otherwise versioned names for assets that can be cached for a long time:
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. A CloudFront invalidation tells edge caches to discard their saved response for a path. The next request for that path returns to the origin and retrieves the current file. There is usually no reason to invalidate every versioned asset, because a changed asset already has a new name and therefore a new cache entry.
The completed architecture is:
5. Request Or Data Flow
Learn three short flows: a cache hit, a cache miss, and a deployment.
A cache key is the lookup identity CloudFront uses for a cached response. For this static site, the object path is the most important part: /logo.webp and /app.42bd.js identify different cached objects.
Flow 1: cache hit
- 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
- 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 | 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 |
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: