Day 05 — Hosting a Static Website on AWS S3 with Terraform (Step-by-Step)

Abdul RaheemAbdul Raheem
3 min read

Today I used Terraform to provision everything needed to host a static website on S3: a uniquely named bucket, website configuration, public read access, and object uploads for index.html and styles.css. Below I explain every block of the code and why it matters.


1) Providers & Versions

terraform {
  required_providers {
    aws = { source = "hashicorp/aws",   version = "6.8.0" }
    random = { source = "hashicorp/random", version = "3.7.2" }
  }
}
  • required_providers pins exact versions → reproducible builds no matter who runs it.

  • We use two providers:

    • AWS for S3.

    • random to generate a unique suffix for the bucket (S3 bucket names must be globally unique).


2) Configure AWS Region

provider "aws" {
  region = "ap-south-1"
}
  • All resources are created in ap-south-1 (Mumbai). Change this if needed.

3) Make a Unique Bucket Name

resource "random_id" "rand-id" {
  byte_length = 8
}
  • Generates 8 bytes → 16 hex chars (e.g., a1b2c3d4e5f6a7b8).

  • We’ll append this to the bucket name to avoid collisions.

resource "aws_s3_bucket" "web-bucket" {
  bucket = "myweb-${random_id.rand-id.hex}"
}
  • Creates the S3 bucket named myweb-<random-hex>.

  • No ACL set here—modern S3 defaults to Bucket Owner Enforced (ACLs off). Public access will be handled with policy.


4) Upload Website Files

resource "aws_s3_object" "index-html" {
  bucket       = aws_s3_bucket.web-bucket.bucket
  source       = "./index.html"
  key          = "index.html"
  content_type = "text/html"
}

resource "aws_s3_object" "styles-css" {
  bucket       = aws_s3_bucket.web-bucket.bucket
  source       = "./styles.css"
  key          = "styles.css"
  content_type = "text/css"
}
  • source points to local files that Terraform will upload.

  • key is the object name in S3.

  • content_type sets the correct MIME type so the browser renders HTML/CSS properly.

Tip: Update your HTML <title> for polish:

<title>Terraform S3 Static Website — DevOps Diaries</title>

5) Allow Public Reads (carefully)

resource "aws_s3_bucket_public_access_block" "public-access" {
  bucket = aws_s3_bucket.web-bucket.id

  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}
  • By default AWS blocks public access. We relax these four switches so a bucket policy can allow public reads.

  • Do this only for public static sites / demos. For production, prefer CloudFront + OAC (see “Hardening” below).

resource "aws_s3_bucket_policy" "mywebapp" {
  bucket = aws_s3_bucket.web-bucket.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Sid       = "PublicReadGetObject",
      Effect    = "Allow",
      Principal = "*",
      Action    = "s3:GetObject",
      Resource  = "arn:aws:s3:::${aws_s3_bucket.web-bucket.id}/*"
    }]
  })
}
  • Grants the world (Principal="*") read-only (s3:GetObject) access to objects in the bucket (/*).

  • We don’t allow listing the bucket or writes—only GET.


6) Turn On Static Website Hosting

resource "aws_s3_bucket_website_configuration" "mywebapp" {
  bucket = aws_s3_bucket.web-bucket.id

  index_document {
    suffix = "index.html"
  }
}
  • Enables S3’s website mode and sets the default root page.

  • Optional: add an error page with error_document { key = "404.html" }.


7) Output the Website URL

output "aws_s3_bucket_website_configuration" {
  value = aws_s3_bucket_website_configuration.mywebapp.website_endpoint
}

How to Run

terraform init
terraform plan
terraform apply
  • After apply, copy the website_endpoint output into your browser. You should see your styled landing page.

  • Clean up when done:

      terraform destroy
    

Troubleshooting

  • 403 Forbidden: Ensure the public access block resource is applied and the bucket policy exists.

  • 404 Not Found: Confirm index.html is in the bucket root and the key is exactly index.html.

  • Wrong MIME: Check content_type values for HTML/CSS.


Production Hardening (Next Steps)

  • Serve via CloudFront + Origin Access Control (OAC) for HTTPS, caching, and no public bucket.

  • Turn on S3 versioning, server access logging, and default encryption.

  • Add a custom domain with Route 53 + CloudFront.


🔗 Follow My Journey

📖 Blogs: Hashnode
💻 Code: GitHub
🐦 Updates: X (Twitter)

0
Subscribe to my newsletter

Read articles from Abdul Raheem directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Abdul Raheem
Abdul Raheem

Cloud DevOps | AWS | Terraform | CI/CD | Obsessed with clean infrastructure. Cloud DevOps Engineer 🚀 | Automating Infrastructure & Securing Pipelines | Bridging Gaps Between Code and Cloud ☁️ I’m on a mission to master DevOps from the ground up—building scalable systems, automating workflows, and integrating security into every phase of the SDLC. Currently working with AWS, Terraform, Docker, CI/CD, and learning the art of cloud-native development.