The Terrform logo on the left in white with a violet background gradient which transitions to a gray background on the right with the AWS logo on the right.

Deploy a Static Website on AWS using Terraform

Use AWS's powerful and scalable infrastructure to serve a high-performance static site using Terraforms modules for easy deployment and asset uploading.

When it comes to deploying static websites, AWS offers a powerful and scalable infrastructure. By leveraging Terraform, an Infrastructure as Code (IaC) tool, we can automate the deployment process while maintaining full control of our infrastructure. In this guide, we’ll walk you through deploying a high-performance static site using Terraform modules on AWS.

Why Terraform and AWS?

Terraform simplifies the process of provisioning and managing infrastructure by allowing you to define everything in code. AWS offers key services such as S3, CloudFront, and Route53, which together can host and deliver a secure, high-performing website with global reach. The combination of these services, automated via Terraform, ensures that your site will load quickly and reliably for users across the world.

Prerequisites

Before starting, ensure you have the following:

  1. An AWS account

  2. AWS CLI installed and configured with appropriate IAM permissions

  3. Terraform installed on your local machine

  4. Domain name registered in AWS Route 53

Terraform Modules and Configuration

We will use several Terraform modules to provision AWS services, including:

Step-by-Step Deployment

1. Set up the Domain and SSL Certificate

The main.tf file initiates the configuration by defining the necessary AWS providers and setting up the required SSL certificates using the ACM module.

terraform {
  required_providers {
    aws = {
      source                = "hashicorp/aws"
      version               = ">= 3.37.0"
      configuration_aliases = [aws.default, aws.acm]
    }
  }
}

locals {
  final_path        = var.hostname != null ? "${var.hostname}.${var.domain}" : var.domain
  alternative_paths = [for h in var.alternative_hostnames : "${h}.${var.domain}"]
}

data "aws_route53_zone" "hosted_zone" {
  provider = aws.default
  name     = var.domain
}

/*
 * Certificate
 */
module "certificate" {
  source = "commitdev/zero/aws//modules/certificate"
  providers = {
    aws = aws.acm
  }

  zone_name         = data.aws_route53_zone.hosted_zone.name
  domain_name       = local.final_path
  alternative_names = local.alternative_paths
}

This section provisions a domain and sets up SSL certificates, enabling secure HTTPS access to your website.

2. Configure S3 for Website Hosting and CDN

We use S3 to store the static site content and configure it to redirect any non-secure HTTP traffic to HTTPS. The CloudFront CDN will provide content distribution globally ensuring highly performant load times for the deployed site.

The website.tf file defines the S3 and CloudFront configurations.

/*
 * Website S3 Endpoints
 */
resource "aws_s3_bucket" "website_bucket" {
  provider = aws.default
  bucket   = local.final_path

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_website_configuration" "website_bucket_config" {
  provider = aws.default
  bucket   = aws_s3_bucket.website_bucket.bucket

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "404.html"
  }
}

resource "aws_s3_bucket_public_access_block" "website_bucket_acl" {
  provider = aws.default
  bucket   = aws_s3_bucket.website_bucket.bucket

  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

data "aws_iam_policy_document" "public_read_get_object" {
  provider = aws.default
  statement {
    sid    = "PublicReadGetObject"
    effect = "Allow"
    principals {
      type        = "AWS"
      identifiers = ["*"]
    }
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.website_bucket.arn}/*"]
  }
}

resource "aws_s3_bucket_policy" "website_bucket_policy" {
  provider = aws.default
  bucket   = aws_s3_bucket.website_bucket.bucket
  policy   = data.aws_iam_policy_document.public_read_get_object.json
}

/*
 * Website CDN
 */
resource "aws_cloudfront_distribution" "website_bucket_cdn" {
  provider = aws.default

  origin {
    domain_name = aws_s3_bucket_website_configuration.website_bucket_config.website_endpoint
    origin_id   = "website-s3-${local.final_path}"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "http-only"
      origin_ssl_protocols   = ["TLSv1", "TLSv1.1", "TLSv1.2"]
    }
  }

  enabled         = true
  is_ipv6_enabled = true
  price_class     = "PriceClass_100"


  default_cache_behavior {
    target_origin_id       = "website-s3-${local.final_path}"
    viewer_protocol_policy = "redirect-to-https"
    compress               = false
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    min_ttl                = 0
    default_ttl            = 86400
    max_ttl                = 31536000

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }
  }

  aliases = [local.final_path]

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = module.certificate.certificate_arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }
}

/*
 * Website DNS Routing
 */
resource "aws_route53_record" "website_record" {
  provider = aws.default
  name     = local.final_path
  type     = "A"
  zone_id  = data.aws_route53_zone.hosted_zone.id

  alias {
    evaluate_target_health = false
    name                   = aws_cloudfront_distribution.website_bucket_cdn.domain_name
    zone_id                = aws_cloudfront_distribution.website_bucket_cdn.hosted_zone_id
  }
}

/*
 * Website S3 Objects
 */
module "local_files" {
  source   = "hashicorp/dir/template"
  base_dir = var.upload_path
}

resource "aws_s3_object" "website_file" {
  provider     = aws.default
  depends_on   = [module.local_files]
  for_each     = module.local_files.files
  bucket       = aws_s3_bucket.website_bucket.bucket
  key          = each.key
  content_type = each.value.content_type
  source       = each.value.source_path
  content      = each.value.content
  etag         = each.value.digests.md5
}

3. Set Up Route 53 DNS Redirects

Route 53 is used to manage the domain name and point it to the CloudFront distribution. Redirects are utilized to ensure that alternatives paths are routed to the correct location.

/*
 * Redirect S3 Endpoints
 */
resource "aws_s3_bucket" "redirect_bucket" {
  provider = aws.default
  bucket   = "redirects.${var.domain}"
}

resource "aws_s3_bucket_website_configuration" "redirect_bucket_config" {
  provider = aws.default
  bucket   = aws_s3_bucket.redirect_bucket.bucket

  redirect_all_requests_to {
    host_name = local.final_path
    protocol  = "https"
  }
}

/*
 * Redirect CDN
 */
resource "aws_cloudfront_distribution" "redirect_bucket_cdn" {
  provider = aws.default

  origin {
    domain_name = aws_s3_bucket_website_configuration.redirect_bucket_config.website_endpoint
    origin_id   = "redirect-s3-${var.domain}"

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "http-only"
      origin_ssl_protocols   = ["TLSv1", "TLSv1.1", "TLSv1.2"]
    }
  }

  enabled         = true
  is_ipv6_enabled = true
  price_class     = "PriceClass_100"


  default_cache_behavior {
    target_origin_id       = "redirect-s3-${var.domain}"
    viewer_protocol_policy = "redirect-to-https"
    compress               = false
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    min_ttl                = 0
    default_ttl            = 86400
    max_ttl                = 31536000

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }
  }

  aliases = local.alternative_paths

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = module.certificate.certificate_arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }
}

/*
 * Redirect DNS Routing
 */
resource "aws_route53_record" "redirect_record" {
  provider = aws.default
  for_each = toset(local.alternative_paths)
  name     = each.key
  type     = "A"
  zone_id  = data.aws_route53_zone.hosted_zone.id

  alias {
    evaluate_target_health = false
    name                   = aws_cloudfront_distribution.redirect_bucket_cdn.domain_name
    zone_id                = aws_cloudfront_distribution.redirect_bucket_cdn.hosted_zone_id
  }
}

This part creates DNS records to point your domain to the CloudFront distribution, ensuring that visitors to your site are routed correctly.

4. Setting Vartiables for Module

To make this module reusable, variables are exposed to allow configurations for the specific deployment attributes.

variable "domain" {
  description = "The domain where content will be hosted at (ex: mainDomain.com)"
  type        = string
}

variable "hostname" {
  description = "The hostname where content will be hosted at (ex: www)"
  type        = string
  nullable    = true
  default     = null
}

variable "alternative_hostnames" {
  description = "The hostnames to redirect from (ex: [missing, legacy])"
  type        = list(string)
  nullable    = false
  default     = []
}

variable "upload_path" {
  description = "The path to upload content from (ex:./website)"
  type        = string
  default     = "dir-that-dne"
}

5. Using the Module

The static-website module is now ready to be utilized in a stack.

module "static-website" {
  providers = {
    aws.default = aws.prod,
    aws.acm     = aws.prod-acm
  }
  source                = "../../modules/static-website"
  domain                = "example.com"
  alternative_hostnames = ["www"]
  upload_path           = "./site-content"
}

Conclusion

By using Terraform, you can quickly deploy and manage a highly performant static site on AWS. With the power of S3, CloudFront, Route 53, and ACM, your site will not only be secure but also load swiftly for users around the globe. Plus, the infrastructure is fully automated and easily replicable using Terraform, making future updates or scaling as simple as possible.

Give it a try, and take advantage of AWS’s robust infrastructure to deliver a top-tier static website!