DEV Community

Cover image for Building High-Performance, Secure Static Websites on a Budget with AWS and Terraform
Omolayo Victor
Omolayo Victor

Posted on • Updated on

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

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
}
Enter fullscreen mode Exit fullscreen mode

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}"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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}/*"
        ]
      }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

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"
          }
        }
      }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

Update:
Checkout a simplified Terraform package based on this here https://github.com/iKnowJavaScript/terraform-aws-complete-static-site

Links;

Happy Terraforming!!!

Top comments (0)