From Zero to a Live, Globally Distributed Website in One Day
Day 25 of the 30-Day Terraform Challenge — and today I built something I can actually show people.
Not just infrastructure. Not just a cluster that responds to curl. An actual website. With a URL. That works in a browser.
Using nothing but Terraform code.
What I Built
A fully serverless static website hosted on AWS S3, served globally through CloudFront, with HTTPS, custom error pages, and environment isolation.
All deployed with one command: terraform apply
The Architecture
User → CloudFront (HTTPS) → S3 Bucket (Static Files)
├── index.html
└── error.html
- S3 bucket stores the HTML files
- Bucket policy makes the files publicly readable
- CloudFront distributes content globally and adds HTTPS
- Terraform manages everything as code
The Project Structure
day25-static-website/
├── modules/
│ └── s3-static-website/
│ ├── main.tf # S3 bucket + CloudFront
│ ├── variables.tf # Configurable inputs
│ └── outputs.tf # CloudFront URL
├── envs/
│ └── dev/
│ ├── main.tf # Module call
│ ├── variables.tf
│ └── terraform.tfvars # Environment config
├── backend.tf # Remote state
└── provider.tf
This structure enforces DRY — the module is reusable, and the environment configuration is minimal.
The Module (Reusable)
The module encapsulates everything needed for a static website:
# modules/s3-static-website/main.tf
resource "aws_s3_bucket" "website" {
bucket = var.bucket_name
force_destroy = var.environment != "production"
}
resource "aws_s3_bucket_website_configuration" "website" {
bucket = aws_s3_bucket.website.id
index_document { suffix = var.index_document }
error_document { key = var.error_document }
}
resource "aws_cloudfront_distribution" "website" {
enabled = true
origin {
domain_name = aws_s3_bucket_website_configuration.website.website_endpoint
origin_id = "s3-website"
}
default_cache_behavior {
viewer_protocol_policy = "redirect-to-https"
}
}
Key design decisions:
-
force_destroy = var.environment != "production"— dev buckets can be destroyed easily, production buckets are protected - CloudFront adds HTTPS automatically — no certificate needed for the default domain
- Module outputs the CloudFront URL so callers can access it
The Environment Configuration (Clean)
Because the module does all the heavy lifting, the dev environment config is tiny:
# envs/dev/main.tf
module "static_website" {
source = "../../modules/s3-static-website"
bucket_name = var.bucket_name
environment = var.environment
index_document = "index.html"
error_document = "error.html"
}
output "cloudfront_url" {
value = "https://${module.static_website.cloudfront_domain_name}"
}
# envs/dev/terraform.tfvars
bucket_name = "my-terraform-website-day25-20260416"
environment = "dev"
That's it. All the complexity lives in the module.
The Deployment
cd envs/dev
terraform init
terraform plan
terraform apply -auto-approve
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
Outputs:
cloudfront_url = "https://d123.cloudfront.net"
The Result
After waiting 10 minutes for CloudFront to propagate:
curl https://d123.cloudfront.net
<!DOCTYPE html>
<html>
<head><title>Terraform Static Website</title></head>
<body>
<h1>🚀 Day 25: Deployed with Terraform!</h1>
<p>Environment: dev</p>
<p>This website was deployed using Terraform on Day 25!</p>
</body>
</html>
A live, globally distributed website. Deployed entirely through code.
Why This Project Matters
It's real. Not a demo. Not a "hello world" that only works on localhost. A real website with a real URL.
It's complete. S3 + CloudFront + HTTPS + error handling + environment isolation.
It's reusable. The module can deploy dev, staging, and production with different variables.
It demonstrates everything from Days 1-24:
- Modules (Day 8-9)
- Remote state (Day 6)
- DRY principle (Day 4)
- Environment isolation (Day 7)
- Tags and best practices (Day 16)
What I Learned
S3 static websites are simple but have limits. No HTTPS on the S3 endpoint — that's why you need CloudFront.
CloudFront takes time. 5-15 minutes to propagate globally. Be patient.
The module pattern is powerful. I can now deploy a static website in any environment with 5 lines of code.
Remote state protects your work. The state file is encrypted in S3, not on my laptop.
The DRY Principle in Practice
Without a module, I would have written 100+ lines of S3 + CloudFront configuration for every environment. With a module, each environment needs only 5 lines.
That's the difference between a one-off script and production-grade infrastructure.
Top comments (0)