In our previous article, Hosting Static Websites on AWS S3 with Terraform, we mentioned that there are several options for hosting static websites, one of which is Amazon CloudFront. When deciding whether to use CloudFront or AWS S3 to host static websites, consider the following scenarios in which CloudFront might be preferable:
Content Delivery Network (CDN) Capabilities: CloudFront is a CDN, whereas AWS S3 is an object storage service. CloudFront caches content at edge locations around the world, which can significantly reduce latency for users accessing our content from different geographic locations. This makes CloudFront ideal for delivering content with low latency and high transfer speeds.
Customization and Security: CloudFront provides more customization options and advanced security features compared to AWS S3. We can configure various caching behaviors, set up SSL/TLS encryption, limit access to our content using signed URLs or cookies, and integrate with other AWS services like AWS WAF (Web Application Firewall) for added security.
Streaming Support: If our static website includes streaming media content (e.g., videos, audio), CloudFront supports streaming protocols like HLS, DASH, and Smooth Streaming. This enables us to deliver high-quality streaming content to a global audience with low latency.
In CloudFront, there are three fundamental concepts: distributions, origins, and cache behaviors. Let's explore each concept.
Distributions
It represents the configuration and settings for delivering content to end users globally. The distributions are associated with one or more origins and cache behaviors, which define how CloudFront handles requests for specific content and how it caches and forwards those requests to the designated origin.
Origin
An origin is the source of the content that CloudFront delivers to users. This can be any HTTP server that serves our content, such as:
Amazon S3 bucket
Custom origin (HTTP or HTTPS)
When we create a distribution, we can specify one or more origins from which CloudFront retrieves content. When a user requests content, CloudFront fetches the content from the specified origin and caches it at edge locations for subsequent requests.
Cache Behaviors
Cache behaviors define how CloudFront handles incoming requests for specific content (based on URL patterns or other request attributes) and how it caches and forwards those requests to the designated origin.
CloudFront evaluates cache behaviors in the order they are defined in the distribution configuration. When a request arrives, CloudFront matches the request path against the path patterns of each cache behavior. Upon finding a match, it applies the caching rules and settings specified for that behavior.
In this post, we will create a Terraform script to host a static website on Amazon CloudFront, allowing users to access the site through a custom domain and HTTPS protocol.
Pre-requisites
An IAM User with programmatic access.
Install the AWS CLI.
Install Terraform CLI.
An SSL certificate issued in AWS Certificate Manager.
The Static Website
In a site
folder, create an index.html
file with the following content:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Under Construction</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f3f3f3;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
max-width: 600px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
text-align: center;
animation: pulse 1.5s infinite alternate;
}
@keyframes pulse {
0% {
transform: scale(1);
}
100% {
transform: scale(1.05);
}
}
h1 {
color: #333;
}
p {
color: #666;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>Under Construction</h1>
<p>We're working hard to bring you something awesome!</p>
<p>In the meantime, please excuse our appearance as we're in the process of building something amazing. Stay tuned for updates.</p>
<p>Thank you for your patience!</p>
</div>
</body>
</html>
The Terraform Script
Create a main.tf
file with the following content:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.31.0"
}
}
backend "local" {}
}
provider "aws" {
region = "<MY_REGION>"
profile = "<MY_AWS_PROFILE>"
max_retries = 2
}
provider "aws" {
alias = "acm_provider"
region = "us-east-1"
profile = "<MY_AWS_PROFILE>"
}
The second AWS provider is to query the SSL certificate. These need to be created in us-east-1
to have HTTPS between viewers and CloudFront.
locals {
bucket_name = "<MY_BUCKET_NAME>"
hosted_zone_name = "<MY_ROUTE53_HOSTED_ZONE_NAME>"
certificate_domain = "<MY_CERTIFICATE_DOMAIN>"
sub_domain = "<MY_SUB_DOMAIN>"
}
The locals
section can be replaced with variables
if needed.
data "aws_route53_zone" "zone" {
name = local.hosted_zone_name
private_zone = false
}
data "aws_acm_certificate" "certificate" {
domain = local.certificate_domain
statuses = ["ISSUED"]
provider = aws.acm_provider
}
Make sure to use the right provider when querying the SSL certificate.
resource "aws_s3_bucket" "bucket" {
bucket = local.bucket_name
force_destroy = true
}
resource "aws_s3_bucket_public_access_block" "bucket-access-block" {
bucket = aws_s3_bucket.bucket.id
ignore_public_acls = true
block_public_acls = true
restrict_public_buckets = true
block_public_policy = true
}
We create a bucket and enable the Block all public access
option for it.
resource "aws_cloudfront_origin_access_control" "oac" {
name = "OAC for ${local.bucket_name}"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
data "aws_iam_policy_document" "bucket-policy-document" {
statement {
actions = ["S3:GetObject"]
sid = "AllowCloudFrontServicePrincipalReadOnly"
effect = "Allow"
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
resources = [
"${aws_s3_bucket.bucket.arn}/*",
]
condition {
test = "StringEquals"
variable = "AWS:SourceArn"
values = [
"${aws_cloudfront_distribution.s3_distribution.arn}"
]
}
}
}
resource "aws_s3_bucket_policy" "bucket-policy" {
bucket = aws_s3_bucket.bucket.id
policy = data.aws_iam_policy_document.bucket-policy-document.json
}
We set up a policy that provides read access to the bucket from CloudFront using the Origin Access Control (OAC).
resource "aws_cloudfront_distribution" "s3_distribution" {
origin {
domain_name = aws_s3_bucket.bucket.bucket_regional_domain_name
origin_id = "origin-${local.bucket_name}"
origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
}
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "origin-${local.bucket_name}"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "allow-all"
}
price_class = "PriceClass_200"
enabled = true
is_ipv6_enabled = false
default_root_object = "index.html"
aliases = ["${local.sub_domain}.${data.aws_route53_zone.zone.name}"]
viewer_certificate {
acm_certificate_arn = data.aws_acm_certificate.certificate.arn
cloudfront_default_certificate = false
minimum_protocol_version = "TLSv1.2_2021"
ssl_support_method = "sni-only"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
To create a CloudFront distribution with Terraform, we need to use the aws_cloudfront_distribution
resource. We can define one or more origins using the origin
property:
domain_name
: DNS domain name of either the S3 bucket or website of our custom origin.origin_id
: Unique identifier for the origin.origin_access_identity
: CloudFront origin access identity previously created.
A default_cache_behavior
is a set of rules that CloudFront applies when it receives a request for our content that does not match any of the other cache behaviors we specify. To define additional behaviors, use the property ordered_cache_behavior
:
allowed_methods
: Determines which HTTP methods CloudFront processes and forwards to our origin.cached_methods
: Determines whether CloudFront caches the responses to requests using the specified HTTP methods.target_origin_id
: The origin where we want to direct requests when a request matches the path pattern.viewer_protocol_policy
: The protocol that users can use to access the files in the origin. Options includeallow-all
,https-only
, orredirect-to-https
.forwarded_values
: Indicates how CloudFront manages query strings, cookies, and headers.path_pattern
: This pattern determines which requests the cache behavior should apply to (only required for theordered_cache_behavior
).
The SSL configuration for the distribution is defined in the viewer_certificate
property:
acm_certificate_arn
: The ARN of the SSL certificate we want to use.cloudfront_default_certificate
: If we want viewers to request our objects using HTTPS (using the default CloudFront domain name).minimum_protocol_version
: Minimum version of the SSL protocol that we want CloudFront to use for HTTPS connections. Can only be set ifcloudfront_default_certificate = false
.ssl_support_method
: How we want CloudFront to serve HTTPS requests. One ofvip
,sni-only
, orstatic-ip
.
The restriction configuration for the distribution is defined by the restrictions
property. Within restrictions
, there is another property called geo_restriction
:
restriction_type
: Method that we want to use to restrict distribution of our content by country:none
,whitelist
, orblacklist
.
The other set of useful properties are:
enabled
: Whether the distribution is enabled to accept user requests for content.is_ipv6_enabled
: Whether the IPv6 is enabled for the distribution.default_root_object
: Object that we want CloudFront to return when a user requests the root URL.price_class
: Price class for the distribution:PriceClass_All
: Use all edge locations. This is the default option and has the highest cost.PriceClass_200
: Use only edge locations in North America, Europe, South Africa, Hong Kong, Singapore, South Korea, Japan, and India. This option has a lower cost thanPriceClass_All
.PriceClass_100
: Use only edge locations in North America and Europe. This option has the lowest cost.
aliases
: Additional domain names, if any, for the distribution.
resource "aws_route53_record" "record" {
zone_id = data.aws_route53_zone.zone.zone_id
name = "${local.sub_domain}.${data.aws_route53_zone.zone.name}"
type = "A"
alias {
name = aws_cloudfront_distribution.s3_distribution.domain_name
zone_id = aws_cloudfront_distribution.s3_distribution.hosted_zone_id
evaluate_target_health = false
}
}
We create a Route 53 A
record per each aliases
item used in the distribution.
resource "aws_s3_object" "html-files" {
for_each = fileset("./site/", "*.html")
bucket = aws_s3_bucket.bucket.id
key = each.value
content_type = "text/html"
source = "./site/${each.value}"
etag = filemd5("./site/${each.value}")
}
We need to upload our website files. The fileset
function returns a set of file paths that match a specific pattern in a given base directory. AWS S3 assigns a default content type of binary/octet-stream
to any uploaded files, so be sure to set the correct content_type
for each file type.
output "route53_name" {
value = aws_route53_record.record.name
}
We create two outputs to display the URLs that can be used to access our static website. Run the following commands to start the deployment:
terraform init
terraform plan -out app.tfplan
terraform apply 'app.tfplan'
Browse to the output URLs to view the static website up and running (the custom domain may take a few minutes to propagate):
You can see the final main.tf
file here. Thanks, and happy coding.