Hosting a static website is simple in theory, but hosting it in a way that is fast, secure, and globally accessible requires the right architecture. AWS provides two services that work extremely well together for this purpose: Amazon S3 for storage and Amazon CloudFront for global delivery.
In this blog, I want to walk you through why S3 + CloudFront is such a powerful combination and how I implemented the entire solution in Terraform.
Why Use S3 and CloudFront Together?
1. S3 as the Website Storage
Amazon S3 is a great place to store static content like HTML, CSS, JavaScript, and images. It's reliable, scalable, and inexpensive. However, S3 alone is not optimized for global content delivery. If your bucket is in Mumbai and a user in Europe tries to access your website, the latency will be noticeable.
2. CloudFront for Global Performance
CloudFront solves this problem by delivering your content through a worldwide network of edge locations. When a user accesses your site, CloudFront serves the content from the nearest location. This reduces delays, speeds up page load time, and improves the overall user experience, no matter where the user is located.
3. Security Through OAC
Instead of making your S3 bucket public, CloudFront can access the bucket on behalf of the users through an Origin Access Control (OAC). This ensures that your S3 bucket remains private while still allowing your website to be accessible globally. It’s a secure and modern approach.
4. HTTPS Support
CloudFront also enables HTTPS by default using an AWS-managed certificate. Even if you don’t have a custom domain, CloudFront provides a secure .cloudfront.net URL for your website.
Terraform Implementation
Below is the exact Terraform code I used to deploy the entire setup. It covers S3 bucket creation, file upload, OAC configuration, bucket policies, and CloudFront distribution.
// Create an S3 bucket with a dynamic name using prefix and name variables
resource "aws_s3_bucket" "website" {
bucket = "${var.bucket_prefix}${var.bucket_name}"
}
// Block public access to the S3 bucket
resource "aws_s3_bucket_public_access_block" "website_public_access_block" {
bucket = aws_s3_bucket.website.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
// Create a CloudFront Origin Access Control for the S3 bucket
resource "aws_cloudfront_origin_access_control" "oac" {
name = "${var.bucket_prefix}-oac"
description = "OAC for ${var.bucket_name}"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
// S3 Bucket Policy to allow access from cloudfront origin access control
resource "aws_s3_bucket_policy" "website_bucket_policy" {
bucket = aws_s3_bucket.website.id
depends_on = [aws_s3_bucket_public_access_block.website_public_access_block]
policy = jsonencode({
"Version" : "2012-10-17",
"Statement" : [
{
"Sid" : "AllowCloudFrontAccess",
"Effect" : "Allow",
"Principal" : {
"Service" : "cloudfront.amazonaws.com"
},
"Action" : [
"s3:GetObject"
],
"Resource" : "${aws_s3_bucket.website.arn}/*"
Condition = {
StringEquals = {
"AWS:SourceArn" = aws_cloudfront_distribution.s3_distribution.arn
}
}
}
]
})
}
// upload files to the S3 bucket
resource "aws_s3_object" "website_files" {
for_each = fileset("${path.module}/www", "**/*")
bucket = aws_s3_bucket.website.id
key = each.value
source = "${path.module}/www/${each.value}"
etag = filemd5("${path.module}/www/${each.value}")
content_type = lookup({
"html" = "text/html",
"css" = "text/css",
"js" = "application/javascript",
"json" = "application/json",
"png" = "image/png",
"jpg" = "image/jpeg",
"jpeg" = "image/jpeg",
"gif" = "image/gif",
"svg" = "image/svg+xml",
"ico" = "image/x-icon",
"txt" = "text/plain"
}, split(".", each.value)[length(split(".", each.value)) - 1], "application/octet-stream")
}
// Create a CloudFront Distribution to serve content from the S3 bucket
resource "aws_cloudfront_distribution" "s3_distribution" {
origin {
domain_name = aws_s3_bucket.website.bucket_regional_domain_name
origin_id = local.origin_id
origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
}
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = local.origin_id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
price_class = "PriceClass_100"
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}
Final Output
After running the usual Terraform commands—init, validate, plan, and apply—you get a CloudFront URL that securely serves your static website. The S3 bucket stays completely private while the content is delivered quickly and securely across the world.
This setup gives you a production-ready hosting environment that is fast, secure, and automated.
Top comments (0)