Static Website Infrastructure on AWS with Terraform
Tags: terraform, tutorial, devops, webdev
Published: 2023-02-05 09:44:07

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](https://www.terraform.io/), 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](#required-utilities)

Install these tools before proceeding:

1. [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
2. Terraform - [Install Terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli)

[Configure the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) 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](#overview)

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

1. [AWS S3 Bucket](https://aws.amazon.com/s3/) as the hosting and storage for the website files and rearouses.
2. [Route 53](https://aws.amazon.com/route53/) as DNS resolver.
3. [AWS Certificate Manager](https://aws.amazon.com/certificate-manager/) for securing the website and managing the ssl certificate.
4. [Amazon CloudFront](https://aws.amazon.com/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](https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=*all) you will be charged around 3$~4$ per month based on the traffic you are getting.

![aws-static-website-hosting-infrastructure-diagram](https://res.cloudinary.com/dt5qg3orv/image/upload/q_auto:eco/v1673982520/static-services-chart_iinfau.png)

* * *

### [Setting Up Variables](#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](#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](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/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](#create-route-53-hosted-zone)

At this step will create a hosted zone to manage our dns records using [Resource: aws\_route53\_zone](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/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](https://ae.godaddy.com/) 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](#issue-ssl-certificate)

We will issue SSL certificate using AWS Certificate Manager with [Resource: aws\_acm\_certificate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/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

```

> <mark>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"</mark>

### [Create CloudFront](#create-cloud-front)

At this step we will create CloudFront with [Resource: aws\_cloudfront\_distribution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution) and connect it to the [S3 Bucket](#create-s3-bucket) we created earlier, then eventually create an A record in the [hosted zone](#create-route-53-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](#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](#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](https://github.com/bahaa-noah/terraform-aws-static-website) 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](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html) 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](https://developer.hashicorp.com/terraform/tutorials/automation/automate-terraform).

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