Deploying a Hugo Website to AWS S3 and CloudFront Using Terraform

DevOps

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

  1. Create your Terraform configuration
provider "aws" {
  region = "us-east-1"
}
  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!


๐Ÿ“š References