DEV Community

Cover image for ->> Day-14 Host A Static Website in AWS S3 And CloudFront (using terraform)
Amit Kushwaha
Amit Kushwaha

Posted on

->> Day-14 Host A Static Website in AWS S3 And CloudFront (using terraform)

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

Common mistake: forgetting /* at the end of the bucket ARN.

>>Deployment

Run:

terraform init
terraform plan
terraform apply
Enter fullscreen mode Exit fullscreen mode

Type yes when prompted to confirm deployment.

Then:

  1. Copy the CloudFront domain_name
  2. Open it in your browser
  3. Your site loads securely over HTTPS 🎉

Access Your Website
After deployment completes, Terraform will output the CloudFront URL:

website url = https://d2nrhnei8a1w1m.cloudfront.net
Enter fullscreen mode Exit fullscreen mode

Cleanup
To destroy all resources and avoid charges:

terraform destroy
Enter fullscreen mode Exit fullscreen mode

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)