Deploying a Hugo Website to AWS S3 and CloudFront Using Terraform
Note: This blog post was reviewed using AI for factual correctness and clarity. All content was tested in my private homelab to ensure accuracy.
๐ Introduction
Welcome to a hands-on guide for deploying a Hugo static website using AWS S3, CloudFront, and Terraform. This stack combines the best of content speed, scalability, and infrastructure automation โ perfect for static sites that need to scale with zero fuss.
โ Prerequisites
Make sure you have:
- An AWS account
- Terraform installed
- Hugo installed and ready to go
๐งฑ Create Your Hugo Website
Already have a Hugo site? Great! If not, hereโs how to start:
$ hugo new site <site-name>
Then, to build it for production:
$ hugo -gc --minify
โ๏ธ Terraform Setup
- Create your Terraform configuration
provider "aws" {
region = "us-east-1"
}
- Initialize Terraform
$ terraform init
๐ Architecture Overview
Hereโs what weโre building:
- S3: Stores your static site content
- CloudFront: Distributes content globally
- Route53: Maps your domain to the CDN
๐ฆ Create the S3 Bucket
Weโll use a private bucket for storing content, served through CloudFront.
module "static_website" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "3.15.1"
bucket = "<bucket-name>"
server_side_encryption_configuration = {
rule = {
apply_server_side_encryption_by_default = {
sse_algorithm = "AES256"
}
}
}
force_destroy = true
}
๐ Note: Use AES256, not KMS, since CloudFront doesnโt play well with KMS keys.
๐๏ธ Hugo Config Note
To avoid CloudFront returning 404s for folder-based routes like /about, set this in config.toml:
uglyURLs = true
๐ Create HTTPS Certificate
Using the ACM module with DNS validation:
data "aws_route53_zone" "main" {
name = "<root-domain-name>"
}
module "static_website_cert" {
source = "terraform-aws-modules/acm/aws"
version = "5.0.0"
domain_name = "<root-domain-name>"
zone_id = data.aws_route53_zone.main.id
validation_method = "DNS"
subject_alternative_names = ["*.<root-domain-name>"]
wait_for_validation = true
}
๐ Important: Certificates for CloudFront must be created in us-east-1.
๐ Create the CloudFront Distribution
module "official_blog_website_cdn" {
source = "terraform-aws-modules/cloudfront/aws"
version = "3.2.1"
aliases = ["blog.<domain-name>"]
default_root_object = "index.html"
origin_access_control = {
static-website = {
origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
}
origin = {
s3 = {
domain_name = module.static_website.s3_bucket_bucket_regional_domain_name
origin_access_control = "static-website"
}
}
default_cache_behavior = {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "s3"
viewer_protocol_policy = "redirect-to-https"
compress = true
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
forwarded_values = {
query_string = false
cookies = {
forward = "none"
}
}
}
viewer_certificate = {
acm_certificate_arn = module.static_website_cert.acm_certificate_arn
minimum_protocol_version = "TLSv1.2_2021"
ssl_support_method = "sni-only"
}
}
๐ Set Bucket Policy for CloudFront
data "aws_iam_policy_document" "static_website_s3_bucket_policy" {
statement {
actions = ["s3:GetObject"]
resources = ["${module.static_website_s3_bucket.s3_bucket_arn}/*"]
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "aws:SourceArn"
values = [module.static_website_cdn.cloudfront_distribution_arn]
}
}
}
resource "aws_s3_bucket_policy" "static_website_s3_bucket_policy" {
bucket = module.static_website_s3_bucket.s3_bucket_id
policy = data.aws_iam_policy_document.static_website_s3_bucket_policy.json
}
๐ Update DNS with Route53
resource "aws_route53_record" "static_website_cdn" {
zone_id = data.aws_route53_zone.main.zone_id
name = "blog.<domain-name>"
type = "A"
alias {
name = module.static_website_cdn.cloudfront_distribution_domain_name
zone_id = module.static_website_cdn.cloudfront_distribution_hosted_zone_id
evaluate_target_health = false
}
}
Use an ALIAS record to link to CloudFront.
๐ข Deploy the Website
$ hugo -gc --minify
$ aws s3 sync public/ s3://<bucket-name> --delete
Then visit your site and enjoy blazing-fast load times!
โ Conclusion
Youโve just built a robust, scalable, and low-maintenance Hugo blog using AWS and Terraform! From now on, updates are
just a hugo + s3 sync away.
Want more automation? Add a GitHub Actions pipeline next!