Building High-Performance, Secure Static Websites on a Budget with AWS and Terraform
Tags: aws, architecture, security, frontend
Published: 2024-05-15 17:50:08

## Introduction

In the ever-evolving landscape of the internet, websites have transitioned from simple information pages to complex systems that power businesses and personal platforms. This journey has been marked by numerous technological advancements, from the early days of static web pages served via Netscape to PHP admin to contemporary frameworks brimming with features that make development a breeze.

Today's digital age demands not only functionally rich websites but also ones that are secure, performant, and cost-effective. Amazon Web Services (AWS) has emerged as a leading cloud provider offering tools to meet these needs.

Herein lies the charm of this guide: we'll embark on a clear and concise journey to deploy a robust frontend architecture using AWS, all orchestrated with the mighty Terraform—an open-source Infrastructure as Code (IaC) tool that simplifies and automates deployment.

This walkthrough is tailored for individuals or businesses striving for an efficient and secure online presence without breaking the bank. We will meticulously set up storage with S3, manage domain names with Route 53, handle access with IAM, accelerate content delivery with CloudFront, and shield the site with WAF. 

The best part? No prior extensive knowledge is required—as long as you grasp the basics of AWS services and Terraform, you're good to go. I bet it will be the easiest way for anyone to create a secure, compliant, highly available, and highly performant website from scratch before it becomes a task for AI assistance.

Feel free to leap over to the [completed code repository on GitHub](https://github.com/iKnowJavaScript/terraform-static-server) _(and don't forget to star it!)_ if you're eager to get your hands on the code right away.

## Prerequisites

- Basic knowledge of AWS services and Terraform.
- Terraform installed on your local machine. If not, you can follow this [guide](https://learn.hashicorp.com/tutorials/terraform/install-cli) to install it.
- A domain name, if you choose to use a custom domain

## Terraform Configuration Explained

### Step 1: Define Variables

First, we define variables that will be used throughout our Terraform configuration. These include the name of the application, the environment, and optional custom domain settings. Add these to a new `inputs.tf` file add the values to `inputs.auto.tfvars` file

```lua
variable "name" {
  description = "Name of the application"
  type        = string
}

variable "environment" {
  description = "Name of the environment"
  type        = string
}

variable "hosted_zone_domain" {
  type        = string
  description = "Hosted zone to add domain and CloudFront CNAME to"
  nullable    = true
}

variable "create_custom_domain" {
  type        = bool
  description = "Whether to use a custom domain or not"
  default     = false
}

variable "custom_domain_name" {
  type        = string
  description = "Custom domain name"
  nullable    = true
}
```

### Step 2: Create S3 Bucket

Next, create a `main.tf` file, we'll create an S3 bucket to store our static website content.

```lua
resource "aws_s3_bucket" "static_bucket" {
  bucket = "${var.name}-${var.environment}"
}
```

### Step 3: Create CloudFront Origin Access Identity

The create `cloudfront.tf` file, we then create a CloudFront Origin Access Identity (OAI). This allows CloudFront to get objects from our S3 bucket on our behalf.

```lua
resource "aws_cloudfront_origin_access_identity" "newOAI" {
  comment = "OAI for ${var.name} S3 bucket"
}
```

### Step 4: Create CloudFront Distribution

We create a CloudFront distribution to deliver our content to users. We configure it to use our S3 bucket as the origin and our OAI for access. We also set up caching, HTTPS redirection, and custom error responses. In `cloudfront.tf` file.

```lua
resource "aws_cloudfront_distribution" "static_content_distribution" {
  origin {
    domain_name = aws_s3_bucket.static_bucket.bucket_regional_domain_name
    origin_id   = "S3Origin"

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.newOAI.cloudfront_access_identity_path
    }
  }


  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"
  comment             = "${var.name} - frontend deployment"

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

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
    compress               = true
  }

  custom_error_response {
    error_code            = 404
    response_page_path    = "/index.html"
    response_code         = 200
    error_caching_min_ttl = 300
  }

  custom_error_response {
    error_code            = 403
    response_page_path    = "/index.html"
    response_code         = 200
    error_caching_min_ttl = 300
  }

  price_class = "PriceClass_100"

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  aliases = [var.create_custom_domain ? var.custom_domain_name : null]


  dynamic "viewer_certificate" {
    for_each = var.create_custom_domain ? [1] : []

    content {
      acm_certificate_arn      = module.dns[0].certificate_arn
      ssl_support_method       = "sni-only"
      minimum_protocol_version = "TLSv1.2_2018"
    }
  }


  dynamic "viewer_certificate" {
    for_each = var.create_custom_domain ? [] : [1]

    content {
      cloudfront_default_certificate = true
    }
  }

  web_acl_id = aws_wafv2_web_acl.web_acl.arn
}
```
The CloudFront distribution is configured with cache behavior, custom error responses, and TLS settings. It serves content over HTTPS, redirects HTTP traffic, and compresses content for better performance.
We also depend on the variable `create_custom_domain` to know whether to use a custom domain or a Cloudfront-provisioned random domain name.

### Step 5: DNS Module for Custom Domain
In `cloudfront.tf` file. Add the following.

```lua
module "dns" {
  count = var.create_custom_domain ? 1 : 0

  source = "./modules/dns"

  hosted_zone_domain = var.hosted_zone_domain
  custom_domain_name = var.custom_domain_name
  cloudflare_domain  = aws_cloudfront_distribution.static_content_distribution.domain_name
  cloudflare_zone_id = aws_cloudfront_distribution.static_content_distribution.hosted_zone_id
}
```

If a custom domain is preferred, this module sets up the necessary DNS records and TLS certificate using ACM for SSL/TLS. It associates the custom domain with the CloudFront distribution. [code on GitHub](https://github.com/iKnowJavaScript/terraform-static-server)

### Step 6: Create IAM User and Policy

In `iam-user.tf` file, We create an IAM user and policy to allow full access to our S3 bucket. This user can be used to upload content to the bucket via AWS CLI or GitHub action - Please comment bellow if you need an article on either one.

```lua
resource "aws_iam_user" "s3_user" {
  name = "s3_full_access_user_for_${var.name}"
}

resource "aws_iam_access_key" "s3_user_key" {
  user = aws_iam_user.s3_user.name
}

resource "aws_iam_user_policy" "s3_full_access" {
  name = "s3_full_access"
  user = aws_iam_user.s3_user.name

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "s3:*"
        Effect = "Allow"
        Resource = [
          aws_s3_bucket.static_bucket.arn,
          "${aws_s3_bucket.static_bucket.arn}/*"
        ]
      }
    ]
  })
}
```

### Step 7: Apply S3 Bucket Policy

In `policy.tf` file, We apply a policy to our S3 bucket to allow our CloudFront OAI to get objects and to enforce server-side encryption for all uploaded objects.

```lua
resource "aws_s3_bucket_policy" "s3policyforOAI" {
  bucket = aws_s3_bucket.static_bucket.id

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Action   = ["s3:GetObject"],
        Effect   = "Allow",
        Resource = "${aws_s3_bucket.static_bucket.arn}/*",
        Principal = {
          AWS = "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${aws_cloudfront_origin_access_identity.newOAI.id}"
        }
      },
      {
        "Sid" : "enforce-encryption-method",
        "Effect" : "Deny",
        "Principal" : "*",
        "Action" : "s3:PutObject",
        "Resource" : "${aws_s3_bucket.static_bucket.arn}/*",
        "Condition" : {
          "StringNotEquals" : {
            "s3:x-amz-server-side-encryption" : "AES256"
          }
        }
      }
    ]
  })
}
```

### Step 8: Create WAF Web ACL

Finally, In `waf.tf` file, we create a WAF Web ACL and associate it with our CloudFront distribution to protect our website from common web exploits, managed by AWS rulesets.

```lua
resource "aws_wafv2_web_acl" "web_acl" {
  name        = "${var.name}-waf"
  description = "WAF ACL for ${var.name} CloudFront distribution"
  scope       = "CLOUDFRONT"

  default_action {
    allow {}
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "${var.name}-web-acl-metric"
    sampled_requests_enabled   = true
  }

  rule {
    name     = "AWS-AWSManagedRulesCommonRuleSet"
    priority = 1

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
        rule_action_override {
          name = "SizeRestrictions_BODY"
          action_to_use {
            count {}
          }
        }
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWS-AWSManagedRulesCommonRuleSet"
      sampled_requests_enabled   = true
    }
  }
}

# Associate the WAF Web ACL with the CloudFront distribution
# resource "aws_wafv2_web_acl_association" "waf_assoc" {
#   resource_arn = aws_cloudfront_distribution.static_content_distribution.web_acl_id
#   web_acl_arn  = aws_wafv2_web_acl.web_acl.arn
# }

```
The creation of a WAF ACL adds a strong layer of security to our CloudFront distribution. We configure it with AWS Managed Rules for common threats, which is an excellent starting point for protecting against a wide range of attacks.

## Terraform CMD

To complete these configurations and have everything running, apply your Terraform configuration by executing `terraform apply` in your terminal. This command will provision all the defined resources in your AWS account.

Remember to review the changes before applying them, ensuring that you understand what resources will be created or modified.

```sh
# To initialize Terraform and install required providers
terraform init

# To plan and review the infrastructure changes
terraform plan

# To apply changes and create the infrastructure
terraform apply
```

After you confirm and apply the changes, Terraform will provide outputs with the necessary information such as your website URL, S3 bucket names, and IAM user credentials which you should secure.


### Conclusion

Deploying a high-performing and secure static website on AWS using Terraform can significantly simplify the process of infrastructure management. The power of Infrastructure as Code (IaC) allows you to version control your infrastructure, track changes, and quickly replicate or destroy environments as needed.

In this article, we've outlined the steps to set up your static hosting environment with security and performance best practices in mind. Our approach ensures that your website remains highly available, performant under load, and resilient against common web vulnerabilities at an optimized cost.

Now that you have your website deployed, you can focus on uploading your content, monitoring performance, and enhancing user experience. As your needs evolve, you can update your Terraform configurations to scale your infrastructure or integrate additional services.

Be sure to check out the [GitHub repository](https://github.com/iKnowJavaScript/terraform-static-server) for the complete code and leave a star behind if you found it helpful. If you encounter any issues or have questions, don't hesitate to comment below or open an issue on GitHub. Your contributions to improving the code are welcome!


#### Links;
- [GitHub repository](https://github.com/iKnowJavaScript/terraform-static-server)
- [Terraform AWS Provider Documentation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) 
- [AWS Documentation](https://docs.aws.amazon.com/index.html)
- [Terraform Installation guide](https://learn.hashicorp.com/tutorials/terraform/install-cli)

Happy Terraforming!!!