Host a Static Website on AWS using S3, CloudFront and Terraform: A Complete Beginner’s Guide
Introduction
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:
- An AWS Account (Free tier works perfectly)
- Sign up at aws.amazon.com
- AWS CLI Installed and Configured
# Install AWS CLI (macOS) brew install awscli # Configure with your credentials aws configure
- You’ll need:
- AWS Access Key ID
- AWS Secret Access Key
- Default region (e.g.,
us-east-1)
- Terraform Installed (version 1.0+)
# macOS brew install terraform # Verify installation terraform --version
- 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:
- Random ID: Creates a unique bucket name (S3 buckets must be globally unique)
- S3 Bucket: Creates private bucket with proper tags
- Website Configuration: Tells S3 to serve index.html by default
- Public Access Block: Keeps bucket private (security!)
- File Upload: Uses
for_eachto upload all website files with correct content types - Origin Access Control: Modern, secure way for CloudFront to access S3
- CloudFront Distribution: Creates global CDN with HTTPS redirect
- 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:
- Copy the CloudFront URL from the output
- Wait 5–10 minutes for CloudFront to fully deploy
- 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:
- Edit your files in the
www/directory - Run terraform apply
terraform apply
- 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:
- Custom Domain: Connect your own domain with Route 53
- SSL Certificate: Add custom HTTPS with AWS Certificate Manager
- CI/CD Pipeline: Auto-deploy on git push using GitHub Actions
- Multiple Environments: Create dev/staging/prod environments
- Monitoring: Add CloudWatch alarms for traffic spikes
- 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