Deploying a Static Website on AWS S3 with Terraform: A Beginner's Guide

Infrastructure as Code becomes truly valuable when you stop experimenting and start building real, usable systems.
On Day 25 of my Terraform challenge, I deployed a fully functional static website on AWS using S3 and CloudFront, entirely managed through Terraform. This wasnโt just about making a website live โ it was about applying production-grade practices: modular design, remote state, DRY configuration, and environment isolation.
๐งฑ Project Structure: Designing for Scale
Before writing any resources, I defined a clean and scalable directory structure:
day25-static-website/
โโโ backend.tf
โโโ provider.tf
โโโ modules/
โ โโโ s3-static-website/
โ โโโ main.tf
โ โโโ variables.tf
โ โโโ outputs.tf
โโโ envs/
โโโ dev/
โโโ main.tf
โโโ variables.tf
โโโ outputs.tf
โโโ terraform.tfvars
This structure separates:
Reusable logic โ
modules/Environment-specific configuration โ
envs/dev/
This is exactly how real-world Terraform codebases are organized.
๐งฉ Why Use a Module?
Instead of defining everything in one file, I created a reusable module: modules/s3-static-website
Why this matters:
If I didnโt use a module:
Iโd duplicate the same resources for dev, staging, and production
Updates would be error-prone
Consistency would break over time
With a module:
Infrastructure logic lives in one place
Environments only pass inputs
Changes propagate cleanly across all environments
๐ This is Terraformโs implementation of the DRY (Donโt Repeat Yourself) principle.
โ๏ธ Module Design Decisions
The module was designed to be:
Reusable
Configurable
Safe
Key variables:
variable "bucket_name" {
description = "Globally unique S3 bucket name"
type = string
}
- No default โ must be explicitly set (avoids collisions)
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "Invalid environment"
}
}
- Enforces controlled environments
variable "index_document" {
default = "index.html"
}
- Defaults reduce unnecessary configuration
โ๏ธ S3 Static Website Configuration
The S3 bucket serves as the origin for the website.
Key components:
Bucket creation
Static website hosting enabled
Public access configured
Bucket policy allowing
GetObject
This transforms S3 from simple storage into a web server.
๐ CloudFront: Global Content Delivery
S3 alone is not enough for production use.
I added a CloudFront distribution to:
Serve content over HTTPS
Reduce latency globally
Cache content efficiently
Key configuration:
viewer_protocol_policy = "redirect-to-https"
This ensures:
- All users access the site securely
CloudFront sits in front of S3, acting as a CDN layer.
๐ Remote State: Protecting Your Infrastructure
Instead of storing Terraform state locally, I configured a remote backend:
backend "s3" {
bucket = "your-terraform-state-bucket"
key = "day25/static-website/dev/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks"
encrypt = true
}
Why this matters:
State is centralized
Team collaboration is possible
State locking prevents conflicts
Encryption protects sensitive data
Without remote state, your infrastructure is fragile.
๐ Deployment Process
From the envs/dev directory:
terraform init
terraform validate
terraform plan
terraform apply
After deployment, Terraform outputs the CloudFront URL:
terraform output cloudfront_domain_name
๐ Live Website
Here is the deployed site:
๐ https://d123abcd.cloudfront.net (replace with your actual URL)
What youโll see:
A simple HTML page
Environment name (
dev)Bucket name
This confirms:
S3 is serving content
CloudFront is distributing it globally
HTTPS is working
๐ DRY Principle in Practice
Without DRY (bad approach):
resource "aws_s3_bucket" "dev" {}
resource "aws_s3_bucket" "staging" {}
resource "aws_s3_bucket" "prod" {}
Problems:
Repetition
Maintenance overhead
High risk of inconsistency
With DRY (module approach):
module "static_website" {
source = "../../modules/s3-static-website"
}
Benefits:
Single source of truth
Clean environment configs
Easy scaling to new environments
๐งช Environment Isolation
The envs/dev folder contains only:
Variable values
Module invocation
This ensures:
Dev, staging, and production are isolated
No accidental cross-environment changes
๐งน Cleanup Matters
After testing, I destroyed all resources:
terraform destroy
Why this is important:
CloudFront incurs cost even when idle
Prevents unnecessary billing
๐ Key Takeaways
Modules are essential for scalable Terraform design
S3 + CloudFront is a production-ready static hosting solution
Remote state is non-negotiable in real environments
DRY is enforced through structure, not discipline
Environment isolation prevents costly mistakes
๐ Final Thoughts
This project ties together everything from the previous 24 days:
Variables
Modules
State management
Real-world AWS services
Itโs no longer just learning Terraform โ itโs using Terraform like an engineer.




