published: 17th of January 2023
Deploying a static site in AWS can be a bit of an affair. Thankfully, Devops tools like Terraform make things a bit more sane.
In a previous post I covered this topic, using Pulumi as the build tool. I really like Pulumi, but I don't see alot of demand for it in my region.
In this post, I will show you how to deploy a static website in AWS with Terraform Cloud.
This post assumes that you already have your Terraform Cloud environment setup and able to deploy resources into AWS.
To get Terraform Cloud setup, check out this post. For connecting Terraform Cloud with AWS, check out this post.
You will also need to register a domain name in Route 53. The process to register a domain can be found in the the docs here. I have previously registered stratuslabs.net and will use that domain for this lab.
The following software versions where used in this post.
The following diagram shows the services we will use to build and host the static site.
I have covered the services used in another post, so I won't go into them in detail here. The only difference, is that I am using Origin Access Control instead of an Origin Access Identity which was not supported by Pulumi at the time of writing.
Alright, you got all that together? Let's crack on!
In the next sections I will cover the Terraform code used to deploy this lab. The code is split into multiple files based on its function.
First, configure your connection to Terraform Cloud in the backend.tf file. Set your organization and the name of the workspace. For this project, I have created a workspace called aws-static-site.
terraform {
cloud {
organization = "<ORGANIZATION>"
workspaces {
name = "aws-static-site"
}
}
}
We define the variables for the project in the terraform.tfvars file.
project_name = "stratus_labs"
domain_name = "stratuslabs.net"
environment = "dev"
The AWS provider configuration is defined in the provider.tf file.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
We have two files to define our S3 buckets. One for our website, and one for the sites logfiles.
The web_bucket.tf file contains the config related to our website.
resource "aws_s3_bucket" "web_bucket" {
bucket = "www.${var.domain_name}"
tags = {
project = var.project_name
environment = var.environment
}
}
resource "aws_s3_bucket_website_configuration" "web_bucket_config" {
bucket = aws_s3_bucket.web_bucket.bucket
index_document {
suffix = "index.html"
}
error_document {
key = "error.html"
}
}
resource "aws_s3_object" "site" {
bucket = aws_s3_bucket.web_bucket.bucket
for_each = fileset("./_site/", "**")
key = each.value
source = "./_site/${each.value}"
content_type = "text/html"
etag = filemd5("./_site/${each.value}")
}
resource "aws_s3_bucket_acl" "web_bucket_acl" {
bucket = aws_s3_bucket.web_bucket.id
acl = "private"
}
data "aws_iam_policy_document" "allow_access_from_cloud_front" {
version = "2012-10-17"
statement {
sid = "PolicyForCloudFrontPrivateContent"
actions = [
"s3:GetObject",
]
resources = [
"${aws_s3_bucket.web_bucket.arn}/*",
]
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "AWS:SourceArn"
values = [aws_cloudfront_distribution.web_distribution.arn]
}
}
}
resource "aws_s3_bucket_policy" "allow_access_from_cloud_front" {
bucket = aws_s3_bucket.web_bucket.bucket
policy = data.aws_iam_policy_document.allow_access_from_cloud_front.json
}
We will also configure a dedicated S3 bucket named log_bucket.tf to store the access logs for our website.
resource "aws_s3_bucket" "log_bucket" {
bucket = "log.www.${var.domain_name}"
force_destroy = true
tags = {
project = var.project_name
environment = var.environment
}
}
resource "aws_s3_bucket_acl" "log_bucket_acl" {
bucket = aws_s3_bucket.log_bucket.id
acl = "private"
}
Our domain related config is defined in a file named www_domain.tf.
# Get data for the pre-purchased domain.
data "aws_route53_zone" "domain" {
name = var.domain_name
}
locals {
record_types = toset(["A", "AAAA"])
}
resource "aws_route53_record" "bare_record" {
zone_id = data.aws_route53_zone.domain.zone_id
name = var.domain_name
for_each = local.record_types
type = each.value
alias {
name = aws_cloudfront_distribution.web_distribution.domain_name
zone_id = aws_cloudfront_distribution.web_distribution.hosted_zone_id
evaluate_target_health = true
}
}
resource "aws_route53_record" "www_record" {
zone_id = data.aws_route53_zone.domain.zone_id
name = "www.${var.domain_name}"
for_each = local.record_types
type = each.value
alias {
name = aws_cloudfront_distribution.web_distribution.domain_name
zone_id = aws_cloudfront_distribution.web_distribution.hosted_zone_id
evaluate_target_health = true
}
}
We want to serve our site via https. The TLS related configuration is defined in a file named www_certificate.tf.
resource "aws_acm_certificate" "certificate" {
domain_name = var.domain_name
validation_method = "DNS"
subject_alternative_names = [
"www.${var.domain_name}"
]
tags = {
project = var.project_name
environment = var.environment
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_route53_record" "certificate_validation" {
for_each = {
for dvo in aws_acm_certificate.certificate.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = data.aws_route53_zone.domain.zone_id
}
resource "aws_acm_certificate_validation" "certificate_validation" {
certificate_arn = aws_acm_certificate.certificate.arn
validation_record_fqdns = [for record in aws_route53_record.certificate_validation : record.fqdn]
}
We want to cache our websites and assets close to our users by using CloudFront as a Content Delivery Network (CDN). The CDN related config is defined in a file name www_cdn.tf.
resource "aws_cloudfront_origin_access_control" "web_bucket_access_policy" {
name = "allow_to_s3_from_cloud_front"
description = "Allows access to an S3 web bucket from CloudFront"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
resource "aws_cloudfront_distribution" "web_distribution" {
origin {
domain_name = aws_s3_bucket.web_bucket.bucket_regional_domain_name
origin_access_control_id = aws_cloudfront_origin_access_control.web_bucket_access_policy.id
origin_id = aws_s3_bucket.web_bucket.id
}
enabled = true
is_ipv6_enabled = true
comment = "CloudFront distribution for our Website"
default_root_object = "index.html"
logging_config {
include_cookies = false
bucket = "log.www.stratuslabs.net.s3.amazonaws.com"
}
aliases = [var.domain_name, "www.${var.domain_name}"]
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = aws_s3_bucket.web_bucket.id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
# For this lab, we only deploy to minimal regions.
price_class = "PriceClass_100"
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.certificate.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
tags = {
project = var.project_name
environment = var.environment
}
}
And that my friends is all the Terraform code required.
All we need to do to deploy the site is run terraform apply and wait about 5 minutes.
You can see this site in all it's glory here.
In this post, we covered the Terraform code required to deploy a Static Website to AWS using Route53, S3 and CloudFront as well as utilize Certificate Manager for TLS certificates. The Terraform code is executed on Terraform Cloud which is also used for the remote state backend.
https://registry.terraform.io/providers/hashicorp/aws/latest/docs
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket
https://dev.to/ankursheel/how-to-upload-multiple-files-to-aws-s3-using-terraform-24bl
https://stackoverflow.com/questions/18296875/amazon-s3-downloads-index-html-instead-of-serving
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_zone
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document