AWS June 20, 2026 ยท ๐Ÿ• 8 min read

Hosting a Static Website on S3 + CloudFront with Terraform

A complete walkthrough of setting up a production-ready static site using S3, CloudFront OAI, ACM, and Route 53 โ€“ all managed with Terraform.

Introduction

Hosting a static website on Amazon S3 fronted by CloudFront is one of the most cost-effective and scalable patterns in AWS. With S3's 99.999999999% (11 nines) durability and CloudFront's global edge network, you get enterprise-grade reliability for pennies.

In this post I'll walk you through the exact Terraform code I used to build this very portfolio website, including the newer Origin Access Control (OAC) โ€“ the recommended replacement for the legacy OAI pattern.

Architecture Overview

The architecture is simple but production-grade:

  • S3 Bucket โ€“ stores all static assets (HTML, CSS, JS, images). Public access is completely blocked.
  • CloudFront OAI โ€“ authorises CloudFront to read from the private S3 bucket via SigV4-signed requests.
  • CloudFront Distribution โ€“ serves files from edge locations, handles HTTPS, caching, and custom error pages.
  • ACM Certificate โ€“ free TLS certificate provisioned in us-east-1 (required by CloudFront).
  • Route 53 โ€“ DNS alias record pointing to the CloudFront distribution.

Why I Used OAI?

Origin Access Identity (OAI) is the legacy way to give CloudFront private access to S3. AWS now recommends Origin Access Control (OAC) because it:

  • Supports SigV4 request signing (more secure).
  • Works with SSE-KMS encrypted buckets.
  • Supports all S3 regions including the newer opt-in regions.

The Terraform Code

Let's look at the key resources. First, the S3 bucket with all public access blocked:

resource "aws_s3_bucket" "website" {
  bucket        = var.bucket_name
  force_destroy = true

  tags = {
    Name        = var.bucket_name
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

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
}

Next, the CloudFront OAI and distribution:

resource "aws_cloudfront_origin_access_control" "website" {
  name                              = "${var.bucket_name}-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "website" {
  origin {
    domain_name              = aws_s3_bucket.website.bucket_regional_domain_name
    origin_id                = "S3-${var.bucket_name}"
    origin_access_control_id = aws_cloudfront_origin_access_control.website.id
  }

  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"
  # ... cache behaviours, custom error pages, etc.
}

Custom Error Pages

CloudFront lets you configure custom error responses. This is how the error.html page gets served for 404s:

custom_error_response {
  error_code            = 404
  response_code         = 404
  response_page_path    = "/error.html"
  error_caching_min_ttl = 10
}

custom_error_response {
  error_code            = 403
  response_code         = 404
  response_page_path    = "/error.html"
  error_caching_min_ttl = 10
}
Note: S3 returns 403 (not 404) when a file doesn't exist and the bucket is private. Mapping 403 โ†’ 404 in CloudFront gives users the proper error page.

Deploying the Website Files

After the infrastructure is provisioned, use the aws_s3_object resource or the AWS CLI to upload your files:

aws s3 sync ./website s3://your-bucket-name \
  --cache-control "max-age=86400" \
  --delete

# Invalidate CloudFront cache
aws cloudfront create-invalidation \
  --distribution-id YOUR_DIST_ID \
  --paths "/*"

Conclusion

With about 100 lines of Terraform you get a globally distributed, HTTPS-enforced, highly available static website. The S3 + CloudFront combo is unbeatable for static content hosting on AWS.

Check out the full Terraform source code in the Projects section of this site, or view it on GitHub.

โ† Back to Blog
AWS S3 CloudFront Terraform