Hosting a Static Website using AWS CloudFront and S3 Bucket

CAW Blogs
CAW Engineering Blogs
10 min readFeb 14, 2023

By Aayush Bajaj (CAW Studios)

Introduction

With growing options these days, there are infinite ways to host a site. You could use a basic hosting provider like GitHub Pages or Netlify. This is good enough for smaller sites, as this requires minimal setup, and the site can be live in mere minutes.

But what if the size is much larger and you don’t want to compromise on load times? In that case, using Amazon Web Services (AWS) CloudFront and S3 Bucket is an efficient way to ensure that your website is always available to users with low latency and high performance.

Overview

We will explore how to set up hosting for a static website using AWS CloudFront and S3 Bucket, along with it using CloudTrail to log all changes. Additionally, we will look at the process of provisioning the infrastructure for Static hosting using IaaC tools like Terraform in GitOps, with some shift-left practices in CI pipelines.

Let’s start by getting to know the tools we’ll be using.

Cloud Front

CloudFront is a service by Amazon that speeds up static and dynamic web content distribution. It includes HTML, CSS, JS, images, and other media content.

CloudFront delivers content through a worldwide network of data centers called Edge Locations. This helps reduce Latency, which is the time delay between the user’s request for content and the delivery of that content from the server.

Route 53

Amazon Route 5 is a highly available and scalable Domain Name System (DNS) web service. You can use Route 53 to perform three main functions in any combination: domain registration, DNS routing, and health checking.

Certificate Manager

AWS Certificate Manager (ACM) is a service that lets you easily provision, manage, and deploy public and private Secure Sockets Layer/Transport Layer Security (SSL/TLS) certificates for use with AWS services and your internal connected resources.

Cloud Trail

CloudTrail enables auditing, security monitoring, and operational troubleshooting by tracking user activity and API usage. CloudTrail logs, continuously monitors, and retains account activity related to actions across your AWS infrastructure, giving you control over storage, analysis, and remediation actions.

WAF

AWS WAF is a web application firewall that lets you monitor the HTTP(S) requests that are forwarded to your protected web application resources

S3

Amazon Simple Storage Service (Amazon S3) is an object storage service that offers data scalability, availability, security, and performance. Customers of all sizes and industries can use Amazon S3 to store and protect any amount of data for various use cases, such as data lakes, websites, mobile applications, backup and restore, archive, enterprise applications, IoT devices, and big data analytics.

TerraForm

Terraform is an open-source infrastructure-as-code software tool created by HashiCorp. Users define and provide data center infrastructure using a declarative configuration language known as HashiCorp Configuration Language, or optionally JSON.

GitHub Actions

GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline. You can create workflows that build and test every pull request to your repository, or deploy merged pull requests to production.

The above diagram represents the two pipelines being run to host a static website we are focussing on the infrastructure deployment here. But the deployment.yaml file comes into play after our infra is provisioned; it is used to push the code into the bucket so that the website can be hosted .

Note: Here the builder in deployment.yml refers to any builder for the technology you are working on for example: hugo, npm etc.

Block Diagram

The below diagram explains the high level AWS Architecture diagram including all the AWS services that will be used to host a static website. This diagram depicts the AWS services that will be used to host the static content and the static content is pushed to S3 bucket using CodePipeline.

Flow of actions with Github, Amazon S3, and CloudFront

A high-level overview of Infra Provisioning via Terraform

The above diagram represents an end to end CI pipeline to provision the required AWS infrastructure required for hosting a static website using CloudFront and S3 bucket. The diagram shows the Git Pull Request approach which triggers a GitHub actions job to clone the Terraform code and execute the TF plan after passing the TFsec gating in the CI pipeline. The TFsec gating is added to ensure it meets an organization security standard with regards to static code analysis.

The Github Actions job will run the TF plan and seek an approval from the users to validate the Terraform plan, once approved the TF apply is run again to provision the final infrastructure. The plan output is logged in GH Actions for audit purposes and the required notifications are sent via Email or Slack Notification.

CloudFront + S3 via Terraform & GitHub Actions Pipelines:

The general steps are as follows:

  1. The repo has Terraform code for Cloudfront, S3, AWS WAF, Cloudtrail etc. Read the Readme.md file in the repository for more details.
  2. The first step is to create a s3 bucket where our state file for terraform infrastructure will be saved and set up a backend configuration. Refer Terraform state file documentation on Statefile and State File locking using AWS DynamoDB.

We also need to enable bucket versioning.

3. Once the bucket is created we set up the backend configuration for terraform in backend.tf. Here the bucket parameter contains the name of the bucket and the key parameter contains the name with which the state file will be created.

terraform {
backend "s3" {
bucket = "caw-aws-aps1-demo-s3-tfstate"
key = "s3-backend-demo-infra-module.tfstate"
region = "ap-south-1"
}
}

4. We also set up the provider in provider.tf. Alias is used to set up providers for two different aws regions as WAF needs to be configured in us-east-1. (https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl#scope). For more information on multiple provider configuration — Link

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.45.0"
}
}
}
provider "aws" {
region = "ap-south-1"
access_key = var.access_key_id
secret_key = var.access_key_secret
}


provider "aws" {
alias = "aws-waf-web-acl-provider"
region = "us-east-1"
access_key = var.access_key_id
secret_key = var.access_key_secret
}

5. Creating the bucket for static website hosting in s3.tf :

resource "aws_s3_bucket" "this-s3-bucket" {
bucket = "${local.parent_org_name}-${local.cloud_provider}-${local.region}-${local.environment}-s3-${local.project}-${var.s3_bucket_name}"
force_destroy = true
}
Resource"aws_s3_bucket_website_configuration" "this-s3-website-configuration" {
bucket = var.bucket-name
index_document {
suffix = "index.html"
}
error_document {
key = "index.html"
}
depends_on = [
aws_s3_bucket.this-s3-bucket
]
}
resource "aws_s3_bucket_policy" "this-s3-policy" {
bucket=var.bucket-name
policy = <<POLICY
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AddPerm",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::${var.bucket-name}/*"
}
]
}
POLICY
depends_on = [
aws_s3_bucket.this-s3-bucket
]
}
resource "aws_s3_bucket_acl" "this-s3-acl_policy" {
bucket=var.bucket-name
access_control_policy {
grant {
grantee {
id = data.aws_canonical_user_id.current.id
type = "CanonicalUser"
}
permission = "READ"
}
grant {
grantee {
id = data.aws_canonical_user_id.current.id
type = "CanonicalUser"
}
permission = "READ_ACP"
}
grant {
grantee {
id = data.aws_canonical_user_id.current.id
type = "CanonicalUser"
}
permission = "WRITE"
}
grant {
grantee {
id = data.aws_canonical_user_id.current.id
type = "CanonicalUser"
}
permission = "WRITE_ACP"
}
grant {
grantee {
type = "Group"
uri = "http://acs.amazonaws.com/groups/global/AllUsers"
}
permission = "READ_ACP"
}
grant {
grantee {
type = "Group"
uri = "http://acs.amazonaws.com/groups/global/AllUsers"
}
permission = "READ"
}
owner {
id = data.aws_canonical_user_id.current.id
}
}
}

6. Creation of the CloudFront distribution in cloudfront.tf . Note: These parameters are for demo / blog purpose only follow CloudFront best practices for production use.

resource "aws_cloudfront_distribution" "this-cdn-portal" {
origin {
domain_name = data.aws_s3_bucket.demo-portal-s3-bucket.bucket_domain_name
origin_id = data.aws_s3_bucket.demo-portal-s3-bucket.bucket_domain_name
}
wait_for_deployment = false
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = data.aws_s3_bucket.demo-portal-s3-bucket.bucket_domain_name
compress = true
cache_policy_id = data.aws_cloudfront_cache_policy.cache_policy_cloudfront.id
viewer_protocol_policy = "allow-all"
}
web_acl_id = aws_wafv2_web_acl.this-waf-web-acl.arn
price_class = "PriceClass_All"
viewer_certificate {
cloudfront_default_certificate = true
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
}

7. Createion of WAF in aws-waf.tf:

resource "aws_wafv2_web_acl" "this-waf-web-acl" {
provider = aws.aws-waf-web-acl-provider
name = "${local.parent_org_name}-${local.cloud_provider}-${local.region}-${local.environment}-waf_web_acl"
description = "Bot Control waf aws acl"
scope = "CLOUDFRONT"


default_action {
allow {}
}
rule {
name = "AWS-AWSManagedRulesBotControlRuleSet"
priority = 0
statement {
managed_rule_group_statement {
vendor_name = "AWS"
name = "AWSManagedRulesBotControlRuleSet"
}
}
override_action {
count {}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "AWS-AWSManagedRulesBotControlRuleSet"
}
}
visibility_config {
sampled_requests_enabled = true
cloudwatch_metrics_enabled = true
metric_name = "AWS-AWSManagedRulesBotControlRuleSet"
}
}

8. We create a workflow to rebuild the site and deploy it automatically after each update with main.yml. The workflow will only be triggered if files are modified after a push action in these directories

name: Plan using Terraform
on:
pull_request:
branches:
- develop
paths:
- 'terraform/dev/**'

9. The terraform code is checked for security issues using TFSEC (https://github.com/aquasecurity/tfsec) This is the static code analysis phase of Terraform code.

- name: Clone repo
uses: actions/checkout@master

- name: tfsec
id: tfsec
uses: aquasecurity/tfsec-action@v1.0.0
with:
soft_fail: true

10. More gatings like this can be added to the CI pipeline, for ex: Tfcost, etc which can project approx. cost of the infra being provisioned by Terraform. If the security check fails, the build is halted.

11. Use GitHub Actions Setup Terraform project to run Terraform jobs in GitHub Actions. (https://github.com/hashicorp/setup-terraform.git)

- name: Check out code
uses: actions/checkout@v2


- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.3.7

12. Configure AWS credentials and Initialize Terraform.

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: ap-south-1
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}


- name: Initialize Terraform
id: init
run: |
terraform init \
-backend-config="access_key="${{ secrets.AWS_ACCESS_KEY_ID }}" \
-backend-config="secret_key="${{ secrets.AWS_SECRET_ACCESS_KEY }}"

13. The credentials are being sourced from github secrets, so that sensitive information does not end up on a public repository even by mistake. To set these up head to your repository->settings->secrets and variables->actions.

14. Configure Terraform Plan and Github outputs that comment on the changes to be made in infrastructure.

- name: Plan Terraform
id: plan
continue-on-error: true
run: |
terraform plan -no-color

15. Once we create a new branch and create a pull request.The Pull request triggers a github action which provisions GitHub Action Runners.

git branch checkout -b demo-project
git add .
git commit -m "First commit"
git push origin demo-project

16. Output of the plan is logged in a PR comment automatically.

With the help of an action github script which basically saves the output from previous steps of Tfsec, init and plan and publishes it as a comment shown above.

  - uses: actions/github-script@v6
if: github.event_name == 'pull_request'
env:
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
with:
script: |
const output = `#### Terraform Tfsec 🤖\`${{ steps.tfsec.outcome }}\'
#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`


#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`


<details><summary>Show Plan</summary>


\`\`\`\n
${process.env.PLAN}
\`\`\`


</details>


*Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.tf_actions_working_dir }}\`, Workflow: \`${{ github.workflow }}\`*`;


github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})

17. An Email and a slack notification are sent to reviewers to review the changes.

18. After approval of the reviewers the Pull request is merged and another pipeline is triggered which provisions the infrastructure.

     - name: Apply Terraform
if: steps.plan.outcome == 'success'
id: apply
continue-on-error: true
run: |
terraform destroy -auto-approve

19. After manual approval has been given and the infrastructure changes are applied.

20. Post the infrastructure deployment, route53 entries need to be configured with the ACM Certificate and added to cloudfront. Terraform Code for route 53 is beyond the scope for this blog and can be configured manually.

21. After the Infrastructure has been deployed the next step would include pushing the built code to the s3 bucket so that it can be hosted. For that we need another pipeline where we synchronize the build files of the project to S3

 aws s3 sync --follow-symlinks --delete --no-progress ./dist/apps/<foldername>/ s3://${{ secrets.<bucketname>  }}/

22. And create an invalidation for cloudfront using the following commands.

aws cloudfront create-invalidation --distribution-id ${{secrets.<distribution_idName>}} --paths "/*"

Github Repositoy

References

Conclusion

The combination of Terraform and GitHub Actions has provided a powerful and secure way to set up a static website using AWS CloudFront and S3 buckets. By using Terraform’s Infrastructure as Code (IaC) approach and GitOps practices, the process of provisioning and configuring the necessary resources is automated and secure. Additionally, security checks like TFSEC are added to CI pipelines to ensure the code is secure. This is an efficient solution for anyone looking to set up a static website in a similar way.

This Blog was written by Aayush Bajaj (CAW Studios)

CAW Studios is a Product Engineering Company of 100+ geeks based out of Hyderabad. We run engineering for other start-ups.

--

--

CAW Blogs
CAW Engineering Blogs

CAW Studios is a Product Engineering Company of 100+ geeks based out of Hyderabad. We run engineering for other start-ups.