<?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: Knirl Amboy</title>
    <description>The latest articles on DEV Community by Knirl Amboy (@knirl).</description>
    <link>https://dev.to/knirl</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%2F4100311%2Fcddabbd0-ea8e-4a45-97d7-e7fc56ead8b2.jpg</url>
      <title>DEV Community: Knirl Amboy</title>
      <link>https://dev.to/knirl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/knirl"/>
    <language>en</language>
    <item>
      <title>No More Clicking: Moving My AWS Architecture to Terraform</title>
      <dc:creator>Knirl Amboy</dc:creator>
      <pubDate>Thu, 22 Oct 2026 10:47:07 +0000</pubDate>
      <link>https://dev.to/knirl/no-more-clicking-moving-my-aws-architecture-to-terraform-1aek</link>
      <guid>https://dev.to/knirl/no-more-clicking-moving-my-aws-architecture-to-terraform-1aek</guid>
      <description>&lt;h2&gt;
  
  
  What was I trying to solve?
&lt;/h2&gt;

&lt;p&gt;We can build cloud architectures by clicking through a console. Yes, that works, but only once. It leaves you something that is difficult to review, reliably repeat and the sequence only exists in your memory. I have already built a highly available AWS architecture manually, but what if I need to replicate the build multiple times? This brings up a problem.&lt;/p&gt;

&lt;p&gt;How do you make an architecture reproducible, reviewable, and safe to change, instead of a one-time sequence of console clicks?&lt;/p&gt;




&lt;h2&gt;
  
  
  What did I build?
&lt;/h2&gt;

&lt;p&gt;Taking the web architecture I built manually on the AWS console, I reimplemented it using an Infrastructure as Code tool Terraform that enables me to automate the creation, management and updates of the architecture.&lt;br&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%2F5887jb089q17214etjki.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%2F5887jb089q17214etjki.png" alt="Diagram" width="691" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This project took on two stages/forms: first one mainly served as my knowledge and understanding of Terraform then the second was the final form which shows improvements in a lot of areas that practices the standards of Terraform usage or in production environments, which will be talked about in another article post.&lt;/p&gt;

&lt;p&gt;Being a new user of this tool, Terraform, I took things easy at the start. It was important for me to understand first the basics and fundamentals of Terraform, how it works.&lt;/p&gt;

&lt;p&gt;So, these are what the project intentionally does not include yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No variables&lt;/li&gt;
&lt;li&gt;No variables.tf&lt;/li&gt;
&lt;li&gt;No outputs.tf&lt;/li&gt;
&lt;li&gt;No modularization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything was hardcoded directly into the resource blocks and every resource lives in one folder instead of being split into reusable modules.&lt;/p&gt;




&lt;h2&gt;
  
  
  Here are the steps I took
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Set up remote state. A small one-time state-bootstrap project created the S3 bucket that holds Terraform’s state.&lt;br&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%2Fgbwznj6jv5difn3oudu5.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%2Fgbwznj6jv5difn3oudu5.png" alt="tfstate" width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Put guard rails in first. I created the &lt;code&gt;.gitignore&lt;/code&gt; and ran &lt;code&gt;git init&lt;/code&gt; before writing any real resources, so state files and secrets could never be committed by accident.&lt;br&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%2F1bwarld878hlaqns8bgt.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%2F1bwarld878hlaqns8bgt.png" alt="gitignore file" width="546" height="508"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build in layers. Each layer got a &lt;code&gt;plan&lt;/code&gt; and then an &lt;code&gt;apply&lt;/code&gt;:&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Networking: VPC, subnets, gateways, route tables&lt;/li&gt;
&lt;li&gt;Security groups&lt;/li&gt;
&lt;li&gt;Database: Multi-AZ RDS&lt;/li&gt;
&lt;li&gt;Compute: launch template and Auto Scaling Group&lt;/li&gt;
&lt;li&gt;Traffic: target group and load balancer&lt;/li&gt;
&lt;li&gt;Storage and monitoring: S3, CloudWatch, SNS
&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%2Fiptziwck16f5cf3j9cgy.png" alt="file structure" width="172" height="384"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Test in the console and browser. Terraform stops at &lt;code&gt;apply&lt;/code&gt;, so load balancing, auto-healing, and database failover were verified by hand.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tear it all down with one command. &lt;code&gt;terraform destroy&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Commit and push the code, with documentation, to GitHub.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Where Terraform made it easier
&lt;/h3&gt;

&lt;p&gt;I never wrote “create the VPC first.” A reference was enough, and Terraform worked out the order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security rules stayed correct as the infrastructure changed&lt;/strong&gt;. Each security group trusts the previous one by reference, not by IP address:&lt;br&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%2Fm53q8lccr2edq3zc9081.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%2Fm53q8lccr2edq3zc9081.png" alt="chained sg" width="452" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secrets never appeared in the code&lt;/strong&gt;. The database password is generated, stored in Secrets Manager, and passed to RDS by reference. It exists nowhere as plaintext.&lt;br&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%2Ff203arsl4fc4bwmrwjmm.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%2Ff203arsl4fc4bwmrwjmm.png" alt="db password" width="549" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every change was previewable&lt;/strong&gt;. &lt;code&gt;terraform plan&lt;/code&gt; showed exactly what would be added or destroyed before anything was touched. Reading plan output became my safety check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Teardown was one command&lt;/strong&gt;. &lt;code&gt;terraform destroy&lt;/code&gt; removed everything in the correct order. In the console, I had to remember that order myself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The setup is documented and repeatable&lt;/strong&gt;. The whole architecture lives in version-controlled files that anyone can read, review, and rebuild.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Challenges I encountered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Deprecated backend parameter**
&lt;/h3&gt;

&lt;p&gt;While setting up remote state for the project, I configured Terraform’s S3 backend using a DynamoDB table for state locking — the standard approach I’d read about in most tutorials. Running &lt;code&gt;terraform init&lt;/code&gt;, though, returned a deprecation warning: dynamodb_table was being phased out in favor of a newer parameter, &lt;code&gt;use_lockfile&lt;/code&gt;, which relies on S3’s own native conditional-write locking instead of requiring a separate DynamoDB table.&lt;/p&gt;

&lt;p&gt;It wasn’t a breaking error — my original setup still worked — but it was a clear signal that the “standard” approach I’d learned was already on its way out. Since I was still early in the build, I switched to &lt;code&gt;use_lockfile&lt;/code&gt; immediately rather than build the rest of the project on a pattern Terraform had already flagged for removal.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Overnight cost uncertainty
&lt;/h3&gt;

&lt;p&gt;Partway through the build, I had to stop for the night with a NAT Gateway and a Multi-AZ RDS instance already live — two of the most expensive, non-free-tier resources in the whole architecture. I didn’t want to tear everything down and lose my progress, but I also didn’t want to wake up to a billing surprise.&lt;/p&gt;

&lt;p&gt;Rather than guess, I worked out the actual math: NAT Gateway at roughly $0.045/hour, multi-AZ RDS at roughly $0.068/hour, multiplied by a ~10-12 hour overnight window — landing at under $1.50 total. That small bit of arithmetic turned an anxious guess into a confident decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways from this project:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Tutorials and guide material can lag behind a tool’s actual current best practices — it’s worth paying attention to deprecation warnings even when they don’t block you, since they’re often a preview of what “wrong” will look like in the future.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cloud cost awareness isn’t just knowing what’s expensive in the abstract — it’s being able to do quick, back-of-envelope math on your specific running resources to make an informed call, instead of either panicking or ignoring the risk entirely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Missed opportunity or mistake I realized:&lt;br&gt;
I wasn't aware that I can just import my existing AWS resources to terraform state. If I did that, I didn't need to destroy my architecture then recreate it using Terraform.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing this approach in reimplementing my web architecture through Terraform, I learned about how Terraform works and how quite helpful it is to make building AWS projects much quicker and efficiently.&lt;/p&gt;

&lt;p&gt;Onto the next article, I will discuss what were the improvements for this project to practice production standards.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>infrastructureascode</category>
      <category>aws</category>
    </item>
    <item>
      <title>How I Built a Self-Healing, Multi-AZ Architecture on AWS — And What Broke Along the Way</title>
      <dc:creator>Knirl Amboy</dc:creator>
      <pubDate>Thu, 17 Sep 2026 05:29:40 +0000</pubDate>
      <link>https://dev.to/knirl/how-i-built-a-self-healing-multi-az-infrastructure-on-aws-and-what-broke-along-the-way-72j</link>
      <guid>https://dev.to/knirl/how-i-built-a-self-healing-multi-az-infrastructure-on-aws-and-what-broke-along-the-way-72j</guid>
      <description>&lt;h2&gt;
  
  
  The Problem I Set Out to Solve
&lt;/h2&gt;

&lt;p&gt;One such example of an AWS project/build can be as simple as setting up one EC2 instance and then installing a web server. The moment that instance crashes or traffic spikes, the whole thing falls over.&lt;/p&gt;

&lt;p&gt;I wanted to build something that answers the question when it comes to "high availability": &lt;strong&gt;what happens when something fails, and does the system recover without a human stepping in&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What did I build?
&lt;/h2&gt;

&lt;p&gt;I set out to build a web architecture with no single point of failure — spanning two Availability Zones, load-balanced, auto-scaling, with a self-healing database — and then to actually &lt;em&gt;break it on purpose&lt;/em&gt; to prove it recovers, instead of just trusting the console said everything was configured correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&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%2Fc93qshzdoey9oxqnz2gw.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%2Fc93qshzdoey9oxqnz2gw.png" alt="Project 1 Architecture Diagram" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A VPC spanning 2 Availability Zones, with public subnets for the load balancer and private subnets for everything else&lt;/li&gt;
&lt;li&gt;1 NAT Gateway for both AZ. &lt;code&gt;Note: This should've been 1 NAT Gateway per AZ, so it should be a total of 2 NAT Gateways&lt;/code&gt; The reason for this is explained in this section, where I discussed the trade-offs I did.
&lt;/li&gt;
&lt;li&gt;An Application Load Balancer distributing traffic to an Auto Scaling Group of EC2 instances&lt;/li&gt;
&lt;li&gt;RDS running MySQL in Multi-AZ mode, with a live standby ready to take over&lt;/li&gt;
&lt;li&gt;CloudWatch monitoring the whole thing, with an alarm and an SNS to notify me.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Challenges I encountered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Incident 1: The ALB That Wouldn't Respond
&lt;/h3&gt;

&lt;p&gt;After wiring up the VPC, security groups, target group, and load balancer, I hit the ALB's DNS name in my browser and got:&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%2Fda56tj1bjrf4iqx68l23.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%2Fda56tj1bjrf4iqx68l23.png" alt="ALB Connection Error" width="800" height="852"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My first assumption was a security group misconfiguration. So I worked through it methodically:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checked the ALB's security group — inbound HTTP 80 from &lt;code&gt;0.0.0.0/0&lt;/code&gt; was correctly set&lt;/li&gt;
&lt;li&gt;Checked subnet placement — the ALB was correctly deployed across both public subnets&lt;/li&gt;
&lt;li&gt;Checked the ALB's scheme — confirmed "Internet-facing," not "Internal"&lt;/li&gt;
&lt;li&gt;Checked the listener — HTTP:80 was correctly forwarding to my target group&lt;/li&gt;
&lt;li&gt;Checked Network ACLs at the subnet level — default allow-all rules were untouched&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every single AWS-side configuration was correct. That's the moment it clicked that the problem probably wasn't in AWS at all — it was somewhere between my browser and AWS. I tried the same URL from my phone on cellular data, and then just paid closer attention to what was actually in my address bar.&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%2Fo2t6bhel1h6c8vb5lkon.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%2Fo2t6bhel1h6c8vb5lkon.png" alt="bare DNS name" width="483" height="33"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The issue: I'd pasted the bare DNS name into Edge, and the browser had silently prepended &lt;code&gt;https://&lt;/code&gt;. My ALB only had an HTTP listener configured — no HTTPS/443 listener existed at all. The browser was trying to reach a port that wasn't listening for that protocol, and quietly timing out instead of giving a clear "connection refused."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; type the URL explicitly as &lt;code&gt;http://...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this taught me:&lt;/strong&gt; when every layer of your infrastructure checks out, stop re-checking the same layer and start questioning your assumptions about the client side. I spent close to 20 minutes re-verifying security groups I'd already verified, when the actual bug was in how I was typing a URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Incident 2: The Page That Loaded, But Told Me Nothing
&lt;/h3&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%2F5u7tlj6jh26mbnmsm0is.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%2F5u7tlj6jh26mbnmsm0is.png" alt="Empty Page" width="251" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the ALB was reachable, my page loaded — but it was supposed to display the serving instance's ID and Availability Zone, and both fields were blank.&lt;/p&gt;

&lt;p&gt;My EC2 instances pull this data from AWS's internal instance metadata service, via a request like:&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;-s&lt;/span&gt; http://169.254.169.254/latest/meta-data/instance-id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a well-documented pattern, and I'd used it based on older references. Apache was running fine — the page loaded — so my instinct was that the problem was somewhere in my HTML generation logic. It wasn't. The metadata request itself was silently failing.&lt;/p&gt;

&lt;p&gt;The root cause: Amazon Linux 2023 requires &lt;strong&gt;IMDSv2&lt;/strong&gt; by default — a token-based authentication flow for metadata requests. My script was using the older tokenless (IMDSv1-style) request pattern, which gets rejected without an explicit error visible in the page output — &lt;code&gt;curl&lt;/code&gt; just returned empty, and my script happily wrote empty values into the HTML.&lt;/p&gt;

&lt;p&gt;The fix required requesting a session token first, then passing it as a header on every subsequent metadata call:&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="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="s2"&gt;"http://169.254.169.254/latest/api/token"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-aws-ec2-metadata-token-ttl-seconds: 21600"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;INSTANCE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-aws-ec2-metadata-token: &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://169.254.169.254/latest/meta-data/instance-id&lt;span class="si"&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%2F56jwr8cto1r0uepcai7l.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%2F56jwr8cto1r0uepcai7l.png" alt="imdsv2-style" width="465" height="314"&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%2Fmre4v3z8wm4nc6fpba5v.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%2Fmre4v3z8wm4nc6fpba5v.png" alt="Instance refresh" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But here's the part that actually tests whether you understand Auto Scaling Groups: fixing the script wasn't enough. My existing instances were already running the &lt;em&gt;old&lt;/em&gt; script — updating a Launch Template only affects instances launched &lt;em&gt;after&lt;/em&gt; the change, not ones already running. I had to publish the corrected script as a new Launch Template version, set it as default, and then trigger an &lt;strong&gt;Instance Refresh&lt;/strong&gt; — which gradually and safely replaced my running instances with new ones, one at a time, keeping at least one healthy throughout the process. That refresh took about 12–15 minutes for two instances, which is worth knowing going in so you don't assume something's stuck.&lt;/p&gt;

&lt;p&gt;Now, load balancer distributes traffic and the instances are spread across two AZs&lt;br&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%2Fo0gtpzd6dfmic8lhretn.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%2Fo0gtpzd6dfmic8lhretn.png" alt=" " width="569" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this taught me:&lt;/strong&gt; "no visible error" doesn't mean "no failure." A script can execute successfully from the OS's perspective while still failing at the thing it was actually meant to do. And infrastructure-as-config (Launch Templates) has a subtlety that infrastructure-as-code makes more obvious later: changing the definition doesn't retroactively change what's already running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Incident 3: Auto Scaling Group "flapping" risk
&lt;/h3&gt;

&lt;p&gt;Initially considered a manual step-scaling setup (scale out at 70% CPU / scale in at 40%). Switched to &lt;strong&gt;target tracking&lt;/strong&gt; at a 50% target instead — reduces configuration surface and lets AWS manage thresholds dynamically rather than risking two closely-tuned static alarms causing rapid scale in/out cycles.&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%2Fnj5hwdh7xwxai0i8sims.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%2Fnj5hwdh7xwxai0i8sims.png" alt=" " width="799" height="723"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Verification &amp;amp; Testing
&lt;/h2&gt;

&lt;p&gt;Configuration alone isn't proof — each piece of this architecture was actually tested:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load balancing&lt;/strong&gt;: confirmed by refreshing the ALB's URL repeatedly and observing the displayed Instance ID alternate between the two running instances.&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%2Fo0gtpzd6dfmic8lhretn.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%2Fo0gtpzd6dfmic8lhretn.png" alt="idaz1" width="569" height="202"&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%2F7p1rxvai65h8kmd92y8b.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%2F7p1rxvai65h8kmd92y8b.png" alt="idaz2" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto healing / monitoring&lt;/strong&gt;: intentionally removed the &lt;code&gt;ec2-sg&lt;/code&gt; inbound rule to cut ALB→EC2 connectivity, then confirmed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Target Group flipped both instances to &lt;code&gt;unhealthy&lt;/code&gt;&lt;br&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%2F95ey7e8ponutpfsxl3fd.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%2F95ey7e8ponutpfsxl3fd.png" alt="tg unhealthy" width="800" height="708"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The CloudWatch alarm transitioned to &lt;code&gt;In alarm&lt;/code&gt;&lt;/p&gt;&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%2F221j0exur877gjkduycp.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%2F221j0exur877gjkduycp.png" alt="alarm" width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An SNS email notification was recieved&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%2F6pc4gx50zoxok0a3b7zx.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%2F6pc4gx50zoxok0a3b7zx.png" alt="SNS notif" width="742" height="776"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The ASG attempted to replace the unhealthy instances (visible in its &lt;strong&gt;Activity log&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then reverted the rule and confirmed full recovery (targets back to &lt;code&gt;healthy&lt;/code&gt;, alarm back to &lt;code&gt;OK&lt;/code&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%2Fbcbrrqtv7k6cm64s8fqn.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%2Fbcbrrqtv7k6cm64s8fqn.png" alt=" " width="800" height="724"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database failover&lt;/strong&gt;: using RDS's "&lt;strong&gt;Reboot with failover&lt;/strong&gt;" option, forced the Multi-AZ database to promote its standby in the second Availability Zone to primary. Verified via the console that the instance's Availability Zone changed before vs. after — confirming automatic failover actually works, not just that the checkbox was enabled.&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%2Fvitiyt2ho3wo9a84a64x.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%2Fvitiyt2ho3wo9a84a64x.png" alt="Database failover" width="772" height="754"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-Offs I Had to Reason Through
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cost vs. Genuine Fault Tolerance: Multi-AZ RDS
&lt;/h3&gt;

&lt;p&gt;AWS's Free Tier RDS template doesn't offer Multi-AZ as an option at all — it's locked behind the "Production" template, which roughly doubles the database's hourly cost. I had a choice: stay on Free Tier and only be able to &lt;em&gt;describe&lt;/em&gt; automatic failover in an interview, or spend a small amount of account credit to actually build and test it.&lt;/p&gt;

&lt;p&gt;I chose to spend the credit. The reasoning: the entire point of this project was proving fault tolerance, not just listing services on a resume. Being able to say "I triggered a forced failover and verified the database moved to a different Availability Zone" is a fundamentally different claim than "I know RDS supports Multi-AZ." For a project meant to demonstrate engineering competence, the second claim doesn't hold up well under a follow-up question.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost vs. Architecture Purity: NAT Gateways per AZ
&lt;/h3&gt;

&lt;p&gt;The "textbook correct" production pattern is one NAT Gateway per Availability Zone, so that a NAT Gateway failure in one AZ doesn't take down internet access for the private subnet in that same AZ. Doing this at scale, for a real company, is the right call.&lt;/p&gt;

&lt;p&gt;For this project, I made a deliberate trade-off in the other direction: I chose to deploy a single NAT Gateway shared across both AZs to &lt;em&gt;manage hourly AWS costs&lt;/em&gt;. I recognize that a production-grade environment requires a NAT Gateway per AZ to prevent cross-AZ dependencies and ensure high availability. However, I got concerned on the adding another NAT Gateway as it can quite stack up costs plus, I wasn't fully aware yet if my free credits on the AWS account would cover it. So, I decided to &lt;em&gt;keep costs low&lt;/em&gt; for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Mattered Here
&lt;/h2&gt;

&lt;p&gt;If I'm honest, the value of this project wasn't in successfully clicking through the AWS console in the right order. It was in the hours I spent chasing down a browser protocol quirk and a metadata API version mismatch. Production systems don't fail in the ways tutorials describe; they fail in small, unglamorous, easy-to-miss ways, and the skill that matters is having a methodical process for narrowing down the cause instead of guessing.&lt;/p&gt;

&lt;p&gt;Next, I'm rebuilding this same architecture in Terraform — taking it from a manual console build to something version-controlled, reviewable, and deployable in minutes, then adding on some improvements.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>infrastructure</category>
      <category>highlyavailable</category>
      <category>awsconsole</category>
    </item>
  </channel>
</rss>
