Using Terraform to host a Secure Static Website with AWS S3 and Cloudfront
By the time you finish reading this article, you will know how to get your static websites up and running securely on AWS using Terraform. This can be a very cost-effective way of hosting a website.
config.tf
Terraform needs plugins called providers to interact with remote systems. This file acts as the main file for the Terraform configuration.
In this case, we are only dealing with AWS but Terraform can also interact with other cloud services such as Azure and Google Cloud.
# config.tf
provider "aws" {
region = "us-east-1"
}
terraform {
required_version = ">= 0.14"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
backend "s3" {
encrypt = true
region = "eu-central-1"
}
}
Here we are specifying the version of Terraform that we are using as well as the version of the AWS provider. This is to ensure that any future breaking changes to Terraform or the AWS provider does not stop our scripts from working.
vars.tf
In this file, we define the variables that we are going to use.
# vars.tf
variable "site_name" {
type = string
description = "The domain name for the website."
}
variable "bucket_name" {
type = string
description = "The name of the bucket without the www. prefix. Normally domain_name."
}
variable "allowed_paths" {
type = list(string)
default = ["*"]
description = "List of bucket items paths can be accessed trough CloudFront."
}
variable "minimum_protocol_version" {
type = string
default = "TLSv1.2_2021"
description = "Minimum version of the SSL protocol used for HTTPS connections. One of: SSLv3, TLSv1, TLSv1.1_2016, TLSv1.2_2018 , TLSv1.2_2019 and TLSv1.2_2021"
}
s3.tf
In this file, we are going to set up the S3 bucket that will store our static website files. I chose to go with “no public access” in order to prevent additional costs and to keep the security at a higher level.
# s3.tf
resource "aws_s3_bucket" "website" {
bucket = vars.bucket_name
}
resource "aws_s3_bucket_public_access_block" "block_public_access" {
depends_on = [aws_cloudfront_distribution.website, aws_s3_bucket_policy.resource_bucket_policy]
bucket = aws_s3_bucket.website.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
acm.tf
Next, we need to set up our SSL certificate.
After running terraform apply
for the first time you need to visit the AWS ACM page and finish the domain validation.
# acm.tf
resource "aws_acm_certificate" "ssl_certificate" {
domain_name = "*.${var.site_name}"
subject_alternative_names = [var.site_name]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
cloudfront.tf
Now that we have done the S3 and SSL certificate we can look at creating the Cloudfront distributions.
# cloudfront.tf
resource "aws_cloudfront_origin_access_identity" "origin_access_identity" {
comment = "Identity used to allow Cloudfront access to S3"
}
data "aws_acm_certificate" "wildcard" {
domain = "*.${var.site_name}"
statuses = ["ISSUED"]
}
resource "aws_cloudfront_distribution" "website" {
enabled = true
wait_for_deployment = false
default_root_object = "index.html"
aliases = [ var.site_name, "www.${var.site_name}"]
origin {
domain_name = aws_s3_bucket.website.bucket_domain_name
origin_id = "S3-${aws_s3_bucket.website.id}"
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.origin_access_identity.cloudfront_access_identity_path
}
}
default_cache_behavior {
allowed_methods = ["GET","HEAD"]
cached_methods = ["GET","HEAD"]
target_origin_id = "S3-${aws_s3_bucket.website.id}"
compress = true
viewer_protocol_policy = "redirect-to-https"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
min_ttl = 0
default_ttl = 86400
max_ttl = 31536000
}
// Replace default CloudFront 403 error with 404.html
custom_error_response {
error_caching_min_ttl = 3600
error_code = 403
response_code = 403
response_page_path = "/404.html"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = data.aws_acm_certificate.wildcard.arn
ssl_support_method = "sni-only"
minimum_protocol_version = var.minimum_protocol_version
}
}
iam.tf
Because we are using a private bucket we need to setup cloudfront permissions for the s3 bucket.
# iam.tf
data "aws_iam_policy_document" "bucket_policy" {
statement {
actions = ["s3:GetObject"]
effect = "Allow"
principals {
identifiers = [aws_cloudfront_origin_access_identity.origin_access_identity.iam_arn]
type = "AWS"
}
resources = [for path in var.allowed_paths: join("/", [aws_s3_bucket.website.arn, trimprefix(path,"/")])]
sid = var.site_name
}
}
resource "aws_s3_bucket_policy" "resource_bucket_policy" {
bucket = aws_s3_bucket.website.id
policy = data.aws_iam_policy_document.bucket_policy.json
}
terraform.tfvars
The tfvars file is used to specify variable values. These will need to be updated for your domain.
# terraform.tfvars
domain_name = "example.com"
bucket_name = "example.com"