Static Site Terraform Setup

Terraform Code to set up AWS S3 bucket and CloudFront Distributions
Code Structure
.
├── modules
│   ├── aws_cloudfront_distribution
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── aws_s3_bucket
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── static_site
│       ├── main.tf
│       ├── outputs.tf
│       ├── providers.tf
│       └── variables.tf
├── backend.tf
├── static_sites.tf
├── outputs.tf
├── README.md
└── variables.tf

Requirements

Name Version
terraform >= 1.6.4
  • HTTP Remote state store for backend
  • AWS account

Providers

Name Version
aws >= 5.29.0

Modules

static_sites

Local module to create static sites with all their necessary resources
./modules/static-sites
Called by main module

Main Inputs

Name Description Type Default Required
static_sites Static Sites string N/A yes
environment System environment string N/A yes
additional_tags Additional resource tags map(string) null no

aws_cloudfront_distribution

Local module to create and configure CloudFront Distributions
./modules/aws_cloudfront_distribution
Called by static-site module

Name Description Type Default Required
environment System environment (dev, staging, prd) string N/A yes
path Path pattern string N/A yes
enabled Whether if the CF distribution is enabled or not bool true no
is_ipv6_enabled Whether if IPv6 enabled or not bool true no
default_root_object File name of the default root object string "index.html" no
price_class Price class for this distribution. string "PriceClass_100" no
aliases List of domain aliases list(string) null no
origins CloudFront origins map(object) {} no
default_cache_behavior Default cache behaviour settings object N/A yes
cache_behaviours Ordered cache behaviours list(object) [] no
geo_restriction_type Method that you want to use to restrict distribution of your content string "none" no
geo_restriction_locations ISO 3166-1-alpha-2 codes for which you want CloudFront either to distribute your content (whitelist) or not distribute your content (blacklist) list(string) [] no
minimum_protocol_version Minimum version of the SSL protocol that you want CloudFront to use for HTTPS connections string "TLSv1.2_2021" no
bucket_id S3 Bucket ID string N/A yes
bucket_arn S3 Bucket ARN string N/A yes

aws_s3_bucket

Local module to create and configure S3 Buckets
./modules/aws_s3_bucket
Called by static-site module

Name Description Type Default Required
environment System environment (dev, staging, prd) string N/A yes
bucket_name Name of the S3 bucket string N/A yes
force_destroy If the service needs to be exposed boolean false no
object_lock_enabled Whether object block is enabled or not bool false no
bucket_accelerate_status Transfer acceleration state of the bucket string "Suspended" no
expected_bucket_owner Account ID of the expected bucket owner string null no
block_public_acls Whether to block public ACLs bool true no
block_public_policy Whether to block public policy bool true no
ignore_public_acls Whether to ignore public ACLs bool true no
restrict_public_buckets Whether to restrict public buckets bool true no
bucket_acl The canned ACL to apply string "private" no
object_ownership Object ownership string "BucketOwnerPreferred" no
versioning_enabled Versioning state of the bucket string "Enabled" no
s3_objects S3 objects to upload into the bucket map(object) {} no

Usage

Mandatory environmental variables

  • TF_HTTP_ADDRESS - Address of the state store
  • TF_VAR_environment - System environment to deploy/manage (dev, staging, prod)

!Important notes!
Highly advised to use separate state stores per environment to lower blast radius

To add a new URI with dedicated CF and S3, add the corresponding code to the static_sites.tf
Replace the '#' with actual number and 'uri_path' with URI like /path

    uri_path = {
      site_number = #
    }

Design decisions

The code has been designed to be modular for easy change and extension, the 2 different resource types have their own modules prepared.

  • Small modules have been written to make it possible to make changes to every related environment/cf/s3 centrally. So after a change, a re-deployment picks the changes up everywhere
  • A wrapper module has been written to reduce code duplication and make the usage easier (static-site module)
  • As many options got a default value as possible in order to make it easier to add new setups, and reduce code duplication
  • Deployments are prepared to be separated by environment

The necessary setting and policies/attachments, access control resources are all managed in the respective modules

Integration into CI/CD

  • The code can be integrated into a CI/CD pipeline by running Terraform for each environment by child pipelines and, storing the states in a remote store relying on TF_VAR_environment environmental variable With this, changes can be deployed to respective environments separately, without affecting other environments, so every change can go through the full deployment cycle
  • Add terraform fmt to the pipeline to ensure code quality
  • Better way to make the pipeline pull always up-to-date modules from an external source rather than using internal modules

Other considerations for production usage

  • Apply human friendly DNS name for the CloudFront endpoints with AWS-managed SSL certificates
  • Use only 1 CloudFront distribution and S3 bucket per environment for simplicity and cost savings
  • Replace site_number with generated value