<?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: Adesanya Adedeji Stephen</title>
    <description>The latest articles on DEV Community by Adesanya Adedeji Stephen (@adedejicloud).</description>
    <link>https://dev.to/adedejicloud</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4029049%2Ff2a677eb-8328-4869-8c32-aee1ee248b3c.jpg</url>
      <title>DEV Community: Adesanya Adedeji Stephen</title>
      <link>https://dev.to/adedejicloud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adedejicloud"/>
    <language>en</language>
    <item>
      <title>Building Self-Healing Infrastructure on AWS: A Hands-On Guide</title>
      <dc:creator>Adesanya Adedeji Stephen</dc:creator>
      <pubDate>Wed, 12 Aug 2026 15:20:22 +0000</pubDate>
      <link>https://dev.to/adedejicloud/building-self-healing-infrastructure-on-aws-a-hands-on-guide-23io</link>
      <guid>https://dev.to/adedejicloud/building-self-healing-infrastructure-on-aws-a-hands-on-guide-23io</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Ever wondered what actually happens when a server crashes in production? Not in theory, but for real. Does your app just die? Does anyone notice? Does anything fix itself?&lt;br&gt;
I decided to stop wondering and build the answer myself. This article walks through a project where I built a small, genuinely self-healing web application on AWS: containers that survive being deliberately broken, a load balancer that routes around failure automatically, a CI/CD pipeline that ships code safely, and alerts that tell me when things go wrong.&lt;br&gt;
By the end of this article, you'll have replicated the whole thing yourself, with your own hands on the keyboard, not just read about it.&lt;br&gt;
Let's get into it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What We're Actually Building&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the bird's eye view before we touch a single line of code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A small Node.js app with a real health check endpoint (not just "am I alive", but "am I actually okay")&lt;/li&gt;
&lt;li&gt;A VPC spread across two AWS Availability Zones, with public and private subnets&lt;/li&gt;
&lt;li&gt;The app running as containers on AWS Fargate (serverless containers, no EC2 servers to babysit)&lt;/li&gt;
&lt;li&gt;An Application Load Balancer that only sends traffic to healthy containers&lt;/li&gt;
&lt;li&gt;A CI/CD pipeline with GitHub Actions that builds and deploys automatically on every push&lt;/li&gt;
&lt;li&gt;CloudWatch dashboards and alarms, wired to email alerts via SNS&lt;/li&gt;
&lt;li&gt;A chaos endpoint to deliberately break things on purpose, so we can watch the system heal itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9qr9j28vi78rfsx6ggj3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9qr9j28vi78rfsx6ggj3.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All of the infrastructure is written in Terraform, so it's repeatable, version-controlled, and destroyable with a single command when you're done experimenting.&lt;/p&gt;

&lt;p&gt;You can find the full, finished code in the repo here: &lt;a href="https://github.com/Adedeji-cloud/resilient-aws-infra" rel="noopener noreferrer"&gt;View the GitHub Repository&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Prerequisites&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before we start, make sure you've got:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account (with a non-root IAM user set up, please don't use root credentials day to day)&lt;/li&gt;
&lt;li&gt;Terraform installed locally&lt;/li&gt;
&lt;li&gt;Docker Desktop installed and running&lt;/li&gt;
&lt;li&gt;The AWS CLI installed and configured (aws configure)&lt;/li&gt;
&lt;li&gt;A GitHub account and a new empty repo to push your code to&lt;/li&gt;
&lt;li&gt;Basic comfort with the command line (we'll be doing a lot of terraform apply)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Quick note throughout this article: anywhere you see the account ID 123456789012, swap it for your own AWS account ID. That number is a placeholder, not a real account.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: The Skeleton&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start with a simple folder structure to keep the application code and the infrastructure code cleanly separated:&lt;/p&gt;

&lt;p&gt;resilient-aws-infra/&lt;br&gt;
├── app/          # The Node.js application + Dockerfile&lt;br&gt;
├── infra/        # All the Terraform code&lt;br&gt;
└── .github/&lt;br&gt;
    └── workflows/  # CI/CD pipeline definition&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi1wg5o6l3j1whstsvkz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi1wg5o6l3j1whstsvkz.png" alt=" " width="800" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;_Keeping app/ and infra/ separate is a small thing, but it pays off. It means your CI/CD pipeline can be told "only rebuild when the app changes", which we'll use later.&lt;br&gt;
_&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: The App&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nothing fancy here. Just a tiny Express app with three routes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;/ — a homepage that says hello&lt;/li&gt;
&lt;li&gt;/health — a real health check, one that actually checks something (memory usage), not just "the process is running"&lt;/li&gt;
&lt;li&gt;/chaos/toggle-unhealthy — a hidden switch we'll use later to deliberately break the app on purpose
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&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;PORT&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="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;forceUnhealthy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;h1&amp;gt;Resilient AWS Infra - it works&amp;lt;/h1&amp;gt;&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="c1"&gt;// A REAL health check - not just "am I running", but "am I actually okay"&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/health&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;memoryUsedMB&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="nf"&gt;memoryUsage&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;heapUsed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&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;isHealthy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;forceUnhealthy&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;memoryUsedMB&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isHealthy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;healthy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;memoryMB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;memoryUsedMB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unhealthy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;memoryMB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;memoryUsedMB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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;forced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;forceUnhealthy&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;// A deliberate "break glass" switch, locked behind a secret header&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHAOS_SECRET&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="nx"&gt;CHAOS_SECRET&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change-me&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/chaos/toggle-unhealthy&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;providedSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&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-Chaos-Secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;providedSecret&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;CHAOS_SECRET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forbidden&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;forceUnhealthy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;forceUnhealthy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;forceUnhealthy&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And the matching package.json:&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;"resilient-aws-infra-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Small demo app for practicing incident response on AWS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&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;"server.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&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;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node server.js"&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;"dependencies"&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;"express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.19.2"&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;That /chaos endpoint might look reckless, but notice it's locked behind a secret header. We'll wire that secret in properly through Terraform later, so it's never sitting around in plain text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Dockerize It&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next, we package the app into a container. Create a Dockerfile inside app/:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--production&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; server.js ./&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FROM node:20-alpine starts from a small, lightweight image that already has Node.js&lt;/li&gt;
&lt;li&gt;WORKDIR /app sets our working folder inside the container&lt;/li&gt;
&lt;li&gt;We copy package.json first and install dependencies before copying the actual code. This is a small trick that speeds up rebuilds, since Docker can reuse the cached dependency layer if only your code changed&lt;/li&gt;
&lt;li&gt;EXPOSE 3000 documents which port the app listens on&lt;/li&gt;
&lt;li&gt;CMD is what runs when the container starts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Heads up on Windows:&lt;/strong&gt; if you're using Notepad to create this file, it'll often save it as Dockerfile.txt instead of Dockerfile. Docker specifically looks for a file named exactly Dockerfile, no extension. Rename it if needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Rename-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Dockerfile.txt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Dockerfile&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the build locally before moving on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; resilient-aws-infra-app &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: The Networking Foundation (VPC)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where we start writing Terraform. Everything from here on lives in the infra/ folder.&lt;/p&gt;

&lt;p&gt;First, tell Terraform which cloud provider and region we're using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# providers.tf&lt;/span&gt;
&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.5.0"&lt;/span&gt;

  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.0"&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="k"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&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;"eu-west-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then define some reusable variables so we're not hardcoding values everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"project_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"resilient-aws-infra"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"aws_region"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"vpc_cidr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"public_subnet_cidrs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.1.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.2.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"private_subnet_cidrs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"10.0.11.0/24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"10.0.12.0/24"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"availability_zones"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eu-west-1a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-1b"&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;Now the actual network. This creates a VPC (your own private slice of AWS), spread across two Availability Zones for resilience, with public subnets (reachable from the internet) and private subnets (where our containers will actually live):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.tf&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_support&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_hostnames&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-vpc"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_internet_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-igw"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&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="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_subnet_cidrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_subnet_cidrs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availability_zones&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;map_public_ip_on_launch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-public-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availability_zones&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&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="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_cidrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_cidrs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availability_zones&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-private-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;availability_zones&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_eip"&lt;/span&gt; &lt;span class="s2"&gt;"nat"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_nat_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;allocation_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_eip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
    &lt;span class="nx"&gt;gateway_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_block&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
    &lt;span class="nx"&gt;nat_gateway_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_nat_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&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="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&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="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&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="nx"&gt;aws_subnet&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="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&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="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&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="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick mental model for the pieces above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public subnets are the front door. Things here can be reached from the internet, like our load balancer.&lt;/li&gt;
&lt;li&gt;Private subnets are the back room. Our actual containers live here, never directly reachable from the outside.&lt;/li&gt;
&lt;li&gt;The Internet Gateway lets public subnets talk to the internet.&lt;/li&gt;
&lt;li&gt;The NAT Gateway lets private subnets reach out (to pull Docker images, for example) without letting the internet reach in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run terraform init, then terraform plan to preview it, and terraform apply to build it for real.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init
terraform plan
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0flpho75uxripqtdd59j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0flpho75uxripqtdd59j.png" alt=" " width="800" height="546"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fin0r3w5qlm7y0sfg6vql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fin0r3w5qlm7y0sfg6vql.png" alt=" " width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxesgfll84wfy0cofjl5j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxesgfll84wfy0cofjl5j.png" alt=" " width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqz3z5dt1ppn325xlm9i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqz3z5dt1ppn325xlm9i.png" alt=" " width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcsshyihfdj11x4uxbu64.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcsshyihfdj11x4uxbu64.png" alt=" " width="800" height="549"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: A Home for Your Docker Image (ECR)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we can run our container in AWS, it needs somewhere to live. That's ECR (Elastic Container Registry), basically a private Docker Hub inside your own AWS account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ecr.tf&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecr_repository"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-app"&lt;/span&gt;
  &lt;span class="nx"&gt;image_tag_mutability&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MUTABLE"&lt;/span&gt;

  &lt;span class="nx"&gt;image_scanning_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;scan_on_push&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="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"ecr_repository_url"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecr_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository_url&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;scan_on_push = true is a nice little freebie: AWS automatically scans every image you push for known vulnerabilities, at no extra cost.&lt;/p&gt;

&lt;p&gt;Apply it, then build and push your image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform apply

&lt;span class="c"&gt;# Authenticate Docker to your new ECR repo&lt;/span&gt;
aws ecr get-login-password &lt;span class="nt"&gt;--region&lt;/span&gt; eu-west-1 | docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; 123456789012.dkr.ecr.eu-west-1.amazonaws.com

&lt;span class="c"&gt;# Build, tag, and push&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; resilient-aws-infra-app &lt;span class="nb"&gt;.&lt;/span&gt;
docker tag resilient-aws-infra-app:latest 123456789012.dkr.ecr.eu-west-1.amazonaws.com/resilient-aws-infra-app:latest
docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/resilient-aws-infra-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F06l4x8zzuf1uzgewxem1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F06l4x8zzuf1uzgewxem1.png" alt=" " width="799" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Security Groups (The Firewall Rules)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we build the load balancer and the containers, let's set up the security groups that control exactly what can talk to what. Think of these as a strict guest list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# security-groups.tf&lt;/span&gt;

&lt;span class="c1"&gt;# The load balancer can be reached by anyone, on port 80&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"alb"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-alb-sg"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow inbound HTTP from the internet"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&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;# The containers can ONLY be reached by the load balancer, on port 3000&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"ecs_service"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-ecs-sg"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow inbound traffic only from the ALB"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb&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="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&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;em&gt;Notice the container security group doesn't open port 3000 to the whole internet, it only trusts traffic coming from the load balancer's own security group. Your containers are never directly exposed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 7: The Load Balancer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the piece that actually makes resilience possible. The Application Load Balancer (ALB) sits in front of your containers, continuously checking their health, and only sends traffic to the ones that pass.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# alb.tf&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-alb"&lt;/span&gt;
  &lt;span class="nx"&gt;internal&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;load_balancer_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"application"&lt;/span&gt;
  &lt;span class="nx"&gt;security_groups&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb&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="nx"&gt;subnets&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&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="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb_target_group"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-tg"&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;target_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ip"&lt;/span&gt;

  &lt;span class="nx"&gt;health_check&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/health"&lt;/span&gt;
    &lt;span class="nx"&gt;port&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"traffic-port"&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;
    &lt;span class="nx"&gt;healthy_threshold&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="nx"&gt;unhealthy_threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="nx"&gt;timeout&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="nx"&gt;interval&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
    &lt;span class="nx"&gt;matcher&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lb_listener"&lt;/span&gt; &lt;span class="s2"&gt;"http"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;load_balancer_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"HTTP"&lt;/span&gt;

  &lt;span class="nx"&gt;default_action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"forward"&lt;/span&gt;
    &lt;span class="nx"&gt;target_group_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"alb_dns_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dns_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;That health_check block is doing the real work here. Every 15 seconds, it hits /health. If it doesn't get a 200 response within 5 seconds, and this happens 3 times in a row, the ALB marks that specific container "unhealthy" and quietly stops sending it traffic. No drama, no downtime for users, just a clean handoff to the containers that are actually working.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 8: ECS (Where the Container Actually Runs)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now for the piece that runs your container. We're using AWS Fargate, which is "serverless containers": you don't manage any EC2 instances, AWS just runs your container for you.&lt;/p&gt;

&lt;p&gt;First, the cluster, the IAM role ECS needs, and somewhere for logs to go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ecs.tf&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_cluster"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-cluster"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"ecs_task_execution"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-ecs-execution-role"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&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;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ecs-tasks.amazonaws.com"&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="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"ecs_task_execution"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_task_execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_log_group"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/ecs/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;retention_in_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, the &lt;strong&gt;Task Definition&lt;/strong&gt;, essentially the recipe for your container: which image to use, how much CPU and memory it needs, and which port it listens on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_task_definition"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;family&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-task"&lt;/span&gt;
  &lt;span class="nx"&gt;requires_compatibilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FARGATE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;network_mode&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"awsvpc"&lt;/span&gt;
  &lt;span class="nx"&gt;cpu&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"256"&lt;/span&gt;
  &lt;span class="nx"&gt;memory&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"512"&lt;/span&gt;
  &lt;span class="nx"&gt;execution_role_arn&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_task_execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;container_definitions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-container"&lt;/span&gt;
      &lt;span class="nx"&gt;image&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;aws_ecr_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository_url&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:latest"&lt;/span&gt;
      &lt;span class="nx"&gt;essential&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="nx"&gt;portMappings&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;containerPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;environment&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;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CHAOS_SECRET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chaos_secret&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;logConfiguration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;logDriver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"awslogs"&lt;/span&gt;
        &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s2"&gt;"awslogs-group"&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
          &lt;span class="s2"&gt;"awslogs-region"&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;
          &lt;span class="s2"&gt;"awslogs-stream-prefix"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ecs"&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="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;A quick note on that &lt;strong&gt;CHAOS_SECRET&lt;/strong&gt; environment variable: it comes from a Terraform variable with no default value, which forces you to supply it yourself in a local &lt;strong&gt;terraform.tfvars&lt;/strong&gt; file, one that's excluded from git with a &lt;strong&gt;.gitignore&lt;/strong&gt; rule. That way the secret never ends up committed to your repo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf (add this)&lt;/span&gt;
&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"chaos_secret"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# terraform.tfvars (never commit this file!)&lt;/span&gt;
&lt;span class="nx"&gt;chaos_secret&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"some-long-random-string-only-you-know"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .gitignore
.terraform/
*.tfstate
*.tfstate.*
*.tfvars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, the &lt;strong&gt;ECS Service&lt;/strong&gt;, the piece that actually keeps containers running, places them in the private subnets, and registers them with the load balancer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_service"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-service"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecs_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;task_definition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecs_task_definition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;desired_count&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="nx"&gt;launch_type&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"FARGATE"&lt;/span&gt;

  &lt;span class="nx"&gt;deployment_circuit_breaker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enable&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;rollback&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="nx"&gt;network_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;subnets&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&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="nx"&gt;id&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_service&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="nx"&gt;assign_public_ip&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="nx"&gt;load_balancer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;target_group_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="nx"&gt;container_name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-container"&lt;/span&gt;
    &lt;span class="nx"&gt;container_port&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_lb_listener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&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;Two details worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;desired_count = 2 is the actual resilience piece. If one container crashes or an entire Availability Zone has a bad day, the other one keeps serving traffic while ECS quietly replaces the failed one.&lt;/li&gt;
&lt;li&gt;deployment_circuit_breaker protects you from a different kind of failure: a genuinely broken deploy. If a new version of your code crashes on startup, ECS will detect that the deployment isn't working and automatically roll back to the last known-good version. We'll test this properly later on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apply everything and check that the app responds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform apply

curl http://&amp;lt;your-alb-dns-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzvjxt53smxpxi7etur9f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzvjxt53smxpxi7etur9f.png" alt=" " width="800" height="195"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 9: Watching It Heal Itself&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the fun part. Let's actually break something on purpose and watch the system fix itself.&lt;/p&gt;

&lt;p&gt;Open a second terminal and poll the app continuously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$true&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="kr"&gt;try&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="nv"&gt;$r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Invoke-WebRequest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-UseBasicParsing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://&amp;lt;your-alb-dns-name&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TimeoutSec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'HH:mm:ss'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; - Status: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StatusCode&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Green&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="kr"&gt;catch&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="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'HH:mm:ss'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; - FAILED"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ForegroundColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Red&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="n"&gt;Start-Sleep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Seconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&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;Then, in your original terminal, trigger the chaos endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Chaos-Secret: your-actual-secret"&lt;/span&gt; http://&amp;lt;your-alb-dns-name&amp;gt;/chaos/toggle-unhealthy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This flips one container into "I am not okay" mode. Watch your polling window, and check the target health directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws elbv2 describe-target-health &lt;span class="nt"&gt;--target-group-arn&lt;/span&gt; &amp;lt;your-target-group-arn&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what happens behind the scenes, roughly on this timeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The broken container starts failing its /health checks&lt;/li&gt;
&lt;li&gt;After 3 failed checks in a row (about 45 seconds), the ALB marks it unhealthy and stops routing to it&lt;/li&gt;
&lt;li&gt;ECS notices the task is unhealthy too, and replaces it with a fresh container&lt;/li&gt;
&lt;li&gt;The new container boots up healthy and rejoins the group&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Meanwhile, your other container has been serving every single request the entire time. That's the whole point of running more than one replica behind a health-checked load balancer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5gn19tzkjkxxi3pjrac0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5gn19tzkjkxxi3pjrac0.png" alt=" " width="799" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're feeling bold, you can even break both containers at the same time by calling the chaos endpoint twice in a row. In my own testing, ECS launched two replacement containers in parallel and had a healthy one back online in under 20 seconds. Genuinely zero visible downtime, even under a double failure.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 10: Seeing What's Happening (CloudWatch)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Breaking things is fun, but you also want to actually see it happening without digging through CLI commands every time. Let's add a CloudWatch dashboard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# monitoring.tf&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_dashboard"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;dashboard_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-dashboard"&lt;/span&gt;

  &lt;span class="nx"&gt;dashboard_body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;widgets&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;type&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"metric"&lt;/span&gt;
        &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
        &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;title&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Healthy vs Unhealthy Targets"&lt;/span&gt;
          &lt;span class="nx"&gt;view&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"timeSeries"&lt;/span&gt;
          &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;
          &lt;span class="nx"&gt;metrics&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="s2"&gt;"AWS/ApplicationELB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HealthyHostCount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"TargetGroup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn_suffix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"LoadBalancer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn_suffix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Healthy"&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"AWS/ApplicationELB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"UnHealthyHostCount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"TargetGroup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn_suffix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"LoadBalancer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn_suffix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Unhealthy"&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
          &lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="nx"&gt;period&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&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="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;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbherpi45tlqo7cqon45j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbherpi45tlqo7cqon45j.png" alt=" " width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, let's get alerted the moment something actually breaks, rather than having to notice it ourselves. This uses an SNS topic (basically a notification channel) with an email subscription, plus a CloudWatch alarm that watches for unhealthy targets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sns_topic"&lt;/span&gt; &lt;span class="s2"&gt;"alerts"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-alerts"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sns_topic_subscription"&lt;/span&gt; &lt;span class="s2"&gt;"email"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;topic_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"email"&lt;/span&gt;
  &lt;span class="nx"&gt;endpoint&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alert_email&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_metric_alarm"&lt;/span&gt; &lt;span class="s2"&gt;"unhealthy_targets"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;alarm_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-unhealthy-targets"&lt;/span&gt;
  &lt;span class="nx"&gt;comparison_operator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GreaterThanThreshold"&lt;/span&gt;
  &lt;span class="nx"&gt;evaluation_periods&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="nx"&gt;metric_name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"UnHealthyHostCount"&lt;/span&gt;
  &lt;span class="nx"&gt;namespace&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS/ApplicationELB"&lt;/span&gt;
  &lt;span class="nx"&gt;period&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
  &lt;span class="nx"&gt;statistic&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Maximum"&lt;/span&gt;
  &lt;span class="nx"&gt;threshold&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="nx"&gt;treat_missing_data&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"notBreaching"&lt;/span&gt;
  &lt;span class="nx"&gt;alarm_actions&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;ok_actions&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;dimensions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;TargetGroup&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn_suffix&lt;/span&gt;
    &lt;span class="nx"&gt;LoadBalancer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn_suffix&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;After applying, check your inbox for a subscription confirmation email from AWS and click it, alerts won't fire until you do.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa7fmq55iujkcupmsgnog.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa7fmq55iujkcupmsgnog.png" alt=" " width="799" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 11: Shipping Code Automatically (CI/CD)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Manually building, tagging, and pushing Docker images gets old fast. Let's automate it with &lt;strong&gt;GitHub Actions&lt;/strong&gt;, so every push to main builds a fresh image and deploys it.&lt;/p&gt;

&lt;p&gt;We'll authenticate using &lt;strong&gt;OIDC&lt;/strong&gt; (OpenID Connect) instead of storing long-lived AWS access keys inside GitHub. In plain terms, GitHub proves its identity to AWS using a short-lived, automatically rotating token, so there's no static secret sitting around that could ever leak.&lt;/p&gt;

&lt;p&gt;First, tell AWS to trust GitHub as an identity provider, and create a tightly-scoped role that only your specific repo can assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# cicd.tf&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_openid_connect_provider"&lt;/span&gt; &lt;span class="s2"&gt;"github"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://token.actions.githubusercontent.com"&lt;/span&gt;
  &lt;span class="nx"&gt;client_id_list&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;thumbprint_list&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"6938fd4d98bab03faadb97b34396831e3780aea1"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"github_actions"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-github-actions"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&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;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Federated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_openid_connect_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;
        &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;StringEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"token.actions.githubusercontent.com:aud"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;StringLike&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"token.actions.githubusercontent.com:sub"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"repo:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"/&lt;/span&gt;&lt;span class="sr"&gt;", var.github_repo)[0]}@*/${split("&lt;/span&gt;&lt;span class="dl"&gt;/"&lt;/span&gt;&lt;span class="s2"&gt;, var.github_repo)[1]}@*:*"&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="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"github_actions"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-github-actions-policy"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&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;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ecr:GetAuthorizationToken"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;Resource&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;"ecr:BatchCheckLayerAvailability"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ecr:GetDownloadUrlForLayer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;"ecr:BatchGetImage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ecr:PutImage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ecr:InitiateLayerUpload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;"ecr:UploadLayerPart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ecr:CompleteLayerUpload"&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecr_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ecs:UpdateService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ecs:DescribeServices"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ecs:DescribeTaskDefinition"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"ecs:RegisterTaskDefinition"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&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="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"iam:PassRole"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_task_execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;A small but important detail: GitHub's OIDC tokens actually include the numeric account and repo IDs baked into the subject claim (something like repo:owner@12345/repo-name@67890:ref:refs/heads/main), not just the plain owner/repo text. The @* wildcards in the condition above account for that. Worth knowing before you find yourself puzzled over an "access denied" error that seems to make no sense.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notice how narrow that policy is: it can push to this specific ECR repository, update this specific ECS service, and nothing else. Not admin access, can't touch your VPC, can't delete anything. If this token ever leaked somehow, the blast radius is tiny.&lt;/p&gt;

&lt;p&gt;Now the actual workflow file, at .github/workflows/deploy.yml:&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;Build and Deploy&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;push&lt;/span&gt;&lt;span class="pi"&gt;:&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;main&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="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;app/**'&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="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;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-west-1&lt;/span&gt;
  &lt;span class="na"&gt;ECR_REPOSITORY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;resilient-aws-infra-app&lt;/span&gt;
  &lt;span class="na"&gt;ECS_CLUSTER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;resilient-aws-infra-cluster&lt;/span&gt;
  &lt;span class="na"&gt;ECS_SERVICE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;resilient-aws-infra-service&lt;/span&gt;
  &lt;span class="na"&gt;ECS_TASK_FAMILY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;resilient-aws-infra-task&lt;/span&gt;
  &lt;span class="na"&gt;CONTAINER_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;resilient-aws-infra-container&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;build-and-deploy&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;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;Configure AWS credentials via OIDC&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::123456789012:role/resilient-aws-infra-github-actions&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;Login to Amazon ECR&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;login-ecr&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/amazon-ecr-login@v2&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;Build, tag, and push image&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-image&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;ECR_REGISTRY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.login-ecr.outputs.registry }}&lt;/span&gt;
          &lt;span class="na"&gt;IMAGE_TAG&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.sha }}&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;docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -t $ECR_REGISTRY/$ECR_REPOSITORY:latest ./app&lt;/span&gt;
          &lt;span class="s"&gt;docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG&lt;/span&gt;
          &lt;span class="s"&gt;docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest&lt;/span&gt;
          &lt;span class="s"&gt;echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" &amp;gt;&amp;gt; $GITHUB_OUTPUT&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;Download current task definition&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;aws ecs describe-task-definition --task-definition $ECS_TASK_FAMILY \&lt;/span&gt;
            &lt;span class="s"&gt;--query taskDefinition &amp;gt; task-definition.json&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;Fill in new image ID in task definition&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;task-def&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/amazon-ecs-render-task-definition@v1&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;task-definition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;task-definition.json&lt;/span&gt;
          &lt;span class="na"&gt;container-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.CONTAINER_NAME }}&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.build-image.outputs.image }}&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;Deploy to ECS&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/amazon-ecs-deploy-task-definition@v2&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;task-definition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.task-def.outputs.task-definition }}&lt;/span&gt;
          &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ECS_SERVICE }}&lt;/span&gt;
          &lt;span class="na"&gt;cluster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ECS_CLUSTER }}&lt;/span&gt;
          &lt;span class="na"&gt;wait-for-service-stability&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the paths: - 'app/**' filter near the top. This means the pipeline only triggers when files inside app/ change, so a pure infrastructure tweak doesn't kick off an unnecessary rebuild.&lt;/p&gt;

&lt;p&gt;Push a small change to app/server.js, and watch the magic happen in your repo's &lt;strong&gt;Actions&lt;/strong&gt; tab.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6hl54v8kxzb0zwf8lkj9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6hl54v8kxzb0zwf8lkj9.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 12: Testing a Bad Deploy on Purpose&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's a scenario every developer has lived through: you ship code that's subtly broken, and it crashes on startup. What happens?&lt;/p&gt;

&lt;p&gt;Let's simulate it. Add this to &lt;strong&gt;server.js&lt;/strong&gt;, right after the chaos secret check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simulate a bad deploy: crash on startup if this "required" config is missing.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&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="nx"&gt;REQUIRED_CONFIG_VALUE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FATAL: REQUIRED_CONFIG_VALUE is not set. Exiting.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We deliberately haven't set that environment variable anywhere, so this simulates a classic misconfigured deploy. Push it to main and watch what happens.&lt;/p&gt;

&lt;p&gt;Here's the sequence I watched play out on my own deploy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;GitHub Actions builds and pushes the broken image, then asks ECS to deploy it&lt;/li&gt;
&lt;li&gt;ECS tries to start new containers with the broken image, they crash almost immediately&lt;/li&gt;
&lt;li&gt;ECS retries a few times (it's patient, giving the deployment a fair shot)&lt;/li&gt;
&lt;li&gt;After enough failures, the deployment circuit breaker kicks in: "deployment failed: tasks failed to start", immediately followed by "rolling back to deployment..."&lt;/li&gt;
&lt;li&gt;ECS automatically reverts to the last known-good version&lt;/li&gt;
&lt;li&gt;Your users never see a thing, the old, working containers kept serving traffic the entire time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ae9xu54qzgv4vuhnd5d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8ae9xu54qzgv4vuhnd5d.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the deployment circuit breaker doing exactly what it's for. Once you're done proving the point, fix the "bug" properly by actually supplying the config value in your task definition, and push a clean deploy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="err"&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;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CHAOS_SECRET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chaos_secret&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"REQUIRED_CONFIG_VALUE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"production"&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;h2&gt;
  
  
  &lt;strong&gt;Cleaning Up&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once you're done experimenting, don't forget to tear everything down. The NAT Gateway in particular bills a small hourly fee for as long as it exists.&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="nb"&gt;cd &lt;/span&gt;infra
terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One small gotcha: if you've pushed images to ECR, Terraform will refuse to delete the (non-empty) repository. Clear the images first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecr batch-delete-image &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repository-name&lt;/span&gt; resilient-aws-infra-app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image-ids&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecr list-images &lt;span class="nt"&gt;--repository-name&lt;/span&gt; resilient-aws-infra-app &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'imageIds[*]'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;What This Project Actually Proves&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Stepping back, here's what got genuinely tested, not just built and left sitting there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single container failure&lt;/strong&gt; — one container breaks, the other keeps serving traffic, ECS replaces the broken one automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Double container failure&lt;/strong&gt; — both containers break at once, ECS replaces both in parallel, still zero visible downtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bad deploy protection&lt;/strong&gt; — genuinely broken code gets caught before it ever reaches real users, and rolls back on its own&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting&lt;/strong&gt; — the moment something breaks, an email lands in your inbox, no manual checking required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated, secure deploys&lt;/strong&gt; — every push to main ships safely, with no long-lived credentials sitting in GitHub&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this was "trust me, it should work in theory". Every single piece was deliberately broken and watched recover, with logs and screenshots to prove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Wrapping Up&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Building something like this yourself teaches you more in an afternoon than reading ten articles about "cloud resilience" ever will. Break things on purpose. Watch what actually happens. Read the logs when it doesn't go the way you expected.&lt;/p&gt;

&lt;p&gt;The full code for everything covered here is available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="//github.com/Adedeji-cloud/resilient-aws-infra"&gt;View the GitHub Repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clone it, terraform apply it, break it, and see for yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbuqvsydvlrq7t0prst3y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbuqvsydvlrq7t0prst3y.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you build on this or spot something worth improving, I'd love to hear about it. Happy building!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>sre</category>
      <category>aws</category>
      <category>portfolio</category>
    </item>
    <item>
      <title>Building a Production-Ready CI/CD Pipeline on AWS: From Empty Terraform Files to a Live, Monitored, Auto-Deploying App</title>
      <dc:creator>Adesanya Adedeji Stephen</dc:creator>
      <pubDate>Sun, 19 Jul 2026 21:38:22 +0000</pubDate>
      <link>https://dev.to/adedejicloud/building-a-production-ready-cicd-pipeline-on-aws-from-empty-terraform-files-to-a-live-monitored-4kbm</link>
      <guid>https://dev.to/adedejicloud/building-a-production-ready-cicd-pipeline-on-aws-from-empty-terraform-files-to-a-live-monitored-4kbm</guid>
      <description>&lt;p&gt;Learn how to build a complete containerized deployment pipeline on AWS— VPC, ECR, ECS Fargate, an Application Load Balancer with HTTPS, CodePipeline/CodeBuild CI/CD, and CloudWatch alarting — using Terraform, across two isolated environments, and how to actually recover when things go wrong along the way.&lt;/p&gt;

&lt;p&gt;Author: Adedeji Adesanya&lt;/p&gt;

&lt;p&gt;Difficulty: Intermediate&lt;/p&gt;

&lt;p&gt;Estimated Time: 6–8 Hours (across two sessions)&lt;/p&gt;

&lt;p&gt;AWS Services Used:&lt;/p&gt;

&lt;p&gt;Amazon VPC&lt;br&gt;
Amazon ECR&lt;br&gt;
AWS IAM&lt;br&gt;
Application Load Balancer (ALB) + AWS Certificate Manager (ACM)&lt;br&gt;
Amazon ECS (Fargate)&lt;br&gt;
AWS CodePipeline + AWS CodeBuild&lt;br&gt;
Amazon CloudWatch + Amazon SNS&lt;br&gt;
Amazon S3 + DynamoDB (Terraform remote state)&lt;/p&gt;

&lt;p&gt;Table of Contents&lt;/p&gt;

&lt;p&gt;Introduction&lt;br&gt;
What You Will Build&lt;br&gt;
Solution Architecture&lt;br&gt;
Prerequisites&lt;br&gt;
Building the Foundation: VPC, ECR, IAM&lt;br&gt;
Fronting the App: ALB and ECS Fargate&lt;br&gt;
Getting the App Actually Running (and What Went Wrong)&lt;br&gt;
Wiring Up CI/CD with CodePipeline&lt;br&gt;
Adding HTTPS to the Load Balancer&lt;br&gt;
The Terraform vs. Pipeline Conflict (and How to Fix It)&lt;br&gt;
Monitoring and Alerting&lt;br&gt;
Governance: Branch Protection and Remote State&lt;br&gt;
Lessons Learned&lt;br&gt;
Production Considerations&lt;br&gt;
Conclusion&lt;/p&gt;

&lt;p&gt;Introduction&lt;/p&gt;

&lt;p&gt;Most Terraform tutorials show you a clean, linear path: write some HCL, run terraform apply, watch resources appear, done. That's useful for learning syntax, but it's not what real infrastructure work actually feels like.&lt;/p&gt;

&lt;p&gt;This project started as a straightforward goal — deploy a containerized web app to AWS using Terraform, with a proper CI/CD pipeline behind it. What it turned into was a two-day exercise in debugging the kind of problems that don't show up in tutorials: stale module variables that didn't match between files, an empty .tf file that looked complete in an editor but was actually zero bytes on disk, a Dockerfile silently missing a COPY instruction, a PowerShell pipe corrupting an authentication token, and — the most interesting one — Terraform and a CI/CD pipeline fighting over who controls the same resource.&lt;/p&gt;

&lt;p&gt;None of these are exotic problems. They're the ordinary friction of building something real. This article walks through the architecture we ended up with, and — more usefully — the specific failures we hit and how each one was actually diagnosed and fixed, not just patched over.&lt;/p&gt;

&lt;p&gt;What You Will Build&lt;/p&gt;

&lt;p&gt;By the end of this project, you'll have two fully isolated environments (dev and prod), each with:&lt;/p&gt;

&lt;p&gt;A custom VPC with public/private subnets across two Availability Zones, a NAT Gateway, and proper routing&lt;br&gt;
A private ECR repository with image scanning and a lifecycle policy&lt;br&gt;
IAM roles scoped to least privilege for both ECS task execution and the running application&lt;br&gt;
An Application Load Balancer with an HTTPS listener (redirecting from HTTP) in front of an ECS Fargate service&lt;br&gt;
A CodePipeline + CodeBuild pipeline that builds, tags, and deploys a new container image automatically on every push to the branch&lt;br&gt;
CloudWatch alarms for 5xx errors, unhealthy targets, low running task count, and high CPU — all notifying an SNS topic by email&lt;br&gt;
Terraform state stored remotely in S3 with DynamoDB locking, instead of local state files&lt;br&gt;
Branch protection on the production branch, requiring a pull request before anything reaches prod&lt;/p&gt;

&lt;p&gt;Solution Architecture&lt;/p&gt;

&lt;p&gt;At a high level:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqn6vtbvgofydpyyud5p2.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqn6vtbvgofydpyyud5p2.jpeg" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each environment (dev, prod) is a completely separate Terraform root module with its own state file, its own VPC, its own pipeline, and its own alarms, sharing only the underlying Terraform modules (the reusable vpc, ecr, iam, alb, ecs, codepipeline, sns, and cloudwatch modules live once, and both environments call them with different variables).&lt;/p&gt;

&lt;p&gt;Prerequisites&lt;/p&gt;

&lt;p&gt;RequirementStatusAWS Account with admin-equivalent IAM permissions✅Terraform installed (we used 1.12.x)✅Docker Desktop✅A GitHub repository✅Basic familiarity with VPC, IAM, and containers✅&lt;/p&gt;

&lt;p&gt;Building the Foundation: VPC, ECR, IAM&lt;/p&gt;

&lt;p&gt;We started with the standard networking layer: a VPC, public and private subnets across two AZs, an Internet Gateway, a NAT Gateway for the private subnets, and route tables. Nothing unusual here — except that this is exactly where our first real problem showed up (more on that in Lessons Learned).&lt;/p&gt;

&lt;p&gt;ECR came next: a single private repository with IMMUTABLE tag mutability (once you push a tag, it can never be overwritten — this matters a lot once CI/CD enters the picture, since it forces every build to have a genuinely unique tag), image scanning on push, and a lifecycle policy expiring untagged images after 14 days and capping tagged images at 10.&lt;/p&gt;

&lt;p&gt;IAM followed the standard ECS pattern of two roles:&lt;/p&gt;

&lt;p&gt;A task execution role, used by ECS itself to pull the image from ECR and write logs to CloudWatch (this one just attaches the AWS-managed AmazonECSTaskExecutionRolePolicy)&lt;br&gt;
A task role, used by the application code at runtime, scoped narrowly to only the DynamoDB, SQS, SNS, S3, and CloudWatch Logs resources matching our naming prefix — not *&lt;/p&gt;

&lt;p&gt;Fronting the App: ALB and ECS Fargate&lt;/p&gt;

&lt;p&gt;The ALB module creates a security group (initially just port 80), the load balancer itself across the public subnets, a target group pointed at port 3000 (our app's port), and an HTTP listener. The ECS module creates a Fargate cluster with Container Insights enabled, a CloudWatch log group, a security group that only accepts traffic from the ALB's security group (not the open internet), a task definition, and the service itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fncd2tiph59awn1r0s92d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fncd2tiph59awn1r0s92d.png" alt=" " width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Getting the App Actually Running (and What Went Wrong)&lt;/p&gt;

&lt;p&gt;We built a simple Express app — an intentionally over-designed storefront landing page for "GlobalMart," styled around an airport-departures-board concept (navy and saffron palette, a flip-board ticker showing product "arrivals" from different countries) rather than a generic template. Dockerized it, pushed it to ECR, pointed the ECS task definition at the image, and applied.&lt;/p&gt;

&lt;p&gt;The service came up with zero healthy tasks. ALB health checks were failing with a 404.&lt;/p&gt;

&lt;p&gt;The cause: the Dockerfile copied server.js into the image but never copied the public/ folder where index.html actually lived. The container was running, Express was serving requests, and every single one of them correctly 404'd — because there was nothing to serve. Adding one line (COPY public ./public), rebuilding, and pushing a new tag fixed it immediately. This is the kind of bug that's invisible in terraform plan and invisible in docker build output — it only shows up once real traffic hits the container, which is exactly why health checks and monitoring matter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fslla8k32v0wi4bm80jt0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fslla8k32v0wi4bm80jt0.png" alt=" " width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F781i9fneqqjkuvl1l76k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F781i9fneqqjkuvl1l76k.png" alt=" " width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkcwtbhqwm0vtcsk5w8n8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkcwtbhqwm0vtcsk5w8n8.png" alt=" " width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wiring Up CI/CD with CodePipeline&lt;/p&gt;

&lt;p&gt;Manually running docker build &amp;amp;&amp;amp; docker tag &amp;amp;&amp;amp; docker push and then hand-editing a Terraform variable every time you want to deploy is fine for a demo, but it's not how real teams ship code. We built a codepipeline Terraform module with:&lt;/p&gt;

&lt;p&gt;An S3 bucket for pipeline artifacts (versioned, encrypted, public access blocked)&lt;br&gt;
A CodeStar Connection to GitHub (this is the one piece Terraform can't fully automate — AWS requires a one-time manual OAuth authorization click in the console after the connection resource is created)&lt;br&gt;
A CodeBuild project running in privileged mode (required for Docker-in-Docker), with a buildspec.yml that logs into ECR, builds the image, tags it with the first 8 characters of the git commit SHA (guaranteeing a unique tag every time, which plays correctly with our IMMUTABLE repository), pushes it, and writes an imagedefinitions.json file&lt;br&gt;
A three-stage CodePipeline: Source (GitHub via the connection) → Build (CodeBuild) → Deploy (native ECS deploy action, which reads imagedefinitions.json and updates the service)&lt;/p&gt;

&lt;p&gt;Once the GitHub connection was authorized in the console, a push to the branch triggered the full pipeline automatically, and it succeeded end-to-end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx1w7jkhs1016595bpuzh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx1w7jkhs1016595bpuzh.png" alt=" " width="799" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwrklcvudx88352vf4xdb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwrklcvudx88352vf4xdb.png" alt=" " width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adding HTTPS to the Load Balancer&lt;/p&gt;

&lt;p&gt;Since we didn't own a domain (ACM certificates require domain validation via DNS or email, which isn't possible against a bare AWS-generated ALB hostname), we generated a self-signed certificate with OpenSSL and imported it directly into ACM — skipping domain validation entirely. This isn't a certificate a browser will trust, but it's genuinely useful for demonstrating correct TLS termination architecture: an HTTP listener on port 80 that redirects (301) to HTTPS on port 443, and an HTTPS listener that terminates TLS at the load balancer and forwards plain HTTP to the target group.&lt;/p&gt;

&lt;p&gt;Getting there involved one Windows-specific detour worth mentioning: aws ecr get-login-password | docker login ... piped through native PowerShell silently corrupted the long base64 authentication token and failed with a 400 Bad Request. Routing the exact same command through cmd /c "..." instead fixed it immediately — a reminder that shell pipeline behavior isn't universal, even for commands copied directly from AWS's own documentation.&lt;/p&gt;

&lt;p&gt;The Terraform vs. Pipeline Conflict (and How to Fix It)&lt;/p&gt;

&lt;p&gt;This was the most instructive bug in the whole project.&lt;/p&gt;

&lt;p&gt;Once CodePipeline started deploying new task definition revisions on every push (bypassing Terraform entirely — it calls the ECS API directly), Terraform's local state fell out of sync with reality. The next time we ran terraform plan for an unrelated change, Terraform saw that the "current" task definition according to its state was several revisions behind what was actually running, and proposed to forcibly roll the service back — silently undoing every deploy the pipeline had made since the last terraform apply.&lt;/p&gt;

&lt;p&gt;This is a structural conflict, not a bug in either tool: Terraform assumes it's the sole owner of everything in its state, and a CI/CD pipeline managing the same resource violates that assumption. The fix is to explicitly tell Terraform to stop caring about that one attribute:&lt;/p&gt;

&lt;p&gt;hclresource "aws_ecs_service" "app" {&lt;br&gt;
  # ...&lt;/p&gt;

&lt;p&gt;lifecycle {&lt;br&gt;
    ignore_changes = [task_definition]&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;With this in place, Terraform continues managing the infrastructure around the service (cluster, networking, security groups) but defers entirely to the pipeline for which image is currently deployed. This is a pattern worth knowing before you combine Terraform-managed ECS with any CI/CD tool that also touches the task definition — without it, your next unrelated terraform apply can quietly roll back production.&lt;/p&gt;

&lt;p&gt;Monitoring and Alerting&lt;/p&gt;

&lt;p&gt;Four CloudWatch alarms per environment, all notifying a single SNS topic subscribed by email:&lt;/p&gt;

&lt;p&gt;ALB 5xx errors — more than 10 target-side 5xx responses in a 5-minute window&lt;br&gt;
Unhealthy target count — any target behind the ALB reporting unhealthy&lt;br&gt;
Low running task count — fewer running ECS tasks than desired (using the ECS/ContainerInsights namespace, which requires Container Insights to be enabled on the cluster)&lt;br&gt;
High CPU — ECS service CPU utilization above 80% sustained for 15 minutes&lt;/p&gt;

&lt;p&gt;The SNS email subscription needs a manual confirmation click after terraform apply — Terraform can create the subscription, but AWS requires the recipient to actively confirm it, the same way it requires manual authorization for the GitHub connection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvt7sit8m87c2ex11qypk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvt7sit8m87c2ex11qypk.png" alt=" " width="799" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Governance: Branch Protection and Remote State&lt;/p&gt;

&lt;p&gt;Two changes made this feel less like a personal sandbox and more like a real team setup:&lt;/p&gt;

&lt;p&gt;Branch protection on prod — configured directly in GitHub (Settings → Branches), requiring a pull request before anything merges into the branch that triggers the production pipeline. No more direct pushes to prod, even by an admin, without a deliberate PR.&lt;/p&gt;

&lt;p&gt;Migrating Terraform state from local files to S3 with DynamoDB locking — local terraform.tfstate files are fine for solo experimentation but are a real liability the moment more than one person (or more than one machine) might run terraform apply against the same infrastructure: concurrent applies can corrupt state, and a deleted or lost local file means losing track of everything Terraform manages. Remote state in S3, combined with a DynamoDB table for locking (so two applies can't run simultaneously), is the standard production pattern.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faefbmr3wup63jfkn3j32.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faefbmr3wup63jfkn3j32.png" alt=" " width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lessons Learned&lt;/p&gt;

&lt;p&gt;Empty files look identical to complete files in an editor. At one point, several .tf files inside a module directory were showing valid, complete content in the code editor — but were actually zero bytes on disk. The content only existed in an unsaved editor buffer. terraform validate correctly reported the module's variables as undeclared, which looked like a logic error but was actually a save error. Always verify file sizes on disk (Get-ChildItem, or ls -la) when a .tf file's content doesn't match what Terraform is complaining about.&lt;/p&gt;

&lt;p&gt;Duplicate resource declarations fail loudly, which is a feature. Copy-pasting a resource block into the wrong file (in our case, aws_subnet blocks ended up duplicated across two files) produces an immediate, unambiguous "Duplicate resource configuration" error — this is one of the few Terraform failure modes that's actually easy to diagnose, because the error names the exact resource and the exact second location.&lt;/p&gt;

&lt;p&gt;PowerShell's pipeline isn't always transparent. Long strings piped between commands (the ECR auth token, in our case) can be silently mangled by PowerShell in ways that produce a downstream error with no obvious connection to the actual cause. When a command that should obviously work fails with a generic error, try routing it through cmd /c before assuming the problem is on the remote end.&lt;/p&gt;

&lt;p&gt;A working terraform plan and a working application are two different claims. Every piece of infrastructure can apply cleanly and still serve a 404 for every request, because Terraform has no visibility into what's actually inside your container image. Health checks, and genuinely hitting the live endpoint after every deploy, are not optional steps.&lt;/p&gt;

&lt;p&gt;Recycle Bin is a legitimate disaster recovery tool. An accidental folder deletion mid-session turned out to be completely recoverable because Windows' standard delete (rather than a hard Remove-Item -Force) sends files to the Recycle Bin first. Combined with git for the source code and the fact that AWS resources don't disappear just because local files do, this turned a moment of genuine panic into a five-minute non-event. It's still worth treating your Terraform state file with real care — it was the one thing that mattered here and wasn't backed up anywhere except that Recycle Bin.&lt;/p&gt;

&lt;p&gt;Branches don't protect you from typing on the wrong one. A git checkout prod earlier in a session, followed by continuing to work without checking git branch again, meant several commits meant for main landed on prod instead. Git doesn't warn you about this, git commit and git push work identically regardless of which branch is checked out. git status at the top of every output tells you the current branch; it's worth reading that line every time, not just the "nothing to commit" part.&lt;/p&gt;

&lt;p&gt;Production Considerations&lt;/p&gt;

&lt;p&gt;This project is intentionally a solid foundation, not a finished production system. Before treating something like this as production-ready, we'd still want:&lt;/p&gt;

&lt;p&gt;A real domain name and a properly validated ACM certificate (our self-signed cert demonstrates the architecture but isn't trusted by any browser)&lt;br&gt;
Auto-scaling policies on the ECS service, instead of a hardcoded desired count&lt;br&gt;
The DynamoDB, SQS, SNS, and S3 resources the IAM task role is already scoped for, but which don't exist yet&lt;br&gt;
A WAF in front of the ALB for basic protection against common web exploits&lt;br&gt;
Centralizing logs and metrics further with CloudWatch Dashboards, rather than checking alarms individually&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;

&lt;p&gt;The infrastructure in this project (a VPC, a container registry, a load balancer, a Fargate service, a CI/CD pipeline, and monitoring) isn't architecturally unusual. What made it worth writing up wasn't the happy path; it was everything that went wrong along the way, and the fact that every failure had a specific, diagnosable cause rather than being a mystery to work around.&lt;/p&gt;

&lt;p&gt;That's the actual shape of infrastructure work: not a sequence of commands that always succeed, but a sequence of hypotheses, verifications, and fixes. The terraform plan that shows "No changes" after a scare is more valuable than the one that shows a clean create on the first try, because it proves the system actually recovered correctly rather than just looking fine.&lt;/p&gt;

&lt;p&gt;If you're building something similar, the advice that mattered most in this project wasn't about any specific AWS service, it was to verify state directly rather than assume it (Get-ChildItem before trusting a file exists, terraform plan before trusting an apply succeeded, curl the actual endpoint before trusting a health check passed), and to fix the root cause of a failure rather than the symptom in front of you.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>terraform</category>
      <category>docker</category>
    </item>
    <item>
      <title>Building a Highly Available Multi-Region Web Application on AWS with Global Accelerator</title>
      <dc:creator>Adesanya Adedeji Stephen</dc:creator>
      <pubDate>Tue, 14 Jul 2026 20:14:25 +0000</pubDate>
      <link>https://dev.to/adedejicloud/building-a-highly-available-multi-region-web-application-on-aws-with-global-accelerator-5b2h</link>
      <guid>https://dev.to/adedejicloud/building-a-highly-available-multi-region-web-application-on-aws-with-global-accelerator-5b2h</guid>
      <description>&lt;p&gt;Learn how to build a fault-tolerant web application that automatically redirects users to a healthy AWS Region during regional outages using Amazon EC2, Application Load Balancer, Route 53 Health Checks, and AWS Global Accelerator.&lt;/p&gt;

&lt;p&gt;Author: Adedeji Adesanya&lt;/p&gt;

&lt;p&gt;Difficulty: Intermediate&lt;/p&gt;

&lt;p&gt;Estimated Time: 2–3 Hours&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Services Used:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon VPC&lt;/li&gt;
&lt;li&gt;Amazon EC2&lt;/li&gt;
&lt;li&gt;Application Load Balancer (ALB)&lt;/li&gt;
&lt;li&gt;Amazon Route 53 Health Checks&lt;/li&gt;
&lt;li&gt;AWS Global Accelerator&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;What You Will Build&lt;/li&gt;
&lt;li&gt;Solution Architecture&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Understanding the Architecture&lt;/li&gt;
&lt;li&gt;Project Overview&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Imagine you've built an e-commerce website hosted in the us-east-1 AWS Region.&lt;/p&gt;

&lt;p&gt;Everything works perfectly—until an unexpected regional outage occurs.&lt;/p&gt;

&lt;p&gt;Suddenly, your application becomes unreachable. Customers can't browse products, place orders, or make payments. Every minute of downtime affects user experience and can result in lost revenue.&lt;/p&gt;

&lt;p&gt;This is why modern cloud applications are designed with high availability and disaster recovery in mind.&lt;/p&gt;

&lt;p&gt;Instead of relying on a single Region, organizations deploy applications across multiple AWS Regions so that if one becomes unavailable, traffic can automatically be redirected to another healthy Region.&lt;/p&gt;

&lt;p&gt;In this tutorial, you'll build exactly that.&lt;/p&gt;

&lt;p&gt;By the end of this guide, you'll have a multi-region web application capable of automatically failing over between two AWS Regions using AWS Global Accelerator and Amazon Route 53 Health Checks.&lt;/p&gt;

&lt;p&gt;Rather than simply deploying infrastructure, you'll learn how multiple AWS services work together to create a resilient architecture suitable for production environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Will Build
&lt;/h2&gt;

&lt;p&gt;By the end of this tutorial, you'll have an architecture that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F25g312cdnoxg9tp6bakg.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F25g312cdnoxg9tp6bakg.jpeg" alt=" " width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At a high level, the architecture consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two independent AWS Regions&lt;/li&gt;
&lt;li&gt;Separate networking infrastructure in each Region&lt;/li&gt;
&lt;li&gt;One EC2 instance running the application in each Region&lt;/li&gt;
&lt;li&gt;One Application Load Balancer per Region&lt;/li&gt;
&lt;li&gt;Route 53 Health Checks for continuous monitoring&lt;/li&gt;
&lt;li&gt;AWS Global Accelerator providing a single global entry point&lt;/li&gt;
&lt;li&gt;Automatic traffic failover during outages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The end result is a web application that continues serving users even when an entire AWS Region becomes unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AWS Global Accelerator?
&lt;/h2&gt;

&lt;p&gt;Many engineers assume Amazon Route 53 is the only solution for routing users globally.&lt;/p&gt;

&lt;p&gt;While Route 53 is an excellent DNS service, DNS-based failover has one important limitation: DNS caching.&lt;/p&gt;

&lt;p&gt;Because DNS records are cached by clients and Internet Service Providers, changes may not take effect immediately.&lt;/p&gt;

&lt;p&gt;AWS Global Accelerator solves this differently.&lt;/p&gt;

&lt;p&gt;Instead of relying on DNS propagation, it provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two static Anycast IP addresses&lt;/li&gt;
&lt;li&gt;Intelligent traffic routing through the AWS Global Network&lt;/li&gt;
&lt;li&gt;Faster failover&lt;/li&gt;
&lt;li&gt;Improved latency&lt;/li&gt;
&lt;li&gt;Better user experience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For applications requiring high availability and low latency, Global Accelerator is often the preferred choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before beginning this project, ensure you have the following:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS Account&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic understanding of EC2&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic understanding of Amazon VPC&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two AWS Regions enabled&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM User with AdministratorAccess (or equivalent permissions)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhc1wrprh4u6z58xn134v.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhc1wrprh4u6z58xn134v.jpeg" alt=" " width="800" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Services Used
&lt;/h2&gt;

&lt;p&gt;Throughout this project, you'll work with several AWS services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Amazon VPC
&lt;/h2&gt;

&lt;p&gt;Amazon VPC provides an isolated virtual network where your AWS resources will run. Each Region will have its own dedicated VPC to ensure complete isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Amazon EC2
&lt;/h2&gt;

&lt;p&gt;Amazon EC2 hosts the web application in both Regions.&lt;/p&gt;

&lt;p&gt;Each EC2 instance runs the same application, allowing either Region to serve user requests independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application Load Balancer
&lt;/h2&gt;

&lt;p&gt;The Application Load Balancer distributes incoming traffic to healthy EC2 instances.&lt;/p&gt;

&lt;p&gt;It also serves as the endpoint monitored by Route 53 Health Checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Amazon Route 53 Health Checks
&lt;/h2&gt;

&lt;p&gt;Health Checks continuously monitor the application's availability.&lt;/p&gt;

&lt;p&gt;If a Region becomes unhealthy, Route 53 reports the failure, allowing Global Accelerator to redirect traffic automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Global Accelerator
&lt;/h2&gt;

&lt;p&gt;Global Accelerator acts as the application's single global entry point.&lt;/p&gt;

&lt;p&gt;Instead of users connecting directly to a specific Region, they connect to Global Accelerator, which routes traffic to the nearest healthy endpoint.&lt;/p&gt;

&lt;p&gt;This makes regional failover seamless and transparent to users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Network Foundation
&lt;/h2&gt;

&lt;p&gt;Before we can deploy our application, we need to build the network that will host it.&lt;/p&gt;

&lt;p&gt;In AWS, every resource lives inside a Virtual Private Cloud (VPC). Since we're building a multi-region architecture, we'll create a separate VPC in each AWS Region.&lt;/p&gt;

&lt;p&gt;This ensures that each Region operates independently, eliminating a single point of failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create the Primary VPC (us-east-1)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Switch your AWS Region to US East (N. Virginia) – us-east-1.&lt;/p&gt;

&lt;p&gt;From the AWS Management Console, navigate to:&lt;/p&gt;

&lt;p&gt;VPC&lt;br&gt;
→ Your VPCs&lt;br&gt;
→ Create VPC&lt;/p&gt;

&lt;p&gt;Choose VPC only and configure it as shown below.&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Name               primary-vpc&lt;br&gt;
IPv4 CIDR Block    10.0.0.0/16&lt;br&gt;
IPv6               None&lt;br&gt;
Tenancy            Default&lt;/p&gt;

&lt;p&gt;After entering the values, click &lt;strong&gt;Create VPC&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fughx4map9xpebbwvatmy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fughx4map9xpebbwvatmy.png" alt=" " width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the VPC has been created successfully, you should see it listed in the VPC dashboard.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3278xhd0b6ruvz2bjvd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw3278xhd0b6ruvz2bjvd.png" alt=" " width="798" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a /16 CIDR Block?
&lt;/h2&gt;

&lt;p&gt;You might be wondering why we chose 10.0.0.0/16.&lt;/p&gt;

&lt;p&gt;A /16 network provides 65,536 IP addresses, giving plenty of room for future expansion.&lt;/p&gt;

&lt;p&gt;Although our lab only uses a few EC2 instances, production environments often require additional subnets for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application servers&lt;/li&gt;
&lt;li&gt;Databases&lt;/li&gt;
&lt;li&gt;NAT Gateways&lt;/li&gt;
&lt;li&gt;Private services&lt;/li&gt;
&lt;li&gt;Kubernetes clusters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Planning your IP address space early helps avoid costly network redesigns later.&lt;/p&gt;

&lt;p&gt;💡 Pro Tip: Always leave room for growth when designing cloud networks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Create the Secondary VPC (us-west-2)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Next, switch your AWS Region to US West (Oregon) – us-west-2.&lt;/p&gt;

&lt;p&gt;Repeat the same process.&lt;/p&gt;

&lt;p&gt;Navigate to:&lt;br&gt;
VPC&lt;br&gt;
→ Your VPCs&lt;br&gt;
→ Create VPC&lt;/p&gt;

&lt;p&gt;Use the following configuration.&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Name             secondary-vpc&lt;br&gt;
IPv4 CIDR Block  10.1.0.0/16&lt;br&gt;
IPv6             None&lt;br&gt;
Tenancy      Default&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2yaedl69ryr5oxs5apr1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2yaedl69ryr5oxs5apr1.png" alt=" " width="758" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice that we used a different CIDR block (10.1.0.0/16) instead of 10.0.0.0/16.&lt;/p&gt;

&lt;p&gt;This is intentional.&lt;/p&gt;

&lt;p&gt;Each VPC must have a unique address range to avoid overlapping IP addresses. Using distinct CIDR blocks makes future networking tasks—such as VPC Peering or AWS Transit Gateway—much simpler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Create Public Subnets
&lt;/h2&gt;

&lt;p&gt;With both VPCs in place, the next step is to create public subnets.&lt;/p&gt;

&lt;p&gt;A subnet divides a VPC into smaller, manageable networks.&lt;/p&gt;

&lt;p&gt;Our EC2 instances and Application Load Balancers will reside in these public subnets so they can receive traffic from the internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Primary Region (us-east-1)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Navigate to:&lt;/p&gt;

&lt;p&gt;VPC&lt;br&gt;
→ Subnets&lt;br&gt;
→ Create subnet&lt;/p&gt;

&lt;p&gt;Select primary-vpc and create the following subnet.&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Name                    primary-public-subnet&lt;br&gt;
Availability Zone   us-east-1a&lt;br&gt;
CIDR Block          10.0.1.0/24&lt;/p&gt;

&lt;p&gt;Click Create subnet&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2y9rvcy5kqmryaaq788.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2y9rvcy5kqmryaaq788.png" alt=" " width="777" height="504"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Secondary Region (us-west-2)
&lt;/h2&gt;

&lt;p&gt;Repeat the process in us-west-2.&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Name                    secondary-public-subnet&lt;br&gt;
Availability Zone   us-west-2a&lt;br&gt;
CIDR Block          10.1.1.0/24&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdii8fj4sh6ewsvcx7iz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdii8fj4sh6ewsvcx7iz.png" alt=" " width="757" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use a /24 Subnet?
&lt;/h2&gt;

&lt;p&gt;Each /24 subnet provides 256 IP addresses.&lt;/p&gt;

&lt;p&gt;This is more than enough for our project while leaving room for additional EC2 instances, load balancers, or other resources in the future.&lt;/p&gt;

&lt;p&gt;Separating the VPC into smaller subnets also improves network organization and scalability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Attach Internet Gateways
&lt;/h2&gt;

&lt;p&gt;By default, a VPC has no internet connectivity.&lt;/p&gt;

&lt;p&gt;To allow resources inside the VPC to communicate with the internet, we need an Internet Gateway (IGW).&lt;/p&gt;

&lt;p&gt;We'll create one for each VPC.&lt;/p&gt;

&lt;p&gt;Navigate to:&lt;/p&gt;

&lt;p&gt;VPC&lt;br&gt;
→ Internet Gateways&lt;br&gt;
→ Create Internet Gateway&lt;/p&gt;

&lt;p&gt;For the primary Region:&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Name    primary-igw&lt;/p&gt;

&lt;p&gt;After creating it, choose Actions → Attach to VPC, then select primary-vpc.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Repeat the same process in us-west-2.&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Setting Value&lt;br&gt;
Name    secondary-igw&lt;/p&gt;

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

&lt;p&gt;we'll configure Route Tables, launch our EC2 instances, install the web application, and prepare the environment for load balancing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying the Web Application and Configuring the Load Balancer
&lt;/h2&gt;

&lt;p&gt;At this stage, our networking foundation is complete.&lt;/p&gt;

&lt;p&gt;Each AWS Region now has its own VPC, public subnet, and Internet Gateway. The next step is to deploy the application that users will access.&lt;/p&gt;

&lt;p&gt;To keep the architecture consistent, we'll deploy an identical web application in both Regions. Each application will run on an Amazon EC2 instance and sit behind an Application Load Balancer (ALB).&lt;/p&gt;

&lt;p&gt;Using identical deployments ensures that either Region can serve traffic if the other becomes unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Create a Security Group for the Web Server
&lt;/h2&gt;

&lt;p&gt;Before launching the EC2 instances, we'll create a security group that controls inbound traffic.&lt;/p&gt;

&lt;p&gt;In the AWS Management Console, navigate to:&lt;/p&gt;

&lt;p&gt;VPC&lt;br&gt;
→ Security Groups&lt;br&gt;
→ Create Security Group&lt;/p&gt;

&lt;p&gt;Use the following configuration for the Primary Region (us-east-1).&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Name            web-server-sg&lt;br&gt;
Description Security group for EC2 web servers&lt;br&gt;
VPC         primary-vpc&lt;/p&gt;

&lt;p&gt;Inbound Rules&lt;br&gt;
Type    Protocol    Port    Source&lt;br&gt;
HTTP    TCP 80  0.0.0.0/0&lt;br&gt;
SSH TCP 22  Your Public IP&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why restrict SSH?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Allowing SSH from 0.0.0.0/0 exposes your server to the entire internet. Restricting it to your public IP significantly improves security while still allowing you to administer the instance.&lt;/p&gt;

&lt;p&gt;Repeat the same process in us-west-2, ensuring the security group is associated with secondary-vpc.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0erqcucp4z6p3dxug6bj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0erqcucp4z6p3dxug6bj.png" alt=" " width="759" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Launch the EC2 Instances
&lt;/h2&gt;

&lt;p&gt;Now we'll deploy the application servers.&lt;/p&gt;

&lt;p&gt;Navigate to:&lt;/p&gt;

&lt;p&gt;EC2&lt;br&gt;
→ Instances&lt;br&gt;
→ Launch Instance&lt;br&gt;
Primary Region (us-east-1)&lt;/p&gt;

&lt;p&gt;Use the following configuration.&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Name                    primary-web-server&lt;br&gt;
AMI                 Amazon Linux 2023&lt;br&gt;
Instance Type           t2.micro&lt;br&gt;
Key Pair            Select your existing key pair&lt;br&gt;
Network                 primary-vpc&lt;br&gt;
Subnet                  primary-public-subnet&lt;br&gt;
Auto-assign Public IP   Enabled&lt;br&gt;
Security Group          web-server-sg&lt;/p&gt;

&lt;p&gt;Repeat the same steps in us-west-2, changing only the instance name to:&lt;br&gt;
                  secondary-web-server&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsochr3wotnlc2rdlis3j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsochr3wotnlc2rdlis3j.png" alt=" " width="770" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Install Apache and Deploy a Simple Web Page
&lt;/h2&gt;

&lt;p&gt;Once the EC2 instances are running, connect to each instance using SSH.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ts1cek9d56wy66u4dml.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ts1cek9d56wy66u4dml.png" alt=" " width="800" height="765"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output should display:&lt;/p&gt;

&lt;p&gt;Active: active (running)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Create the Application Homepage
&lt;/h2&gt;

&lt;p&gt;Instead of using Apache's default page, we'll create a simple HTML page so it's easy to identify which Region is serving requests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zsmziejlr4zu5c0au3e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zsmziejlr4zu5c0au3e.png" alt=" " width="800" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7aa7u2kv5udwjniw2xuy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7aa7u2kv5udwjniw2xuy.png" alt=" " width="764" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Verify the Web Servers
&lt;/h2&gt;

&lt;p&gt;Open a web browser and enter the public IP address of each EC2 instance.&lt;/p&gt;

&lt;p&gt;You should see the custom page indicating which Region is serving the request.&lt;/p&gt;

&lt;p&gt;This simple verification confirms that Apache is running correctly and the application is reachable before introducing the load balancer.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Don't skip this step.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Verifying the application directly helps isolate problems early. If the page doesn't load now, it certainly won't work behind the load balancer later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Create an Application Load Balancer
&lt;/h2&gt;

&lt;p&gt;Now we'll place each EC2 instance behind an Application Load Balancer.&lt;/p&gt;

&lt;p&gt;Navigate to:&lt;/p&gt;

&lt;p&gt;EC2&lt;br&gt;
→ Load Balancers&lt;br&gt;
→ Create Load Balancer&lt;br&gt;
→ Application Load Balancer&lt;/p&gt;

&lt;p&gt;For the Primary Region, use the following configuration.&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Name                 primary-alb&lt;br&gt;
Scheme               Internet-facing&lt;br&gt;
IP Address Type      IPv4&lt;br&gt;
VPC              primary-vpc&lt;br&gt;
Subnets              Select the public subnet(s)&lt;/p&gt;

&lt;p&gt;Listener&lt;br&gt;
Protocol         Port&lt;br&gt;
HTTP                 80&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 11: Create a Target Group
&lt;/h2&gt;

&lt;p&gt;Before the ALB can forward traffic, it needs a target group.&lt;/p&gt;

&lt;p&gt;Configure it as follows.&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Target Type    Instances&lt;br&gt;
Name               primary-target-group&lt;br&gt;
Protocol       HTTP&lt;br&gt;
Port               80&lt;br&gt;
Health Check Path   /&lt;/p&gt;

&lt;p&gt;Register the primary-web-server instance as the target.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;Repeat the process in us-west-2, creating:&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
secondary-alb&lt;br&gt;
secondary-target-group&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn3831pt1pnvkop9vhu79.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn3831pt1pnvkop9vhu79.png" alt=" " width="753" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 12: Verify Load Balancer Health
&lt;/h2&gt;

&lt;p&gt;Once the Application Load Balancer has been created, wait a few minutes while it performs health checks against the registered EC2 instance.&lt;/p&gt;

&lt;p&gt;When everything is configured correctly, the target status should change to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Healthy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the target remains Unhealthy, check the following:&lt;/p&gt;

&lt;p&gt;Is Apache running?&lt;br&gt;
Does the security group allow inbound HTTP (port 80)?&lt;br&gt;
Is the health check path set to /?&lt;br&gt;
Is the EC2 instance registered with the correct target group?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4n1hl5fapki6fhs8rywo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4n1hl5fapki6fhs8rywo.png" alt=" " width="766" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring Amazon Route 53 Health Checks
&lt;/h2&gt;

&lt;p&gt;So far, we've built two identical application environments:&lt;/p&gt;

&lt;p&gt;A Primary Region in us-east-1&lt;br&gt;
A Secondary Region in us-west-2&lt;/p&gt;

&lt;p&gt;Both applications are running successfully behind their respective Application Load Balancers.&lt;/p&gt;

&lt;p&gt;However, our architecture still has one major limitation.&lt;/p&gt;

&lt;p&gt;If the primary application goes offline, AWS has no way of knowing that it should stop sending traffic to it.&lt;/p&gt;

&lt;p&gt;This is where Amazon Route 53 Health Checks come into play.&lt;/p&gt;

&lt;p&gt;They continuously monitor the health of your application's endpoints. If an endpoint becomes unhealthy, other AWS services—such as Global Accelerator—can use that information to automatically redirect traffic to a healthy endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Route 53 Health Checks Work
&lt;/h2&gt;

&lt;p&gt;At regular intervals, Route 53 sends requests to your application's endpoint.&lt;/p&gt;

&lt;p&gt;If the application responds successfully, the endpoint is considered healthy.&lt;/p&gt;

&lt;p&gt;If it fails to respond after a specified number of consecutive checks, Route 53 marks it as unhealthy.&lt;/p&gt;

&lt;p&gt;In our project, we'll configure a health check for each Region's Application Load Balancer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpj8gn7lyc5r8y0jmfugd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpj8gn7lyc5r8y0jmfugd.png" alt=" " width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 13: Copy the Load Balancer DNS Names
&lt;/h2&gt;

&lt;p&gt;Before creating the health checks, we need the DNS name of each Application Load Balancer.&lt;/p&gt;

&lt;p&gt;Navigate to:&lt;/p&gt;

&lt;p&gt;EC2&lt;br&gt;
→ Load Balancers&lt;/p&gt;

&lt;p&gt;Select primary-alb.&lt;/p&gt;

&lt;p&gt;Copy the value under DNS name.&lt;/p&gt;

&lt;p&gt;It should look similar to:&lt;/p&gt;

&lt;p&gt;primary-alb-123456789.us-east-1.elb.amazonaws.com&lt;/p&gt;

&lt;p&gt;Repeat the process for secondary-alb.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 14: Create the Primary Health Check
&lt;/h2&gt;

&lt;p&gt;Navigate to:&lt;/p&gt;

&lt;p&gt;Route 53&lt;br&gt;
→ Health Checks&lt;br&gt;
→ Create Health Check&lt;/p&gt;

&lt;p&gt;Configure it using the following settings.&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Name                    Primary-ALB-Health-Check&lt;br&gt;
What to monitor         Endpoint&lt;br&gt;
Specify endpoint by Domain Name&lt;br&gt;
Domain Name         Primary ALB DNS Name&lt;br&gt;
Protocol            HTTP&lt;br&gt;
Port                    80&lt;br&gt;
Path                    /&lt;br&gt;
Request Interval    30 Seconds&lt;br&gt;
Failure Threshold   3&lt;/p&gt;

&lt;p&gt;Leave all other settings at their default values.&lt;/p&gt;

&lt;p&gt;Click Create Health Check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 15: Create the Secondary Health Check
&lt;/h2&gt;

&lt;p&gt;Repeat the same process for the Application Load Balancer in us-west-2.&lt;/p&gt;

&lt;p&gt;Use the following configuration.&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Name                    Secondary-ALB-Health-Check&lt;br&gt;
Domain Name         Secondary ALB DNS&lt;br&gt;
Protocol            HTTP&lt;br&gt;
Port                    80&lt;br&gt;
Path                    /&lt;br&gt;
Request Interval    30 Seconds&lt;br&gt;
Failure Threshold   3&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 16: Verify the Health Checks
&lt;/h2&gt;

&lt;p&gt;Once both health checks have been created, wait a few minutes.&lt;/p&gt;

&lt;p&gt;Route 53 needs time to send its first requests and determine the health of each endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initially, the status may display:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Checking...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After a short period, both health checks should report:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Healthy&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn57kcy6v321pvykn4nhw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn57kcy6v321pvykn4nhw.png" alt=" " width="800" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Health Check Results
&lt;/h2&gt;

&lt;p&gt;When Route 53 marks an endpoint as Healthy, it means:&lt;/p&gt;

&lt;p&gt;The endpoint is reachable.&lt;br&gt;
The application is responding successfully.&lt;br&gt;
The configured path (/) returned a successful HTTP response.&lt;/p&gt;

&lt;p&gt;If an endpoint becomes Unhealthy, Route 53 will stop considering it available.&lt;/p&gt;

&lt;p&gt;This health information becomes especially important in the next part of the tutorial, where AWS Global Accelerator uses it to decide where incoming traffic should be routed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Troubleshooting Health Checks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your health check doesn't become healthy after several minutes, don't panic. The issue is usually one of the following:&lt;/p&gt;

&lt;p&gt;Problem Solution&lt;br&gt;
&lt;strong&gt;Apache is not running&lt;/strong&gt;   Start the Apache service using sudo systemctl start httpd&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Port 80 is blocked&lt;/strong&gt;  Verify the EC2 security group allows inbound HTTP traffic&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incorrect health check path&lt;/strong&gt; Ensure the path is /&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong DNS name&lt;/strong&gt;  Confirm you're using the ALB DNS name, not the EC2 public IP&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target group is unhealthy&lt;/strong&gt;   Verify the EC2 instance is healthy behind the ALB&lt;/p&gt;

&lt;p&gt;Taking a few minutes to troubleshoot now will save time later when configuring Global Accelerator.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Accomplished
&lt;/h2&gt;

&lt;p&gt;At this point, we've successfully:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Created health checks for both Application Load Balancers.&lt;/li&gt;
&lt;li&gt;Verified that both application endpoints are healthy.&lt;/li&gt;
&lt;li&gt;Enabled continuous monitoring of our application.&lt;/li&gt;
&lt;li&gt;Prepared the architecture for automatic failover.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next part, we'll configure AWS Global Accelerator and connect it to both Application Load Balancers. This will provide users with a single global entry point and enable automatic traffic failover whenever a Region becomes unavailable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring AWS Global Accelerator
&lt;/h2&gt;

&lt;p&gt;Up to this point, we've built two independent application environments:&lt;/p&gt;

&lt;p&gt;Primary Region (us-east-1)&lt;br&gt;
Secondary Region (us-west-2)&lt;/p&gt;

&lt;p&gt;Each environment has its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon VPC&lt;/li&gt;
&lt;li&gt;EC2 Instance&lt;/li&gt;
&lt;li&gt;Application Load Balancer&lt;/li&gt;
&lt;li&gt;Route 53 Health Check&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final piece is giving users one consistent entry point regardless of which Region is serving the application.&lt;/p&gt;

&lt;p&gt;This is where AWS Global Accelerator comes in.&lt;/p&gt;

&lt;p&gt;Unlike DNS-based routing, Global Accelerator provides two static Anycast IP addresses and uses the AWS global network to direct users to the best available endpoint. If a Region becomes unhealthy, traffic is automatically routed to another healthy Region with minimal disruption.&lt;/p&gt;

&lt;p&gt;In this section, we'll configure AWS Global Accelerator and connect it to the Application Load Balancers we created earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  How AWS Global Accelerator Works
&lt;/h2&gt;

&lt;p&gt;When a user accesses your application, they don't connect directly to an EC2 instance or an Application Load Balancer.&lt;/p&gt;

&lt;p&gt;Instead, they connect to the Global Accelerator endpoint.&lt;/p&gt;

&lt;p&gt;Global Accelerator evaluates the health of each configured endpoint and routes traffic to the optimal Region.&lt;/p&gt;

&lt;p&gt;If an endpoint becomes unavailable, traffic is redirected automatically without requiring DNS updates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2gy2mznn9ggj80h89r1z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2gy2mznn9ggj80h89r1z.png" alt=" " width="785" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 17: Create a Global Accelerator
&lt;/h2&gt;

&lt;p&gt;From the AWS Management Console, navigate to:&lt;/p&gt;

&lt;p&gt;AWS Global Accelerator&lt;br&gt;
→ Create Accelerator&lt;/p&gt;

&lt;p&gt;Configure the accelerator using the following settings.&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Name              Multi-Region-Web-App&lt;br&gt;
IP Address Type   IPv4&lt;br&gt;
Enabled           Yes&lt;/p&gt;

&lt;p&gt;Leave the remaining settings at their default values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Click Create Accelerator.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After a few moments, AWS will provision your accelerator and assign two static Anycast IP addresses.&lt;/p&gt;

&lt;p&gt;These IP addresses remain the same even if the backend infrastructure changes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh7ehlvem9pxl8uy80jxj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh7ehlvem9pxl8uy80jxj.png" alt=" " width="753" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 18: Configure the Listener
&lt;/h2&gt;

&lt;p&gt;Once the accelerator has been created, the next step is to configure a listener.&lt;/p&gt;

&lt;p&gt;A listener defines which ports Global Accelerator accepts from clients.&lt;/p&gt;

&lt;p&gt;Use the following configuration.&lt;/p&gt;

&lt;p&gt;Setting Value&lt;br&gt;
Protocol    TCP&lt;br&gt;
Port            80&lt;/p&gt;

&lt;p&gt;Click Next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhd13r4pwe5tpl8n0tbwk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhd13r4pwe5tpl8n0tbwk.png" alt=" " width="727" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 19: Create an Endpoint Group
&lt;/h2&gt;

&lt;p&gt;Global Accelerator organizes endpoints into Endpoint Groups.&lt;/p&gt;

&lt;p&gt;Each endpoint group represents an AWS Region.&lt;/p&gt;

&lt;p&gt;We'll create one endpoint group for each Region.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Primary Endpoint Group&lt;/strong&gt;&lt;br&gt;
Setting                   Value&lt;br&gt;
Region                   us-east-1&lt;br&gt;
Traffic Dial             100%&lt;br&gt;
Health Check Protocol    HTTP&lt;br&gt;
Health Check Port    80&lt;br&gt;
Health Check Path    /&lt;/p&gt;

&lt;p&gt;Repeat the process for the secondary Region.&lt;/p&gt;

&lt;p&gt;Secondary Endpoint Group&lt;br&gt;
Setting                 Value&lt;br&gt;
Region                us-west-2&lt;br&gt;
Traffic Dial            100%&lt;br&gt;
Health Check Protocol   HTTP&lt;br&gt;
Health Check Port   80&lt;br&gt;
Health Check Path   /&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 20: Add the Application Load Balancers
&lt;/h2&gt;

&lt;p&gt;Within each endpoint group, add the corresponding Application Load Balancer.&lt;/p&gt;

&lt;p&gt;Endpoint Group 1&lt;br&gt;
Endpoint        Weight&lt;br&gt;
primary-alb     128&lt;br&gt;
Endpoint Group       2&lt;br&gt;
Endpoint        Weight&lt;br&gt;
secondary-alb       128&lt;/p&gt;

&lt;p&gt;The endpoint weight determines how traffic is distributed among endpoints within the same group. Since each group contains only one ALB, the default weight is appropriate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 21: Deploy the Accelerator
&lt;/h2&gt;

&lt;p&gt;Review your configuration and click Create Accelerator.&lt;/p&gt;

&lt;p&gt;Provisioning typically takes a few minutes.&lt;/p&gt;

&lt;p&gt;Once complete, you'll receive:&lt;/p&gt;

&lt;p&gt;A Global Accelerator DNS name&lt;br&gt;
Two static Anycast IP addresses&lt;/p&gt;

&lt;p&gt;Your application can now be accessed through a single global endpoint instead of directly through either Application Load Balancer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxnvzvc3v1tbev2l0y54g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxnvzvc3v1tbev2l0y54g.png" alt=" " width="795" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying the Configuration
&lt;/h2&gt;

&lt;p&gt;Open a browser and navigate to the Global Accelerator DNS name.&lt;/p&gt;

&lt;p&gt;You should see your application load successfully.&lt;/p&gt;

&lt;p&gt;Depending on your location and routing policies, the request may be served from either the primary or secondary Region.&lt;/p&gt;

&lt;p&gt;At this point, everything appears normal—but the real value of this architecture becomes evident when a Region fails.&lt;/p&gt;

&lt;p&gt;We'll test that in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use AWS Global Accelerator Instead of DNS Failover?
&lt;/h2&gt;

&lt;p&gt;This is a common question among engineers.&lt;/p&gt;

&lt;p&gt;Traditional DNS failover relies on DNS record updates and caching.&lt;/p&gt;

&lt;p&gt;Because DNS responses are cached by clients and Internet Service Providers, failover may not happen immediately.&lt;/p&gt;

&lt;p&gt;AWS Global Accelerator takes a different approach.&lt;/p&gt;

&lt;p&gt;Instead of relying on DNS changes, it uses the AWS global network and continuously monitors endpoint health.&lt;/p&gt;

&lt;p&gt;This allows traffic to be redirected much faster, improving both availability and user experience.&lt;/p&gt;

&lt;p&gt;For applications where uptime is critical, Global Accelerator is often a better choice than DNS-only failover.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Accomplished
&lt;/h2&gt;

&lt;p&gt;At this point, we've successfully:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Created an AWS Global Accelerator&lt;/li&gt;
&lt;li&gt;Configured a listener&lt;/li&gt;
&lt;li&gt;Added endpoint groups for two AWS Regions&lt;/li&gt;
&lt;li&gt;Registered both Application Load Balancers&lt;/li&gt;
&lt;li&gt;Verified that the accelerator is healthy and ready to serve traffic
The architecture is now fully configured.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next section, we'll intentionally simulate a regional outage to validate the failover process and observe how Global Accelerator automatically redirects traffic to the healthy Region.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Automatic Failover
&lt;/h2&gt;

&lt;p&gt;A highly available architecture isn't complete until it's been tested.&lt;/p&gt;

&lt;p&gt;Up to this point, we've deployed our application across two AWS Regions, configured health checks, and set up AWS Global Accelerator to route traffic intelligently.&lt;/p&gt;

&lt;p&gt;Now it's time to answer the most important question:&lt;/p&gt;

&lt;p&gt;What happens if the primary Region becomes unavailable?&lt;/p&gt;

&lt;p&gt;Instead of assuming the architecture will work as expected, we'll intentionally simulate a failure and observe how AWS responds.&lt;/p&gt;

&lt;p&gt;Testing failover is a critical part of any disaster recovery strategy. It provides confidence that your application can continue serving users even when unexpected failures occur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 22: Verify the Application Before Testing
&lt;/h2&gt;

&lt;p&gt;Before simulating a failure, open the AWS Global Accelerator DNS name in your browser.&lt;/p&gt;

&lt;p&gt;The application should load successfully.&lt;/p&gt;

&lt;p&gt;Depending on your location and Global Accelerator's routing decisions, you should see the page served from your primary Region.&lt;/p&gt;

&lt;p&gt;This confirms that the application is healthy and ready for testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 23: Simulate a Regional Failure
&lt;/h2&gt;

&lt;p&gt;There are several ways to simulate a failure.&lt;/p&gt;

&lt;p&gt;For this lab, we'll stop the Apache web server running on the EC2 instance in the Primary Region.&lt;/p&gt;

&lt;p&gt;Connect to the primary EC2 instance using SSH.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5vife9iqpk33xlrwd5a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5vife9iqpk33xlrwd5a.png" alt=" " width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, the Application Load Balancer in the primary Region will no longer receive successful responses from the web server.&lt;/p&gt;

&lt;p&gt;After several failed health checks, the target will be marked as Unhealthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 24: Observe Automatic Failover
&lt;/h2&gt;

&lt;p&gt;Wait a few moments while Route 53 Health Checks detect the failure.&lt;/p&gt;

&lt;p&gt;Once the endpoint is marked as unhealthy, AWS Global Accelerator automatically stops routing traffic to the primary Region.&lt;/p&gt;

&lt;p&gt;Refresh your browser using the same Global Accelerator DNS name.&lt;/p&gt;

&lt;p&gt;You should now see the application being served from the Secondary Region (us-west-2).&lt;/p&gt;

&lt;p&gt;Notice that the URL remains exactly the same.&lt;/p&gt;

&lt;p&gt;No DNS changes are required.&lt;/p&gt;

&lt;p&gt;No manual intervention is needed.&lt;/p&gt;

&lt;p&gt;From the user's perspective, the application remains available despite the failure of one Region.&lt;/p&gt;

&lt;p&gt;This seamless transition is one of the key advantages of AWS Global Accelerator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 25: Restore the Primary Region
&lt;/h2&gt;

&lt;p&gt;Now let's bring the primary application back online.&lt;/p&gt;

&lt;p&gt;Reconnect to the EC2 instance in us-east-1 and start the Apache service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fadl5cmbde28u3sp2si18.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fadl5cmbde28u3sp2si18.png" alt=" " width="799" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a few successful health checks, Route 53 will mark the endpoint as healthy again.&lt;/p&gt;

&lt;p&gt;Global Accelerator will automatically resume routing traffic to the primary Region based on its endpoint health.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Observed
&lt;/h2&gt;

&lt;p&gt;This test demonstrates several important concepts about highly available architectures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application health is continuously monitored.&lt;/li&gt;
&lt;li&gt;Failed endpoints are automatically removed from service.&lt;/li&gt;
&lt;li&gt;Traffic is redirected without requiring DNS changes.&lt;/li&gt;
&lt;li&gt;Users continue accessing the application through the same global endpoint.&lt;/li&gt;
&lt;li&gt;Once the failed Region recovers, it is automatically added back into service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These behaviors are essential for building resilient cloud applications that can withstand infrastructure failures with minimal impact on end users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;One of the biggest lessons from this project is that high availability isn't just about deploying resources across multiple Regions.&lt;/p&gt;

&lt;p&gt;It's about designing an architecture that can detect failures, respond automatically, and recover without requiring manual intervention.&lt;/p&gt;

&lt;p&gt;That's exactly what we've accomplished by combining Route 53 Health Checks with AWS Global Accelerator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning Up Your AWS Resources
&lt;/h2&gt;

&lt;p&gt;Congratulations! You've successfully built and tested a highly available multi-region web application on AWS.&lt;/p&gt;

&lt;p&gt;If you created these resources in your own AWS account, it's important to remove them once you're done with the lab. Leaving resources running can result in unexpected charges, especially for services such as AWS Global Accelerator and Application Load Balancers.&lt;/p&gt;

&lt;p&gt;In this section, we'll clean up the environment in a logical order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 26: Delete the AWS Global Accelerator
&lt;/h2&gt;

&lt;p&gt;Navigate to:&lt;/p&gt;

&lt;p&gt;AWS Global Accelerator&lt;br&gt;
→ Accelerators&lt;/p&gt;

&lt;p&gt;Select the accelerator you created and choose Delete.&lt;/p&gt;

&lt;p&gt;Deleting the accelerator first ensures that it no longer references your Application Load Balancers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 27: Delete the Route 53 Health Checks
&lt;/h2&gt;

&lt;p&gt;Next, navigate to:&lt;/p&gt;

&lt;p&gt;Route 53&lt;br&gt;
→ Health Checks&lt;/p&gt;

&lt;p&gt;Delete both health checks:&lt;/p&gt;

&lt;p&gt;Primary-ALB-Health-Check&lt;br&gt;
Secondary-ALB-Health-Check&lt;/p&gt;

&lt;p&gt;Since these health checks are no longer needed, removing them prevents unnecessary monitoring costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 28: Delete the Application Load Balancers
&lt;/h2&gt;

&lt;p&gt;Navigate to:&lt;/p&gt;

&lt;p&gt;EC2&lt;br&gt;
→ Load Balancers&lt;/p&gt;

&lt;p&gt;Delete:&lt;/p&gt;

&lt;p&gt;primary-alb&lt;br&gt;
secondary-alb&lt;/p&gt;

&lt;p&gt;Keep in mind that AWS may take a few minutes to completely remove each load balancer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 29: Delete the Target Groups
&lt;/h2&gt;

&lt;p&gt;After the load balancers have been deleted, navigate to:&lt;/p&gt;

&lt;p&gt;EC2&lt;br&gt;
→ Target Groups&lt;/p&gt;

&lt;p&gt;Delete:&lt;/p&gt;

&lt;p&gt;primary-target-group&lt;br&gt;
secondary-target-group&lt;/p&gt;

&lt;p&gt;Target groups cannot be deleted while they are still associated with an active load balancer, which is why this step comes after deleting the ALBs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 30: Terminate the EC2 Instances
&lt;/h2&gt;

&lt;p&gt;Navigate to:&lt;/p&gt;

&lt;p&gt;EC2&lt;br&gt;
→ Instances&lt;/p&gt;

&lt;p&gt;Select both EC2 instances and choose:&lt;/p&gt;

&lt;p&gt;Instance State&lt;br&gt;
→ Terminate Instance&lt;/p&gt;

&lt;p&gt;Wait until their status changes to Terminated before proceeding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 31: Delete the Security Groups
&lt;/h2&gt;

&lt;p&gt;Navigate to:&lt;/p&gt;

&lt;p&gt;VPC&lt;br&gt;
→ Security Groups&lt;/p&gt;

&lt;p&gt;Delete the custom security groups you created for this project.&lt;/p&gt;

&lt;p&gt;If AWS prevents you from deleting a security group, double-check that no resources are still attached to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 32: Delete the Internet Gateways
&lt;/h2&gt;

&lt;p&gt;Navigate to:&lt;/p&gt;

&lt;p&gt;VPC&lt;br&gt;
→ Internet Gateways&lt;/p&gt;

&lt;p&gt;Before deleting each Internet Gateway, you'll need to detach it from its VPC.&lt;/p&gt;

&lt;p&gt;Select the Internet Gateway, then choose:&lt;/p&gt;

&lt;p&gt;Actions&lt;br&gt;
→ Detach from VPC&lt;/p&gt;

&lt;p&gt;Once detached, delete the Internet Gateway.&lt;/p&gt;

&lt;p&gt;Repeat this process for both Regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 33: Delete the Subnets
&lt;/h2&gt;

&lt;p&gt;Navigate to:&lt;/p&gt;

&lt;p&gt;VPC&lt;br&gt;
→ Subnets&lt;/p&gt;

&lt;p&gt;Delete the public subnet in each Region.&lt;/p&gt;

&lt;p&gt;AWS will prevent subnet deletion if resources still exist within it, so ensure all EC2 instances have been terminated first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 34: Delete the VPCs
&lt;/h2&gt;

&lt;p&gt;Finally, navigate to:&lt;/p&gt;

&lt;p&gt;VPC&lt;br&gt;
→ Your VPCs&lt;/p&gt;

&lt;p&gt;Delete:&lt;/p&gt;

&lt;p&gt;primary-vpc&lt;br&gt;
secondary-vpc&lt;/p&gt;

&lt;p&gt;At this point, all resources created during this project should have been removed from your AWS account.&lt;/p&gt;

&lt;p&gt;Cleanup Checklist&lt;/p&gt;

&lt;p&gt;Use this checklist to verify that you've removed everything:&lt;/p&gt;

&lt;p&gt;✅ AWS Global Accelerator deleted&lt;br&gt;
✅ Route 53 Health Checks deleted&lt;br&gt;
✅ Application Load Balancers deleted&lt;br&gt;
✅ Target Groups deleted&lt;br&gt;
✅ EC2 Instances terminated&lt;br&gt;
✅ Security Groups deleted&lt;br&gt;
✅ Internet Gateways detached and deleted&lt;br&gt;
✅ Public Subnets deleted&lt;br&gt;
✅ VPCs deleted&lt;/p&gt;

&lt;p&gt;Completing this checklist helps ensure you don't incur unnecessary AWS charges after finishing the lab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion and Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building cloud infrastructure is one thing.&lt;/p&gt;

&lt;p&gt;Building infrastructure that can withstand failure is another.&lt;/p&gt;

&lt;p&gt;_In this tutorial, we went beyond deploying a simple web application. We designed and implemented a highly available architecture capable of automatically redirecting traffic when an AWS Region becomes unavailable.&lt;br&gt;
_&lt;br&gt;
Along the way, we explored how several AWS services work together to deliver a resilient solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon VPC provided isolated networking environments.&lt;/li&gt;
&lt;li&gt;Amazon EC2 hosted the application in each Region.&lt;/li&gt;
&lt;li&gt;Application Load Balancers distributed traffic and performed health checks on application instances.&lt;/li&gt;
&lt;li&gt;Amazon Route 53 Health Checks continuously monitored endpoint availability.&lt;/li&gt;
&lt;li&gt;AWS Global Accelerator provided a single global entry point and intelligent traffic routing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most valuable part of this project wasn't creating the infrastructure—it was validating that the architecture behaved as expected during a simulated regional outage. Watching traffic automatically shift to the healthy Region reinforced an important principle of cloud engineering:&lt;/p&gt;

&lt;p&gt;A resilient architecture isn't defined by the resources you deploy. It's defined by how those resources respond when failures occur.&lt;/p&gt;

&lt;p&gt;This project also highlighted the importance of testing disaster recovery plans. Designing for failure is only part of the process; regularly validating your recovery strategy is what builds confidence in production systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;Although this project demonstrates the core concepts of high availability and multi-region failover, there are several ways it could be enhanced for a production environment:&lt;/p&gt;

&lt;p&gt;Replace standalone EC2 instances with Auto Scaling Groups to improve scalability and resilience.&lt;br&gt;
Use Amazon RDS Multi-AZ or Amazon Aurora Global Database for highly available databases.&lt;br&gt;
Secure the application with HTTPS using AWS Certificate Manager (ACM).&lt;br&gt;
Protect the application using AWS WAF and AWS Shield.&lt;br&gt;
Improve global performance with Amazon CloudFront.&lt;br&gt;
Provision the infrastructure using Terraform or AWS CloudFormation instead of manually creating resources.&lt;br&gt;
Centralize monitoring and alerting with Amazon CloudWatch and AWS CloudTrail.&lt;/p&gt;

&lt;p&gt;These enhancements reflect how similar architectures are commonly implemented in production environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This project deepened my understanding of high availability, disaster recovery, and global traffic management on AWS. More importantly, it reinforced the value of designing systems with failure in mind rather than treating resilience as an afterthought.&lt;/p&gt;

&lt;p&gt;I hope this guide has helped you understand not only how to build a multi-region architecture, but also why each component plays a critical role in delivering a reliable application.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you followed along, I'd love to hear about your experience. Did you encounter any challenges? Were there any improvements or alternative approaches you explored? Feel free to share your thoughts in the comments, I'd be interested to learn how you approached the project.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>linux</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
