How to set up site redirects in CloudFront using Terraform?

How to set up site redirects in CloudFront using Terraform?

ยท

7 min read

If you need to set up a redirect for a domain to a subdomain and wonder how to do it in AWS, here is the answer. In my case, I wanted to redirect my apex domain (cntechy.com) to my blog subdomain (blog.cntechy.com). I also wanted https redirect and www. requests to redirect to the naked subdomain.

IaC tool: Terraform.

AWS services used: S3, Cloudfront, Cloudfront functions, Route 53, ACM.

We could try to do it with S3 alone, by naming the buckets the same as the domain name, but it has several limitations such as the lack of SSL/TLS support.

Cloudfront gives us the SSL/TLS capabilities and Cloudfront functions handle the redirection logic.

Create the S3 bucket

First, we will create a S3 bucket with the apex domain name (cntechy.com). Then, we will block all public access to the bucket. Then, we will enable static site hosting for S3.

# S3 bucket to host website content
resource "aws_s3_bucket" "web_app_bucket" {
  bucket = var.domain_name

  tags = {
    Name        = "s3 bucket for root to blog"
  }
  force_destroy = true
}

# Block all public access
resource "aws_s3_bucket_public_access_block" "s3_bucket_policy" {
  bucket = aws_s3_bucket.web_app_bucket.id

  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

# 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 the OAC and cache policy for 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" "root_redirects_app_policy" {
  name                              = "root to blog redirects OAC"
  description                       = "root to blog redirects OAC policy"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

Create a default cache policy.


# Default cache policy for Cloudfront
resource "aws_cloudfront_cache_policy" "root_redirects_default_cache_policy" {
  name        = "root_redirects-cache-policy"
  comment     = "root_redirects-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 function

The block below will create a CloudFront function which will redirect domains based on the host header of the request.

resource "aws_cloudfront_function" "redirect_apex_to_blog" {
  name    = "redirect_apex_www_www_blog_to_blog_cntechy_com"
  runtime = "cloudfront-js-1.0"
  comment = "redirect apex, www* , www.blog.* to blog.cntechy.com"
  publish = true
  code    = file("${path.module}/code/redirect.js")
}

The code for domain redirection runs in a modified JavaScript runtime which has a limited set of features. Ensure that the code is valid and can run in the runtime. You can read more about it here.

The code shown below will redirect all http/https requests to cntechy.com, www.cntechy.com and www.blog.cntechy.com to blog.cntechy.com while preserving the URI.

function handler(event) {
  // Redirect from apex to subdomain (blog).
  var request = event.request;

  if (request.headers.host) {
    var host = request.headers.host.value;
    if (host === "cntechy.com" || host === "www.cntechy.com" || host === "www.blog.cntechy.com" ) {
      return {
        statusCode: 302,
        statusDescription: "Found",
        headers: {
          location: { value: `https://blog.cntechy.com${request.uri}` }
        },
      };
    }
  }
  return event.request;
}

Generate the certificates in Amazon Certificate Manager

To ensure that Cloudfront can serve https requests, we need to generate an SSL cert for all the domains we are going to serve content through.

In my case, I have 3 domains. My main domain is cntechy.com (which is var.domain_name) is passed in as domain_name argument. The remaining 2 domains will be listed under the subject_alternative_names argument.

It is important to note that the certificate needs to be issued in us-east-1 region as it needs to be used by Cloudfront. Read more about the requirements here.

We will need to validate the certs against the domain to verify that we do indeed own the domain. This is done through the aws_acm_certificate_validation resource block. This resource represents a successful validation of an ACM 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"
  subject_alternative_names = [
    "www.${var.domain_name}", "www.blog.${var.domain_name}"
  ]

  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]
}

Add the DNS records in Route 53

This step is essential to complete the validation for issuance of the certs.

# 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         = data.aws_route53_zone.domain_hosted_id.zone_id

}

Create the CloudFront distribution

Finally, we can create the CloudFront distribution.

While creating the Cloudfront distribution, we need to add all the domains we want to serve under the aliases argument.

We will associate the CloudFront function to the distribution with the function_association block under the default_cache_behavior block. The function will be executed for each incoming request as set up by the value of viewer-request in the event_type argument.

We will associate the certificate to the distribution with the viewer_certificate block.

We will explicitly provide an array of dependencies just to ensure that this distribution is created only after all those dependencies have been created. In our case, these dependencies are the certificate, validation records in Route 53, and the CloudFront function.


#  Cloudfront Distribution for S3
resource "aws_cloudfront_distribution" "root_redirects_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.root_redirects_app_policy.id
  }

  aliases = [var.domain_name, "www.${var.domain_name}", "www.blog.${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.root_redirects_default_cache_policy.id

    viewer_protocol_policy = "redirect-to-https"

    function_association {
      event_type   = "viewer-request"
      function_arn = aws_cloudfront_function.redirect_apex_to_blog.arn
    }
  }

  price_class = "PriceClass_All"

  restrictions {
    geo_restriction {
        restriction_type = "none"
        locations = []
    }
  }

  viewer_certificate {
    acm_certificate_arn = aws_acm_certificate.domain_cert.arn
    ssl_support_method = "sni-only"
    minimum_protocol_version = "TLSv1.2_2021"
  }

  depends_on = [ aws_acm_certificate.domain_cert, aws_route53_record.validation, aws_cloudfront_function.redirect_apex_to_blog ]
}

Create Route 53 records

We will create 3 DNS records to route the domains to Cloudfront. These records will be A records. AWS allows us to use Alias records under A type to point to any AWS resource. We will use the alias block to point to the CloudFront distribution.

# Route 53 record for root domain
resource "aws_route53_record" "staticapp" {
    zone_id = data.aws_route53_zone.domain_hosted_id.zone_id
    name    = var.domain_name
    type    = "A"
    allow_overwrite = true

    alias {
        name = aws_cloudfront_distribution.root_redirects_app_cf.domain_name
        zone_id = aws_cloudfront_distribution.root_redirects_app_cf.hosted_zone_id
        evaluate_target_health = false
    }
}

# Route 53 record for www domain
resource "aws_route53_record" "www_staticapp" {
    zone_id = data.aws_route53_zone.domain_hosted_id.zone_id
    name    = "www.${var.domain_name}"
    type    = "A"
    allow_overwrite = true

    alias {
        name = aws_cloudfront_distribution.root_redirects_app_cf.domain_name
        zone_id = aws_cloudfront_distribution.root_redirects_app_cf.hosted_zone_id
        evaluate_target_health = false
    }
}

#  Route 53 record for www blog domain
resource "aws_route53_record" "www_blog_staticapp" {
    zone_id = data.aws_route53_zone.domain_hosted_id.zone_id
    name    = "www.blog.${var.domain_name}"
    type    = "A"
    allow_overwrite = true

    alias {
        name = aws_cloudfront_distribution.root_redirects_app_cf.domain_name
        zone_id = aws_cloudfront_distribution.root_redirects_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

Verify

After the successful creation of all the resources, in my case, if you navigate to any of the domains cntechy.com, www.cntechy.com, www.blog.cntechy.com, you will be redirected to the blog.cntechy.com.

You can open the Dev Tools and observe the requests being made. If you request the apex naked domain cntechy.com the request is made as an http request. This will be redirected to https domain with 301 status code. This is done by Cloudfront as we have put the viewer_protocol_policy as redirect-to-https.

Then, the https request will be redirected to blog.cntechy.com by the CloudFront distribution. The redirect status code is 302 as set by us in the function. You can verify that it was indeed done by the CloudFront function as we can see the X-Cache header of FunctionGeneratedResponse from cloudfront

Then the request is made to blog.cntechy.com which is successful with a status code of 200.

Github repo

The final code of this article can be found at https://github.com/this-santhoshss/site-redirection-s3-cloudfront

ย