Deploying a Static Website on AWS S3 with Terraform: A Beginner's Guide
Today, we will use AWS S3 to launch a basic static website as part of a friendly project for beginners. We will integrate AWS CloudFront to ensure rapid content delivery over HTTPS while maintaining security. We will use Terraform best practices, such as version control, remote state management, and modular architecture, throughout this project.
You will have a fully functional static website powered by Terraform that is hosted on AWS S3 and accessible through CloudFront by the end of this course!
PermalinkProject overview
This task assumes you already have a domain and a private S3 bucket for remote state management. However, the project is modularized in such a way that it may be deployed without a custom domain, relying solely on the CloudFront distribution domain for access.
The S3 bucket used for the website is private and will only be accessible through the CloudFront distribution, assuring security.
We’ll cover the following components:
S3 Bucket: For securely storing the website’s files, with public access enabled only for CloudFront.
CloudFront Distribution: To deliver the website content over HTTPS, with edge locations to speed up delivery.
Route53 (Optional): For associating your custom domain with the CloudFront distribution.
PermalinkProject Structure
terraform-s3-static/
├── env/
│ └── dev/
│ ├── main.tf
│ ├── outputs.tf
│ ├── terraform.tfvars
│ └── vars.tf
├── modules/
│ ├── acm
│ ├── cloudfront
│ ├── route53
│ └── s3-static-website
└── static/
├── images/
├── index.html
└── styles.css
PermalinkUsing Modules
PermalinkS3
Terraform modules are critical for reusability and maintainability. We designed a module for the S3 static website configuration that can be reused in other contexts. Let us look at the s3-static-website module.
# Generate a unique suffix for the S3 bucket name to ensure global uniqueness.
resource "random_string" "bucket_suffix_id" {
length = 8
lower = true
special = false
min_lower = 8
}
# Create an S3 bucket for hosting the static website.
resource "aws_s3_bucket" "static_website" {
bucket = "kami-website-${random_string.bucket_suffix_id.result}"
force_destroy = true # Ensures the bucket is deleted even if it contains objects
tags = {
Name = "kami-website"
Environment = var.environment
}
}
# Configure the S3 bucket for static website hosting.
resource "aws_s3_bucket_website_configuration" "static_website_configurations" {
bucket = aws_s3_bucket.static_website.id
# Specify the index document for the website
index_document {
suffix = "index.html"
}
}
# Define the S3 bucket policy to grant permissions to the CloudFront origin access control (OAC).
resource "aws_s3_bucket_policy" "bucket_policy" {
bucket = aws_s3_bucket.static_website.id
policy = var.cloudfront_oac_policy.json
}
# Define a local variable to manage the static files and their paths.
locals {
files = {
"index.html" = "static/index.html"
"styles.css" = "static/styles.css"
"images/dancing_cat.gif" = "static/images/dancing_cat.gif"
}
}
# Upload static files to the S3 bucket.
resource "aws_s3_object" "static_files" {
for_each = local.files
bucket = aws_s3_bucket.static_website.id
key = each.key
source = "${path.module}/../../${each.value}"
content_type = lookup({
"index.html" = "text/html",
"styles.css" = "text/css",
"images/dancing_cat.gif" = "image/gif"
}, each.key, "application/octet-stream") # Default content type
}
This module defines an S3 bucket for hosting a static website and uploads the necessary files.
To ensure the bucket name is globally unique, we use a random string suffix.
We use the aws_s3_object
resource to upload our static website files (HTML, CSS, images) to the S3 bucket. This is done using the for_each
loop to iterate over each file in the local.files
map
Outputs
To ensure the S3 bucket can be referenced by other modules, we define an output for the aws_s3_bucket
resource.
hclCopy codeoutput "aws_s3_bucket" {
description = "The S3 bucket details for the static website."
value = aws_s3_bucket.static_website
}
This output exposes the S3 bucket details so they can be easily passed into the CloudFront and Route53 modules for further configuration.
PermalinkACM
The ACM module generates an SSL/TLS certificate for use in securing the CloudFront delivery of the static website. This ensures that your website is safely served via HTTPS. This is how it is done:
Requesting an SSL Certificate: We utilize the aws_acm_certificate
resource to get a certificate for our domain. This is usually done using DNS validation; however, the certificate will be issued after the validation is complete.
resource "aws_acm_certificate" "cert" {
domain_name = var.domain_name
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
Outputs
The output exposes the ARN of the SSL certificate, which will be used in the CloudFront distribution to associate the certificate for HTTPS support.
It also provides the domain validation options for the certificate, which are necessary for DNS validation.
output "acm_certificate_arn" {
value = aws_acm_certificate.cert.arn
}
output "aws_acm_certificate_validation" {
value = aws_acm_certificate.cert.domain_validation_options
}
- The domain validation options will be used in the Route53 module to create the DNS validation record, proving ownership of the domain.
PermalinkRoute53 ( Optional )
The Route 53 module is responsible for setting up DNS records to point a custom domain to the CloudFront distribution. It’s optional because you can still access your static website through the default CloudFront domain, but using Route 53 provides a more professional and user-friendly experience, especially if you're using your own domain name.
You begin by retrieving the hosted zone for your website from the aws_route53_zone data source. Next, generate the necessary DNS records for ACM certificate validation using the domain validation parameters. Finally, you will establish an alias record that directs your domain to the CloudFront distribution. This ensures that once your ACM certificate has been authenticated, your custom domain will redirect users to CloudFront for content delivery.
data "aws_route53_zone" "selected_zone" {
name = var.domain_name
}
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in var.domain_validation_options :
dvo.domain_name => {
name = dvo.resource_record_name
type = dvo.resource_record_type
value = dvo.resource_record_value
}
}
zone_id = data.aws_route53_zone.selected_zone.id
name = each.value.name
type = each.value.type
records = [each.value.value]
ttl = 60
}
resource "aws_route53_record" "cloudfront_record" {
name = var.domain_name
type = "A"
zone_id = data.aws_route53_zone.selected_zone.zone_id
alias {
evaluate_target_health = false
name = var.cloudfront.domain_name
zone_id = var.cloudfront.hosted_zone_id
}
}
PermalinkCloudfront
This configuration ensures that CloudFront securely connects to the S3 bucket using an Origin Access Control (OAC). It configures a CloudFront distribution with custom domain options and secure HTTPS access using an ACM certificate when a custom domain is specified. If no custom domain is specified, CloudFront uses its own certificate by default.
# Define an Origin Access Control (OAC) to securely connect CloudFront to the S3 bucket
resource "aws_cloudfront_origin_access_control" "oac" {
name = "s3-cloudfront-oac-${var.environment}"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
# CloudFront distribution configuration for serving content from the S3 bucket
resource "aws_cloudfront_distribution" "s3_distribution" {
enabled = true
aliases = var.use_custom_domain ? [var.domain_name] : [] # Custom domain name for the distribution
default_root_object = "index.html"
is_ipv6_enabled = true
wait_for_deployment = true
# Default caching behavior for the distribution
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = var.aws_s3_bucket.id
viewer_protocol_policy = "redirect-to-https" # Enforce HTTPS
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
# Origin configuration linking the S3 bucket with CloudFront
origin {
domain_name = var.aws_s3_bucket.bucket_domain_name
origin_id = var.aws_s3_bucket.id
origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
}
# Restrict access based on geographic region (currently open to all)
restrictions {
geo_restriction {
restriction_type = "none"
}
}
# Viewer certificate for HTTPS using an ACM certificate
viewer_certificate {
acm_certificate_arn = var.use_custom_domain ? var.acm_certificate : null
cloudfront_default_certificate = var.use_custom_domain ? false : true
minimum_protocol_version = "TLSv1.2_2021" # Ensure a secure TLS version
ssl_support_method = "sni-only"
}
}
# IAM policy document allowing CloudFront to access S3 objects
data "aws_iam_policy_document" "cloudfront_oac" {
statement {
principals {
identifiers = ["cloudfront.amazonaws.com"]
type = "Service"
}
actions = ["s3:GetObject"] # Grant access to retrieve objects from S3
resources = ["${var.aws_s3_bucket.arn}/*"]
condition {
test = "StringEquals"
values = [aws_cloudfront_distribution.s3_distribution.arn]
variable = "AWS:SourceArn" # Restrict access based on the CloudFront ARN
}
}
}
Variable
The critical variable in use is use_custom_domain
, which determines whether a custom domain is used with CloudFront. If set to true
, CloudFront will use the provided ACM certificate for HTTPS, and the custom domain will be applied. If set to false
, CloudFront defaults to using its own certificate, and no custom domain is configured.
variable "use_custom_domain" {
type = bool
description = "Indicates whether a custom domain is being used."
default = false
}
Outputs
This provides the CloudFront distribution's domain name, which can be used in Route 53 for DNS configuration. If a custom domain is used, this value will be crucial for setting up the DNS record to point to the CloudFront distribution
output "cloudfront_domain" {
value = module.cloudfront.cloudfront_distribution.domain_name
}
Main TF file
In the dev/main.tf
file, the modules are organized based on whether a custom domain is used.
ACM Module: The acm_certificate
module is conditionally created based on the create_custom_domain
variable. If the variable is true
, the module is instantiated to handle the ACM certificate creation. If false
, the module is skipped.
module "acm_certificate" {
count = var.create_custom_domain ? 1 : 0
source = "../../modules/acm"
domain_name = var.domain_name
}
S3 Module: The s3
module is always created to set up the S3 static website, and it passes the cloudfront_oac_policy
from the CloudFront module.
module "s3" {
source = "../../modules/s3-static-website"
cloudfront_oac_policy = module.cloudfront.cloudfront_oac_policy
}
CloudFront Module: The cloudfront
module configures the distribution, with the ACM certificate and domain name conditionally applied based on the create_custom_domain
variable. If create_custom_domain
is true
, it uses the ACM certificate; otherwise, it defaults to CloudFront’s default certificate.
module "cloudfront" {
source = "../../modules/cloudfront"
domain_name = var.domain_name
aws_s3_bucket = module.s3.aws_s3_bucket
environment = var.environment
acm_certificate = var.create_custom_domain ? module.acm_certificate.acm_certificate_arn : null
use_custom_domain = var.create_custom_domain
}
Route 53 Module: The route53
module is conditionally created based on the create_custom_domain
variable. It sets up DNS records for the custom domain only if the domain is specified.
module "route53" {
count = var.create_custom_domain ? 1 : 0
source = "../../modules/route53"
cloudfront = module.cloudfront.cloudfront_distribution
domain_validation_options = var.create_custom_domain ? module.acm_certificate.aws_acm_certificate_validation : null
domain_name = var.domain_name
}
This conditional logic ensures that resources such as the ACM certificate and Route 53 records are only created if a custom domain is specified, maintaining efficiency and reducing unnecessary resources when not needed
PermalinkConclusion
In this article, we have looked at how to use Terraform to automate the deployment of a static website, from setting up S3 hosting to managing certificates with ACM and deploying via CloudFront. We have also observed Route 53's integration with CloudFront for a custom domain setup. By modularizing the process, we ensure scalability and flexibility across multiple deployment situations. Using Git for version control, paired with correct tagging, makes it easier to keep track of changes and do any necessary rollbacks.
You can get the whole code and implementation in the repository here.