DEV Community

Cover image for Host A Static Website in AWS S3 and CloudFront (Using Terraform)
Adarsh Gupta
Adarsh Gupta

Posted on

Host A Static Website in AWS S3 and CloudFront (Using Terraform)

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

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.

Reference Video


@piyushsachdeva

Top comments (0)