Skip to main content

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

875 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
# Bucket Name
variable "bucketname" {
  description   = "S3 Bucket Name"
  type          = string
  default       = "jkw-cf-dev"
}

# CloudFront Alias
variable "cf_alias" {
  description   = "CloudFront Alias Name"
  type          = string
  default       = "jkw-cf-dev.jklug.work"
}


## Managed Certificate

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

# Route 53 Domain Name
variable "domain_name" {
  description   = "Route 53 Domain Name"
  type          = string
  default       = "jkw-cf-dev.jklug.work"
}

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 Certificate
resource "aws_acm_certificate" "managed_cert" {
  domain_name       = var.domain_name
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

# Route 53 Entry for Certificate Validation
resource "aws_route53_record" "cert_validation" {

  for_each = {
    for dvo in aws_acm_certificate.managed_cert.domain_validation_options : dvo.domain_name => dvo
  }

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

# Certificate Validation Status
resource "aws_acm_certificate_validation" "cert_validation" {
  certificate_arn         = aws_acm_certificate.managed_cert.arn
  validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}

S3 Bucket
#

  • s3_bucket.tf
# S3 Bucket
resource "aws_s3_bucket" "s3bucket" {
  bucket = var.bucketname

  tags = {
    Name = "Example-Bucket"
  }
}

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

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}
  • object_ownership = "BucketOwnerEnforced" Bucket owner has full control over uploaded objects in the S3 bucket

CloudFront Distribution
#

  • cloudfront.tf
# CloudFront Distribution
resource "aws_cloudfront_distribution" "s3_distribution" {
  origin {
    domain_name              = aws_s3_bucket.s3bucket.bucket_regional_domain_name
    origin_access_control_id = aws_cloudfront_origin_access_control.default.id
    origin_id                = aws_s3_bucket.s3bucket.id  # Use bucket name as origin_id
  }

  enabled             = true
  is_ipv6_enabled     = false
  comment             = "Some comment"
  default_root_object = "index.html"

  aliases = [var.cf_alias]

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = aws_s3_bucket.s3bucket.id # Use bucket name as origin_id

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https" # Redirect HTTP to HTTPS
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  price_class = "PriceClass_200"  # Limit delivery to North America and Europe

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

  tags = {
    Environment = "production"
  }

  # Wait until certificate is validated
  depends_on = [aws_acm_certificate_validation.cert_validation]

  # Managed TLS Certificate
  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.managed_cert.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }
}


# Route 53 CNAME Record for CloudFront
resource "aws_route53_record" "cloudfront_cname" {
  zone_id = var.route53_zone_id
  name    = var.domain_name
  type    = "CNAME"
  ttl     = 300

  records = [aws_cloudfront_distribution.s3_distribution.domain_name]

  depends_on = [aws_cloudfront_distribution.s3_distribution]
}


## Permissions

# CloudFront AccessControl
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 Access Policy
resource "aws_s3_bucket_policy" "s3bucket_policy" {
  bucket = aws_s3_bucket.s3bucket.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = {
        Service = "cloudfront.amazonaws.com"
      }
      Action   = "s3:GetObject"
      Resource = "${aws_s3_bucket.s3bucket.arn}/*"
      Condition = {
        StringEquals = {
          "AWS:SourceArn" = aws_cloudfront_distribution.s3_distribution.arn
        }
      }
    }]
  })
}

Outputs
#

  • outputs.tf
# CloudFront Endpoint
output "cloudfront_endpoint" {
  description = "CloudFront_Endpoint"
  value       = aws_cloudfront_distribution.s3_distribution.domain_name
}

# S3 Bucket ARN
output "s3bucket_arn" {
  description = "S3 Bucket ARN"
  value       = aws_s3_bucket.s3bucket.arn
}

# Managed Certificate ARN
output "acm_certificate_arn" {
  description = "Managed Certificate ARN"
  value       = aws_acm_certificate.managed_cert.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
#

The deployment takes about 8 minutes.

# Create network stack
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

Outputs:

acm_certificate_arn = "arn:aws:acm:us-east-1:012345678912:certificate/dd579a0e-4a3d-4004-8085-38071adb732f"
cloudfront_endpoint = "dlvw16p5rrfwk.cloudfront.net"
s3bucket_arn = "arn:aws:s3:::jkw-cf-dev"



Test the CloudFront Distribution
#

S3 Bucket
#

HTML File
#

Create an index.html file:

  • index.html
<!DOCTYPE html>
<html>

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

<body>
	<h1>Hi there</h1>
  <p>Some HTML content </p>
</body>

</html>

Upload HTML File to S3 Bucket
#

Copy the index.html file into the S3 Bucket:

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

Verify CloudFront
#

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

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

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

<body>
        <h1>Hi there</h1>
  <p>Some HTML content </p>
</body>

</html>



Cleanup
#

Delete HTML File
#

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

# Delete the index.html file
aws s3 rm s3://jkw-cf-dev/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