Part 2: Complete AWS Infrastructure
In Part 1, we set up automated deployments to S3. While functional, your site is still missing crucial production features: a custom domain, SSL certificate, and global content delivery. In this part, we’ll complete the AWS infrastructure stack using Terraform to create a professional, high-performance hosting solution.
This is Part 2 of our Complete Guide to Hugo on AWS. Make sure you’ve completed Part 1: CI/CD Pipeline first.
Why CloudFront and Route 53?
While you can serve a Hugo site directly from S3, using CloudFront and Route 53 provides significant benefits:
- 🌍 Global performance: CloudFront’s edge locations serve content from the closest geographic location to users
- 🔒 SSL/TLS encryption: Automatic HTTPS with certificates from AWS Certificate Manager
- 🏷️ Custom domains: Professional URLs instead of S3 bucket URLs
- ⚡ Advanced caching: Fine-grained control over cache behavior for different content types
- 🛡️ DDoS protection: Built-in protection against distributed denial-of-service attacks
- 💰 Cost optimization: CloudFront can reduce S3 data transfer costs for high-traffic sites
Architecture overview
Our complete setup will include:
- Route 53 hosted zone for DNS management
- ACM SSL certificate for HTTPS encryption
- S3 bucket for static file storage (from Part 1)
- CloudFront distribution for global content delivery
- Origin Access Control (OAC) for secure S3 access
- Updated IAM policies for CloudFront invalidation
Prerequisites
Before starting, ensure you have:
- Completed Part 1: CI/CD Pipeline
- A domain name that you can configure DNS for
- The existing Terraform state from Part 1
Setting up the complete infrastructure
Let’s extend our Terraform configuration from Part 1. Update your providers to include the us-east-1 region (required for CloudFront certificates):
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "your-terraform-state-bucket"
key = "hugo-infrastructure/terraform.tfstate"
region = "us-east-1"
}
}
# CloudFront requires certificates to be in us-east-1
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
}
provider "aws" {
region = "us-east-2"
}
locals {
domain_name = "example.com"
bucket_name = "example.com"
}
Route 53 hosted zone
First, create the Route 53 hosted zone for your domain:
resource "aws_route53_zone" "main" {
name = local.domain_name
tags = {
Name = local.domain_name
Environment = "production"
}
}
# Output the name servers for domain configuration
output "name_servers" {
description = "Name servers for the domain"
value = aws_route53_zone.main.name_servers
}
ACM SSL certificate
Create an SSL certificate with both the root domain and www subdomain:
resource "aws_acm_certificate" "main" {
provider = aws.us_east_1
domain_name = local.domain_name
subject_alternative_names = ["www.${local.domain_name}"]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
tags = {
Name = local.domain_name
}
}
# DNS validation records
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in aws_acm_certificate.main.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 = aws_route53_zone.main.zone_id
}
# Certificate validation
resource "aws_acm_certificate_validation" "main" {
provider = aws.us_east_1
certificate_arn = aws_acm_certificate.main.arn
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}
S3 bucket configuration
Update your S3 bucket from Part 1 with additional security settings:
resource "aws_s3_bucket" "website" {
bucket = local.bucket_name
tags = {
Name = local.bucket_name
Environment = "production"
}
}
resource "aws_s3_bucket_versioning" "website" {
bucket = aws_s3_bucket.website.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "website" {
bucket = aws_s3_bucket.website.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "website" {
bucket = aws_s3_bucket.website.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
CloudFront distribution
Create a CloudFront distribution with Origin Access Control for secure S3 access:
# Origin Access Control for CloudFront
resource "aws_cloudfront_origin_access_control" "main" {
name = local.domain_name
description = "OAC for ${local.domain_name}"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
resource "aws_cloudfront_distribution" "main" {
origin {
domain_name = aws_s3_bucket.website.bucket_regional_domain_name
origin_access_control_id = aws_cloudfront_origin_access_control.main.id
origin_id = "S3-${local.bucket_name}"
}
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
aliases = [local.domain_name, "www.${local.domain_name}"]
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-${local.bucket_name}"
compress = true
viewer_protocol_policy = "redirect-to-https"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
# Cache behavior for static assets
ordered_cache_behavior {
path_pattern = "*.css"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-${local.bucket_name}"
compress = true
viewer_protocol_policy = "redirect-to-https"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
min_ttl = 31536000
default_ttl = 31536000
max_ttl = 31536000
}
ordered_cache_behavior {
path_pattern = "*.js"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "S3-${local.bucket_name}"
compress = true
viewer_protocol_policy = "redirect-to-https"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
min_ttl = 31536000
default_ttl = 31536000
max_ttl = 31536000
}
# Custom error pages
custom_error_response {
error_code = 404
response_code = 404
response_page_path = "/404.html"
}
price_class = "PriceClass_100" # Use only North America and Europe edge locations
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate_validation.main.certificate_arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
tags = {
Name = local.domain_name
Environment = "production"
}
}
S3 bucket policy for CloudFront
Allow CloudFront to access your S3 bucket:
data "aws_iam_policy_document" "website_bucket_policy" {
statement {
sid = "AllowCloudFrontServicePrincipal"
effect = "Allow"
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.website.arn}/*"]
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "AWS:SourceArn"
values = [aws_cloudfront_distribution.main.arn]
}
}
}
resource "aws_s3_bucket_policy" "website" {
bucket = aws_s3_bucket.website.id
policy = data.aws_iam_policy_document.website_bucket_policy.json
}
Route 53 DNS records
Create DNS records pointing to your CloudFront distribution:
# Root domain A record
resource "aws_route53_record" "main" {
zone_id = aws_route53_zone.main.zone_id
name = local.domain_name
type = "A"
alias {
name = aws_cloudfront_distribution.main.domain_name
zone_id = aws_cloudfront_distribution.main.hosted_zone_id
evaluate_target_health = false
}
}
# WWW subdomain A record
resource "aws_route53_record" "www" {
zone_id = aws_route53_zone.main.zone_id
name = "www.${local.domain_name}"
type = "A"
alias {
name = aws_cloudfront_distribution.main.domain_name
zone_id = aws_cloudfront_distribution.main.hosted_zone_id
evaluate_target_health = false
}
}
Output important values
output "cloudfront_distribution_id" {
description = "CloudFront distribution ID"
value = aws_cloudfront_distribution.main.id
}
output "s3_bucket_name" {
description = "Name of the S3 bucket"
value = aws_s3_bucket.website.bucket
}
output "website_url" {
description = "Website URL"
value = "https://${local.domain_name}"
}
Updating IAM permissions for CloudFront
Now that you have CloudFront set up, you need to add CloudFront invalidation permissions to your deployment policy from Part 1:
resource "aws_iam_policy" "hugo_deploy" {
name = "hugo_deploy"
path = "/service/personal/"
description = "Policy for deploying Hugo site to S3 and invalidating CloudFront"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "S3Operations"
Effect = "Allow"
Action = [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket",
"s3:DeleteObject",
"s3:PutObjectAcl"
]
Resource = [
"arn:aws:s3:::${local.bucket_name}",
"arn:aws:s3:::${local.bucket_name}/*"
]
},
{
Sid = "CloudFrontInvalidation"
Effect = "Allow"
Action = [
"cloudfront:CreateInvalidation"
]
Resource = [
aws_cloudfront_distribution.main.arn
]
}
]
})
}
Deploying the infrastructure
Update your variables: Replace
example.comwith your actual domain name in thelocalsblock.Initialize and apply Terraform:
terraform init terraform plan terraform applyConfigure your domain: After Terraform completes, note the name servers from the output and configure them with your domain registrar.
Update your Hugo configuration: Modify your
config.tomlto use CloudFront invalidation:[deployment] [[deployment.targets]] URL = "s3://your-domain.com?region=us-east-2" cloudFrontDistributionID = "YOUR_DISTRIBUTION_ID"
Testing your setup
After deployment, test your infrastructure:
- DNS propagation: Use
digor online tools to verify your DNS records point to CloudFront - SSL certificate: Visit your site and verify the SSL certificate is valid
- Cache behavior: Check that static assets receive long cache headers
- Global performance: Test your site speed from different geographic locations
Performance optimization tips
- Enable Gzip compression in CloudFront for text-based content
- Use appropriate cache TTLs for different content types
- Consider using CloudFront functions for redirects and header modifications
- Monitor CloudWatch metrics to optimize cache hit ratios
- Use Route 53 health checks for uptime monitoring
Cost considerations
This setup includes several AWS services with different pricing models:
- Route 53: $0.50/month per hosted zone + query charges
- CloudFront: Free tier includes 1TB data transfer and 10M requests
- S3: Storage costs are minimal for static sites
- ACM: SSL certificates are free when used with CloudFront
What’s next?
You now have a production-ready Hugo hosting solution with custom domain, SSL, and global CDN! However, we haven’t yet implemented security protections against malicious traffic.
In Part 3: Security with AWS WAF, we’ll add:
- AWS WAF for web application protection
- Rate limiting and DDoS protection
- IP allowlists/blocklists
- Security monitoring and alerting
Series Navigation
📚 Complete Hugo on AWS Guide
- Overview & Setup
- Part 1: GitHub Actions CI/CD
- Part 2: AWS Infrastructure ← You are here
- Part 3: AWS WAF Security
- Part 4: Monitoring & Operations
Continue to Part 3: Security with AWS WAF →
