Skip to main content

Command Palette

Search for a command to run...

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

Updated
โ€ข5 min read
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.


1 views