Hosting a Static Website on AWS using Terraform

Hosting a Static Website on AWS using Terraform

Terraform might seem a bit daunting at first, but it quickly becomes addictive. As with any tool, the best way to achieve mastery is through consistent practice. Here is one such practice project that helped me gain deeper insights into Terraform.

Prerequisites

  1. Install terraform.

  2. Create an AWS account.

  3. AWS CLI installed.

Task

Host a static website using AWS S3 and CloudFront Distribution.

Steps:

  1. Configure AWS CLI on the system.

  2. Create S3 bucket.

  3. Create IAM policy to access the S3 bucket.

  4. Create CloudFront Distribution.

  5. Add static pages to S3.

  6. Deploy and test website.

It is best to have some basic understanding of AWS S3 and CloudFront before creating this project. Here S3 bucket will only be used for storing the website files. The website will be hosted using CloudFront.

What is Terraform?

Terraform is an Infrastructure as Code (IaC) tool used to create cloud-based resources, such as AWS infrastructure. To get started with Terraform, there are a few essential files you need to be aware of:

  • main.tf: Contains the main components to be created. While it can include all configurations, for readability, we use separate files as outlined below.

  • provider.tf: Specifies the cloud provider details, user credentials, etc.

  • variables.tf: Defines global variables used across the project.

  • values.tfvars: Specifies values for the global variables.

  • output.tf: Specifies output values to be returned.

The names of the files are irrelevant for this project, this is just a brief overview of the files. For more details, refer to this great article by Spacelift.

To create any resource in terraform we need to refer to the documentation.

How to access resource attributes:

Suppose we have the below resource a.

resource "a" "b"{
attribute1 = ""
attribute2 = ""
}

If we need to access attribute1 of a anywhere in the project we can use the syntax: a.b.attribute1

Short note on using variables:

To make it more challenging we will use variables instead of static values.

  • Syntax to declare variables: ( variables.tf file)

      variable "variable-name" 
      {   type = variable-type
          default = default-value
      }
    
  • Syntax to access the variables:

      var.variable-name
    
  • Syntax to specify values dynamically: ( *.tfvars file )

      variable-name="test"
    
  • Terraform command to use the variables:

      terraform apply -var-file=values.tfvars
    

Implementing the Task

Configure AWS CLI:

We need to configure AWS CLI so that terraform can communicate with AWS and create the resources. Here, I have updated my Access Keys in the ~/.aws/credentials file on my system.

[test]   #profile used in provider.tf
aws_access_key_id = YOUR_ACCESS_KEY_ID
aws_secret_access_key = YOUR_SECRET_ACCESS_KEY
region = YOUR_DEFAULT_REGION
output = json

The Terraform Configuration:

NOTE: You can first create the Terraform files with static hardcoded values, later you can convert these to variables as shown.

Example: Initially S3 resource could be created like so,

resource "aws_s3_bucket" "s3" {
  bucket = "abc-test" #using static values instaed of variables
  tags = {
    Name        = "abc-test" #using static values instaed of variables
    Environment = "dev" #using static values instaed of variables
  }
}

Let’s create the Terraform files:

  1. Create provider.tf file

     provider "aws" {
       region  = "us-east-2"
       profile = "test"
     }
    
  2. Create S3 setup, use s3.tf file

    • Creates an S3 Bucket:

      • Uses variables for the bucket name and tags.

      • Tags include the bucket name and environment.

    • Sets Ownership Controls:

      • Ensures the bucket owner has preferred ownership of objects.
    • Restricts Public Access:

      • Blocks public ACLs and policies.

      • Ignores public ACLs and restricts public bucket access.

    • Defines IAM Policy for CDN Access:

      • Allows only the CDN to access the S3 bucket contents.

      • Specifies actions and resources for the policy.

    • Attaches IAM Policy to S3 Bucket:

      • Applies the defined IAM policy to the S3 bucket.
        #Create an S3 bucket
        resource "aws_s3_bucket" "s3" {
          bucket = var.s3_name #using variables from variables.tf file
          tags = {
            Name        = var.s3_name #using variables from variables.tf file
            Environment = var.env #using variables from variables.tf file
          }
        }
        #Sets Ownership Controls
        resource "aws_s3_bucket_ownership_controls" "s3_owner" {
          bucket = aws_s3_bucket.s3.id
          rule {
            object_ownership = "BucketOwnerPreferred"
          }
        }
        #Restricts public access to the S3 bucket
        resource "aws_s3_bucket_public_access_block" "s3_access" {
          bucket = aws_s3_bucket.s3.id

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

        #IAM policy that allows only the CDN to access the S3 bucket contents
        data "aws_iam_policy_document" "s3_policy" {
          statement {
            actions   = ["s3:GetObject"]
            resources = ["${aws_s3_bucket.s3.arn}/*"]

            principals {
              type        = "AWS"
              identifiers = [aws_cloudfront_origin_access_identity.s3_distribution.iam_arn]
            }
          }
        }
        #Attaches IAM Policy to S3 Bucket
        resource "aws_s3_bucket_policy" "allow_access_from_another_account" {
          bucket = aws_s3_bucket.s3.id
          policy = data.aws_iam_policy_document.s3_policy.json
        }
  1. Create CloudFront setup, use cdn.tf file

    1. Fetch S3 bucket details into Local Variables:

      • s3_origin_id: Sets the origin ID using the S3 bucket name from s3.tf.

      • s3_domain_name: Retrieves the regional domain name of the S3 bucket from s3.tf.

    2. Creates CloudFront Origin Access Identity:

      • Establishes an origin access identity (OAI) for CloudFront to access the S3 bucket. An OAI is a special CloudFront user that gives CloudFront permission to access private content in an S3 bucket. This is used in the s3_origin_config block.
    3. Configures CloudFront Distribution:

      • Origin Settings:

        • Uses the S3 bucket’s domain name and origin ID.

        • Configures S3 origin with the CloudFront origin access identity.

The following settings can be left as default:

  • General Settings:

    • Enables the distribution and IPv6.

    • Sets the default root object to index.html.

  • Default Cache Behavior:

    • Allows various HTTP methods.

    • Specifies cached methods.

    • Sets target origin ID.

    • Configures forwarded values (no query strings, no cookies).

    • Enforces HTTPS by redirecting HTTP requests.

    • Sets TTL values to 0.

  • Price Class:

    • Uses PriceClass_200 for cost management.
  • Restrictions:

    • No geographical restrictions.
  • Tags:

    • Tags the distribution with the environment variable.
  • Viewer Certificate:

    • Uses the default CloudFront certificate.
    1. Handles Custom Error Responses:
  • Configures custom error responses for 403 and 400 errors.

  • Sets a minimum TTL for error caching.

  • Redirects to index.html with a 200 response code for these errors.

    #Defines Local Variables
    locals {
      s3_origin_id   = "${var.s3_name}-origin"
      s3_domain_name = aws_s3_bucket.s3.bucket_regional_domain_name
    }

    #Creates CloudFront Origin Access Identity
    resource "aws_cloudfront_origin_access_identity" "s3_distribution" {}

    #Configures CloudFront Distribution
    resource "aws_cloudfront_distribution" "s3_distribution" {
      origin {
        domain_name              = local.s3_domain_name
        origin_id                = local.s3_origin_id

         s3_origin_config {
          origin_access_identity = aws_cloudfront_origin_access_identity.s3_distribution.cloudfront_access_identity_path
        }
      }

      enabled             = true
      is_ipv6_enabled     = true
      default_root_object = "index.html"

      default_cache_behavior {
        allowed_methods  = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
        cached_methods   = ["GET", "HEAD"]
        target_origin_id = local.s3_origin_id

        forwarded_values {
          query_string = false

          cookies {
            forward = "none"
          }
        }

        viewer_protocol_policy = "redirect-to-https"
        min_ttl                = 0
        default_ttl            = 0
        max_ttl                = 0
      }



      price_class = "PriceClass_200"

      restrictions {
        geo_restriction {
          restriction_type = "none"
        }
      }

      tags = {
        Environment = var.env
      }

      viewer_certificate {
        cloudfront_default_certificate = true
      }
    # Handles Custom Error Responses
    custom_error_response {
        error_code            = 403
        error_caching_min_ttl = 30
        response_code         = 200
        response_page_path    = "/index.html"
      }
      custom_error_response {
        error_code            = 400
        error_caching_min_ttl = 30
        response_code         = 200
        response_page_path    = "/index.html"
      }

    }
  1. Create variables.tf file

     variable "s3_name" {
       type = string
     }
    
     variable "env" {
       type = string
     }
     variable "region" {
       type = string
     }
    
  2. Create values.tf file

     s3_name = "tftest"
     env     = "dev"
     region = "us-east-2"
    

    All the files created above are available here.

    Deploy Terraform:

    Run the terraform commands in the terminal.

     terraform init
     terraform validate
     terraform plan -var-file=values.tfvars
     terraform apply -var-file=values.tfvars -auto-approve
    

Verify in AWS console:

S3

CloudFront

Upload static content into S3:

Create invalidation in CloudFront:

Test the website:

Access the website using the CloudFront URL from the terraform apply command output.

Clean up:

Delete resources using terraform destroy command. Make sure to delete the contents in S3 before running terraform destroy.

Conclusion

Using Terraform to manage your AWS infrastructure can save you time and reduce errors. By using variables and separate configuration files, you can easily manage and scale your resources.

This GitHub repository contains Terraform templates I created for other AWS resources as part of my learning journey.

References:

  1. FreeCodeCamp

  2. Abhishek Veeramalla