<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[MUJOU Blog]]></title><description><![CDATA[MUJOU Blog]]></description><link>https://mujou.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 30 Sep 2026 13:34:32 GMT</lastBuildDate><atom:link href="https://mujou.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Complete Guide to Building Enterprise Static Sites with Terraform: Auto Cache Invalidation, Cross-Account Logging & More]]></title><description><![CDATA[Co-authored with AI
TL;DR
🎯 What: Complete enterprise-grade Terraform module for AWS static sites that solves real-world production challenges other modules ignore.
🚀 Key Problems Solved:

✅ Auto cache invalidation - No more manual CloudFront clear...]]></description><link>https://mujou.hashnode.dev/the-complete-guide-to-building-enterprise-static-sites-with-terraform-auto-cache-invalidation-cross-account-logging-and-more</link><guid isPermaLink="true">https://mujou.hashnode.dev/the-complete-guide-to-building-enterprise-static-sites-with-terraform-auto-cache-invalidation-cross-account-logging-and-more</guid><category><![CDATA[AWS]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[S3]]></category><category><![CDATA[cloudfront]]></category><category><![CDATA[Static Website]]></category><category><![CDATA[aws-cross-account]]></category><category><![CDATA[multiple domains]]></category><dc:creator><![CDATA[Thu San]]></dc:creator><pubDate>Sat, 14 Jun 2025 04:57:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749876752762/d0e6d8f3-3d0c-45b9-b690-cd2a8445924c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Co-authored with AI</em></p>
<h2 id="heading-tldr">TL;DR</h2>
<p>🎯 <strong>What</strong>: Complete enterprise-grade Terraform module for AWS static sites that solves real-world production challenges other modules ignore.</p>
<p>🚀 <strong>Key Problems Solved</strong>:</p>
<ul>
<li><p>✅ <strong>Auto cache invalidation</strong> - No more manual CloudFront clearing</p>
</li>
<li><p>✅ <strong>Cross-account logging</strong> - Enterprise compliance &amp; centralized logs</p>
</li>
<li><p>✅ <strong>Wildcard domains</strong> - Perfect for PR previews (<code>pr123.dev.example.com</code>)</p>
</li>
<li><p>✅ <strong>Enterprise security</strong> - OAC, IAM least-privilege, TLS 1.2+</p>
</li>
</ul>
<p>⚡ <strong>Time Savings</strong>: 5 minutes setup vs 2-3 hours manual configuration</p>
<p>📦 <strong>Get Started</strong>:</p>
<pre><code class="lang-plaintext">source = "thu-san/static-site/aws"
enable_cache_invalidation = true
</code></pre>
<p>🔗 <strong>Registries</strong>: <a target="_blank" href="https://registry.terraform.io/modules/thu-san/static-site/aws/latest">Terraform</a> | <a target="_blank" href="https://search.opentofu.org/module/thu-san/static-site/aws/latest">OpenTofu</a> | <a target="_blank" href="https://github.com/thu-san/terraform-aws-static-site">GitHub</a></p>
<p>📖 <strong>This Guide Covers</strong>: Basic setup → Enterprise features → PR preview deployments → Architecture deep-dive → Troubleshooting</p>
<hr />
<h2 id="heading-introduction">Introduction</h2>
<p>Static website hosting on AWS is deceptively simple on the surface - create an S3 bucket, add a CloudFront distribution, configure some DNS records, and you're done. But in real-world enterprise environments, you quickly run into challenges that turn this "simple" setup into a complex, maintenance-heavy infrastructure.</p>
<p>After managing dozens of static site deployments across different organizations, I identified recurring pain points that existing Terraform modules and manual setups consistently failed to address. This led me to create a comprehensive Terraform module that solves these enterprise challenges with built-in automation and intelligent defaults.</p>
<p>In this comprehensive guide, I'll walk you through everything you need to know about deploying enterprise-grade static sites on AWS using this advanced Terraform module, including real-world use cases, architecture decisions, and step-by-step implementation examples.</p>
<h2 id="heading-the-problem-with-current-solutions">The Problem with Current Solutions</h2>
<h3 id="heading-manual-cache-management-is-broken">Manual Cache Management is Broken</h3>
<p>Most AWS static site setups require manual CloudFront cache invalidation after content updates. This creates several problems:</p>
<ul>
<li><p><strong>Stale Content</strong>: Users see outdated versions until caches expire</p>
</li>
<li><p><strong>Manual Overhead</strong>: DevOps teams waste time on repetitive invalidation tasks</p>
</li>
<li><p><strong>CI/CD Complexity</strong>: Build pipelines need additional invalidation logic</p>
</li>
<li><p><strong>Cost Inefficiency</strong>: Blanket <code>/*</code> invalidations are expensive and unnecessary</p>
</li>
</ul>
<h3 id="heading-enterprise-requirements-are-afterthoughts">Enterprise Requirements are Afterthoughts</h3>
<p>Standard tutorials and modules often ignore enterprise needs:</p>
<ul>
<li><p><strong>Cross-Account Logging</strong>: Security teams need centralized log aggregation</p>
</li>
<li><p><strong>Compliance</strong>: Audit trails across multiple AWS accounts</p>
</li>
<li><p><strong>Wildcard Domains</strong>: PR preview environments and multi-tenant architectures</p>
</li>
<li><p><strong>Security</strong>: Proper IAM boundaries and least-privilege access</p>
</li>
</ul>
<h3 id="heading-maintenance-burden">Maintenance Burden</h3>
<p>Static sites shouldn't require constant attention, but many setups do:</p>
<ul>
<li><p>Certificate renewals and domain validation issues</p>
</li>
<li><p>CloudFront behavior rule conflicts</p>
</li>
<li><p>Security policy updates across environments</p>
</li>
<li><p>Scaling invalidation logic as content grows</p>
</li>
</ul>
<h2 id="heading-the-solution-enterprise-ready-terraform-module">The Solution: Enterprise-Ready Terraform Module</h2>
<p>I've built a Terraform module that addresses these challenges with intelligent automation and enterprise-grade features. Here's what makes it different:</p>
<h3 id="heading-built-in-automatic-cache-invalidation">🔄 Built-in Automatic Cache Invalidation</h3>
<p>Unlike other modules that require separate tools or manual processes, this module includes a complete Lambda-based invalidation system that responds to S3 events in real-time.</p>
<p><strong>How it works:</strong></p>
<ol>
<li><p>S3 bucket events trigger SQS messages when files are uploaded</p>
</li>
<li><p>Lambda function processes events in batches for cost efficiency</p>
</li>
<li><p>Intelligent path mapping determines which CloudFront paths to invalidate</p>
</li>
<li><p>Dead letter queue handles any failed invalidations for debugging</p>
</li>
</ol>
<h3 id="heading-native-cross-account-cloudfront-logging">📊 Native Cross-Account CloudFront Logging</h3>
<p>Enterprise environments often require centralized logging across AWS accounts. This module supports cross-account CloudWatch log delivery out of the box, enabling:</p>
<ul>
<li><p>Security team oversight across multiple development accounts</p>
</li>
<li><p>Centralized compliance and audit trails</p>
</li>
<li><p>Cost optimization through shared logging infrastructure</p>
</li>
<li><p>Simplified access control through dedicated logging accounts</p>
</li>
</ul>
<h3 id="heading-advanced-domain-management">🌐 Advanced Domain Management</h3>
<p>Full wildcard domain support with automatic certificate management makes this module perfect for:</p>
<ul>
<li><p><strong>PR Preview Deployments</strong>: <code>pr123.dev.example.com</code>, <code>pr456.dev.example.com</code></p>
</li>
<li><p><strong>Multi-tenant Applications</strong>: <code>client1.app.example.com</code>, <code>client2.app.example.com</code></p>
</li>
<li><p><strong>Environment Isolation</strong>: <code>staging.example.com</code>, <code>prod.example.com</code></p>
</li>
</ul>
<h3 id="heading-security-first-architecture">🛡️ Security-First Architecture</h3>
<p>Every component follows security best practices:</p>
<ul>
<li><p>Private S3 buckets with CloudFront Origin Access Control (OAC)</p>
</li>
<li><p>Minimum TLS 1.2 enforcement</p>
</li>
<li><p>IAM roles with least-privilege access</p>
</li>
<li><p>All public access blocked on S3 buckets</p>
</li>
</ul>
<h2 id="heading-complete-implementation-guide">Complete Implementation Guide</h2>
<h3 id="heading-basic-setup-getting-started-in-5-minutes">Basic Setup: Getting Started in 5 Minutes</h3>
<p>Let's start with the simplest possible configuration and build up to enterprise features.</p>
<h4 id="heading-step-1-provider-configuration">Step 1: Provider Configuration</h4>
<p>The module requires two AWS providers - one for your primary region and one for <code>us-east-1</code> (required for CloudFront certificates):</p>
<pre><code class="lang-plaintext"># Configure primary provider (your preferred region)
provider "aws" {
  region = "eu-west-1"  # or your preferred region
}

# Configure us-east-1 provider (required for CloudFront)
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}
</code></pre>
<h4 id="heading-step-2-basic-module-configuration">Step 2: Basic Module Configuration</h4>
<pre><code class="lang-plaintext">module "static_site" {
  source  = "thu-san/static-site/aws"
  version = "~&gt; 1.2"

  # Required parameters
  s3_bucket_name               = "my-company-website-bucket"
  cloudfront_distribution_name = "my-company-website"

  # Basic tagging
  tags = {
    Environment = "production"
    Project     = "company-website"
    Owner       = "platform-team"
  }

  providers = {
    aws           = aws
    aws.us_east_1 = aws.us_east_1
  }
}
</code></pre>
<h4 id="heading-step-3-deploy-and-test">Step 3: Deploy and Test</h4>
<pre><code class="lang-bash"><span class="hljs-comment"># Initialize and apply</span>
terraform init
terraform plan
terraform apply

<span class="hljs-comment"># Upload test content</span>
aws s3 cp ./website/ s3://my-company-website-bucket/ --recursive

<span class="hljs-comment"># Access your site</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Your site is available at: <span class="hljs-subst">$(terraform output cloudfront_distribution_domain_name)</span>"</span>
</code></pre>
<p><strong>Result</strong>: You now have a production-ready static site with:</p>
<ul>
<li><p>Private S3 bucket with versioning</p>
</li>
<li><p>CloudFront distribution with optimal caching</p>
</li>
<li><p>HTTPS enforcement and security headers</p>
</li>
<li><p>Automatic compression and HTTP/2</p>
</li>
</ul>
<h3 id="heading-intermediate-setup-custom-domain-with-automatic-dns">Intermediate Setup: Custom Domain with Automatic DNS</h3>
<p>Adding a custom domain with automated certificate management and DNS configuration:</p>
<pre><code class="lang-plaintext">module "static_site" {
  source  = "thu-san/static-site/aws"
  version = "~&gt; 1.2"

  s3_bucket_name               = "my-company-website-bucket"
  cloudfront_distribution_name = "my-company-website"

  # Custom domain configuration
  domain_names     = ["example.com", "www.example.com"]
  hosted_zone_name = "example.com"  # Your Route53 hosted zone

  tags = {
    Environment = "production"
    Project     = "company-website"
  }

  providers = {
    aws           = aws
    aws.us_east_1 = aws.us_east_1
  }
}
</code></pre>
<p><strong>What happens automatically:</strong></p>
<ol>
<li><p>ACM certificate created in <code>us-east-1</code> for both domains</p>
</li>
<li><p>DNS validation records added to Route53</p>
</li>
<li><p>Certificate validation completed automatically</p>
</li>
<li><p>A and AAAA records created pointing to CloudFront</p>
</li>
<li><p>CloudFront configured with custom domain and certificate</p>
</li>
</ol>
<h3 id="heading-advanced-setup-enterprise-features">Advanced Setup: Enterprise Features</h3>
<p>Now let's implement the enterprise features that set this module apart:</p>
<h4 id="heading-auto-cache-invalidation">Auto Cache Invalidation</h4>
<pre><code class="lang-plaintext">module "static_site" {
  source  = "thu-san/static-site/aws"
  version = "~&gt; 1.2"

  s3_bucket_name               = "my-company-website-bucket"
  cloudfront_distribution_name = "my-company-website"
  domain_names                 = ["example.com", "www.example.com"]
  hosted_zone_name            = "example.com"

  # Enable automatic cache invalidation
  enable_cache_invalidation = true
  invalidation_mode        = "custom"

  # Smart invalidation patterns
  invalidation_path_mappings = [
    {
      source_pattern     = "^assets/images/.*"
      invalidation_paths = ["/assets/images/*"]
      description        = "Invalidate image cache on any image upload"
    },
    {
      source_pattern     = "^(index\\.html|about\\.html)$"
      invalidation_paths = ["/*"]
      description        = "Full cache clear on main page changes"
    },
    {
      source_pattern     = "^blog/.*\\.html$"
      invalidation_paths = ["/blog/*"]
      description        = "Invalidate blog section on blog updates"
    }
  ]

  # Optional: Customize Lambda and SQS settings
  invalidation_lambda_config = {
    memory_size         = 256  # Increase for large sites
    timeout            = 600   # 10 minutes for complex invalidations
    log_retention_days = 14    # Keep logs longer for debugging
  }

  invalidation_sqs_config = {
    batch_size           = 50   # Process in smaller batches
    batch_window_seconds = 30   # Faster processing
  }

  providers = {
    aws           = aws
    aws.us_east_1 = aws.us_east_1
  }
}
</code></pre>
<h4 id="heading-cross-account-logging">Cross-Account Logging</h4>
<p>For enterprise environments with centralized logging:</p>
<pre><code class="lang-plaintext"># Assuming you have a centralized logging account
module "static_site" {
  source  = "thu-san/static-site/aws"
  version = "~&gt; 1.2"

  s3_bucket_name               = "my-company-website-bucket"
  cloudfront_distribution_name = "my-company-website"
  domain_names                 = ["example.com"]
  hosted_zone_name            = "example.com"

  # Cross-account logging configuration
  log_delivery_destination_arn = "arn:aws:logs:us-east-1:LOGGING-ACCOUNT-ID:delivery-destination:central-cloudfront-logs"

  # Custom log record fields for enhanced monitoring
  log_record_fields = [
    "timestamp",
    "c-ip",
    "sc-status",
    "cs-method",
    "cs-uri-stem",
    "cs-uri-query",
    "cs-referer",
    "cs-user-agent",
    "edge-location",
    "time-taken"
  ]

  # Custom S3 delivery path for organized logs
  s3_delivery_configuration = [
    {
      suffix_path               = "/cloudfront/{DistributionId}/{yyyy}/{MM}/{dd}/{HH}"
      enable_hive_compatible_path = true  # Better for analytics tools
    }
  ]

  providers = {
    aws           = aws
    aws.us_east_1 = aws.us_east_1
  }
}
</code></pre>
<h2 id="heading-real-world-use-case-pr-preview-deployments">Real-World Use Case: PR Preview Deployments</h2>
<p>One of the most powerful applications of this module is creating automated PR preview environments. Here's a complete implementation:</p>
<h3 id="heading-architecture-overview">Architecture Overview</h3>
<ul>
<li><p><strong>Main site</strong>: <code>example.com</code> serves from S3 root</p>
</li>
<li><p><strong>PR previews</strong>: <code>pr123.dev.example.com</code> serves from <code>/pr123/</code> folder</p>
</li>
<li><p><strong>Automatic routing</strong>: CloudFront function handles subdomain-to-folder mapping</p>
</li>
<li><p><strong>Wildcard SSL</strong>: Single certificate covers all PR subdomains</p>
</li>
</ul>
<h3 id="heading-complete-implementation">Complete Implementation</h3>
<pre><code class="lang-plaintext"># CloudFront function for intelligent PR routing
resource "aws_cloudfront_function" "pr_router" {
  name    = "pr-preview-router"
  runtime = "cloudfront-js-2.0"
  comment = "Routes PR preview requests to appropriate S3 folders"
  publish = true

  code = &lt;&lt;-EOT
    function handler(event) {
      var request = event.request;
      var host = request.headers.host.value;

      // Extract PR number from subdomain (e.g., pr123.dev.example.com)
      var prMatch = host.match(/^pr(\d+)\./);
      if (prMatch) {
        var prNumber = prMatch[1];
        // Prepend PR folder to the URI
        request.uri = '/pr' + prNumber + request.uri;
      }

      // Handle directory requests by appending index.html
      if (request.uri.endsWith('/')) {
        request.uri += 'index.html';
      }

      // Handle missing file extensions for SPA routing
      if (!request.uri.includes('.') &amp;&amp; !request.uri.endsWith('/')) {
        request.uri += '/index.html';
      }

      return request;
    }
  EOT
}

module "pr_preview_site" {
  source  = "thu-san/static-site/aws"
  version = "~&gt; 1.2"

  s3_bucket_name               = "my-company-pr-previews"
  cloudfront_distribution_name = "pr-preview-distribution"

  # Wildcard domain configuration
  domain_names = [
    "dev.example.com",      # Main development site
    "*.dev.example.com"     # Wildcard for PR previews
  ]
  hosted_zone_name = "example.com"

  # Attach the PR routing function
  cloudfront_function_associations = [{
    event_type   = "viewer-request"
    function_arn = aws_cloudfront_function.pr_router.arn
  }]

  # Enable auto-invalidation for rapid PR updates
  enable_cache_invalidation = true
  invalidation_mode        = "direct"  # Simple 1:1 path mapping

  # Subfolder support for better SPA handling
  subfolder_root_object = "index.html"
  default_root_object   = "index.html"

  tags = {
    Environment = "development"
    Project     = "pr-previews"
    Purpose     = "automated-testing"
  }

  providers = {
    aws           = aws
    aws.us_east_1 = aws.us_east_1
  }
}
</code></pre>
<h3 id="heading-cicd-integration">CI/CD Integration</h3>
<p>Here's how you'd integrate this with your CI/CD pipeline:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># GitHub Actions example</span>
<span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">PR</span> <span class="hljs-string">Preview</span>
<span class="hljs-attr">on:</span>
  <span class="hljs-attr">pull_request:</span>
    <span class="hljs-attr">types:</span> [<span class="hljs-string">opened</span>, <span class="hljs-string">synchronize</span>]

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">deploy-preview:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v3</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Build</span> <span class="hljs-string">site</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">run</span> <span class="hljs-string">build</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">to</span> <span class="hljs-string">S3</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          PR_NUMBER=${{ github.event.pull_request.number }}
          aws s3 sync ./dist/ s3://my-company-pr-previews/pr${PR_NUMBER}/ --delete
</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Comment</span> <span class="hljs-string">PR</span> <span class="hljs-string">with</span> <span class="hljs-string">preview</span> <span class="hljs-string">URL</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/github-script@v6</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">script:</span> <span class="hljs-string">|
            const prNumber = context.payload.pull_request.number;
            const previewUrl = `https://pr${prNumber}.dev.example.com`;
</span>
            <span class="hljs-string">github.rest.issues.createComment({</span>
              <span class="hljs-attr">issue_number:</span> <span class="hljs-string">prNumber,</span>
              <span class="hljs-attr">owner:</span> <span class="hljs-string">context.repo.owner,</span>
              <span class="hljs-attr">repo:</span> <span class="hljs-string">context.repo.repo,</span>
              <span class="hljs-attr">body:</span> <span class="hljs-string">`🚀</span> <span class="hljs-attr">Preview deployed:</span> <span class="hljs-string">${previewUrl}`</span>
            <span class="hljs-string">});</span>
</code></pre>
<p><strong>Result</strong>: Every PR automatically gets its own preview URL with instant cache invalidation and HTTPS.</p>
<h2 id="heading-architecture-deep-dive">Architecture Deep Dive</h2>
<h3 id="heading-security-design-decisions">Security Design Decisions</h3>
<h4 id="heading-why-origin-access-control-oac-over-origin-access-identity-oai">Why Origin Access Control (OAC) over Origin Access Identity (OAI)?</h4>
<p>OAC is AWS's newer, more secure method for CloudFront-to-S3 communication:</p>
<ul>
<li><p><strong>Better security</strong>: Uses short-term tokens instead of long-lived credentials</p>
</li>
<li><p><strong>AWS Signature V4</strong>: More robust authentication</p>
</li>
<li><p><strong>Future-proof</strong>: OAI is being phased out by AWS</p>
</li>
</ul>
<h4 id="heading-iam-role-design">IAM Role Design</h4>
<p>The module creates minimal IAM roles with specific purposes:</p>
<pre><code class="lang-plaintext"># Cache invalidation Lambda role (created automatically)
data "aws_iam_policy_document" "lambda_assume_role" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

# Minimal permissions for invalidation
data "aws_iam_policy_document" "lambda_permissions" {
  statement {
    effect = "Allow"
    actions = [
      "cloudfront:CreateInvalidation",
      "cloudfront:GetInvalidation"
    ]
    resources = [aws_cloudfront_distribution.main.arn]
  }

  statement {
    effect = "Allow"
    actions = [
      "sqs:ReceiveMessage",
      "sqs:DeleteMessage",
      "sqs:GetQueueAttributes"
    ]
    resources = [aws_sqs_queue.invalidation.arn]
  }
}
</code></pre>
<h3 id="heading-cost-optimization-strategies">Cost Optimization Strategies</h3>
<h4 id="heading-intelligent-invalidation-batching">Intelligent Invalidation Batching</h4>
<p>The Lambda function implements several cost-saving features:</p>
<ol>
<li><p><strong>Path Deduplication</strong>: Removes duplicate invalidation paths</p>
</li>
<li><p><strong>Wildcard Optimization</strong>: Converts multiple similar paths to wildcards</p>
</li>
<li><p><strong>Batch Processing</strong>: Groups invalidations to minimize API calls</p>
</li>
</ol>
<pre><code class="lang-python"><span class="hljs-comment"># Example of intelligent path optimization (in Lambda)</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">optimize_invalidation_paths</span>(<span class="hljs-params">paths</span>):</span>
    <span class="hljs-string">"""Optimize paths to minimize CloudFront invalidation costs"""</span>

    <span class="hljs-comment"># Remove duplicates</span>
    unique_paths = list(set(paths))

    <span class="hljs-comment"># Group by directory for wildcard opportunities</span>
    directories = {}
    <span class="hljs-keyword">for</span> path <span class="hljs-keyword">in</span> unique_paths:
        dir_path = <span class="hljs-string">'/'</span>.join(path.split(<span class="hljs-string">'/'</span>)[:<span class="hljs-number">-1</span>]) + <span class="hljs-string">'/'</span>
        <span class="hljs-keyword">if</span> dir_path <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> directories:
            directories[dir_path] = []
        directories[dir_path].append(path)

    optimized = []
    <span class="hljs-keyword">for</span> dir_path, dir_paths <span class="hljs-keyword">in</span> directories.items():
        <span class="hljs-keyword">if</span> len(dir_paths) &gt; <span class="hljs-number">3</span>:  <span class="hljs-comment"># Use wildcard if &gt;3 files in directory</span>
            optimized.append(dir_path + <span class="hljs-string">'*'</span>)
        <span class="hljs-keyword">else</span>:
            optimized.extend(dir_paths)

    <span class="hljs-keyword">return</span> optimized[:<span class="hljs-number">1000</span>]  <span class="hljs-comment"># CloudFront max 1000 paths per invalidation</span>
</code></pre>
<h4 id="heading-sqs-batch-processing">SQS Batch Processing</h4>
<p>SQS batching reduces Lambda invocations and costs:</p>
<ul>
<li><p><strong>Batch Window</strong>: Collect events for 60 seconds before processing</p>
</li>
<li><p><strong>Batch Size</strong>: Process up to 100 events per Lambda invocation</p>
</li>
<li><p><strong>Dead Letter Queue</strong>: Handle failures without losing events</p>
</li>
</ul>
<h3 id="heading-monitoring-and-observability">Monitoring and Observability</h3>
<h4 id="heading-cloudwatch-metrics">CloudWatch Metrics</h4>
<p>The module automatically creates useful CloudWatch dashboards and alarms:</p>
<pre><code class="lang-plaintext"># Key metrics to monitor (created automatically)
resource "aws_cloudwatch_metric_alarm" "cache_hit_rate" {
  alarm_name          = "${var.cloudfront_distribution_name}-cache-hit-rate"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "CacheHitRate"
  namespace           = "AWS/CloudFront"
  period              = "300"
  statistic           = "Average"
  threshold           = "80"
  alarm_description   = "This metric monitors CloudFront cache hit rate"

  dimensions = {
    DistributionId = aws_cloudfront_distribution.main.id
  }
}
</code></pre>
<h4 id="heading-lambda-function-monitoring">Lambda Function Monitoring</h4>
<p>Built-in monitoring for the invalidation system:</p>
<ul>
<li><p><strong>Execution Duration</strong>: Track Lambda performance</p>
</li>
<li><p><strong>Error Rate</strong>: Monitor failed invalidations</p>
</li>
<li><p><strong>DLQ Messages</strong>: Alert on systematic failures</p>
</li>
<li><p><strong>Cost Tracking</strong>: Monitor invalidation API costs</p>
</li>
</ul>
<h2 id="heading-troubleshooting-common-issues">Troubleshooting Common Issues</h2>
<h3 id="heading-certificate-validation-failures">Certificate Validation Failures</h3>
<p><strong>Problem</strong>: ACM certificate stuck in "Pending Validation"</p>
<p><strong>Solution</strong>: Ensure your Route53 hosted zone is properly configured:</p>
<pre><code class="lang-plaintext"># Verify hosted zone configuration
data "aws_route53_zone" "main" {
  name = var.hosted_zone_name
}

# Check if hosted zone exists
output "hosted_zone_id" {
  value = data.aws_route53_zone.main.zone_id
}
</code></pre>
<h3 id="heading-cache-invalidation-not-working">Cache Invalidation Not Working</h3>
<p><strong>Problem</strong>: Files uploaded to S3 don't trigger invalidations</p>
<p><strong>Debugging steps</strong>:</p>
<ol>
<li><p><strong>Check SQS Queue</strong>: Verify messages are being sent</p>
</li>
<li><p><strong>Lambda Logs</strong>: Check CloudWatch logs for errors</p>
</li>
<li><p><strong>IAM Permissions</strong>: Ensure Lambda can access CloudFront</p>
</li>
<li><p><strong>S3 Event Notifications</strong>: Verify bucket events are configured</p>
</li>
</ol>
<pre><code class="lang-bash"><span class="hljs-comment"># Debug SQS queue</span>
aws sqs get-queue-attributes \
  --queue-url $(terraform output sqs_queue_url) \
  --attribute-names All

<span class="hljs-comment"># Check Lambda logs</span>
aws logs describe-log-streams \
  --log-group-name $(terraform output lambda_log_group_name)
</code></pre>
<h3 id="heading-cross-account-logging-issues">Cross-Account Logging Issues</h3>
<p><strong>Problem</strong>: Logs not appearing in destination account</p>
<p><strong>Common causes</strong>:</p>
<ol>
<li><p>Incorrect destination ARN</p>
</li>
<li><p>Missing permissions in destination account</p>
</li>
<li><p>Log delivery destination not properly configured</p>
</li>
</ol>
<p><strong>Verification</strong>:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Test log delivery destination</span>
aws logs describe-destinations \
  --destination-name-prefix central-cloudfront-logs
</code></pre>
<h2 id="heading-performance-optimization">Performance Optimization</h2>
<h3 id="heading-cloudfront-configuration">CloudFront Configuration</h3>
<p>The module uses optimized CloudFront settings:</p>
<pre><code class="lang-plaintext"># Optimal caching behaviors (configured automatically)
cache_behavior {
  # Static assets - long cache
  path_pattern           = "/assets/*"
  viewer_protocol_policy = "redirect-to-https"
  cache_policy_id        = data.aws_cloudfront_cache_policy.caching_optimized.id
  compress               = true
}

cache_behavior {
  # HTML files - short cache for faster updates
  path_pattern           = "*.html"
  viewer_protocol_policy = "redirect-to-https"
  cache_policy_id        = data.aws_cloudfront_cache_policy.caching_disabled.id
  compress               = true
}
</code></pre>
<h3 id="heading-s3-optimization">S3 Optimization</h3>
<ul>
<li><p><strong>Transfer Acceleration</strong>: Enabled for faster uploads</p>
</li>
<li><p><strong>Versioning</strong>: Enabled for rollback capability</p>
</li>
<li><p><strong>Lifecycle Policies</strong>: Optional for cost management</p>
</li>
</ul>
<h2 id="heading-migration-from-existing-setups">Migration from Existing Setups</h2>
<h3 id="heading-from-manual-aws-setup">From Manual AWS Setup</h3>
<p>If you have an existing manual S3 + CloudFront setup:</p>
<ol>
<li><p><strong>Inventory current resources</strong>:</p>
<pre><code class="lang-bash"> <span class="hljs-comment"># List existing S3 buckets</span>
 aws s3 ls

 <span class="hljs-comment"># List CloudFront distributions</span>
 aws cloudfront list-distributions
</code></pre>
</li>
<li><p><strong>Import existing resources</strong> (optional):</p>
<pre><code class="lang-bash"> <span class="hljs-comment"># Import S3 bucket</span>
 terraform import module.static_site.aws_s3_bucket.main your-existing-bucket

 <span class="hljs-comment"># Import CloudFront distribution</span>
 terraform import module.static_site.aws_cloudfront_distribution.main DISTRIBUTION_ID
</code></pre>
</li>
<li><p><strong>Plan migration</strong>:</p>
<pre><code class="lang-bash"> terraform plan
</code></pre>
</li>
</ol>
<h3 id="heading-from-other-terraform-modules">From Other Terraform Modules</h3>
<p>Migration strategy depends on the existing module, but generally:</p>
<ol>
<li><p><strong>Parallel deployment</strong>: Deploy new module alongside existing</p>
</li>
<li><p><strong>DNS cutover</strong>: Update Route53 records to point to new distribution</p>
</li>
<li><p><strong>Cleanup</strong>: Remove old resources after verification</p>
</li>
</ol>
<h2 id="heading-advanced-customization">Advanced Customization</h2>
<h3 id="heading-custom-cloudfront-functions">Custom CloudFront Functions</h3>
<p>You can extend the module with custom CloudFront functions:</p>
<pre><code class="lang-plaintext">resource "aws_cloudfront_function" "security_headers" {
  name    = "security-headers"
  runtime = "cloudfront-js-2.0"
  publish = true

  code = &lt;&lt;-EOT
    function handler(event) {
      var response = event.response;
      var headers = response.headers;

      // Add security headers
      headers['strict-transport-security'] = {
        value: 'max-age=31536000; includeSubdomains; preload'
      };
      headers['x-content-type-options'] = { value: 'nosniff' };
      headers['x-frame-options'] = { value: 'DENY' };
      headers['referrer-policy'] = { value: 'strict-origin-when-cross-origin' };

      return response;
    }
  EOT
}

module "static_site" {
  source = "thu-san/static-site/aws"

  # ... other configuration ...

  # Attach custom function
  cloudfront_function_associations = [
    {
      event_type   = "viewer-response"
      function_arn = aws_cloudfront_function.security_headers.arn
    }
  ]
}
</code></pre>
<h3 id="heading-environment-specific-configurations">Environment-Specific Configurations</h3>
<p>Use Terraform workspaces or separate variable files:</p>
<pre><code class="lang-plaintext"># terraform.tfvars.prod
s3_bucket_name = "mycompany-website-prod"
domain_names   = ["example.com", "www.example.com"]
enable_cache_invalidation = true

# terraform.tfvars.staging  
s3_bucket_name = "mycompany-website-staging"
domain_names   = ["staging.example.com"]
enable_cache_invalidation = false  # Save costs in staging
</code></pre>
<h2 id="heading-whats-next">What's Next?</h2>
<h3 id="heading-planned-features">Planned Features</h3>
<p>I'm actively developing additional features:</p>
<ul>
<li><p><strong>Multi-region deployments</strong>: Global content replication</p>
</li>
<li><p><strong>Advanced analytics</strong>: Custom CloudWatch dashboards</p>
</li>
<li><p><strong>Blue-green deployments</strong>: Zero-downtime content updates</p>
</li>
<li><p><strong>Content optimization</strong>: Automatic image compression and WebP conversion</p>
</li>
</ul>
<h3 id="heading-contributing">Contributing</h3>
<p>The module is open source and welcomes contributions:</p>
<ul>
<li><p><strong>GitHub Repository</strong>: <a target="_blank" href="https://github.com/thu-san/terraform-aws-static-site">terraform-aws-static-site</a></p>
</li>
<li><p><strong>Terraform Registry</strong>: <a target="_blank" href="https://registry.terraform.io/modules/thu-san/static-site/aws/latest">thu-san/static-site/aws</a></p>
</li>
<li><p><strong>OpenTofu Registry</strong>: <a target="_blank" href="https://search.opentofu.org/module/thu-san/static-site/aws/latest">thu-san/static-site/aws</a></p>
</li>
</ul>
<h3 id="heading-getting-help">Getting Help</h3>
<ul>
<li><p><strong>Issues</strong>: Report bugs or request features on GitHub</p>
</li>
<li><p><strong>Discussions</strong>: Join the community discussion for questions</p>
</li>
<li><p><strong>Documentation</strong>: Comprehensive examples in the repository</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Building enterprise-grade static sites on AWS doesn't have to be complex or maintenance-heavy. This Terraform module encapsulates years of best practices and lessons learned from production deployments.</p>
<p>The key differentiators - automatic cache invalidation, cross-account logging, and wildcard domain support - solve real-world problems that development teams face every day. Whether you're deploying a simple marketing site or a complex multi-tenant application with PR previews, this module provides the enterprise features you need with the simplicity you want.</p>
<p>Try it out in your next project and let me know how it works for you. I'm always looking for feedback and real-world use cases to continue improving the module.</p>
<hr />
<p><strong>Have you implemented similar enterprise features in your static site deployments? What challenges did you face? Share your experience in the comments below!</strong></p>
]]></content:encoded></item></channel></rss>