AWS Networking with Terraform : Deploying a CloudFront distribution for S3 Static Website
Tags: aws, awscommunitybuilder, terraform, upskilling
Published: 2024-03-08 04:53:34

In a previous blog post, we delved into the mechanics of hosting a static website using Amazon S3. Now, we're taking it up a notch by integrating Amazon CloudFront, a Content Delivery Network (CDN), into our architecture. By doing so, we aim to optimize website performance and elevate user experience to new heights. In this post, we'll walk through the process of seamlessly integrating CloudFront with S3 using Terraform modules.

## What is CloudFront:
Amazon CloudFront is a web service that speeds up distribution of your static and dynamic web content, such as .html, .css, .js, and image files, to your users. CloudFront delivers your content through a worldwide network of data centers called edge locations. When a user requests content that you're serving with CloudFront, the request is routed to the edge location that provides the lowest latency (time delay), so that content is delivered with the best possible performance.

If the content is already in the edge location with the lowest latency, CloudFront delivers it immediately.

If the content is not in that edge location, CloudFront retrieves it from an origin that you've defined—such as an Amazon S3 bucket, a MediaPackage channel, or an HTTP server (for example, a web server) that you have identified as the source for the definitive version of your content.

CloudFront speeds up the distribution of your content by routing each user request through the AWS backbone network to the edge location that can best serve your content.

## Architecture Diagram:
Let's start by outlining the architecture we'll be working with:
![CloudFront Architecture Diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/35eo7cmmr25j6v1ykjy0.png)

## Step 1: Create an S3 bucket with a unique name and host the static website by uploading files.

```terraform
####################################################
# S3 static website bucket
####################################################
resource "aws_s3_bucket" "s3-static-website" {
  bucket = var.bucket_name
  tags = merge(var.common_tags, {
    Name = "${var.naming_prefix}-s3-bucket"
  })
}

####################################################
# S3 public access settings
####################################################
resource "aws_s3_bucket_public_access_block" "static_site_bucket_public_access" {
  bucket = aws_s3_bucket.s3-static-website.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

####################################################
# S3 bucket static website configuration
####################################################
resource "aws_s3_bucket_website_configuration" "static_site_bucket_website_config" {
  bucket = aws_s3_bucket.s3-static-website.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "error.html"
  }
}

####################################################
# Upload files to S3 Bucket 
####################################################
resource "aws_s3_object" "provision_source_files" {
  bucket = aws_s3_bucket.s3-static-website.id

  # webfiles/ is the Directory contains files to be uploaded to S3
  for_each = fileset("${var.source_files}/", "**/*.*")

  key          = each.value
  source       = "${var.source_files}/${each.value}"
  content_type = each.value
}
```

## Step 2: Configure a CloudFront distribution to serve as the CDN for our website.
```terraform
data "aws_s3_bucket" "selected_bucket" {
  bucket = var.s3_bucket_id
}

####################################################
# Create AWS Cloudfront distribution
####################################################
resource "aws_cloudfront_origin_access_control" "cf-s3-oac" {
  name                              = "CloudFront S3 OAC"
  description                       = "CloudFront S3 OAC"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "cf-dist" {
  enabled             = true
  default_root_object = "index.html"

  origin {
    domain_name              = data.aws_s3_bucket.selected_bucket.bucket_regional_domain_name
    origin_id                = var.s3_bucket_id
    origin_access_control_id = aws_cloudfront_origin_access_control.cf-s3-oac.id
  }

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = var.s3_bucket_id
    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }
    viewer_protocol_policy = "allow-all"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  price_class = "PriceClass_All"

  restrictions {
    geo_restriction {
      restriction_type = "whitelist"
      locations        = ["IN", "US", "CA"]
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }

  tags = merge(var.common_tags, {
    Name = "${var.naming_prefix}-cloudfront"
  })
}
```

## Step 3: Update the S3 Bucket policy to allow access from CloudFront.
To ensure that our S3 bucket only allows access through CloudFront, we'll update its bucket policy accordingly.

```terraform
####################################################
# S3 bucket policy to allow access from cloudfront
####################################################
data "aws_iam_policy_document" "s3_bucket_policy" {
  statement {
    actions   = ["s3:GetObject"]
    resources = ["${var.bucket_arn}/*"]
    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }
    condition {
      test     = "StringEquals"
      variable = "AWS:SourceArn"
      values   = [var.cloudfront_distribution_arn]
    }
  }
}

resource "aws_s3_bucket_policy" "static_site_bucket_policy" {
  bucket = var.bucket_id
  policy = data.aws_iam_policy_document.s3_bucket_policy.json
}
```

## Steps to Run Terraform
Follow these steps to execute the Terraform configuration:
```terraform
terraform init
terraform plan 
terraform apply -auto-approve
```

Upon successful completion, Terraform will provide relevant outputs.
```terraform
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.

Outputs:

cloudfront_domain_name = "http://d1rwkmekbjnbkd.cloudfront.net"
```

## Testing the outcome
S3 Bucket

![S3 Bucket](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/obv3ybq7qsv23ni8t4l5.png)

Block public access:

![S3 Block Public Access](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/og9kpn08fdngefcnkude.png)

Static Website setting:

![S3 Static Website Hosting](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wpafw5un8hwqse1q3rp6.png)

CloudFront Distribution:

![CloudFront Distribution](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jfdylovqde9ktws8cpry.png)

CloudFront Distribution Origin as S3 with Origin Access Control OAC:

![CDN S3 Origin with OAC](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c0lc9752ar6dzj4hisnm.png)

S3 Bucket Policy to allow access from cloudfront

![S3 Bucket Policy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nhin1f0c51xlu8c1rgbw.png)

Using cloudfront domain name to access S3 static website:

![Accessing Website](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/taso0o73ptg82xw6n9la.png)

Website accessed from country which is not in allowlist (used soft VPN)

![CF AllowList](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2s3zi5q0yg799ngifdgp.png)

Cache invalidation occurred after TTL timeout and now website has different image (after static website content was updated)

![Cache Invalidation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o04hrz3nzwqe07dkzoy1.png)

## Cleanup
Remember to stop AWS components to avoid large bills.
```terraform
terraform destroy -auto-approve
```

## Notes:
1. Cache Invalidation can be forced from console for a cloudfront distribution
2. Deprecated Origin Access Identity OAI also can be used instead of Origin Access Control OAC

Incorporating Amazon CloudFront into our static website architecture significantly enhances performance and improves user experience. 

In the next module, we'll extend our exploration to leverage CloudFront with EC2 servers, further optimizing content delivery for dynamic web applications.

## Resources:
GitHub Link: https://github.com/chinmayto/terraform-aws-cloudfront-s3
AWS CloudFront: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html