Static Website Infrastructure on AWS with Terraform cover image

Static Website Infrastructure on AWS with Terraform

Bahaa Noah • January 19, 2023

AWS

Terraform

IaC

S3

CloudFormation

Route53

ACM

As a software engineer, you likely understand the importance of automation and reproducibility in your workflow. One way to achieve this is by using Terraform, it was one of the technologies that I learned in 2022 and since then it has been the goto tool for me for any infrastructure work. In this post, I'll take you through how to use Terraform to create the necessary resources for hosting a static website on AWS, making your life a lot easier.

Required Utilities

Install these tools before proceeding:

  1. AWS CLI
  2. Terraform - Install Terraform

Configure the AWS CLI with a user that has sufficient privileges to create all the resources we will be using in this blog, and it's always a best practice to give least privileges rather than an admin access. Verify that the CLI can authenticate properly by running aws sts get-caller-identity.

Overview

We will utilize the following services to create static website hosting infrastructure:

  1. AWS S3 Bucket as the hosting and storage for the website files and rearouses.
  2. Route 53 as DNS resolver.
  3. AWS Certificate Manager for securing the website and managing the ssl certificate.
  4. Amazon CloudFront to optimize the performance and improve security.

Using all of these services should be free of charge except for Route 53 will charge you around 00.51$ and in general if you are not in the free tier you will be charged around 3$~4$ per month based on the traffic you are getting.

aws-static-website-hosting-infrastructure-diagram


Setting Up Variables

After setting up the tools, let's create the following environment variables to store commonly used values. First things first we will create our main.tf file and place the following in it.

terraform {
  required_version = "~> 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

locals {

  # change that to your domain name
  domain_name = "bahaanoah.com"

  # I like to tag everything that's being created by terraform
  tags = {
    "terraform" = true
  }
}

# I will be using UAE region but feel free to use whichever you prefer
provider "aws" {
  region = "me-central-1"
}

Create S3 Bucket

At this step we are going to create S3 bucket with public access and add website configuration to it using Resource: aws_s3_bucket .

Create s3.tf file and add the following code:

resource "aws_s3_bucket" "this" {
  bucket        = local.domain_name
  tags          = local.tags
  force_destroy = true
}


resource "aws_s3_bucket_policy" "allow_public_access" {
  bucket = aws_s3_bucket.this.id
  policy = jsonencode(
    {
      Statement = [
        {
          Action    = "s3:GetObject"
          Effect    = "Allow"
          Principal = "*"
          Resource  = "arn:aws:s3:::${local.domain_name}/*"
          Sid       = "Stmt1661600983594"
        },
      ]
      Version = "2012-10-17"
    }
  )
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration {
    status = "Disabled"
  }
}

resource "aws_s3_bucket_website_configuration" "this" {
  bucket = aws_s3_bucket.this.bucket

  index_document {
    suffix = "index.html"
  }
  error_document {
    key = "404/index.html"
  }
}

Once done go ahead and apply what we have created so far.

terraform init
terraform apply

Create Route 53 Hosted Zone

At this step will create a hosted zone to manage our dns records using Resource: aws_route53_zone.

Create route53.tf file and place the code below:

resource "aws_route53_zone" "primary" {
  name = local.domain_name
  tags = local.tags
}

Go ahead and apply the changes.

terraform apply

After applying this change you should see a hosted zone with NS and SOA records created in route 53, copy the NS record values and update your domain nameservers, in my case I have my domain registered in Godaddy all I needed to do is change the default nameservers and use custom nameservers the add the NS values from AWS there.

Issue SSL Certificate

We will issue SSL certificate using AWS Certificate Manager with Resource: aws_acm_certificate and create some DNS records in route 53 to validate it at this step.

Create acm.tf file and place the code below:

# if you are using different region from us-east-1 you will need to do that step 
# because cloudfront only works with certificates issued in us-east-1
provider "aws" {
  alias   = "virginia"
  region  = "us-east-1"
}

resource "aws_acm_certificate" "this" {
  domain_name       = local.domain_name
  validation_method = "DNS"

  tags = local.tags
  lifecycle {
    create_before_destroy = true
  }
  provider = aws.virginia

}

resource "aws_route53_record" "validation" {
  for_each = {
    for dvo in aws_acm_certificate.this.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         = aws_route53_zone.primary.zone_id
  provider        = aws.virginia
  depends_on = [
    aws_acm_certificate.this,
    aws_route53_zone.primary
  ]
}

Go ahead and apply the changes.

terraform apply

If you connected your domain to the NS record from previous step it should take a couple of minutes and your certificate status will change to "Issued"

Create CloudFront

At this step we will create CloudFront with Resource: aws_cloudfront_distribution and connect it to the S3 Bucket we created earlier, then eventually create an A record in the hosted zone we created that's aliased to this cloudfront distribution.

Create cloudfront.tf file and place the code below:

resource "aws_cloudfront_distribution" "this" {
  origin {
    domain_name         = aws_s3_bucket_website_configuration.this.website_endpoint
    origin_id           = aws_s3_bucket_website_configuration.this.website_endpoint
    connection_attempts = 3
    connection_timeout  = 10


    custom_origin_config {
      http_port                = 80
      https_port               = 443
      origin_keepalive_timeout = 5
      origin_protocol_policy   = "http-only"
      origin_read_timeout      = 30
      origin_ssl_protocols = [
        "TLSv1",
        "TLSv1.1",
        "TLSv1.2",
      ]
    }
  }

  enabled         = true
  is_ipv6_enabled = true

  aliases = [local.domain_name]

  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = aws_s3_bucket_website_configuration.this.website_endpoint
    viewer_protocol_policy = "redirect-to-https"
    compress               = true
    min_ttl                = 0
    default_ttl            = 3600 
    max_ttl                = 86400
    smooth_streaming       = false
    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }
  }


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

  tags = local.tags

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

  depends_on = [
    aws_s3_bucket.this,
    aws_acm_certificate.this
  ]
}

Go ahead and apply the changes.

terraform apply

Once applied let's create the DNS record, open route53.tf file and add the code below:

# update the resource name to your domain name
resource "aws_route53_record" "bahaanoah-com" {
  name    = local.domain_name
  type    = "A"
  zone_id = aws_route53_zone.primary.zone_id

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

}

Go ahead and apply the changes.

terraform apply

If you reached that point and got everything to be working well done!, you should have your infrastructure up and running, go ahead and upload your website in the S3 bucket we created and check it out.

Cleanup

If you are not going to use it and was just trying and would like to clean up destroy everything to avoid any charges in the future.

terraform destroy

Conclusion

Terraform is a powerful tool for automating the process of creating and managing infrastructure on AWS. By using the services outlined in this post, you can easily set up a secure and performant static website that is easy to maintain and update. However, it is important to keep in mind that this is just the basic setup and there are many other options and features that can be added and configured for different needs and use cases.

As a next step, I would suggest to have a look at this github repository that's containing the code I used to create the Infrastructure for my website it's pretty much the same as we explained with some extra steps for www redirection. I would also recommend having a look at CloudFront Invalidation to manage your cache better and finally if you are working in a team I suggest to have some CI/CD pipeline to automate your infrastructure deployments with some code review process in place, find out more here Running Terraform in Automation.

Thanks for reading, I hope this was helpful. Please feel free to reach out if you need more help or have any suggestions.