DEV Community

Cover image for Hosting a Static Website with AWS S3 & CloudFront
Brian Mengo
Brian Mengo

Posted on

Hosting a Static Website with AWS S3 & CloudFront

Architecture and Key Concepts
The setup consists of a private S3 bucket hosting all static website files, which are then served globally via CloudFront distribution with multiple edge locations. This setup focuses on performance, security, and cost efficiency.

Architecture Flow

Browser -> CloudFront(Https) -> Private S3 Bucket
Enter fullscreen mode Exit fullscreen mode

-S3 bucket stores static content such as index.html, CSS, JavaScript, images, and videos.

  • The bucket is private, inaccessible directly by users.
  • CloudFront, acting as a CDN, sits in front of the bucket and delivers cached content from edge locations nearest to users worldwide.
  • Origin Access Control (OAC) securely restricts access so only CloudFront can fetch content from the S3 bucket.
  • Bucket policies authorize CloudFront to access objects without making the bucket public.

Benefits of this architecture:

  1. Security: Keeps S3 bucket private; users never access S3 directly.
  2. Performance: CloudFront caches content regionally, reducing latency.
  3. Cost efficiency: Data transfer is regional, lowering bandwidth costs.
  4. Scalability: Supports global users seamlessly through distributed edge locations.

Core Components Defined:
S3 Bucket (aws_s3_bucket):

Created with a specified name (using variable input).
Configured as private with a Public Access Block resource preventing public access.

Public Access Block (aws_s3_bucket_public_access_block):

Explicitly disables all public access settings ensuring bucket privacy.

resource "aws_s3_bucket_public_access_block" "block" {
  bucket = aws_s3_bucket.S3private.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
Enter fullscreen mode Exit fullscreen mode

Origin Access Control (aws_cloudfront_origin_access_control):

Configured for CloudFront to securely access the S3 bucket with signing_behavior set to always and signing_protocol sigv4.

resource "aws_cloudfront_origin_access_control" "OAC" {
  name                              = "Demo-OAC"
  description                       = "Example Policy"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}
Enter fullscreen mode Exit fullscreen mode

Bucket Policy (aws_s3_bucket_policy):


Allows CloudFront service principal to perform s3:GetObject requests on the bucket contents.
Utilizes JSON-encoded IAM policy including a condition restricting access to the CloudFront distribution’s ARN for added security.

resource "aws_s3_bucket_policy" "allow_cf_access" {
  bucket = aws_s3_bucket.S3private.id
  depends_on = [aws_s3_bucket_public_access_block.block]

  policy = jsonencode({

    "Statement" : [{
      "Sid"      : "AllowCloudFrontServicePrincipal",
      "Effect"   : "Allow",
      "Principal" : {
        "Service"     : "cloudfront.amazonaws.com"
      },
      "Action" : [
        "s3:GetObject",
        ],
      "Resource"  : "${aws_s3_bucket.S3private.arn}/*"
      "Condition" = {
        "StringEquals" = {
          "AWS:SourceArn": "arn:aws:cloudfront::559050222413:distribution/E2FIT39W38INIF"
        }
      }

    }]
    "Version" = "2012-10-17"
})

}
Enter fullscreen mode Exit fullscreen mode

S3 Objects Upload (aws_s3_object):

Uses for_each with Terraform’s fileset function to upload multiple files from a local www folder.
Sets proper content types dynamically per file extension using a lookup function to ensure correct MIME types.

resource "aws_s3_object" "object" {
  bucket = aws_s3_bucket.S3private.id
  for_each = fileset("C:\\Users\\maxycomppoint\\Downloads\\Documents\\AWS_TF\\Day14\\www", "**/*")
  key    = each.value
  source = "C:\\Users\\maxycomppoint\\Downloads\\Documents\\AWS_TF\\Day14\\www\\${each.value}"
  etag = filemd5("C:\\Users\\maxycomppoint\\Downloads\\Documents\\AWS_TF\\Day14\\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

CloudFront Distribution (aws_cloudfront_distribution):

Defines the distribution sourcing from the S3 bucket using the Origin Access Control.
Configured with a default root object (index.html), HTTPS redirection, minimal and default TTLs for caching, and limited allowed HTTP methods (GET, HEAD).
Uses CloudFront default SSL certificate (custom certificates from ACM left as an exercise).

resource "aws_cloudfront_distribution" "s3_distribution" {
  origin {
    domain_name              = aws_s3_bucket.S3private.bucket_regional_domain_name
    origin_access_control_id = aws_cloudfront_origin_access_control.OAC.id
    origin_id                = local.origin_id
  }

  enabled             = true
  is_ipv6_enabled     = true
  comment             = "Some comment"
  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_200"

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

viewer_certificate {
    cloudfront_default_certificate = true
  }
}
Enter fullscreen mode Exit fullscreen mode

bucket_regional_domain_name not bucket_domain_name — use the regional domain. The older non-regional domain doesn't work correctly with OAC and can cause intermittent errors.

viewer_protocol_policy = "redirect-to-https" — HTTP requests are automatically upgraded to HTTPS. One line. Done.

default_root_object = "index.html" — when someone visits the root URL, CloudFront serves index.html rather than returning a 403.

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.

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)