← Back to list

Host a Static Website on AWS using S3, CloudFront and Terraform: A Complete Beginner’s Guide

Introduction

Naveen G · 2025-12-25 19:16 · 0 claps · 9.9 min read
#aws #terraform #aws-terraform-tutorial #amazon-web-services #aws-beginner
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud

Host a Static Website on AWS using S3, CloudFront and Terraform: A Complete Beginner’s Guide

Introduction

Have you ever wondered how to host a website that’s fast, secure, and globally accessible? In this comprehensive guide, I’ll walk you through deploying a static website on AWS using infrastructure-as-code with Terraform. By the end of this tutorial, you’ll have a production-ready website served through a global content delivery network (CDN).

What You’ll Learn:

  • Setting up AWS S3 for static website hosting
  • Configuring CloudFront for global content delivery
  • Automating infrastructure deployment with Terraform
  • Implementing security best practices with Origin Access Control
  • Understanding how these services work together

Who This Guide Is For:

  • Beginners curious about cloud infrastructure
  • Developers wanting to learn Terraform
  • Anyone looking to host a fast, scalable static website

📋 Prerequisites

Before we begin, make sure you have:

  1. An AWS Account (Free tier works perfectly)
  1. AWS CLI Installed and Configured
  • # Install AWS CLI (macOS) brew install awscli # Configure with your credentials aws configure
  1. You’ll need:
  • AWS Access Key ID
  • AWS Secret Access Key
  • Default region (e.g., us-east-1)
  1. Terraform Installed (version 1.0+)
  • # macOS brew install terraform # Verify installation terraform --version
  1. Basic Knowledge Of:
  • HTML/CSS (we’ll provide sample files)
  • Command line basics
  • Text editor (VS Code recommended)

🏗️ Understanding the Architecture

Before diving into code, let’s understand what we’re building:

User Request → CloudFront (CDN) → Origin Access Control → Private S3 Bucket → Website Files

Key Components Explained:

1. Amazon S3 (Simple Storage Service)

  • Stores your website files (HTML, CSS, JavaScript, images)
  • Highly durable and scalable object storage
  • Configured as a private bucket for security

2. CloudFront

  • AWS’s Content Delivery Network (CDN)
  • Caches content at edge locations worldwide
  • Provides HTTPS and faster load times for global users
  • Reduces load on your S3 bucket

3. Origin Access Control (OAC)

  • Secure way for CloudFront to access private S3 buckets
  • Prevents direct public access to S3
  • Replaces the older Origin Access Identity (OAI)

4. Terraform

  • Infrastructure-as-Code (IaC) tool
  • Automates AWS resource creation
  • Makes infrastructure reproducible and version-controlled

Why This Architecture?

Security: S3 bucket remains private; only CloudFront can access it Performance: Global CDN caching reduces latency Cost-Effective: Pay only for storage and data transfer Scalable: Handles traffic spikes automatically Maintainable: All infrastructure is code-based

📁 Project Setup

Let’s set up our project structure:

# Create project directory
mkdir aws-static-website-terraform
cd aws-static-website-terraform
# Create necessary files
touch main.tf variables.tf outputs.tf
# Create website directory
mkdir www
cd www
touch index.html style.css script.js
cd ..

Your structure should look like:

aws-static-website-terraform/
├── main.tf              # Main Terraform configuration
├── variables.tf         # Input variables
├── outputs.tf           # Output values
└── www/                 # Website files
    ├── index.html
    ├── style.css
    └── script.js

🎨 Step 1: Create Your Website Files

First, let’s create a simple but modern static website.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AWS Static Website</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>🚀 Welcome to My AWS-Hosted Website</h1>
            <p>Powered by S3, CloudFront & Terraform</p>
        </header>

        <main>
            <section class="info-card">
                <h2>✨ Features</h2>
                <ul>
                    <li>Hosted on Amazon S3</li>
                    <li>Delivered via CloudFront CDN</li>
                    <li>Deployed with Terraform</li>
                    <li>Secure with Origin Access Control</li>
                </ul>
            </section>

            <section class="info-card">
                <h2>🎯 Project Status</h2>
                <p id="status">Loading...</p>
                <button id="clickBtn">Click Me!</button>
                <p>Clicks: <span id="counter">0</span></p>
            </section>

            <section class="info-card">
                <h2>🌐 Technologies Used</h2>
                <div class="tech-grid">
                    <span class="tech-badge">AWS S3</span>
                    <span class="tech-badge">CloudFront</span>
                    <span class="tech-badge">Terraform</span>
                    <span class="tech-badge">HTML/CSS/JS</span>
                </div>
            </section>
        </main>

        <footer>
            <p>Deployed with ❤️ using Infrastructure as Code</p>
        </footer>
    </div>

    <script src="script.js"></script>
</body>
</html>

style.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 20px;
}
.container {
    max-width: 800px;
    margin: 0 auto;
    background: white;
    border-radius: 20px;
    box-shadow: 0 20px 60px rgba(0,0,0,0.3);
    overflow: hidden;
    animation: fadeIn 0.6s ease-in;
}
@keyframes fadeIn {
    from { opacity: 0; transform: translateY(20px); }
    to { opacity: 1; transform: translateY(0); }
}
header {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 40px;
    text-align: center;
}
header h1 {
    font-size: 2.5em;
    margin-bottom: 10px;
}
header p {
    font-size: 1.2em;
    opacity: 0.9;
}
main {
    padding: 40px;
}
.info-card {
    background: #f8f9fa;
    padding: 30px;
    border-radius: 15px;
    margin-bottom: 20px;
    border-left: 5px solid #667eea;
}
.info-card h2 {
    color: #333;
    margin-bottom: 20px;
}
.info-card ul {
    list-style: none;
    padding-left: 0;
}
.info-card li {
    padding: 10px 0;
    padding-left: 25px;
    position: relative;
}
.info-card li::before {
    content: "✓";
    position: absolute;
    left: 0;
    color: #667eea;
    font-weight: bold;
}
#clickBtn {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    padding: 12px 30px;
    border-radius: 25px;
    font-size: 1em;
    cursor: pointer;
    transition: transform 0.2s;
    margin: 15px 0;
}
#clickBtn:hover {
    transform: scale(1.05);
}
#status {
    color: #28a745;
    font-weight: bold;
    font-size: 1.1em;
}
#counter {
    color: #667eea;
    font-weight: bold;
    font-size: 1.2em;
}
.tech-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
    gap: 10px;
}
.tech-badge {
    background: #667eea;
    color: white;
    padding: 10px 15px;
    border-radius: 20px;
    text-align: center;
    font-size: 0.9em;
    font-weight: bold;
}
footer {
    background: #2d3748;
    color: white;
    text-align: center;
    padding: 20px;
}
@media (max-width: 600px) {
    header h1 {
        font-size: 1.8em;
    }

    .container {
        margin: 10px;
    }
}

script.js

// Initialize counter
let clickCount = 0;
// Update status message
document.addEventListener('DOMContentLoaded', function() {
    const statusEl = document.getElementById('status');
    statusEl.textContent = '✅ Website Successfully Deployed!';

    // Animate status
    statusEl.style.animation = 'pulse 2s infinite';
});
// Handle button clicks
document.getElementById('clickBtn').addEventListener('click', function() {
    clickCount++;
    document.getElementById('counter').textContent = clickCount;

    // Fun messages
    if (clickCount === 10) {
        alert('🎉 Wow! You really like clicking!');
    } else if (clickCount === 50) {
        alert('🚀 You are a clicking champion!');
    }
});
// Add pulse animation
const style = document.createElement('style');
style.textContent = `
    @keyframes pulse {
        0%, 100% { opacity: 1; }
        50% { opacity: 0.7; }
    }
`;
document.head.appendChild(style);

🔧 Step 2: Configure Terraform Files

Now let’s write the Terraform code that creates our AWS infrastructure.

variables.tf

This file defines input variables for our configuration:

variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-east-1"
}
variable "bucket_prefix" {
  description = "Prefix for S3 bucket name"
  type        = string
  default     = "my-static-website"
}
variable "website_files" {
  description = "Map of website files to upload"
  type        = map(string)
  default = {
    "index.html" = "text/html"
    "style.css"  = "text/css"
    "script.js"  = "application/javascript"
  }
}

What this does:

  • Defines the AWS region (us-east-1 is Virginia, typically cheapest)
  • Sets a prefix for bucket naming (must be globally unique)
  • Maps file extensions to MIME types (tells browsers how to handle files)

main.tf

This is where the magic happens. Let’s break it down section by section:

# Configure Terraform and AWS Provider
terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
provider "aws" {
  region = var.aws_region
}
# Generate random suffix for unique bucket name
resource "random_id" "bucket_suffix" {
  byte_length = 4
}
# Create S3 Bucket for Static Website
resource "aws_s3_bucket" "website" {
  bucket = "${var.bucket_prefix}-${random_id.bucket_suffix.hex}"

  tags = {
    Name        = "Static Website Bucket"
    Environment = "Production"
    ManagedBy   = "Terraform"
  }
}
# Configure S3 Bucket as Website
resource "aws_s3_bucket_website_configuration" "website" {
  bucket = aws_s3_bucket.website.id
  index_document {
    suffix = "index.html"
  }
  error_document {
    key = "index.html"
  }
}
# Block Public Access to S3 Bucket (Security Best Practice)
resource "aws_s3_bucket_public_access_block" "website" {
  bucket = aws_s3_bucket.website.id
  block_public_acls       = true
  block_public_policy     = false  # Need false for CloudFront policy
  ignore_public_acls      = true
  restrict_public_buckets = false
}
# Upload Website Files to S3
resource "aws_s3_object" "website_files" {
  for_each = var.website_files
  bucket       = aws_s3_bucket.website.id
  key          = each.key
  source       = "${path.module}/www/${each.key}"
  content_type = each.value
  etag         = filemd5("${path.module}/www/${each.key}")
}
# Create CloudFront Origin Access Control
resource "aws_cloudfront_origin_access_control" "website" {
  name                              = "website-oac-${random_id.bucket_suffix.hex}"
  description                       = "OAC for static website"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}
# Create CloudFront Distribution
resource "aws_cloudfront_distribution" "website" {
  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"
  price_class         = "PriceClass_100"  # Use only NA and Europe edge locations
  origin {
    domain_name              = aws_s3_bucket.website.bucket_regional_domain_name
    origin_id                = "S3-${aws_s3_bucket.website.id}"
    origin_access_control_id = aws_cloudfront_origin_access_control.website.id
  }
  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD", "OPTIONS"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "S3-${aws_s3_bucket.website.id}"
    viewer_protocol_policy = "redirect-to-https"
    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }
    min_ttl     = 0
    default_ttl = 3600   # 1 hour
    max_ttl     = 86400  # 24 hours
  }
  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
  viewer_certificate {
    cloudfront_default_certificate = true
  }
  tags = {
    Name        = "Static Website Distribution"
    Environment = "Production"
    ManagedBy   = "Terraform"
  }
}
# S3 Bucket Policy to Allow CloudFront Access
resource "aws_s3_bucket_policy" "website" {
  bucket = aws_s3_bucket.website.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowCloudFrontServicePrincipal"
        Effect = "Allow"
        Principal = {
          Service = "cloudfront.amazonaws.com"
        }
        Action   = "s3:GetObject"
        Resource = "${aws_s3_bucket.website.arn}/*"
        Condition = {
          StringEquals = {
            "AWS:SourceArn" = aws_cloudfront_distribution.website.arn
          }
        }
      }
    ]
  })
  depends_on = [aws_s3_bucket_public_access_block.website]
}

Key Points Explained:

  1. Random ID: Creates a unique bucket name (S3 buckets must be globally unique)
  2. S3 Bucket: Creates private bucket with proper tags
  3. Website Configuration: Tells S3 to serve index.html by default
  4. Public Access Block: Keeps bucket private (security!)
  5. File Upload: Uses for_each to upload all website files with correct content types
  6. Origin Access Control: Modern, secure way for CloudFront to access S3
  7. CloudFront Distribution: Creates global CDN with HTTPS redirect
  8. Bucket Policy: Allows only CloudFront to read from S3

outputs.tf

This file displays important information after deployment:

output "website_url" {
  description = "CloudFront distribution URL"
  value       = "https://${aws_cloudfront_distribution.website.domain_name}"
}
output "s3_bucket_name" {
  description = "Name of the S3 bucket"
  value       = aws_s3_bucket.website.id
}
output "cloudfront_distribution_id" {
  description = "CloudFront distribution ID"
  value       = aws_cloudfront_distribution.website.id
}

🚀 Step 3: Deploy Your Infrastructure

Now for the exciting part — let’s deploy everything!

Initialize Terraform

terraform init

What happens here:

  • Downloads the AWS provider plugin
  • Initializes the backend
  • Prepares the working directory

You should see:

Terraform has been successfully initialized!

Preview Changes

terraform plan

What this does:

  • Shows what resources will be created
  • No actual changes are made
  • Good practice before applying

Review the output carefully. You should see:

  • S3 bucket creation
  • S3 objects (your files)
  • CloudFront distribution
  • IAM policies

Deploy Resources

terraform apply

Type yes when prompted.

What happens:

  • All AWS resources are created
  • Website files are uploaded to S3
  • CloudFront distribution is deployed (takes 5–10 minutes)

Expected Output:

Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
cloudfront_distribution_id = "E1ABCD234EFGH"
s3_bucket_name = "my-static-website-a1b2c3d4"
website_url = "https://d123xyz.cloudfront.net"

🎉 Step 4: Access Your Website

After deployment completes:

  1. Copy the CloudFront URL from the output
  2. Wait 5–10 minutes for CloudFront to fully deploy
  3. Open the URL in your browser

You should see your beautiful static website live on the internet! 🌐

Troubleshooting

Getting 403 Forbidden?

  • CloudFront might still be deploying
  • Check CloudFront console — status should be “Deployed”

Files not loading?

  • Verify files uploaded to S3: aws s3 ls s3://your-bucket-name/
  • Check MIME types in S3 console

Changes not appearing?

  • CloudFront caches content
  • Create an invalidation: aws cloudfront create-invalidation --distribution-id YOUR_ID --paths "/*"

🔍 Understanding the Costs

This setup is very cost-effective:

S3 Storage:

  • First 50 TB: $0.023 per GB
  • Your small website: < $0.01/month

CloudFront:

  • First 1 TB transfer: $0.085 per GB
  • Low traffic website: $1–5/month

Total Estimated Cost: Less than $5/month for moderate traffic

AWS Free Tier includes:

  • 5 GB S3 storage for 12 months
  • 50 GB CloudFront data transfer for 12 months

🛠️ Making Updates

Want to update your website? It’s easy:

  1. Edit your files in the www/ directory
  2. Run terraform apply
  • terraform apply
  1. Invalidate CloudFront cache
  • aws cloudfront create-invalidation \ --distribution-id YOUR_DISTRIBUTION_ID \ --paths "/*"

Terraform automatically detects file changes using the etag parameter!

🧹 Cleanup (Important!)

When you’re done experimenting, destroy all resources to avoid charges:

terraform destroy

Type yes when prompted.

This will:

  • Delete the CloudFront distribution
  • Delete all S3 objects
  • Delete the S3 bucket
  • Remove all associated policies

Cost after cleanup: $0 🎉

🎓 What You’ve Learned

Congratulations! You’ve now:

✅ Created a production-ready static website hosting setup ✅ Implemented AWS security best practices ✅ Automated infrastructure with Terraform ✅ Set up global content delivery with CloudFront ✅ Learned about Origin Access Control ✅ Understood Infrastructure as Code principles

🚀 Next Steps

Want to level up? Try these enhancements:

  1. Custom Domain: Connect your own domain with Route 53
  2. SSL Certificate: Add custom HTTPS with AWS Certificate Manager
  3. CI/CD Pipeline: Auto-deploy on git push using GitHub Actions
  4. Multiple Environments: Create dev/staging/prod environments
  5. Monitoring: Add CloudWatch alarms for traffic spikes
  6. Compression: Enable gzip/brotli compression in CloudFront

📚 Additional Resources

💡 Key Takeaways

Security First:

  • Always keep S3 buckets private
  • Use Origin Access Control for CloudFront
  • Enable HTTPS by default

Cost Optimization:

  • CloudFront caching reduces S3 requests
  • Use appropriate cache TTLs
  • Monitor with CloudWatch

Best Practices:

  • Use Infrastructure as Code
  • Tag all resources properly
  • Version control your configurations
  • Test before applying

Proof Of Work

🤝 Conclusion

You’ve just deployed a professional-grade static website using AWS and Terraform! This setup is used by companies of all sizes for hosting documentation, landing pages, SPAs, and more.

The skills you’ve learned here apply to much larger infrastructure projects. Terraform’s declarative approach and AWS’s managed services make building cloud infrastructure both powerful and accessible.

What did you build with this tutorial? Share your CloudFront URL in the comments below!

Found this helpful?

👏 Give this article some claps 💬 Leave a comment with your experience 🔗 Share with fellow developers ⭐ Star the GitHub repository

Happy cloud computing! ☁️

Questions? Feel free to reach out or leave a comment below. I’m happy to help troubleshoot any issues!


메타데이터
post_id
759e1c2255d7
slug
host-a-static-website-on-aws-using-s3-cloudfront-and-terraform-a-complete-beginners-guide-759e1c2255d7
url
https://medium.com/@naveen_15/host-a-static-website-on-aws-using-s3-cloudfront-and-terraform-a-complete-beginners-guide-759e1c2255d7
canonical_url
https://medium.com/@naveen_15/host-a-static-website-on-aws-using-s3-cloudfront-and-terraform-a-complete-beginners-guide-759e1c2255d7
author_url
https://medium.com/@naveen_15
status
ok
fetched_at
2026-07-10 04:31:59