Introduction:
This mini project demonstrates how to deploy a static website on AWS using Terraform. We'll create a complete static website hosting solution using S3 for storage and CloudFront for global content delivery.
The Problem with Public S3 Websites?
Hosting a static website directly from S3 by enabling public access has multiple issues:
- Security risk: Anyone can access your bucket if misconfigured
- Higher latency: Users far from the bucket region experience slower loads
- No fine-grained access control
- No proper HTTPS management
Production systems require private storage and controlled access, which is where CloudFront comes in.
Architecture
Instead of users accessing S3 directly, we introduce CloudFront as a secure middle layer.
Architecture Flow:
User → CloudFront (HTTPS) → Private S3 Bucket
What’s happening here?
- Users only talk to CloudFront
- CloudFront fetches content from S3 securely
- S3 remains completely private
- Content is cached globally at edge locations
Why Use CloudFront with S3?
Performance
CloudFront caches your static files at edge locations worldwide.
A user in India hits a nearby edge location instead of an S3 bucket in us-east-1.
Security
The S3 bucket is not public.
Only CloudFront can access it using Origin Access Control (OAC).
Cost Efficiency
Data transfer from S3 → CloudFront is cheaper than S3 → Internet.
Prerequisites
Before starting, make sure you have:
- An AWS account
- Terraform installed
- AWS CLI configured
- A basic static website (HTML/CSS/JS)
Step-by-Step Terraform Implementation
Step 1: Create a Private S3 Bucket
We start by creating an S3 bucket and explicitly blocking all public access.
# S3 bucket for static website hosting
resource "aws_s3_bucket" "website" {
bucket_prefix = var.bucket_prefix
}
# Make S3 bucket private
resource "aws_s3_bucket_public_access_block" "website" {
bucket = aws_s3_bucket.website.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
Step 2: Upload Static Files
Uploading files via Terraform requires care. Browsers rely on Content-Type headers to render files properly.
# Upload website files to S3
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")
}
Using
filemd5()ensures Terraform updates only changed files.
Step 3: Create Origin Access Control (OAC)
# Origin Access Control for CloudFront (Recommended over OAI)
resource "aws_cloudfront_origin_access_control" "oac" {
name = "oac-${var.bucket_prefix}"
description = "OAC for static website"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
Step 4: Configure the CloudFront Distribution
Now we configure CloudFront to:
- Use the private S3 bucket as origin
- Serve index.html as the default page
# CloudFront Distribution
resource "aws_cloudfront_distribution" "s3_distribution" {
origin {
domain_name = aws_s3_bucket.website.bucket_regional_domain_name
origin_id = "S3-${aws_s3_bucket.website.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 = "S3-${aws_s3_bucket.website.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
}
}
Common mistake: forgetting
/*at the end of the bucket ARN.
>>Deployment
Run:
terraform init
terraform plan
terraform apply
Type yes when prompted to confirm deployment.
Then:
- Copy the CloudFront
domain_name - Open it in your browser
- Your site loads securely over HTTPS 🎉
Access Your Website
After deployment completes, Terraform will output the CloudFront URL:
website url = https://d2nrhnei8a1w1m.cloudfront.net
Cleanup
To destroy all resources and avoid charges:
terraform destroy
Type yes when prompted to confirm destruction.
Conclusion
Serving static websites directly from public S3 buckets might work for demos, but production systems demand security, performance, and control.
Using CloudFront with a private S3 bucket - provisioned via Terraform - gives you a scalable, secure, and professional setup that mirrors real-world architectures.
Happy building ☁️🚀
Reference
>> Connect With Me
If you enjoyed this post or want to follow my #30DaysOfAWSTerraformChallenge journey, feel free to connect with me here:
💼 LinkedIn: Amit Kushwaha
🐙 GitHub: Amit Kushwaha
📝 Hashnode / Amit Kushwaha
🐦 Twitter/X: Amit Kushwaha







Top comments (0)