Deploying a cloudfront distribution

Written by

·

·

Serving files directly from S3 works, but it has two problems that surface quickly in production. The first is latency — an S3 bucket lives in one region, and a user in Singapore hitting a bucket in us-east-1 feels that distance on every request. The second is cost — S3 data transfer rates are higher than CloudFront’s, and every request hits the bucket directly with no caching in between.

CloudFront solves both. It is a content delivery network with over 750 edge locations across more than 440 cities worldwide. When a user requests a file, the request goes to the nearest edge location. If the file is cached there, it is served immediately without touching the origin at all. If not, CloudFront fetches it from the origin, caches it at the edge, and serves all subsequent requests from cache until the TTL expires. The origin — whether that is S3, an Application Load Balancer, or an EC2 instance — only gets hit for the first request per edge location.


How a Distribution Works

When you create a CloudFront distribution, you configure two main things: an origin and a cache behavior. The origin is where CloudFront fetches content when it is not cached. The cache behavior defines how CloudFront handles requests — which HTTP methods to allow, how long to cache responses, and whether to forward headers, cookies, or query strings to the origin.

Every distribution gets a domain name that looks like d1234abcd.cloudfront.net. That is what users hit until you configure a custom domain. Requests flow like this:

  • User requests a file from the CloudFront domain
  • The request routes to the nearest edge location
  • If the file is in the edge cache and the TTL has not expired — cache hit, served immediately
  • If not — CloudFront fetches from the origin, caches it, serves the response
  • All subsequent requests from any user at that edge location are served from cache

The default TTL is 24 hours if your origin does not send a Cache-Control header. If your origin does send one, CloudFront honors it. That distinction matters more than most people realize when it comes to deploying updates.


Creating a Distribution with an S3 Origin

Imagine you have just built a React single-page application. The production build produces a dist/ folder of static HTML, JavaScript, and CSS files. You have uploaded those to an S3 bucket and now want to serve them globally through CloudFront.

The first thing to decide is Origin Access Control. CloudFront supports a mechanism called OAC — Origin Access Control — that lets you keep the S3 bucket completely private while still allowing CloudFront to read from it. This is the current AWS recommendation and replaces the older Origin Access Identity approach. Without OAC, you have two bad options: make the bucket public, or try to restrict access in other ways that are harder to maintain.

Start by creating the OAC:

aws cloudfront create-origin-access-control \ --origin-access-control-config \ Name=my-app-oac,\ OriginAccessControlOriginType=s3,\ SigningBehavior=always,\ SigningProtocol=sigv4

Note the Id value in the response — you need it when creating the distribution. Now create the distribution, pointing it at the S3 bucket and attaching the OAC:

aws cloudfront create-distribution \ --distribution-config '{ "CallerReference": "my-app-dist-001", "Origins": { "Quantity": 1, "Items": [{ "Id": "my-app-s3-origin", "DomainName": "my-app-bucket.s3.us-east-1.amazonaws.com", "S3OriginConfig": { "OriginAccessIdentity": "" }, "OriginAccessControlId": "YOUR_OAC_ID" }] }, "DefaultCacheBehavior": { "TargetOriginId": "my-app-s3-origin", "ViewerProtocolPolicy": "redirect-to-https", "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", "AllowedMethods": { "Quantity": 2, "Items": ["GET", "HEAD"] } }, "DefaultRootObject": "index.html", "Enabled": true, "Comment": "My React App" }'

The CachePolicyId above is the ID for CloudFront’s managed CachingOptimized policy, which is the right default for static assets. ViewerProtocolPolicy set to redirect-to-https means HTTP requests are automatically redirected to HTTPS — users get a secure connection without any changes to your application code.

If you forget to set DefaultRootObject to index.html, visiting the root URL of your distribution returns a 403 error. CloudFront does not know which file to serve when someone navigates to https://d1234abcd.cloudfront.net/ without a path. This is one of the most common CloudFront gotchas and one of the easier ones to miss when creating the distribution.


Locking Down the S3 Bucket

Creating the distribution with OAC is only half the step. The S3 bucket policy still needs to explicitly allow CloudFront to read from it. Until you add this policy, every request from CloudFront to S3 returns a 403 and your distribution serves nothing.

aws s3api put-bucket-policy \ --bucket my-app-bucket \ --policy '{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::my-app-bucket/*", "Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/YOUR_DISTRIBUTION_ID" } } }] }'

The AWS:SourceArn condition is what makes OAC meaningfully more secure than a general bucket policy. It restricts read access not just to the CloudFront service but to your specific distribution. Without that condition, any CloudFront distribution in any AWS account could potentially read from your bucket by pointing at it as an origin. With it, only your distribution can.


Caching and TTL

By default, CloudFront caches objects for 24 hours. For a static React app where the HTML, JavaScript, and CSS files change with every deployment, 24 hours means users could be served a stale version of your application for up to a day after you release an update.

The right approach depends on the type of file. Your index.html — the entry point — should have a short or zero TTL because it is the file that references your other assets. Your JavaScript and CSS bundles should have a very long TTL — a year is common — because they are immutable. You change the filename (using a content hash like app.a1b2c3d4.js) rather than the content, so an old URL will never receive new content.

Set the cache behavior on the origin by adding Cache-Control headers to your S3 objects when you upload them:

# Short TTL for the entry point aws s3 cp dist/index.html s3://my-app-bucket/ \ --cache-control "no-cache, no-store, must-revalidate" # Long TTL for hashed static assets aws s3 cp dist/ s3://my-app-bucket/ \ --recursive \ --exclude "index.html" \ --cache-control "public, max-age=31536000, immutable"

CloudFront honors these headers. The edge caches index.html briefly and the hashed assets for up to a year. When you deploy a new version of the app, the new index.html references different hashed filenames, so users automatically get the new assets without any manual cache clearing.


Invalidating the Cache

Sometimes you need to force CloudFront to stop serving a cached file before its TTL expires — a critical bug fix, a wrong file uploaded, a configuration change that needs to take effect immediately. That is what invalidations are for.

aws cloudfront create-invalidation \ --distribution-id YOUR_DISTRIBUTION_ID \ --paths "/index.html"

For clearing everything at once, a wildcard path counts as a single invalidation path regardless of how many files it matches:

aws cloudfront create-invalidation \ --distribution-id YOUR_DISTRIBUTION_ID \ --paths "/*"

The first 1,000 invalidation paths per month are free. Beyond that, each path costs $0.005. A wildcard like /* counts as one path, so a single full-cache invalidation is always within the free tier if you are not running them constantly.

Where teams get into trouble is running full invalidations on every deployment as part of a CI/CD pipeline. If you are deploying ten times a day, that is 300 invalidations a month — still within the free tier. But it means every user gets a cache miss on their next request after each deploy, which increases latency and drives up origin traffic. The file versioning approach with hashed filenames is the right long-term pattern because you never need to invalidate assets that have not changed.


Custom Domains and HTTPS

The default CloudFront domain works, but in production you almost always want your own domain — app.yourdomain.com instead of d1234abcd.cloudfront.net. This requires two things: an ACM certificate for your domain, and a CNAME record pointing your domain at the CloudFront distribution.

The certificate must be requested in us-east-1, regardless of where your infrastructure lives. CloudFront is a global service and only reads ACM certificates from the North Virginia region. This catches people every time — if you request the certificate in eu-west-1 where the rest of your stack lives, the certificate simply does not appear as an option when configuring the distribution.

# Must be run against us-east-1 regardless of your stack region aws acm request-certificate \ --domain-name app.yourdomain.com \ --validation-method DNS \ --region us-east-1

After DNS validation completes, update your distribution with the custom domain and certificate, then add a CNAME record in your DNS provider pointing app.yourdomain.com at your CloudFront distribution domain name.


Price Classes

By default, CloudFront routes requests through all of its edge locations globally. That gives you the best possible latency for every user but also means you are paying for data transfer in every region — including South America and India, where rates are roughly double the US and Europe rate.

If your users are concentrated in North America and Europe, switching to Price Class 100 removes the more expensive regions from your distribution and reduces your data transfer bill with minimal latency impact for your actual user base:

Price Class Regions Included Use When
Price Class All All edge locations globally Global user base or latency is the primary concern
Price Class 200 All regions except South America and Oceania Broad global reach with moderate cost reduction
Price Class 100 US, Canada, Mexico, Europe, Israel only User base is primarily North America and Europe

The decision is straightforward: look at where your users actually are before committing to Price Class All. Paying for edge locations in regions that generate no traffic is one of the more common CloudFront cost problems and one of the easier ones to avoid.


Final Thoughts

A CloudFront distribution in front of S3 is not much more complicated than an S3 bucket alone, but the details that matter most are easy to overlook the first time: setting the default root object, updating the bucket policy after enabling OAC, requesting the ACM certificate in us-east-1, and thinking through TTL before you deploy rather than after users start seeing stale content.

  • OAC keeps the S3 bucket private — the bucket policy must be updated after creating the distribution
  • Missing DefaultRootObject causes a 403 on the root URL
  • ACM certificates for CloudFront must be in us-east-1
  • File versioning with hashed filenames is better than relying on invalidations
  • Price Class 100 reduces cost significantly if your users are in North America and Europe

The biggest CloudFront mistakes are not technical — they are operational. Long TTLs on files that change, invalidations on every deploy, and Price Class All on a product with a regional user base. Getting the caching strategy right before launch is significantly less painful than fixing it after users start complaining about stale content.

Contact Me

Building AI systems, AWS architectures, and cloud-native applications.
Open to collaboration, consulting, and conversation.

© Shivansh Jain