TerraformAWS
AWS S3 + CloudFront Static Site
Host a static website on S3 with CloudFront CDN, HTTPS via ACM, and origin access control.
s3cloudfrontcdnstatic-sitehttps
Prerequisites
- •
Domain registered in Route 53 (or external) - •
AWS Certificate Manager certificate in us-east-1 for your domain - •
Terraform >= 1.5.0
Template Code
# ─────────────────────────────────────────────────────────────────────────────
# AWS S3 + CloudFront Static Website
# ─────────────────────────────────────────────────────────────────────────────
variable "domain_name" {}
variable "certificate_arn" {}
variable "environment" { default = "production" }
locals {
s3_bucket_name = "${var.environment}-${replace(var.domain_name, ".", "-")}-site"
}
# ── S3 Bucket (no public access) ──────────────────────────────────────────────
resource "aws_s3_bucket" "site" {
bucket = local.s3_bucket_name
tags = { Environment = var.environment }
}
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
}
# ── CloudFront Origin Access Control ─────────────────────────────────────────
resource "aws_cloudfront_origin_access_control" "site" {
name = local.s3_bucket_name
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
# ── CloudFront Distribution ───────────────────────────────────────────────────
resource "aws_cloudfront_distribution" "site" {
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
aliases = [var.domain_name]
price_class = "PriceClass_100" # US + Europe + Asia
origin {
domain_name = aws_s3_bucket.site.bucket_regional_domain_name
origin_id = "S3-${local.s3_bucket_name}"
origin_access_control_id = aws_cloudfront_origin_access_control.site.id
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-${local.s3_bucket_name}"
viewer_protocol_policy = "redirect-to-https"
compress = true
forwarded_values {
query_string = false
cookies { forward = "none" }
}
min_ttl = 0
default_ttl = 86400 # 1 day
max_ttl = 604800 # 7 days
}
# Custom 404 → index.html for SPA routing
custom_error_response {
error_code = 404
response_code = 200
response_page_path = "/index.html"
}
restrictions {
geo_restriction { restriction_type = "none" }
}
viewer_certificate {
acm_certificate_arn = var.certificate_arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
tags = { Environment = var.environment }
}
# ── S3 Bucket Policy (allow CloudFront only) ──────────────────────────────────
data "aws_iam_policy_document" "site_bucket_policy" {
statement {
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.site.arn}/*"]
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "AWS:SourceArn"
values = [aws_cloudfront_distribution.site.arn]
}
}
}
resource "aws_s3_bucket_policy" "site" {
bucket = aws_s3_bucket.site.id
policy = data.aws_iam_policy_document.site_bucket_policy.json
}
output "cloudfront_url" { value = "https://${aws_cloudfront_distribution.site.domain_name}" }
output "cloudfront_dist_id" { value = aws_cloudfront_distribution.site.id }
output "s3_bucket_name" { value = aws_s3_bucket.site.bucket }
Usage
terraform init terraform apply -var="domain_name=www.example.com" -var="certificate_arn=arn:aws:acm:..."