In this article, I will show you how to deploy a static website using S3, CloudFront. As a bonus, I will also guide you on how to set it up to serve the site through your domain. We are going to use Terraform, an infrastructure as code (IAC) tool to deploy these resources in AWS. Terraform helps in provisioning, modifying, and deleting of resources in the cloud.
Before we begin, we need to have a domain added in AWS Route 53, Terraform installed and an AWS profile set up in your machine.
S3 bucket initialization
The code below creates a S3 bucket with the name declared in the variable domain_name
.
resource "aws_s3_bucket" "web_app_bucket" {
bucket = var.domain_name
tags = {
Name = "s3 bucket for ${var.domain_name}"
}
force_destroy = true
}
The block below will ensure that we block all public access to the S3 bucket.
# Block all public access
resource "aws_s3_bucket_public_access_block" "s3public" {
bucket = aws_s3_bucket.web_app_bucket.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
Enable static website hosting for the S3 bucket.
# Static website configuration
resource "aws_s3_bucket_website_configuration" "web_app_bucket_config" {
bucket = aws_s3_bucket.web_app_bucket.id
index_document {
suffix = "index.html"
}
error_document {
key = "index.html"
}
}
Create a CloudFront distribution
Create an Origin Access Control (OAC) that secures the S3 bucket and makes it accessible only through the CloudFront distribution.
# Origin Access Control resource
resource "aws_cloudfront_origin_access_control" "static_app_policy" {
name = "static app 2 OAC"
description = "static app 2 OAC policy"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
Create a default cache policy. The code below allows all cookies and query strings to pass through while restricting any headers. Modify these values according to your needs.
# Default cache policy for Cloudfront
resource "aws_cloudfront_cache_policy" "static_app_default_cache_policy" {
name = "staticapp2-cache-policy"
comment = "staticapp2-cache-policy"
default_ttl = 50
max_ttl = 100
min_ttl = 1
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config {
cookie_behavior = "all"
}
headers_config {
header_behavior = "none"
}
query_strings_config {
query_string_behavior = "all"
}
enable_accept_encoding_brotli = true
enable_accept_encoding_gzip = true
}
}
Create the CloudFront distribution. We will associate the previously created OAC and default cache policy with the distribution.
Note: We will be updating the code below when we reach the SSL cert section.
# Cloudfront Distribution for S3
resource "aws_cloudfront_distribution" "static_app_cf" {
origin {
domain_name = aws_s3_bucket.web_app_bucket.bucket_regional_domain_name
origin_id = var.cf_s3_origin_id
origin_access_control_id = aws_cloudfront_origin_access_control.static_app_policy.id
}
enabled = true
is_ipv6_enabled = true
comment = var.domain_name
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = var.cf_s3_origin_id
cache_policy_id = aws_cloudfront_cache_policy.static_app_default_cache_policy.id
viewer_protocol_policy = "redirect-to-https"
}
price_class = "PriceClass_All"
restrictions {
geo_restriction {
restriction_type = "none"
locations = []
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}
Create the S3 bucket policy
Create a S3 bucket policy so the files hosted in S3 is accessible only to the CloudFront distribution. First, we generate the IAM policy and then associate that policy with the s3 bucket created in the previous steps. The policy allows read access to all the objects in the bucket only to the CloudFront distribution.
The condition
block ensures that the files in S3 are accessible only if the requestor's source ARN (amazon resource name) matches the ARN of the CloudFront distribution.
# Bucket policy for S3 bucket, only allow from cloudfront distribution
data "aws_iam_policy_document" "web_app_s3_bucket_policy" {
statement {
effect = "Allow"
sid = "CloudFrontAllowRead"
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.web_app_bucket.arn}/*"]
principals {
type = "Service"
identifiers = [ "cloudfront.amazonaws.com"]
}
condition {
test = "ArnEquals"
values = [ aws_cloudfront_distribution.static_app_cf.arn ]
variable = "aws:SourceArn"
}
}
}
# Associate bucket policy to S3 bucket
resource "aws_s3_bucket_policy" "web_app_policy" {
bucket = aws_s3_bucket.web_app_bucket.id
policy = data.aws_iam_policy_document.web_app_s3_bucket_policy.json
}
Create a SSL certificate for our domain name
The aws_acm_certificate
block issues a new SSL cert with our domain name. It will be created by AWS Certificate Manager (ACM).
The aws_acm_certificate_validation
ensures that the SSL certificate created by ACM is validated. This resource provides a way to automate the validation process. It is used in conjunction with the aws_acm_certificate
resource, which represents the ACM certificate request itself. To use this resource block, you specify the aws_acm_certificate
resource's ARN (Amazon Resource Name) as an argument. Terraform then waits for the certificate request to be validated by polling the ACM service. Once the certificate is validated, Terraform completes the provisioning process.
The aws_route53_record
will insert a DNS entry in the Route 53 hosted zone of our domain. The hosted zone id is passed in as a variable. This record is used to validate that we own the domain for which we are issuing the certificate.
# ACM cert for the domain name validation
resource "aws_acm_certificate" "domain_cert" {
provider = aws.us_east_1
domain_name = var.domain_name
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
# validating the certs
resource "aws_acm_certificate_validation" "domain_cert_validation" {
provider = aws.us_east_1
certificate_arn = aws_acm_certificate.domain_cert.arn
validation_record_fqdns = [for record in aws_route53_record.validation : record.fqdn]
}
# record for domain name validation
resource "aws_route53_record" "validation" {
for_each = {
for dvo in aws_acm_certificate.domain_cert.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 = var.hosted_zone_id
}
Modify the CloudFront creation block to associate the SSL cert.
The aliases
will have the domain name.
The viewer_certificate
block is where we associate the SSL cert.
We will also add an explicit depends_on
array so that the CloudFront distribution waits for the SSL cert to be issued and validated.
# Cloudfront Distribution for S3
resource "aws_cloudfront_distribution" "static_app_cf" {
origin {
domain_name = aws_s3_bucket.web_app_bucket.bucket_regional_domain_name
origin_id = var.cf_s3_origin_id
origin_access_control_id = aws_cloudfront_origin_access_control.static_app_policy.id
}
####### added
aliases = [ var.domain_name ]
enabled = true
is_ipv6_enabled = true
comment = var.domain_name
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = var.cf_s3_origin_id
cache_policy_id = aws_cloudfront_cache_policy.static_app_default_cache_policy.id
viewer_protocol_policy = "redirect-to-https"
}
price_class = "PriceClass_All"
restrictions {
geo_restriction {
restriction_type = "none"
locations = []
}
}
####### modified
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.domain_cert.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
####### added
depends_on = [ aws_acm_certificate.domain_cert, aws_route53_record.validation ]
}
Create a DNS record to serve the website through your domain
The route 53 record resource will insert a DNS A
record as an alias which point to the CloudFront distribution.
# Route 53 record for domain
resource "aws_route53_record" "staticapp" {
zone_id = var.hosted_zone_id
name = var.domain_name
type = "A"
allow_overwrite = true
alias {
name = aws_cloudfront_distribution.static_app_cf.domain_name
zone_id = aws_cloudfront_distribution.static_app_cf.hosted_zone_id
evaluate_target_health = false
}
}
Deploy the project in AWS using Terraform
Initialize the terraform.
terraform init
Validate terraform.
terraform validate
Execute terraform plan
command. This command creates an execution plan, which lets you preview the changes that Terraform plans to make.
terraform plan
Execute terraform. When running the command below, you should provide all the required variable values in the terraform.tfvars
file.
terraform apply
Navigate to the newly created S3 bucket in AWS and upload all the static website files. In my case, I have uploaded a simple index.html file.
After uploading the file(s), you should be able to see your website through the domain URL. If you visit the Network tab in the Developer Tools, you should see a Hit from cloudfront
value in the X-cache header. This means that your website was served by CloudFront cache.
Github repo
The final code of this article can be found at https://github.com/this-santhoshss/static-site-terraform-aws