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

  1. 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

  2. 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.

  3. 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:

  1. Create an S3 bucket with a random name using the create-s3-bucket script:./bin/create-s3-bucket.sh

  2. Substitute the bucket name to .tfvars.

  3. 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:

  1. 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.

  2. 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

  1. resource "aws_s3_bucket_website_configuration" "website"

    • This defines a resource to configure an S3 bucket for website hosting.

  2. bucket = data.aws_s3_bucket.website.id

    • This links the configuration to an existing S3 bucket fetched via data.aws_s3_bucket.website.

  3. index_document

    • Defines the default page users see when accessing the site.

  4. 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:

  1. 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.

  2. aws_s3_bucket_public_access_block

    • Blocks all forms of public access to the S3 bucket by enabling these restrictions

  3. 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. content_type = lookup(local.content_types, regex("\\.[^.]+$", each.value), null)

    • Determines the correct MIME type for each file using local.content_type

  6. 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:

  1. provider = aws.acm_provider

    • Specifies which AWS provider configuration to use.

  2. domain_name = "static-web.${var.domain_name}"

    • Defines the primary domain the SSL certificate will secure.

  3. subject_alternative_names = ["*.static-web.${var.domain_name}"]

    • Adds a wildcard domain (*) for subdomains.

  4. lifecycle { create_before_destroy = true }

    • Ensures zero downtime when replacing the certificate.

Certificate Validation

Explanation:

  1. 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

  2. timeouts { create = "30m" }

    • Extends Terraform’s timeout for certificate validation to 30 minutes.

Certificate Validation

Explanation:

  1. 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:

  1. origin

    • Specifies the source of the content that CloudFront will distribute.

  2. aliases

    • A list of domain names (CNAMEs) associated with the distribution. This allows CloudFront to respond to requests for these custom domain names.

  3. 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:

  1. Simplicity

  2. Low Latency

  3. Cost-Effective

  4. 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

  1. Run Terraform to deploy the infrastructure:

  2. terraform init

  3. terraform apply

This setup ensures a secure, scalable, and highly available static website deployment using AWS and Terraform. 🚀