Building High-Performance, Secure Static Websites on a Budget with AWS and Terraform

RMAG news

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 (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 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

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.

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.

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.

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.

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

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.

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.

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.

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.

# 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 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

Terraform AWS Provider Documentation
AWS Documentation
Terraform Installation guide

Happy Terraforming!!!

Leave a Reply

Your email address will not be published. Required fields are marked *