Static Website with AWS S3, CloudFront, and Terraform
3/6/20254 min read
To deploy a static website with AWS and Terraform, a robust and scalable solution involves using S3 for storage, CloudFront for content distribution, and Route 53 for DNS management. In this guide, we will walk through the process of setting up an S3-backed static website using Terraform.
Prerequisite
Terraform cloud account
Once you create the account, we will create a Project and a Workspace in order to use it as a remote store for the tfstate
AWS account
After the account creation we will need to setup AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY env both locally and in the Terraform Workspace.
Domain name
A domain name is required to be used. We have to create a hosted zone for the domain name before continuing with the article.
Project Folder Structure
Populating the files
Local Variables
First we create a locals.tf file containing local variables that can be used in every .tf file we will create:
Input Variables
First we create a locals.tf file containing local variables that can be used in every .tf file we will create:
.tfvars file
This is a variable definitions file. It contains the values of the variables to be passed during the execution of terraform:
This file should not be committed to any VCS, thus we add it to .gitignore file. To give an example of the content of the file, we create a .tfvars.example file.
Amazon S3
Amazon S3 is used to store our static website files. Before diving into the resources definition using Terraform, we need to create a bucket with a unique name in out account:
Create an S3 bucket with a random name using the create-s3-bucket script:./bin/create-s3-bucket.sh
Substitute the bucket name to .tfvars.
Use the bucket name in Terraform configurations.
Content of ./bin/create-s3-bucket.sh
S3 Bucket
We retrieve the bucket using a data source instead of resource.
Explanation:
data "aws_s3_bucket" "website"
This defines a data source, meaning Terraform will look up an existing S3 bucket instead of provisioning a new one.
bucket = var.bucket_name
This specifies the bucket name to fetch, using the value stored in the Terraform variable var.bucket_name.
S3 Website Configuration
We configure the bucket to act as a static website.
Breakdown
resource "aws_s3_bucket_website_configuration" "website"
This defines a resource to configure an S3 bucket for website hosting.
bucket = data.aws_s3_bucket.website.id
This links the configuration to an existing S3 bucket fetched via data.aws_s3_bucket.website.
index_document
Defines the default page users see when accessing the site.
error_document
Specifies the file to display when an error occurs (e.g., 404 Not Found).
Bucket Ownership and Access Control
Ensuring proper ownership and private access:
Explanation:
aws_s3_bucket_ownership_controls
Defines who owns objects uploaded to the S3 bucket.
object_ownership = "BucketOwnerPreferred" ensures that the bucket owner retains control over uploaded objects, even if uploaded by other AWS accounts or IAM users.
aws_s3_bucket_public_access_block
Blocks all forms of public access to the S3 bucket by enabling these restrictions
aws_s3_bucket_acl
Ensures the Access Control List (ACL) for the bucket is set to "private", meaning:
Only the bucket owner has access.
No public or external access is granted.
Uploading Static Files
Deploy all static files from the dist_dir.
Explanation:
for_each = fileset(local.dist_dir, "**")
Uses Terraform’s fileset() function to list all files inside the directory defined in local.dist_dir.
The ** pattern ensures that all files, including those in subdirectories, are included.
Terraform will iterate over each file, treating each one as an individual aws_s3_object resource.
bucket = data.aws_s3_bucket.static_website.id
Specifies the existing S3 bucket where files will be uploaded.
Uses data.aws_s3_bucket.static_website.id, which retrieves the bucket ID dynamically.
key = each.key
Defines the destination path for the file inside S3.
Since each.key represents the file’s relative path within local.dist_dir, the file structure is preserved.
source = "${local.dist_dir}/${each.key}"
Specifies the absolute path to the local file being uploaded.
Ensures Terraform knows where to find the file on disk.
content_type = lookup(local.content_types, regex("\\.[^.]+$", each.value), null)
Determines the correct MIME type for each file using local.content_type
etag = filemd5("${local.dist_dir}/${each.value}")
Prevents unnecessary uploads by using a checksum (MD5 hash) of the file.
If the file hasn’t changed, Terraform won’t re-upload it, optimizing deployments.
SSL with AWS ACM
To enable HTTPS, we provision an SSL certificate using AWS Certificate Manager.
Explanation:
provider = aws.acm_provider
Specifies which AWS provider configuration to use.
domain_name = "static-web.${var.domain_name}"
Defines the primary domain the SSL certificate will secure.
subject_alternative_names = ["*.static-web.${var.domain_name}"]
Adds a wildcard domain (*) for subdomains.
lifecycle { create_before_destroy = true }
Ensures zero downtime when replacing the certificate.
Certificate Validation
Explanation:
validation_record_fqdns = [for record in aws_route53_record.ssl_cert_validation : record.fqdn]
Extracts the fully qualified domain names (FQDNs) from Route 53 DNS records created for certificate validation.
Uses Terraform’s for loop to gather all fqdn values
timeouts { create = "30m" }
Extends Terraform’s timeout for certificate validation to 30 minutes.
Certificate Validation
Explanation:
for_each = {...} (Dynamic DNS Record Creation)
Uses Terraform’s for loop to create multiple DNS records dynamically.
CloudFront (CDN Configuration)
Origin Access Control (OAC)
This resource creates an origin access control (OAC) for a CloudFront distribution that restricts access to the origin.
CloudFront Distribution
This resource defines the CloudFront distribution, which is responsible for delivering content from an S3 bucket to end users.
Explanation:
origin
Specifies the source of the content that CloudFront will distribute.
aliases
A list of domain names (CNAMEs) associated with the distribution. This allows CloudFront to respond to requests for these custom domain names.
viewer_certificate
Specifies the SSL/TLS certificate settings for the distribution
CloudFront Function for Redirection
A CloudFront function to redirect "www." to the root domain.
cloudfront_function.js file
We decided to go with CloudFront Function because it provide some crucial benefits in high-traffic environment compared to Lambda Functions:
Simplicity
Low Latency
Cost-Effective
Ease of Deployment
* more on this at official documentation: https://aws.amazon.com/it/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/
S3 Bucket Policy for CloudFront
Ensure CloudFront has read-only access to the S3 bucket.
Route 53 - Domain Configuration
Hosted Zone
This defines a data block that retrieves information about an existing Route 53 hosted zone.
DNS Records
We define A records to point to CloudFront.
How to Set Up
Run Terraform to deploy the infrastructure:
terraform init
terraform apply
This setup ensures a secure, scalable, and highly available static website deployment using AWS and Terraform. 🚀