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


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
}
Prints the public website endpoint (e.g.,
http://myweb-xxxx.s3-website.ap-south-1.amazonaws.com
).Note: S3 website endpoints are HTTP only. For HTTPS, use CloudFront.
How to Run
terraform init
terraform plan
terraform apply
After
apply
, copy thewebsite_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 exactlyindex.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)
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.