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