DEV Community

Cover image for πŸš€ Terraform Day 14: Hosting a Secure Static Website with S3 & CloudFront
Jeeva
Jeeva

Posted on

πŸš€ Terraform Day 14: Hosting a Secure Static Website with S3 & CloudFront

🧱 Architecture Overview

High-level design:
Users β†’ CloudFront (HTTPS + Edge Cache)
↓
Private S3 Bucket

Key principles:
S3 bucket is NOT public
Only CloudFront can read objects
CloudFront caches content globally
Terraform manages everything

1️⃣ Why Not Use S3 Public Website Hosting Directly?
Direct S3 hosting problems:
Bucket must be public
No edge caching
Higher latency
Security risks

CloudFront solves this:
βœ… Global edge locations
βœ… HTTPS by default
βœ… Lower latency
βœ… Reduced S3 costs
βœ… Private S3 bucket

2️⃣ Creating a Private S3 Bucket with Terraform
Terraform creates the bucket and blocks all public access.

resource "aws_s3_bucket" "site" {
bucket = var.bucket_name
}

resource "aws_s3_bucket_public_access_block" "site" {
bucket = aws_s3_bucket.site.id

block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
πŸ“Œ This ensures no direct public access to S3.

3️⃣ Origin Access Control (OAC) β€” Secure CloudFront Access
AWS now recommends Origin Access Control (OAC) instead of the deprecated OAI.
resource "aws_cloudfront_origin_access_control" "oac" {
name = "s3-oac"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
_
βœ” Only CloudFront can access S3
βœ” Modern & secure approach_

4️⃣ S3 Bucket Policy for CloudFront Access
The bucket policy allows only CloudFront to read objects.
resource "aws_s3_bucket_policy" "site_policy" {
bucket = aws_s3_bucket.site.id

policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "cloudfront.amazonaws.com"
}
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.site.arn}/*"
Condition = {
StringEquals = {
"AWS:SourceArn" = aws_cloudfront_distribution.site.arn
}
}
}]
})
}

_πŸ” This prevents:
Direct S3 access
Unauthorized reads
_

5️⃣ Uploading Static Files Using Terraform (for_each + fileset)
Terraform uploads HTML, CSS, JS files dynamically.

resource "aws_s3_object" "files" {
for_each = fileset("${path.module}/site", "*/")

bucket = aws_s3_bucket.site.id
key = each.value
source = "${path.module}/site/${each.value}"
etag = filemd5("${path.module}/site/${each.value}")
}

Why this matters:
No manual uploads
Files tracked in Terraform
MD5 ensures cache correctness

6️⃣ Creating the CloudFront Distribution
CloudFront configuration includes:
Private S3 origin
HTTPS redirection
Caching behavior
Default root object

resource "aws_cloudfront_distribution" "site" {
enabled = true
default_root_object = "index.html"

origin {
domain_name = aws_s3_bucket.site.bucket_regional_domain_name
origin_id = "s3-origin"
origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
}

default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
viewer_protocol_policy = "redirect-to-https"
target_origin_id = "s3-origin"
}

viewer_certificate {
cloudfront_default_certificate = true
}
}

βœ” HTTPS enabled
βœ” Fast global delivery

7️⃣ Troubleshooting Terraform Errors (Important Skill)
The video demonstrates real debugging, including:
Incorrect IAM principals
Wrong resource ARNs
Policy syntax issues

Key lesson:
Terraform errors are part of learning β€” reading them carefully is essential.

8️⃣ Accessing the Live Website
After terraform apply, Terraform outputs:
CloudFront distribution domain name
Example:
https://d123abcd.cloudfront.net

βœ” Website served securely over HTTPS
βœ” Cached globally

🏁 Conclusion
Day 14 is a major real-world milestone in Terraform learning.
You’ve now built:
A secure static website
A globally cached architecture
A fully automated Terraform project

This project mirrors production-grade AWS setups and prepares you for:
Portfolio projects
Interviews
Real DevOps work

Top comments (0)