Skip to main content

AWS CloudFront - Static Website Hosting with S3 and Cloudfront, Managed TLS Certificate, Terraform Configuration with Loop for Several Domains

949 words·
AWS CloudFront S3 Terraform AWS Certificate Manager (ACM) Route 53
Table of Contents

CloudFront Terraform Configuration
#

File and Folder Structure
#

The file and folder structure of the Terraform project looks like this:

aws-cloudfront-terraform
├── certificate.tf
├── cloudfront.tf
├── outputs.tf
├── s3_bucket.tf
├── terraform.tf
└── variables.tf

Project Folder
#

# Create Terraform project folder
TF_PROJECT_NAME=aws-cloudfront-terraform
mkdir $TF_PROJECT_NAME && cd $TF_PROJECT_NAME

Terraform Provider
#

  • terraform.tf
# Terraform Provider
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# Provider AWS Region: us-east-1
provider "aws" {
  region = "us-east-1"
}

Variables
#

  • variables.tf
# CloudFront Domains
variable "domains" {
  description = "List of domain names"
  type        = list(string)
  default     = ["cf-dev.jklug.work", "cf-prod.jklug.work"]
}

# Route 53 Hosted Zone ID
variable "route53_zone_id" {
  description = "Route 53 Hosted Zone ID"
  type        = string
  default     = "Z05838622L1DSISmyzone" 
}

Managed TLS Certificate
#

The “aws_acm_certificate_validation” resources is necessary for the dependency of the CloudFront distribution. If the managed certificate is not yet validated, the creation of the CF distribution failes.

  • certificate.tf
# AWS Managed TLS Certificates
resource "aws_acm_certificate" "managed_cert" {
  for_each = toset(var.domains)

  domain_name       = each.key
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

# Route 53 Entries for Certificate Validation
resource "aws_route53_record" "cert_validation" {
  for_each = { for domain in var.domains : domain => tolist(aws_acm_certificate.managed_cert[domain].domain_validation_options) }

  zone_id = var.route53_zone_id
  name    = each.value[0].resource_record_name
  type    = each.value[0].resource_record_type
  records = [each.value[0].resource_record_value]
  ttl     = 300
}

# Certificate Validation Status
resource "aws_acm_certificate_validation" "cert_validation" {
  for_each = aws_acm_certificate.managed_cert

  certificate_arn         = each.value.arn
  validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn if record.name == each.value.domain_name]
}

S3 Buckets
#

  • s3_bucket.tf
# S3 Buckets
resource "aws_s3_bucket" "s3bucket" {
  for_each = toset(var.domains)

  bucket = replace(each.key, ".", "-") # Replace "." with "-" for valid S3 names

  tags = {
    Name = "Bucket for ${each.key}"
  }
}

# Bucket Ownership
resource "aws_s3_bucket_ownership_controls" "s3_ownership" {
  for_each = aws_s3_bucket.s3bucket

  bucket = each.value.id

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

CloudFront Distributions
#

  • cloudfront.tf
# CloudFront Distributions
resource "aws_cloudfront_distribution" "s3_distribution" {
  for_each = aws_s3_bucket.s3bucket

  origin {
    domain_name              = each.value.bucket_regional_domain_name
    origin_access_control_id = aws_cloudfront_origin_access_control.default.id
    origin_id                = each.value.id
  }

  enabled             = true
  is_ipv6_enabled     = false
  comment             = "CloudFront for ${each.key}"
  default_root_object = "index.html"

  aliases = [each.key]

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = each.value.id

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  price_class = "PriceClass_200"

  restrictions {
    geo_restriction {
      restriction_type = "whitelist"
      locations        = ["AT", "DE"]
    }
  }

  tags = {
    Environment = "production"
  }

  depends_on = [aws_acm_certificate_validation.cert_validation]

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.managed_cert[each.key].arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }
}

# Route 53 CNAME Records for CloudFront
resource "aws_route53_record" "cloudfront_cname" {
  for_each = aws_cloudfront_distribution.s3_distribution

  zone_id = var.route53_zone_id
  name    = each.key
  type    = "CNAME"
  ttl     = 300

  records = [each.value.domain_name]

  depends_on = [aws_cloudfront_distribution.s3_distribution]
}


## Permissions

# CloudFront Access Control
resource "aws_cloudfront_origin_access_control" "default" {
  name                              = "s3-oac"
  description                       = "CloudFront access to S3"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

# S3 Bucket Policies for CloudFront Access
resource "aws_s3_bucket_policy" "s3bucket_policy" {
  for_each = aws_s3_bucket.s3bucket

  bucket = each.value.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = {
        Service = "cloudfront.amazonaws.com"
      }
      Action   = "s3:GetObject"
      Resource = "${each.value.arn}/*"
      Condition = {
        StringEquals = {
          "AWS:SourceArn" = aws_cloudfront_distribution.s3_distribution[each.key].arn
        }
      }
    }]
  })
}

Outputs
#

  • outputs.tf
# CloudFront Endpoints
output "cloudfront_endpoints" {
  description = "CloudFront endpoints for each domain"
  value       = { for domain in var.domains : domain => aws_cloudfront_distribution.s3_distribution[domain].domain_name }
}

# S3 Bucket ARNs
output "s3bucket_arns" {
  description = "S3 bucket ARNs for each domain"
  value       = { for domain in var.domains : domain => aws_s3_bucket.s3bucket[domain].arn }
}

# ACM Certificate ARNs
output "acm_certificate_arns" {
  description = "ACM certificate ARNs for each domain"
  value       = { for domain in var.domains : domain => aws_acm_certificate.managed_cert[domain].arn }
}



Apply Configuration
#

Initialize Terraform Project
#

This will download and install the AWS Terraform provider:

# Initialize the Terraform project
terraform init

Validate Configuration Files
#

# Validates the syntax and structure of Terraform configuration files
terraform validate

# Shell output:
Success! The configuration is valid.

Plan the Deployment
#

# Dry run / preview changes before applying them
terraform plan

Apply the Configuration
#

# Create network stack
terraform apply -auto-approve

# Shell output:
Apply complete! Resources: 17 added, 0 changed, 0 destroyed.

Outputs:

acm_certificate_arns = {
  "cf-dev.jklug.work" = "arn:aws:acm:us-east-1:012345678912:certificate/a42f0b20-e5b1-471a-b6e8-b6f0bbe9ab16"
  "cf-prod.jklug.work" = "arn:aws:acm:us-east-1:012345678912:certificate/b013b2e6-8a04-4c5e-bba8-af430d9622da"
}
cloudfront_endpoints = {
  "cf-dev.jklug.work" = "dlll9jcwi43qe.cloudfront.net"
  "cf-prod.jklug.work" = "d1yr80iknb11wq.cloudfront.net"
}
s3bucket_arns = {
  "cf-dev.jklug.work" = "arn:aws:s3:::cf-dev-jklug-work"
  "cf-prod.jklug.work" = "arn:aws:s3:::cf-prod-jklug-work"
}



Test the CloudFront Distributions
#

S3 Buckets
#

Create HTML Files
#

Create some HTML files:

# dev-index.html
cat <<EOF > dev-index.html
<!DOCTYPE html>
<html>

<head>
	<title>jklug.work</title>
</head>

<body>
  <h1>Dev</h1>
  <p>cf-dev.jklug.work</p>
</body>

</html>
EOF
# prod-index.html
cat <<EOF > prod-index.html
<!DOCTYPE html>
<html>

<head>
	<title>jklug.work</title>
</head>

<body>
  <h1>Prod</h1>
  <p>cf-prod.jklug.work</p>
</body>

</html>
EOF

Upload HTML Files to S3 Buckets
#

Upload an index.html files into the S3 Buckets:

# Upload the dev-index.html file
aws s3 cp dev-index.html s3://cf-dev-jklug-work/index.html

# Upload the prod-index.html file
aws s3 cp prod-index.html s3://cf-prod-jklug-work/index.html

Verify CloudFront Distributions
#

Dev Distribution
#

# Curl the CloudFront domain name
curl https://cf-dev.jklug.work

# Shell output:
<!DOCTYPE html>
<html>

<head>
        <title>jklug.work</title>
</head>

<body>
  <h1>Dev</h1>
  <p>cf-dev.jklug.work</p>
</body>

</html>

Prod Distribution
#

# Curl the CloudFront domain name
curl https://cf-prod.jklug.work

# Shell output:
<!DOCTYPE html>
<html>

<head>
        <title>jklug.work</title>
</head>

<body>
  <h1>Prod</h1>
  <p>cf-prod.jklug.work</p>
</body>

</html>



Cleanup
#

Delete HTML Files
#

Remove the index.html file inside the S3 bucket, otherwise it can’t deleted:

# Delete the index.html file: Dev
aws s3 rm s3://cf-dev-jklug-work/index.html

# Delete the index.html file: Prod
aws s3 rm s3://cf-prod-jklug-work/index.html

Delete AWS Resources
#

# Delete resources
terraform destroy -auto-approve



Links #

# Terraform Provider for AWS CloudFront Docu
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution