<?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: Atul Vishwakarma</title>
    <description>The latest articles on DEV Community by Atul Vishwakarma (@vatul16).</description>
    <link>https://dev.to/vatul16</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%2F530617%2F3e6b3efc-a674-477c-be19-95c2214adbf3.jpg</url>
      <title>DEV Community: Atul Vishwakarma</title>
      <link>https://dev.to/vatul16</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vatul16"/>
    <language>en</language>
    <item>
      <title>Stop Hardcoding AWS Credentials: How to Manage Secrets Securely</title>
      <dc:creator>Atul Vishwakarma</dc:creator>
      <pubDate>Fri, 10 Jul 2026 02:30:00 +0000</pubDate>
      <link>https://dev.to/vatul16/stop-hardcoding-aws-credentials-how-to-manage-secrets-securely-3bl</link>
      <guid>https://dev.to/vatul16/stop-hardcoding-aws-credentials-how-to-manage-secrets-securely-3bl</guid>
      <description>&lt;p&gt;It is a cloud engineer's worst nightmare.&lt;/p&gt;

&lt;p&gt;You finish a long coding session, type &lt;code&gt;git push origin main&lt;/code&gt;, and go to sleep. You wake up the next morning to hundreds of automated emails from AWS and a billing dashboard showing a $5,000 charge.&lt;/p&gt;

&lt;p&gt;What happened? You accidentally pushed your AWS Access Key and Secret Key to a public GitHub repository. Within seconds, automated bots scraped your keys and spun up dozens of expensive EC2 instances across the globe to mine cryptocurrency.&lt;/p&gt;

&lt;p&gt;Today, we are going to make sure this never happens to you. Here is how to stop hardcoding your AWS credentials and manage your secrets securely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bad Way: Hardcoding in Your App
&lt;/h2&gt;

&lt;p&gt;When you are just learning how to use the AWS SDK (like boto3 for Python or the AWS SDK for Node.js), it is incredibly tempting to just paste your keys directly into your code so it works:&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;// ❌ NEVER DO THIS!&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AWS&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;aws-sdk&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;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;S3&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;accessKeyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AKIAIOSFODNN7EXAMPLE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;secretAccessKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this code gets pushed to GitHub, your AWS account is completely compromised. We need to separate our &lt;em&gt;code&lt;/em&gt; from our &lt;em&gt;configuration&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Good Way (Local Development): AWS CLI Profiles
&lt;/h2&gt;

&lt;p&gt;Instead of putting credentials in your code, you should store them securely on your local machine using the AWS Command Line Interface (CLI).&lt;/p&gt;

&lt;p&gt;If you have the AWS CLI installed, open your terminal and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will prompt you to enter your Access Key, Secret Key, default region (e.g., &lt;code&gt;us-east-1&lt;/code&gt;), and output format.&lt;/p&gt;

&lt;p&gt;Once you do this, AWS creates a hidden file on your machine. If you are developing on a Linux system (like Fedora) or macOS, this file lives at &lt;code&gt;~/.aws/credentials&lt;/code&gt;. On Windows, it is at &lt;code&gt;C:\Users\YOUR_USERNAME\.aws\credentials&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&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%2Fmt12v85u4g06mt2sbd23.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%2Fmt12v85u4g06mt2sbd23.png" alt=" " width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, when you run your code, the AWS SDK will automatically look for this hidden file and use the credentials inside it. Your code becomes completely clean:&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;// ✅ THE SAFE WAY&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AWS&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;aws-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The SDK automatically pulls from your ~/.aws/credentials file!&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;S3&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; 

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Good Way (Pipelines &amp;amp; CI/CD): Environment Variables
&lt;/h2&gt;

&lt;p&gt;If you are running your code inside a Docker container or a GitHub Actions pipeline, you can't manually run &lt;code&gt;aws configure&lt;/code&gt;. Instead, you use &lt;strong&gt;Environment Variables&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;AWS SDKs are programmed to automatically look for these specific variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;AWS_DEFAULT_REGION&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are working locally with a &lt;code&gt;.env&lt;/code&gt; file, it should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_DEFAULT_REGION=us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&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%2Fxvm4edlhqm2hrs51lkzk.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%2Fxvm4edlhqm2hrs51lkzk.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Crucial Step:&lt;/strong&gt; You must add &lt;code&gt;.env&lt;/code&gt; to your &lt;code&gt;.gitignore&lt;/code&gt; file. If you track your &lt;code&gt;.env&lt;/code&gt; file in Git, you are right back to the nightmare scenario we talked about at the beginning!&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ultimate Fix (Production): AWS Secrets Manager
&lt;/h2&gt;

&lt;p&gt;The methods above are perfect for personal projects, local development, and simple CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;However, when you are dealing with production microservices at scale, managing dozens of &lt;code&gt;.env&lt;/code&gt; files becomes a massive security liability. In a true production environment, the best practice is to migrate credential management to &lt;strong&gt;AWS Secrets Manager&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By using Secrets Manager, you eliminate hardcoded secrets entirely, automate key rotation, and establish SOC2-aligned auditability so you know exactly which microservice accessed which secret and when.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Security in the cloud isn't just an afterthought; it is step zero. By utilizing &lt;code&gt;aws configure&lt;/code&gt; locally and environment variables in your pipelines, you can push your code to GitHub with complete peace of mind.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(If you are using these secured credentials to write infrastructure code, be sure to bookmark my&lt;/em&gt; &lt;a href="https://atulcodes.hashnode.dev/ultimate-terraform-cheatsheet-devops" rel="noopener noreferrer"&gt;&lt;em&gt;Ultimate Terraform Cheatsheet&lt;/em&gt;&lt;/a&gt; &lt;em&gt;to speed up your workflow!)&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this security tip helped you out today, you can&lt;/em&gt; &lt;a href="https://buymeacoffee.com/vatul16" rel="noopener noreferrer"&gt;&lt;em&gt;☕ buy me a coffee here&lt;/em&gt;&lt;/a&gt; &lt;em&gt;to support my work!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>security</category>
      <category>github</category>
    </item>
    <item>
      <title>Infrastructure as Code 101: Provisioning an AWS EC2 Instance with Terraform</title>
      <dc:creator>Atul Vishwakarma</dc:creator>
      <pubDate>Thu, 02 Jul 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/vatul16/infrastructure-as-code-101-provisioning-an-aws-ec2-instance-with-terraform-44d8</link>
      <guid>https://dev.to/vatul16/infrastructure-as-code-101-provisioning-an-aws-ec2-instance-with-terraform-44d8</guid>
      <description>&lt;p&gt;If you are currently managing your AWS infrastructure by logging into the console, clicking through menus, checking boxes, and hitting "Create," you are practicing what DevOps engineers jokingly call "&lt;strong&gt;ClickOps.&lt;/strong&gt;"&lt;/p&gt;

&lt;p&gt;While clicking around is fine for learning, it is a nightmare for production. Manual clicks are impossible to track, prone to human error, and incredibly slow to replicate. If your server goes down, you have to remember exactly which boxes you checked six months ago to rebuild it.&lt;/p&gt;

&lt;p&gt;The solution is &lt;strong&gt;Infrastructure as Code (IaC)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Today, we are diving into &lt;strong&gt;Terraform&lt;/strong&gt;, the industry standard for IaC. I am going to show you how to stop clicking and start coding by provisioning a basic Linux AWS EC2 instance using just a few lines of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Terraform?
&lt;/h2&gt;

&lt;p&gt;Created by HashiCorp, Terraform is a tool that allows you to define both cloud and on-premise resources in human-readable configuration files. It uses a declarative language called HCL (HashiCorp Configuration Language).&lt;/p&gt;

&lt;p&gt;"Declarative" means you just tell Terraform what you want (e.g., "I want one t2.micro EC2 instance"), and Terraform figures out how to communicate with AWS APIs to make it happen.&lt;/p&gt;

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

&lt;p&gt;Before we write our code, you need three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An &lt;strong&gt;AWS Account&lt;/strong&gt; (the Free Tier is perfect).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;AWS CLI&lt;/strong&gt; installed and configured on your machine with your IAM credentials (so Terraform has permission to talk to your account).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt; installed on your local machine.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 1: Define the Provider
&lt;/h2&gt;

&lt;p&gt;Terraform can manage infrastructure on AWS, Azure, Google Cloud, and even GitHub! To tell Terraform we want to talk specifically to AWS, we need to configure a "Provider."&lt;/p&gt;

&lt;p&gt;Create a new folder on your computer, open it in your terminal, and create a file called &lt;code&gt;main.tf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Add this code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~&amp;gt; 5.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "us-east-1" # Change this to your preferred region
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Write the EC2 Resource Block
&lt;/h2&gt;

&lt;p&gt;Now, we tell Terraform exactly what resource we want to build. Below your provider block in &lt;code&gt;main.tf&lt;/code&gt;, add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_instance" "my_first_server" {
  ami           = "ami-0c7217cdde317cfec" # Ubuntu 22.04 LTS in us-east-1
  instance_type = "t2.micro"

  tags = {
    Name = "AtulCodes-Terraform-Server"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Breaking down the code:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;resource&lt;/code&gt;: Tells Terraform we are creating a new piece of infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"aws_instance"&lt;/code&gt;: The specific type of resource (an EC2 instance).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"my_first_server"&lt;/code&gt;: This is just a local name we use to reference this server within our Terraform code. AWS never sees this name.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ami&lt;/code&gt;: The Amazon Machine Image ID. Note: AMIs are region-specific! The one above is for Ubuntu in us-east-1. If you are in ap-south-1 (Mumbai), you will need to grab a different AMI ID from the AWS console.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;instance_type&lt;/code&gt;: We are using t2.micro because it qualifies for the AWS Free Tier.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tags&lt;/code&gt;: This is how we actually name the server inside the AWS Console.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&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%2Fk0y258miadkwgj3lko50.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%2Fk0y258miadkwgj3lko50.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: The Core Terraform Workflow
&lt;/h2&gt;

&lt;p&gt;Terraform relies on three magical commands to get your infrastructure into the cloud: &lt;code&gt;init&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, and &lt;code&gt;apply&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Initialize the Directory
&lt;/h3&gt;

&lt;p&gt;Open your terminal in your project folder and run:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This command looks at your &lt;code&gt;main.tf&lt;/code&gt; file, sees you want to use AWS, and downloads the necessary AWS plugins in the background.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Plan the Deployment
&lt;/h3&gt;

&lt;p&gt;Next, we want to see what Terraform is going to do before it actually does it.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;You will get a large output showing exactly what resources will be created. Look for the green &lt;code&gt;+&lt;/code&gt; signs. At the bottom, it should say: &lt;code&gt;Plan: 1 to add, 0 to change, 0 to destroy&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&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%2F03i7ws7w694kwebihpdw.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%2F03i7ws7w694kwebihpdw.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Apply the Code
&lt;/h3&gt;

&lt;p&gt;If the plan looks good, it is time to deploy!&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Terraform will show you the plan one more time and ask you to type &lt;code&gt;yes&lt;/code&gt; to confirm. Type &lt;code&gt;yes&lt;/code&gt; and hit enter. Within a few seconds, Terraform will communicate with AWS and spin up your server!&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Verify in the AWS Console
&lt;/h2&gt;

&lt;p&gt;Don't just take my word for it. Log into your AWS Management Console, navigate to the &lt;strong&gt;EC2 Dashboard&lt;/strong&gt;, and click on &lt;strong&gt;Instances&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You will see a brand-new server named &lt;code&gt;AtulCodes-Terraform-Server&lt;/code&gt; sitting right there, in a "Running" state. You provisioned that without clicking a single button in the UI!&lt;/p&gt;

&lt;blockquote&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%2Fbv194ril5bvh09ioev6r.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%2Fbv194ril5bvh09ioev6r.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: Destroy the Evidence (Crucial!)
&lt;/h2&gt;

&lt;p&gt;One of the best features of IaC is how easy it is to clean up. If you leave this server running, AWS might eventually charge you for it.&lt;/p&gt;

&lt;p&gt;To tear down everything we just built, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type &lt;code&gt;yes&lt;/code&gt; when prompted. Terraform will gracefully delete the EC2 instance it created.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;You have officially taken your first step into Infrastructure as Code. By writing your infrastructure as code, you can version control it in Git, review it in Pull Requests, and ensure your staging environments are exact replicas of your production environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next week, I am dropping my Ultimate Terraform Cheatsheet!&lt;/strong&gt; It will cover all the essential commands, state management, and variable syntax you need for your daily DevOps workflows. Make sure you hit the subscribe button so you don't miss it.&lt;/p&gt;




&lt;p&gt;If this tutorial helped you deploy your first Terraform resource, you can &lt;a href="https://buymeacoffee.com/vatul16" rel="noopener noreferrer"&gt;☕ buy me a coffee here&lt;/a&gt; to support my work!&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>devops</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>TIL: How to Fix the "Port Already in Use" Error in Docker</title>
      <dc:creator>Atul Vishwakarma</dc:creator>
      <pubDate>Tue, 30 Jun 2026 03:30:00 +0000</pubDate>
      <link>https://dev.to/vatul16/til-how-to-fix-the-port-already-in-use-error-in-docker-1b65</link>
      <guid>https://dev.to/vatul16/til-how-to-fix-the-port-already-in-use-error-in-docker-1b65</guid>
      <description>&lt;p&gt;We have all been there. You just built a fresh Docker image, you confidently type your d&lt;code&gt;ocker run&lt;/code&gt; command into the terminal, and &lt;strong&gt;BAM&lt;/strong&gt;. You are hit with a massive wall of red text that looks something like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Error starting userland proxy: listen tcp4 0.0.0.0:3000: bind: address already in use.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This error (often referred to as &lt;code&gt;EADDRINUSE&lt;/code&gt;) simply means that something else on your computer is already using the port you are trying to assign to your Docker container.&lt;/p&gt;

&lt;p&gt;Today I Learned (TIL) the fastest way to find that rogue process and terminate it so you can get back to coding. Here is the two-step fix for Mac and Linux users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Find the Rogue Process
&lt;/h2&gt;

&lt;p&gt;To find out exactly which application is hoarding your port, we will use the &lt;code&gt;lsof&lt;/code&gt; (List Open Files) command.&lt;/p&gt;

&lt;p&gt;Open your terminal and type this command, replacing &lt;code&gt;3000&lt;/code&gt; with whatever port is giving you trouble:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-i&lt;/span&gt; :3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will return a list of processes using that port. Look under the &lt;strong&gt;PID&lt;/strong&gt; (Process ID) column. You need that number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Kill the Process
&lt;/h2&gt;

&lt;p&gt;Now that we have the Process ID, we can force it to shut down using the &lt;code&gt;kill&lt;/code&gt; command. Pass the &lt;code&gt;-9&lt;/code&gt; flag to ensure it terminates immediately, followed by the PID you found in Step 1:&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;kill&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt; 14235
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it! Your port is now free. You can run your &lt;code&gt;docker run -p 3000:3000 my-app&lt;/code&gt; command again, and it will spin up perfectly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus: What if it's a "Ghost" Docker Container?
&lt;/h2&gt;

&lt;p&gt;Sometimes, the port is being blocked because you started a Docker container earlier, forgot about it, and left it running in the background.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;lsof&lt;/code&gt; doesn't solve your problem, quickly check your active Docker containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the container using your port, grab its Container ID, and stop it gracefully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stop &amp;lt;CONTAINER_ID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;📚 Catch up on this Docker Series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: &lt;a href="https://dev.to/vatul16/how-to-dockerize-a-nodejs-app-in-5-easy-steps-44d4"&gt;How to Dockerize a Node.js App in 5 Easy Steps&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Part 2: &lt;a href="https://dev.to/vatul16/automate-your-docker-builds-with-github-actions-4nbp"&gt;Automate Your Docker Builds with GitHub Actions&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this quick tip saved you some debugging time today, you can &lt;a href="https://buymeacoffee.com/vatul16" rel="noopener noreferrer"&gt;☕ buy me a coffee here&lt;/a&gt; to support my work!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>linux</category>
      <category>devops</category>
      <category>todayilearned</category>
    </item>
    <item>
      <title>TerraGuard AI: Adding an AI Brain to Terraform Drift Detection — Design Decisions, Trade-offs, and Lessons Learned</title>
      <dc:creator>Atul Vishwakarma</dc:creator>
      <pubDate>Mon, 29 Jun 2026 05:30:00 +0000</pubDate>
      <link>https://dev.to/vatul16/terraguard-ai-adding-an-ai-brain-to-terraform-drift-detection-design-decisions-trade-offs-and-7dg</link>
      <guid>https://dev.to/vatul16/terraguard-ai-adding-an-ai-brain-to-terraform-drift-detection-design-decisions-trade-offs-and-7dg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/vatul16/TerraGuard-AI" rel="noopener noreferrer"&gt;https://github.com/vatul16/TerraGuard-AI&lt;/a&gt; — full source, Terraform infra, drift detection engine, and CI/CD workflows.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When I finished &lt;a href="https://github.com/vatul16/terratier" rel="noopener noreferrer"&gt;TerraTier&lt;/a&gt;, the 3-tier AWS architecture project I wrote about last time, I wanted to build something that pushed in a different direction. TerraTier was about the infrastructure layer — networking, security boundaries, secrets management. This project is about the operational layer: what happens &lt;em&gt;after&lt;/em&gt; the infrastructure is running, when someone inevitably makes a change outside of Terraform.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;TerraGuard AI&lt;/strong&gt; — an event-driven drift detection system that catches manual AWS console changes, classifies their risk using an LLM, and routes context-rich alerts intelligently instead of just firing a generic notification every time a description field changes. A Node.js API on ECS Fargate, a PostgreSQL database on RDS, and an ALB are the infrastructure being monitored. GitHub Actions, Python, and Groq's API for Llama 3.3 are how the detection and analysis work.&lt;/p&gt;

&lt;p&gt;This article is about the decisions behind why it works the way it does, and what I'd change.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with standard drift detection
&lt;/h2&gt;

&lt;p&gt;The textbook implementation of Terraform drift detection is: run &lt;code&gt;terraform plan -detailed-exitcode&lt;/code&gt; on a cron job, check the exit code, send an alert if it's non-zero. This works, and it takes maybe 30 lines of shell script to implement. The problem isn't the detection — it's what happens to the humans on the other end of the alert.&lt;/p&gt;

&lt;p&gt;In any environment where more than one person touches the AWS console, drift is nearly constant. Someone tags a security group directly in the console, or updates a description, or changes a retention period on a log group that wasn't in Terraform yet. Each of these fires the exact same alert as "port 22 opened to 0.0.0.0/0 on a production security group." After the third or fourth time the on-call engineer wakes up to an alert and finds a tag change, they start doing what humans do with noisy signals: they ignore them.&lt;/p&gt;

&lt;p&gt;Alert fatigue is a well-documented problem in SRE literature, but most drift detection tooling solves it by giving you more configuration options — exception lists, ignore patterns, threshold tuning. That's fighting symptoms. The actual cause is that the alerting system has no concept of severity. It knows something changed; it doesn't know what that means.&lt;/p&gt;

&lt;p&gt;The insight that shaped this project is that detection and classification are two separate problems, and they need different tools. Terraform is genuinely excellent at detection — it has the full resource schema, it talks directly to the AWS APIs, and its plan output is deterministic and reliable. It has no idea what a change means from a security or business perspective. That's a reasoning problem, and it's exactly what language models are good at.&lt;/p&gt;

&lt;p&gt;So TerraGuard AI uses Terraform to detect drift, and an LLM to classify it. The two tools do the parts they're each actually suited for.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 1: AI for classification, not detection
&lt;/h2&gt;

&lt;p&gt;This distinction is worth dwelling on, because it's easy to imagine AI being used to &lt;em&gt;find&lt;/em&gt; drift — some vector-similarity approach comparing current state to desired state, or something. That would be using AI to badly replicate something Terraform already does perfectly.&lt;/p&gt;

&lt;p&gt;The LLM layer in this project only ever sees the parsed output of &lt;code&gt;terraform plan&lt;/code&gt;. Drift is already confirmed by the time the AI gets involved. What the AI does is answer a different set of questions: Is this change a security risk or a configuration nuance? Should it be reverted or adopted into the Terraform codebase? What is the exact command someone should run to remediate it, right now, without having to look anything up?&lt;/p&gt;

&lt;p&gt;The prompt asks Groq's Llama 3.3 to act as a Senior Cloud Security Engineer and return a structured JSON response with a risk level (&lt;code&gt;CRITICAL&lt;/code&gt; / &lt;code&gt;HIGH&lt;/code&gt; / &lt;code&gt;MEDIUM&lt;/code&gt; / &lt;code&gt;LOW&lt;/code&gt; / &lt;code&gt;INFO&lt;/code&gt;), a score out of 10, an impact summary, a recommended action (&lt;code&gt;REVERT&lt;/code&gt; / &lt;code&gt;ADOPT&lt;/code&gt; / &lt;code&gt;INVESTIGATE&lt;/code&gt; / &lt;code&gt;MONITOR&lt;/code&gt;), and a concrete remediation command. The temperature is set to 0.1 — not 0, because zero-temperature responses can be oddly mechanical, but close enough to zero that the analysis is consistent and factual rather than creative.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CRITICAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;risk_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Security group ingress rule added exposing SSH port 22 to all internet traffic.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;impact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Any host on the internet can attempt SSH brute-force or exploitation against ECS tasks.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REVERT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;remediation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;terraform apply -target=aws_security_group.ecs_tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Opening port 22 to 0.0.0.0/0 is a critical security misconfiguration...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Based on that response, the routing is simple: &lt;code&gt;CRITICAL&lt;/code&gt; or &lt;code&gt;HIGH&lt;/code&gt; goes to Slack immediately, plus a GitHub Issue for the audit trail. &lt;code&gt;MEDIUM&lt;/code&gt;, &lt;code&gt;LOW&lt;/code&gt;, or &lt;code&gt;INFO&lt;/code&gt; creates a GitHub Issue that goes into the backlog — no Slack notification, no on-call page.&lt;/p&gt;

&lt;p&gt;The practical effect is that the Slack channel only gets messages that actually need a human to look at something now. The rest creates a self-organizing backlog of minor drift that can be handled during normal working hours. That's not a feature — it's the entire point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 2: Groq over AWS Bedrock
&lt;/h2&gt;

&lt;p&gt;The obvious choice for an AWS-native project would be Amazon Bedrock. The LLM call would live inside the VPC, authentication would use the same IAM role the rest of the infrastructure uses, and there would be no external API dependency.&lt;/p&gt;

&lt;p&gt;I chose Groq instead, for two reasons that turned out to matter more than I expected.&lt;/p&gt;

&lt;p&gt;The first is speed. Groq's custom LPU hardware runs Llama 3.3 70B at a genuinely unusual pace — we're talking single-digit seconds for a complete risk analysis. Bedrock inference at a comparable model size is slower, and when drift detection runs in a GitHub Actions workflow with a 6-hour cron cadence, latency matters less than it would in a real-time system. But for local development and testing, where you're running the detector manually and waiting for output, the speed difference makes iteration significantly less frustrating.&lt;/p&gt;

&lt;p&gt;The second reason is cost. Groq's free tier is 14,400 requests per day, which is more than enough for a drift detection workflow that runs every 6 hours and only calls the API when drift is actually found. Bedrock charges per-token with no free tier. For a personal portfolio project that also needs to pay for ECS, RDS, NAT Gateway, and an ALB, not paying for inference is a meaningful constraint.&lt;/p&gt;

&lt;p&gt;The trade-off is the external dependency. The drift detector now requires an outbound HTTPS connection from the GitHub Actions runner to &lt;code&gt;api.groq.com&lt;/code&gt;, and if Groq is down or rate-limiting, the analysis step fails. I handled this with a fallback in the Python script: if the Groq call fails or returns unparseable JSON, the detector falls back to a &lt;code&gt;MEDIUM&lt;/code&gt; risk assessment with a manual review recommendation rather than crashing entirely. The drift is still logged and acted on; it just lacks the AI analysis.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 3: GitHub Actions over Lambda
&lt;/h2&gt;

&lt;p&gt;The architecturally "purer" approach to scheduled drift detection is a Lambda function triggered by EventBridge on a cron schedule, with an additional EventBridge rule listening to CloudTrail events for real-time detection when a console change happens. That's closer to the enterprise architecture in the project's original design.&lt;/p&gt;

&lt;p&gt;I built it on GitHub Actions instead, and I think that was the right call for this stage.&lt;/p&gt;

&lt;p&gt;The practical argument: GitHub Actions is free, already configured for the project, and doesn't require provisioning, IAM-scoping, or maintaining a separate compute resource. A Lambda function adds at least three more Terraform resources to manage, an ECR image or deployment package to maintain, and CloudWatch Logs to check when something goes wrong — all of which is real work that doesn't directly demonstrate anything new. The drift detection workflow in GitHub Actions does the same job with less infrastructure surface area and a simpler debugging story (&lt;code&gt;Actions&lt;/code&gt; tab in GitHub, logs right there).&lt;/p&gt;

&lt;p&gt;The conceptual argument: the drift detector is not a latency-sensitive workload. A 6-hour detection window is fine for a portfolio project, and even for many real production environments, catching a security group change within 6 hours is a meaningful improvement over catching it when someone manually reviews the AWS console. Real-time detection via CloudTrail + EventBridge is in the project roadmap, but it's a layer added on top of a working baseline, not a prerequisite.&lt;/p&gt;

&lt;p&gt;Where this decision has a genuine cost: the GitHub Actions runner needs AWS credentials. That means &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; as repository secrets — long-lived credentials stored in GitHub, which is a less secure pattern than OIDC role assumption. The Terraform IAM code in the project already scaffolds the OIDC setup (&lt;code&gt;aws_iam_openid_connect_provider&lt;/code&gt;, &lt;code&gt;aws_iam_role&lt;/code&gt; with a GitHub OIDC trust policy), and migrating to keyless authentication is the highest-priority item on the roadmap. I built with access keys first to get the workflow working without debugging OIDC trust policies at the same time, which is a trade-off I'd make again in the same situation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 4: Minimal infrastructure being monitored
&lt;/h2&gt;

&lt;p&gt;A reasonable design question for a drift detector is: what infrastructure should it monitor? You could make the case that the more complex the infrastructure, the more impressive the demo — VPCs with 20 resources, multiple environments, Auto Scaling Groups, the works.&lt;/p&gt;

&lt;p&gt;I went the opposite direction. The monitored infrastructure is as simple as it can be while still being realistic: one ECS Fargate service, one RDS PostgreSQL database, one ALB. That's a pattern that appears in real production environments constantly — it's the architecture for a backend API behind a load balancer with persistent storage. Nothing exotic.&lt;/p&gt;

&lt;p&gt;The reason for keeping it simple is that the interesting thing in this project is the drift detection and classification layer, not the infrastructure. A 40-resource VPC with multiple tiers and Auto Scaling Groups would take most of the explanation budget to describe, and would make the CI/CD and drift detection workflows harder to follow for someone reading the code. By keeping the infrastructure simple and well-understood, someone reading the repository can quickly get past "what does this infrastructure do" and get to "how does the drift detection actually work" — which is the part worth explaining.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 5: Separating infrastructure and application deployment
&lt;/h2&gt;

&lt;p&gt;The most practically important single line in the entire &lt;code&gt;ecs.tf&lt;/code&gt; file is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ignore_changes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;task_definition&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;Without this, &lt;code&gt;terraform apply&lt;/code&gt; and the &lt;code&gt;deploy-app.yml&lt;/code&gt; GitHub Actions workflow would fight each other. The deploy workflow updates the ECS task definition to use a new ECR image URI (tagged with the git SHA) every time new code is pushed to &lt;code&gt;app/**&lt;/code&gt;. If Terraform's ECS service resource didn't ignore task definition changes, the next &lt;code&gt;terraform apply&lt;/code&gt; — triggered by the drift detector or by a manual infra change — would overwrite the CI/CD-deployed image with whatever image URI is in the Terraform variables. Deploying a new version of the app would immediately cause "drift" against the Terraform-managed state.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ignore_changes&lt;/code&gt; lifecycle setting tells Terraform: "I know this attribute might differ from what's in the config, and that's intentional — CI/CD owns it, not you." This is the standard pattern for separating infrastructure management (Terraform's job) from application deployment (CI/CD's job) on ECS, and it's one of those things that sounds obvious in retrospect but takes a debugging session to discover if you haven't seen it before.&lt;/p&gt;

&lt;p&gt;The deploy workflow itself follows a pattern worth understanding: it doesn't just push an image and call &lt;code&gt;aws ecs update-service --force-new-deployment&lt;/code&gt;. It downloads the &lt;em&gt;current&lt;/em&gt; live task definition from AWS, replaces only the container image URI, registers the modified definition as a new revision, and then updates the service to use that revision. The reason for this sequence is that the task definition contains Secrets Manager ARNs, IAM role ARNs, environment variable sets, and health check configuration that are all managed by Terraform. Overwriting the entire task definition from a CI/CD workflow would blow away those fields. Downloading the live definition and patching only the image preserves everything else.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 6: Secrets Manager with ECS secrets references, not environment variables
&lt;/h2&gt;

&lt;p&gt;The Node.js application gets its database connection details through a combination of plain environment variables and a &lt;code&gt;secrets&lt;/code&gt; block in the ECS task definition.&lt;/p&gt;

&lt;p&gt;Non-sensitive values — &lt;code&gt;DB_HOST&lt;/code&gt;, &lt;code&gt;DB_PORT&lt;/code&gt;, &lt;code&gt;DB_NAME&lt;/code&gt;, &lt;code&gt;DB_USER&lt;/code&gt;, &lt;code&gt;NODE_ENV&lt;/code&gt;, &lt;code&gt;PORT&lt;/code&gt; — are passed as plain environment variables. They're not sensitive: knowing the RDS hostname doesn't help an attacker get into the database.&lt;/p&gt;

&lt;p&gt;The password is different. The ECS &lt;code&gt;secrets&lt;/code&gt; block references the Secrets Manager secret by ARN, and at container startup, the ECS agent fetches the value, injects it as &lt;code&gt;DB_PASSWORD&lt;/code&gt; in the container's environment, and never writes it anywhere that shows up in the task definition JSON, CloudWatch logs, or the AWS console. The only evidence it existed is the ARN reference.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;secrets&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;"DB_PASSWORD"&lt;/span&gt;
    &lt;span class="nx"&gt;valueFrom&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_password&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also required a specific IAM policy attached to the ECS &lt;em&gt;execution&lt;/em&gt; role (not the task role — the agent that starts containers, not the container itself):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"secretsmanager:GetSecretValue"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_password&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scoping matters. The policy grants access to exactly one secret ARN, not &lt;code&gt;"Resource": "*"&lt;/code&gt;. If an attacker found a way to escalate privileges from inside the container to the execution role, they could read one specific secret with a known ARN, not any secret in the account. That's a meaningful reduction in blast radius for a one-line change.&lt;/p&gt;




&lt;h2&gt;
  
  
  A race condition I hit and what it taught me
&lt;/h2&gt;

&lt;p&gt;When I added the Node.js app port (&lt;code&gt;3000&lt;/code&gt;) to the Terraform configuration — replacing the placeholder &lt;code&gt;80&lt;/code&gt; that was there from the initial setup — running &lt;code&gt;terraform apply&lt;/code&gt; produced an error I hadn't seen before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: deleting ELBv2 Target Group: ResourceInUse: Target group is currently in use by a listener or a rule
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happened: changing the port on an ALB target group forces replacement (Terraform creates a new target group, then tries to delete the old one). But the ALB listener still referenced the old target group when Terraform tried to delete it. The listener update and the target group deletion were running in the wrong order — or more precisely, in parallel, and the deletion raced against the listener update and lost.&lt;/p&gt;

&lt;p&gt;The fix was simply running &lt;code&gt;terraform apply&lt;/code&gt; a second time. The first apply created the new target group and partially updated the listener, but the listener change hadn't completed by the time the old target group deletion ran. The second apply found the listener already pointing to the new target group and successfully deleted the old one.&lt;/p&gt;

&lt;p&gt;I mention this because it's the kind of error that's alarming-looking and turns out to be completely benign — and knowing that pattern ("Terraform apply failed with ResourceInUse, run it again") is one of those things that takes a long time to learn from documentation and about five seconds to learn from experience. If you hit it: apply again.&lt;/p&gt;

&lt;p&gt;The deeper lesson is about Terraform's dependency graph. Terraform builds an explicit dependency graph from the &lt;code&gt;depends_on&lt;/code&gt; and implicit references in your configuration, and parallelizes everything it can within that graph. When you're modifying a resource that has something depending on it &lt;em&gt;and&lt;/em&gt; you're replacing the resource rather than updating it in-place, the order of operations for the replacement can be non-obvious. Looking at the &lt;code&gt;terraform plan&lt;/code&gt; output more carefully — specifically the &lt;code&gt;-/+&lt;/code&gt; indicators for "destroy and create replacement" — before applying would have flagged this in advance.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd build next
&lt;/h2&gt;

&lt;p&gt;The most significant gap in the current design is real-time detection. Running &lt;code&gt;terraform plan&lt;/code&gt; every 6 hours means there's a window of up to 6 hours between a manual console change and the drift detection alert. For most configuration changes that's probably fine. For a security group rule opening port 22 to the internet, it's not.&lt;/p&gt;

&lt;p&gt;The architecture for closing that gap exists: an EventBridge rule that listens to CloudTrail events for specific API calls (&lt;code&gt;AuthorizeSecurityGroupIngress&lt;/code&gt;, &lt;code&gt;ModifyDBInstance&lt;/code&gt;, &lt;code&gt;AttachRolePolicy&lt;/code&gt;, and similar high-risk mutations), triggering a Lambda function that runs the plan-and-classify pipeline on demand, within seconds of the change. That's the enterprise-grade pattern that the project's original design was based on, and it's what I'd add in v2.&lt;/p&gt;

&lt;p&gt;OIDC authentication for GitHub Actions is the other immediate priority. Storing &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; as repository secrets is functional but carries unnecessary risk — those credentials persist until they're rotated, and rotating them requires remembering they exist. The OIDC setup in &lt;code&gt;iam.tf&lt;/code&gt; is already scaffolded; it's a workflow change and a secret deletion away from being the default.&lt;/p&gt;

&lt;p&gt;A drift history store is something I've sketched but not built. Right now, every drift detection run is stateless — the result goes to Slack or GitHub Issues, and that's the end of it. If you wanted to answer "how often does our infrastructure drift, in which direction, and is it getting better or worse over time?", there's nowhere to look. A DynamoDB table storing detection results per run, with a simple dashboard, would turn TerraGuard AI from a reactive alerting tool into something with genuine observability over infrastructure compliance trends.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;The most useful framing I found for this project is that it's not an AI project with some infrastructure bolted on, and it's not an infrastructure project with some Python bolted on. It's an operations automation project that happens to use AI for the part where you'd otherwise need a human to make a judgment call.&lt;/p&gt;

&lt;p&gt;That framing matters because it determines what you actually have to understand to build it well. The Groq integration is a dozen lines of Python — the interesting engineering is in understanding when to call it, with what inputs, and what to do with the output, which requires thinking about the infrastructure, the CI/CD pipeline, the IAM model, and the alerting routing as a system. None of those are complicated individually. The interesting work is making them compose.&lt;/p&gt;

&lt;p&gt;The full source is at &lt;a href="https://github.com/vatul16/terraguard-ai" rel="noopener noreferrer"&gt;https://github.com/vatul16/terraguard-ai&lt;/a&gt; — Terraform, Python detector, GitHub Actions workflows, and the Node.js app. I'm actively looking for Cloud/DevOps Engineer roles as I transition from full-stack development, and I'd genuinely enjoy talking through any part of this with someone who has questions about a specific decision. You can find me on &lt;a href="https://linkedin.com/in/vatul16" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previous project: &lt;a href="https://github.com/vatul16/terratier" rel="noopener noreferrer"&gt;TerraTier&lt;/a&gt; — Production-grade 3-tier AWS architecture with Terraform, Auto Scaling Groups, two ALBs, four subnet tiers, and SSM Session Manager.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>devops</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Automate Your Docker Builds with GitHub Actions</title>
      <dc:creator>Atul Vishwakarma</dc:creator>
      <pubDate>Thu, 25 Jun 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/vatul16/automate-your-docker-builds-with-github-actions-4nbp</link>
      <guid>https://dev.to/vatul16/automate-your-docker-builds-with-github-actions-4nbp</guid>
      <description>&lt;p&gt;Over the last two weeks, we solved two massive DevOps problems. First, we stopped deploying manually by &lt;a href="https://dev.to/vatul16/stop-deploying-manually-how-to-build-your-first-cicd-pipeline-with-github-actions-80c"&gt;building a CI/CD pipeline with GitHub Actions&lt;/a&gt;. Then, we solved the "it works on my machine" problem by &lt;a href="https://dev.to/vatul16/how-to-dockerize-a-nodejs-app-in-5-easy-steps-44d4"&gt;containerizing a Node.js app with Docker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Today, we are going to combine these two superpowers.&lt;/p&gt;

&lt;p&gt;Running &lt;code&gt;docker build&lt;/code&gt; manually on your laptop every time you change a line of code is tedious. Instead, we are going to configure GitHub Actions to automatically build your Docker image and push it to Docker Hub the second you push your code to the &lt;code&gt;main&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;Let's automate it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A free &lt;a href="https://hub.docker.com/" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt; account.&lt;/li&gt;
&lt;li&gt;A basic Node.js application with a &lt;code&gt;Dockerfile&lt;/code&gt; (If you don't have one, follow my &lt;a href="https://dev.to/vatul16/how-to-dockerize-a-nodejs-app-in-5-easy-steps-44d4"&gt;5-step Docker tutorial here&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;A GitHub repository with an active Actions workflow (Learn &lt;a href="https://dev.to/vatul16/stop-deploying-manually-how-to-build-your-first-cicd-pipeline-with-github-actions-80c"&gt;how to set one up in 10 minutes here&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Create a Docker Hub Access Token
&lt;/h2&gt;

&lt;p&gt;To let GitHub securely push images to your Docker Hub account, we need to give it a "password" in the form of an Access Token.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to Docker Hub.&lt;/li&gt;
&lt;li&gt;Click your profile picture in the top right and select &lt;strong&gt;Account Settings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;Personal access tokens&lt;/strong&gt; then &lt;strong&gt;Generate new token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name it &lt;code&gt;github-actions-token&lt;/code&gt;, give it "Read &amp;amp; Write" permissions, and click Generate.&lt;/li&gt;
&lt;li&gt;Copy this token immediately (you won’t be able to see it again).&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&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%2Fgrmwoq2soyeax4md75i2.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%2Fgrmwoq2soyeax4md75i2.png" alt="Docker PAT Generation Image" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2: Add Your Secrets to GitHub
&lt;/h2&gt;

&lt;p&gt;Now, we need to hand that token over to GitHub safely. Never hardcode passwords in your code!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your repository on GitHub.&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;Settings&lt;/strong&gt; tab at the top.&lt;/li&gt;
&lt;li&gt;On the left sidebar, scroll down to &lt;strong&gt;Secrets and variables&lt;/strong&gt; and click &lt;strong&gt;Actions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click the green &lt;strong&gt;New repository secret&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Create two distinct secrets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;DOCKER_USERNAME&lt;/code&gt; | Secret: Your actual Docker Hub username.&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;DOCKER_PASSWORD&lt;/code&gt; | Secret: The Access Token you copied in Step 1.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&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%2F3d7s22moqu1oa2nu9lzi.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%2F3d7s22moqu1oa2nu9lzi.png" alt="Github Secrets" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: Update Your GitHub Actions Workflow
&lt;/h2&gt;

&lt;p&gt;Open up the &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; file we created a couple of weeks ago. We are going to add a new job to our pipeline.&lt;/p&gt;

&lt;p&gt;Replace your existing file contents with this updated YAML code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Node.js CI/CD and Docker Build

on:
  push:
    branches: [ "main" ]

jobs:
  build-and-push-docker-image:
    runs-on: ubuntu-latest

    steps:
    # Step A: Check out the repository code
    - name: Checkout code
      uses: actions/checkout@v3

    # Step B: Log in to Docker Hub using our secure secrets
    - name: Log in to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}

    # Step C: Build the Docker image and push it to Docker Hub
    - name: Build and push
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        # Replace 'yourusername' with your actual Docker Hub username
        tags: yourusername/my-node-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Make sure to change &lt;code&gt;yourusername&lt;/code&gt; on the very last line to your actual Docker Hub username!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Push and Watch the Automation
&lt;/h2&gt;

&lt;p&gt;Save your file, commit the changes, and push it up to GitHub.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git add .github/workflows/ci.yml
git commit -m "feat: add docker build and push automation"
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Head over to the &lt;strong&gt;Actions&lt;/strong&gt; tab in your GitHub repository. You will see your new workflow spinning up. GitHub is now renting a tiny Ubuntu server, checking out your code, logging into Docker Hub, building your image based on your &lt;code&gt;Dockerfile&lt;/code&gt;, and pushing the final product to the cloud.&lt;/p&gt;

&lt;p&gt;Once you get the green checkmark, go check your Docker Hub profile. Your brand-new image will be sitting right there, ready to be pulled by any server in the world.&lt;/p&gt;

&lt;blockquote&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%2Fex20793zpaquza0p8ef5.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%2Fex20793zpaquza0p8ef5.png" alt="GitHub Actions Result" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;You have just built a professional-grade continuous delivery pipeline.&lt;/p&gt;

&lt;p&gt;From now on, all you have to do is write your code and type &lt;code&gt;git push&lt;/code&gt;. The robots handle the rest. This is the exact workflow used by top tech companies, and you just implemented it for free.&lt;/p&gt;

&lt;p&gt;Next week, we are going to look at a quick debugging trick: how to fix the dreaded "Port Already in Use" error when working with Docker locally.&lt;/p&gt;




&lt;p&gt;If you are enjoying this DevOps series, you can &lt;a href="https://buymeacoffee.com/vatul16" rel="noopener noreferrer"&gt;☕ buy me a coffee here&lt;/a&gt; to support my work.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>githubactions</category>
      <category>cicd</category>
    </item>
    <item>
      <title>How to Dockerize a Node.js App in 5 Easy Steps</title>
      <dc:creator>Atul Vishwakarma</dc:creator>
      <pubDate>Tue, 23 Jun 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/vatul16/how-to-dockerize-a-nodejs-app-in-5-easy-steps-44d4</link>
      <guid>https://dev.to/vatul16/how-to-dockerize-a-nodejs-app-in-5-easy-steps-44d4</guid>
      <description>&lt;p&gt;"But it works on my machine!" If you are a developer, you have likely said this exact phrase. Your code runs perfectly on your laptop, but the moment you hand it to a coworker or try to deploy it to a production server, it crashes. The Node version is wrong, a dependency is missing, or the environment variables are mismatched.&lt;/p&gt;

&lt;p&gt;This is exactly the problem &lt;strong&gt;Docker&lt;/strong&gt; solves.&lt;/p&gt;

&lt;p&gt;Docker allows you to package your application and all of its dependencies into a single, standardized unit. Today, I am going to show you how to containerize a simple Node.js application in 5 easy steps so it runs perfectly anywhere, every single time.&lt;/p&gt;

&lt;p&gt;Before we write the code, let's clear up the biggest point of confusion for beginners.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Concept: Image vs. Container
&lt;/h2&gt;

&lt;p&gt;People often use these terms interchangeably, but they are completely different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Docker Image: This is the recipe. It is a static, read-only file that contains your source code, libraries, dependencies, and tools. An image does not "run."&lt;/li&gt;
&lt;li&gt;The Docker Container: This is the cake. It is the running, active instance of your Docker Image. You can start, stop, and delete a container.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You build an Image once, and you can spin up a hundred Containers from it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.docker.com/products/docker-desktop/" rel="noopener noreferrer"&gt;&lt;strong&gt;Docker Desktop&lt;/strong&gt;&lt;/a&gt; installed on your machine.&lt;/li&gt;
&lt;li&gt;A basic understanding of the terminal.&lt;/li&gt;
&lt;li&gt;Node.js installed locally (just for our initial setup).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Create a Simple Node.js App
&lt;/h2&gt;

&lt;p&gt;We need something to containerize. Let's build a dead-simple Express server. Open your terminal, create a new folder, and initialize a project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir my-docker-app
cd my-docker-app
npm init -y
npm install express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create a file named server.js and paste in this code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const express = require('express');
const app = express();
const PORT = 3000;

app.get('/', (req, res) =&amp;gt; {
  res.send('Hello from inside a Docker Container! 🐳');
});

app.listen(PORT, () =&amp;gt; {
  console.log(`Server is running on port ${PORT}`);
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, open your package.json file and add a start script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"scripts": {
  "start": "node server.js"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&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%2Fbb1hfh6evwbibhm4g2fu.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%2Fbb1hfh6evwbibhm4g2fu.png" alt=" " width="315" height="207"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2: Write the Dockerfile
&lt;/h2&gt;

&lt;p&gt;The Dockerfile is a text document that contains all the commands needed to assemble your Image. In the root of your project folder, create a new file named exactly Dockerfile (no file extension).&lt;/p&gt;

&lt;p&gt;Add the following instructions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 1. Specify the base image
FROM node:18-alpine

# 2. Set the working directory inside the container
WORKDIR /usr/src/app

# 3. Copy package.json and package-lock.json first
COPY package*.json ./

# 4. Install dependencies
RUN npm install

# 5. Copy the rest of your application code
COPY . .

# 6. Expose the port your app runs on
EXPOSE 3000

# 7. Define the command to run your app
CMD ["npm", "start"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why do we copy the &lt;code&gt;package.json&lt;/code&gt; file before the rest of the code?&lt;/strong&gt; Docker builds images in layers and caches them. If you change a single line in &lt;code&gt;server.js&lt;/code&gt;, Docker doesn't need to re-install all your npm packages. It uses the cached layer for the dependencies, making your builds incredibly fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add a .dockerignore File
&lt;/h2&gt;

&lt;p&gt;You wouldn't push your &lt;code&gt;node_modules&lt;/code&gt; folder to GitHub, and you shouldn't copy it into your Docker image either. Your container should install its own clean dependencies.&lt;/p&gt;

&lt;p&gt;Create a file named &lt;code&gt;.dockerignore&lt;/code&gt; in your root directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules
npm-debug.log
.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&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%2Fegwnmnjuvkrc9ry15ek9.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%2Fegwnmnjuvkrc9ry15ek9.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 4: Build the Docker Image
&lt;/h2&gt;

&lt;p&gt;Now it is time to bake the recipe. Open your terminal in the root of your project and run this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker build -t my-node-app .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;-t&lt;/code&gt; flag "tags" your image with a readable name (&lt;code&gt;my-node-app&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;.&lt;/code&gt; at the very end tells Docker to look for the Dockerfile in the current directory. Do not forget the period!&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&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%2Fd4dbu1v5l0jfbgfof9fh.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%2Fd4dbu1v5l0jfbgfof9fh.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: Run the Docker Container
&lt;/h2&gt;

&lt;p&gt;Your image is built. Now, let's bring it to life by spinning up a container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -p 3000:3000 -d my-node-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;-p 3000:3000&lt;/code&gt; flag maps port 3000 on your physical laptop to port 3000 inside the isolated container.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;-d&lt;/code&gt; flag runs the container in "detached" mode so it runs in the background, keeping your terminal usable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open your web browser and navigate to &lt;code&gt;http://localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&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%2Fplsrqh8v8dq0c58mxa0b.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%2Fplsrqh8v8dq0c58mxa0b.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Congratulations! You just built and ran your first Dockerized application.&lt;/p&gt;

&lt;p&gt;Because the environment is bundled directly into the image, you could send this exact &lt;code&gt;my-node-app&lt;/code&gt; image to a colleague running Windows, a friend on a Mac, or a Linux server on AWS, and it will run flawlessly. No configuration required.&lt;/p&gt;

&lt;p&gt;Next week, we are going to take this a step further and combine it with the CI/CD pipeline we built last time. I will show you how to automatically build this Docker image using GitHub Actions every time you push code.&lt;/p&gt;

&lt;p&gt;Make sure to subscribe to the blog so you don't miss it!&lt;/p&gt;




&lt;p&gt;If this tutorial helped you finally understand Docker, you can &lt;a href="https://buymeacoffee.com/vatul16" rel="noopener noreferrer"&gt;☕ buy me a coffee here&lt;/a&gt; to support my work!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>node</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Production-Grade 3-Tier AWS Architecture with Terraform: Design Decisions, Trade-offs, and Lessons Learned</title>
      <dc:creator>Atul Vishwakarma</dc:creator>
      <pubDate>Fri, 19 Jun 2026 02:30:00 +0000</pubDate>
      <link>https://dev.to/vatul16/building-a-production-grade-3-tier-aws-architecture-with-terraform-design-decisions-trade-offs-370f</link>
      <guid>https://dev.to/vatul16/building-a-production-grade-3-tier-aws-architecture-with-terraform-design-decisions-trade-offs-370f</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/vatul16/terratier" rel="noopener noreferrer"&gt;https://github.com/vatul16/terratier&lt;/a&gt; — full Terraform source, module docs, and architecture diagram.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When I set out to build this project, I didn't want another "deploy a VM and call it infrastructure" tutorial repo. I wanted something that would force me to think through the same questions a platform team actually argues about: how many subnet tiers do you really need, where do secrets live, how do you let engineers SSH in without handing out keys forever, and what's the cheapest way to stay highly available without going broke on NAT Gateway bills.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;TerraTier&lt;/strong&gt; — a small Go/Node.js goal-tracking app, deployed across a fully isolated, auto-scaling, four-tier network on AWS, provisioned entirely through modular Terraform. The app itself is deliberately boring (it's a CRUD list of goals). The infrastructure underneath it is the actual point of the project, and this article walks through why it looks the way it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with most "3-tier Terraform" examples
&lt;/h2&gt;

&lt;p&gt;Search for AWS 3-tier Terraform examples and you'll find a lot of repositories that split a VPC into public, private, and database subnets, drop a web server in private, and call it done. That's a reasonable starting point, but it collapses two very different concerns into one "private" tier: the stateless web/API layer that talks to the internet (indirectly, via a load balancer) and the application layer that's allowed to talk to the database. If your web tier gets compromised, in that model, it's sitting in the same subnet — and often the same security group — as anything that can reach your data.&lt;/p&gt;

&lt;p&gt;I wanted the network topology itself to enforce a stricter rule: nothing can reach the database except the backend tier, and nothing can reach the backend tier except the frontend tier and the internal load balancer. So the VPC here has four subnet tiers instead of three, each duplicated across two Availability Zones:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Public&lt;/strong&gt; — Internet Gateway route, NAT Gateways, the public-facing ALB, and the bastion host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend private&lt;/strong&gt; — the Node.js Express tier, reachable only from the public ALB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend private&lt;/strong&gt; — the Go API tier, reachable only from an &lt;em&gt;internal&lt;/em&gt; ALB that the frontend talks to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database isolated&lt;/strong&gt; — RDS PostgreSQL, with no route to the internet at all, reachable only from the backend's security group.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That extra split is a small addition in Terraform — one more &lt;code&gt;aws_subnet&lt;/code&gt; resource block, one more security group, one more ALB — but it changes the blast radius of a compromised frontend instance from "can reach the database" to "can reach exactly one internal load balancer on one port."&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ALBs instead of one
&lt;/h2&gt;

&lt;p&gt;This is probably the single decision in the repo that most resembles a real production pattern rather than a tutorial shortcut. The public ALB load-balances browser traffic across the frontend Auto Scaling Group on port 3000. The frontend, in turn, doesn't call backend instances directly — it calls a &lt;em&gt;second&lt;/em&gt;, internal-only ALB, which load-balances across the backend Auto Scaling Group on port 8080.&lt;/p&gt;

&lt;p&gt;The alternative — having the frontend call backend instances directly via private IPs, or through a Cloud Map service registry — would save the cost of a second ALB (roughly $16–20/month plus LCU charges). I chose the internal ALB anyway, for a few reasons that matter more once you have more than one backend instance: it gives the backend tier the same health-checked, load-balanced semantics as the frontend tier; it means backend instances can scale, fail, and get replaced without the frontend needing to know anything about individual instance IPs; and it gives me a single, consistent mental model — "every tier that has more than one instance sits behind an ALB" — instead of two different patterns for two tiers that conceptually do the same kind of horizontal scaling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets Manager, not environment variables baked into an AMI
&lt;/h2&gt;

&lt;p&gt;The RDS master password is generated once, at apply time, with Terraform's &lt;code&gt;random_password&lt;/code&gt; resource — 16 characters, with a curated set of special characters that won't break a Postgres connection string. It's written to a single Secrets Manager secret (&lt;code&gt;{environment}-{project}-db-credentials&lt;/code&gt;) as a JSON blob containing the username, password, host, port, and database name together, so the backend only ever needs one secret ARN, not five separate values to wire through.&lt;/p&gt;

&lt;p&gt;At boot, the backend's user-data script calls &lt;code&gt;aws secretsmanager get-secret-value&lt;/code&gt;, parses the result with &lt;code&gt;jq&lt;/code&gt;, and passes the individual fields into the Docker container as environment variables. The instance's IAM role grants exactly one Secrets Manager permission — &lt;code&gt;GetSecretValue&lt;/code&gt; and &lt;code&gt;DescribeSecret&lt;/code&gt;, scoped to that one secret's ARN, nothing else. No password ever gets written to a Dockerfile, a Docker image layer, or a &lt;code&gt;.env&lt;/code&gt; file checked into git.&lt;/p&gt;

&lt;p&gt;I'll be upfront about the limitation here, because it's the kind of thing an interviewer will probe and you should be ready to discuss it honestly: the password still flows through a Terraform &lt;em&gt;variable&lt;/em&gt; (&lt;code&gt;var.db_password&lt;/code&gt;), which means it exists in plan output and state, even though the variable itself is marked &lt;code&gt;sensitive = true&lt;/code&gt;. The cleaner pattern is to have RDS generate and manage its own master password natively (&lt;code&gt;manage_master_user_password = true&lt;/code&gt;, an RDS feature that creates and rotates the secret for you, with Terraform never touching the plaintext at all). I built it the way I did first because I wanted to understand the full credential lifecycle by hand before reaching for the feature that hides it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bastion host &lt;em&gt;and&lt;/em&gt; SSM, deliberately redundant
&lt;/h2&gt;

&lt;p&gt;Every EC2 instance in this stack — bastion, frontend, backend — gets the same IAM instance profile, which includes the &lt;code&gt;AmazonSSMManagedInstanceCore&lt;/code&gt; managed policy. That alone is enough to &lt;code&gt;aws ssm start-session --target &amp;lt;instance-id&amp;gt;&lt;/code&gt; into any instance with no SSH key, no open port 22 from the internet, and a full audit trail in CloudTrail of who connected and when.&lt;/p&gt;

&lt;p&gt;So why keep the bastion at all? Two reasons. First, pragmatically: SSM Session Manager occasionally has friction in CI environments, narrow corporate proxy setups, or when you specifically need to forward a local port (&lt;code&gt;aws ssm start-session ... --document-name AWS-StartPortForwardingSession&lt;/code&gt;) and just want a plain &lt;code&gt;ssh -L&lt;/code&gt; tunnel instead of remembering the SSM syntax. Second, for this project specifically: a bastion host is the pattern most reviewers and interviewers will recognize immediately, and I wanted the repo to demonstrate both the "traditional" approach and the modern, keyless approach side by side, with the security trade-offs of each visible in the Terraform itself (the bastion's security group only allows SSH from &lt;code&gt;var.allowed_ssh_cidrs&lt;/code&gt;, which defaults to "change this" rather than &lt;code&gt;0.0.0.0/0&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking the cheaper failure mode: single NAT Gateway
&lt;/h2&gt;

&lt;p&gt;NAT Gateways are billed per-hour &lt;em&gt;and&lt;/em&gt; per-GB processed, and they're one of the easiest places for a demo environment's AWS bill to quietly balloon. The VPC module supports both &lt;code&gt;single_nat_gateway = true&lt;/code&gt; (one NAT Gateway, shared by both AZs' private subnets) and &lt;code&gt;false&lt;/code&gt; (one NAT Gateway per AZ, fully redundant). The dev environment defaults to &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That default is an explicit cost/availability trade-off, not an oversight: if the AZ hosting the single NAT Gateway has an outage, outbound internet access from the &lt;em&gt;other&lt;/em&gt; AZ's private subnets breaks too — even though those subnets' EC2 instances are otherwise healthy. For a portfolio project that's torn down between demos, that's an acceptable risk for roughly half the NAT cost. For an actual production workload, flipping the flag to &lt;code&gt;false&lt;/code&gt; is a one-line &lt;code&gt;terraform.tfvars&lt;/code&gt; change, because the module was written to support both from day one rather than hardcoding the cheap option.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when an instance boots
&lt;/h2&gt;

&lt;p&gt;The launch templates for both ASGs run a user-data script that does the same rough sequence of things, and getting this script right was where I spent most of my actual debugging time on this project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install Docker and the AWS CLI v2.&lt;/li&gt;
&lt;li&gt;(Backend only) Pull database credentials from Secrets Manager, with retry logic — because the very first time an ASG instance boots, RDS and the backend's DNS record might genuinely not be resolvable yet, and a script that fails fast on a transient DNS hiccup will throw the instance into a boot-loop of CrashLoopBackOff-style ASG churn.&lt;/li&gt;
&lt;li&gt;(Frontend only) Poll the internal ALB's hostname and port with &lt;code&gt;nc -z&lt;/code&gt; in a retry loop before starting the frontend container, so the frontend doesn't come up, fail its first few requests to a backend that isn't ready yet, and confuse anyone watching the ALB's health checks.&lt;/li&gt;
&lt;li&gt;Pull the application's Docker image and run it with &lt;code&gt;docker run --restart unless-stopped&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Install and configure the CloudWatch Agent to ship the user-data log and basic CPU/memory metrics.&lt;/li&gt;
&lt;li&gt;Drop a cron entry that independently re-checks the container's health every 5 minutes and restarts Docker/the container if it's unhealthy — a cheap, ASG-independent self-healing layer on top of the ALB's own health checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The retry loops in steps 2 and 3 are the unglamorous but important part. The first version of this script didn't have them, and the very first &lt;code&gt;terraform apply&lt;/code&gt; after a from-scratch deploy failed about a third of the time, simply because RDS or the internal ALB's DNS hadn't fully propagated by the time the EC2 instances finished booting — a classic race condition in any "spin up dependent infrastructure simultaneously" deployment. Adding bounded retry loops (with logging at every attempt, so you can actually see what happened in CloudWatch Logs afterward) turned that into a non-issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability: metrics, logs, and three layers of health checking
&lt;/h2&gt;

&lt;p&gt;The Go backend exposes Prometheus-format metrics at &lt;code&gt;/metrics&lt;/code&gt; — request counters labeled by path, and dedicated counters for goal-add and goal-remove operations — using the official &lt;code&gt;prometheus/client_golang&lt;/code&gt; library. That's not wired up to a Prometheus server in this repo (there's no managed Prometheus or Grafana here yet), but the endpoint exists and is ready to be scraped, which matters more than it sounds: instrumenting an application for metrics is a decision you make in the application's code, and it's far easier to do it from the start than to retrofit it later.&lt;/p&gt;

&lt;p&gt;Health checking happens at three independent layers, deliberately overlapping rather than relying on a single mechanism: the ALB target group's own health check (&lt;code&gt;GET /health&lt;/code&gt;, every 30 seconds, 2 successes to mark healthy / 3 failures to mark unhealthy); a cron-based self-check on each instance every 5 minutes that restarts the container if it's failing locally; and CloudWatch Alarms watching aggregate CPU utilization and unhealthy target counts, wired to an &lt;code&gt;alarm_actions&lt;/code&gt; list that's empty by default but ready to point at an SNS topic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd build next
&lt;/h2&gt;

&lt;p&gt;A few things didn't make it into v1, on purpose — I'd rather ship something complete at a smaller scope than something half-finished at a larger one. In rough priority order:&lt;/p&gt;

&lt;p&gt;A CI/CD pipeline is the most obvious gap. Right now, deploying a new image means running &lt;code&gt;build_and_push.sh&lt;/code&gt; and then manually triggering (or waiting for) an ASG instance refresh. A GitHub Actions workflow that runs &lt;code&gt;terraform plan&lt;/code&gt; on every pull request, builds and pushes images on merge, and triggers a rolling instance refresh would turn this from "infrastructure I deploy by hand" into "infrastructure that deploys itself," which is really the whole point of the discipline.&lt;/p&gt;

&lt;p&gt;Moving from Docker Hub to Amazon ECR removes both the anonymous-pull rate limiting that public Docker Hub images are subject to and the need to pass Docker Hub credentials into instance user-data at all — ECR authentication can ride entirely on the existing IAM instance profile.&lt;/p&gt;

&lt;p&gt;And finally, remote state. The S3 backend block is already scaffolded and commented out in &lt;code&gt;provider.tf&lt;/code&gt;, because local state is fine for solo development but becomes a real liability the moment more than one person — or one CI pipeline plus one person — needs to run &lt;code&gt;terraform apply&lt;/code&gt; against the same environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;None of the individual pieces here are exotic — VPCs, ALBs, ASGs, RDS, Secrets Manager, and IAM are about as standard an AWS toolkit as exists. What I think is actually worth showing in an interview isn't any single resource block; it's the reasoning behind where the boundaries are drawn — which tier can talk to which, where a secret lives versus where it's read, what happens in the 90 seconds between "instance is running" and "instance is actually ready to serve traffic" — and being able to articulate the trade-off in each decision rather than just the decision itself.&lt;/p&gt;

&lt;p&gt;The full code is on GitHub at &lt;a href="https://github.com/vatul16/terratier" rel="noopener noreferrer"&gt;https://github.com/vatul16/terratier&lt;/a&gt;, along with a deeper architectural breakdown in &lt;code&gt;ARCHITECTURE.md&lt;/code&gt; and auto-generated input/output documentation for every Terraform module. I'm currently looking for Cloud/DevOps Engineer roles — feel free to reach out on &lt;a href="https://linkedin.com/in/vatul16" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; if you'd like to talk through any part of this in more depth.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Stop Deploying Manually: How to Build Your First CI/CD Pipeline with GitHub Actions</title>
      <dc:creator>Atul Vishwakarma</dc:creator>
      <pubDate>Mon, 15 Jun 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/vatul16/stop-deploying-manually-how-to-build-your-first-cicd-pipeline-with-github-actions-80c</link>
      <guid>https://dev.to/vatul16/stop-deploying-manually-how-to-build-your-first-cicd-pipeline-with-github-actions-80c</guid>
      <description>&lt;p&gt;If you are still manually running tests and deploying your code from your local terminal, you are wasting valuable time.&lt;/p&gt;

&lt;p&gt;When I first started diving into DevOps and Cloud engineering, the concept of CI/CD (Continuous Integration / Continuous Deployment) felt incredibly intimidating. I thought I needed a complex Jenkins server or a massive AWS architecture just to automate my workflows.&lt;/p&gt;

&lt;p&gt;It turns out, if your code is already on GitHub, you can build your first automated pipeline in under 10 minutes using GitHub Actions.&lt;/p&gt;

&lt;p&gt;Today, I’ll show you exactly how to set up a basic workflow that automatically tests your code every time you push to your repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A GitHub account&lt;/li&gt;
&lt;li&gt;A basic understanding of Git (git add, git commit, git push)&lt;/li&gt;
&lt;li&gt;A sample project (we will use a simple Node.js project for this example, but the concepts apply to any language).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create Your Workflow File&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GitHub Actions looks for a very specific folder structure in your repository to know what to run.&lt;/p&gt;

&lt;p&gt;In the root directory of your project, create a new folder called &lt;code&gt;.github&lt;/code&gt;, and inside that, create a folder called &lt;code&gt;workflows&lt;/code&gt;. Finally, create a YAML file inside it. You can name it whatever you want, but &lt;code&gt;ci.yml&lt;/code&gt; is standard.&lt;/p&gt;

&lt;p&gt;Your path should look like this: &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Write the YAML Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;YAML is the language of DevOps. It relies heavily on indentation, so make sure your spacing is exact!&lt;/p&gt;

&lt;p&gt;Open your &lt;code&gt;ci.yml&lt;/code&gt; file and paste the following code:&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;Node.js CI Pipeline&lt;/span&gt;

&lt;span class="c1"&gt;# 1. When should this workflow run?&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# 2. What jobs should it execute?&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-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# We need a virtual machine to run our code&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. What are the exact steps?&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Step A: Check out the code from our repository&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@v3&lt;/span&gt;

    &lt;span class="c1"&gt;# Step B: Set up the Node.js environment&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node.js&lt;/span&gt;
      &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v3&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;18.x'&lt;/span&gt;

    &lt;span class="c1"&gt;# Step C: Install dependencies&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Dependencies&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

    &lt;span class="c1"&gt;# Step D: Run our tests&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Tests&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking Down the Code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;on&lt;/code&gt;: This tells GitHub exactly when to trigger the pipeline. In our case, it runs every time someone pushes code or opens a pull request to the main branch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;runs-on&lt;/code&gt;: GitHub provisions a fresh, temporary Ubuntu server (a runner) specifically to execute your commands.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;steps&lt;/code&gt;: This is the chronological list of commands. We check out the code, install Node.js, install our npm packages, and finally, run the tests.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Push and Watch the Magic&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Save your file, commit it, and push it to GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add .github/workflows/ci.yml
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: add github actions CI pipeline"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, navigate to your repository on GitHub and click the &lt;strong&gt;"Actions"&lt;/strong&gt; tab at the top.&lt;/p&gt;

&lt;p&gt;You will see your workflow running in real-time! If your tests pass, you will get a satisfying green checkmark ✅. If they fail, you will get a red X and a log detailing exactly what broke, preventing bad code from making it to production.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Takeaway
&lt;/h3&gt;

&lt;p&gt;Congratulations! You just implemented Continuous Integration.&lt;/p&gt;

&lt;p&gt;This is the foundational building block of modern DevOps. From here, you can expand this exact same file to automatically push Docker images to AWS, trigger serverless deployments, or send a Slack message when a build fails.&lt;/p&gt;

&lt;p&gt;Automation is about letting the machines do the repetitive work so you can focus on building cool things.&lt;/p&gt;

&lt;p&gt;I'll be sharing more DevOps and Cloud tutorials here as I build and learn. If this tutorial saved you some time, you can &lt;a href="https://buymeacoffee.com/vatul16" rel="noopener noreferrer"&gt;☕ buy me a coffee here&lt;/a&gt; to support my work!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>githubactions</category>
      <category>cloud</category>
      <category>cicd</category>
    </item>
    <item>
      <title>From Zero to Infrastructure-as-Code Hero: My 30-Day Terraform Journey</title>
      <dc:creator>Atul Vishwakarma</dc:creator>
      <pubDate>Wed, 29 Apr 2026 07:18:55 +0000</pubDate>
      <link>https://dev.to/vatul16/from-zero-to-infrastructure-as-code-hero-my-30-day-terraform-journey-526k</link>
      <guid>https://dev.to/vatul16/from-zero-to-infrastructure-as-code-hero-my-30-day-terraform-journey-526k</guid>
      <description>&lt;p&gt;After 30 days of consistent learning, building, breaking, and debugging… I’ve officially completed the &lt;strong&gt;30 Days of AWS Terraform Challenge&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What started as a curiosity about Infrastructure as Code (IaC) has evolved into a deep, hands-on understanding of how modern cloud systems are designed, automated, and maintained.&lt;/p&gt;

&lt;p&gt;This isn’t just a completion post—it’s a reflection on what it really takes to go from &lt;strong&gt;beginner to confident practitioner in Terraform and DevOps&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌍 &lt;strong&gt;Why Terraform?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;In today’s cloud-first world, managing infrastructure manually is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Error-prone&lt;/li&gt;
&lt;li&gt;❌ Hard to scale&lt;/li&gt;
&lt;li&gt;❌ Nearly impossible to audit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Terraform changes that by enabling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Declarative infrastructure&lt;/li&gt;
&lt;li&gt;✅ Version-controlled environments&lt;/li&gt;
&lt;li&gt;✅ Repeatable deployments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But more importantly, it introduces a &lt;strong&gt;mindset shift&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Treat infrastructure like application code.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  📈 &lt;strong&gt;The Journey: From Basics to Production-Grade Systems&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;Phase 1: Foundations (Days 1–10)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I started with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Providers &amp;amp; resources&lt;/li&gt;
&lt;li&gt;State files&lt;/li&gt;
&lt;li&gt;Basic AWS services (EC2, S3, IAM)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 Key realization:&lt;br&gt;
Terraform isn’t just about creating resources—it’s about managing their lifecycle.&lt;/p&gt;


&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;Phase 2: Scaling &amp;amp; Logic (Days 11–20)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This is where things got interesting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expressions &amp;amp; Functions&lt;/strong&gt; → dynamic configurations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta-arguments (count, for_each)&lt;/strong&gt; → scalable infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modules&lt;/strong&gt; → reusable and clean architecture&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provisioners&lt;/strong&gt; → bootstrapping resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 Key realization:&lt;br&gt;
Clean, modular code is the difference between a demo and production-ready infrastructure.&lt;/p&gt;


&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;Phase 3: Real-World Architectures (Days 21–28)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Here’s where theory met reality:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🏗️ 2-tier &amp;amp; 3-tier architectures&lt;/li&gt;
&lt;li&gt;🌐 VPC design with public/private subnets&lt;/li&gt;
&lt;li&gt;⚖️ Load Balancers + Auto Scaling&lt;/li&gt;
&lt;li&gt;🔐 IAM policies &amp;amp; governance&lt;/li&gt;
&lt;li&gt;📊 Observability with CloudWatch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 Key realization:&lt;br&gt;
Infrastructure is not about individual services—it’s about &lt;strong&gt;how they work together&lt;/strong&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;Phase 4: DevOps Maturity (Days 29–30)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The final stretch focused on automation and reliability:&lt;/p&gt;
&lt;h4&gt;
  
  
  🔁 GitOps with ArgoCD
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Self-healing Kubernetes deployments&lt;/li&gt;
&lt;li&gt;Git as single source of truth&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  🔍 Drift Detection (Final Milestone)
&lt;/h4&gt;

&lt;p&gt;Using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan &lt;span class="nt"&gt;-detailed-exitcode&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I built a pipeline that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detects infrastructure drift&lt;/li&gt;
&lt;li&gt;Automatically remediates it&lt;/li&gt;
&lt;li&gt;Notifies the team&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 This was the biggest “aha” moment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Infrastructure that fixes itself is the end goal.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧠 &lt;strong&gt;Key Skills I Gained&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Infrastructure Design
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;High availability architectures&lt;/li&gt;
&lt;li&gt;Secure networking (NAT, private subnets)&lt;/li&gt;
&lt;li&gt;Scalable systems (ASG + ALB)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ Terraform Mastery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Modules &amp;amp; reusability&lt;/li&gt;
&lt;li&gt;Remote state management (S3 + DynamoDB)&lt;/li&gt;
&lt;li&gt;Data sources &amp;amp; dynamic blocks&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ DevOps Automation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;CI/CD with GitHub Actions&lt;/li&gt;
&lt;li&gt;GitOps workflows&lt;/li&gt;
&lt;li&gt;Drift detection pipelines&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ✅ Security &amp;amp; Governance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;IAM best practices&lt;/li&gt;
&lt;li&gt;Policy enforcement&lt;/li&gt;
&lt;li&gt;Secrets management awareness&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚠️ &lt;strong&gt;Challenges Along the Way&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This journey wasn’t smooth—and that’s the point.&lt;/p&gt;

&lt;p&gt;Some real struggles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debugging Terraform state issues&lt;/li&gt;
&lt;li&gt;Handling provider deprecations&lt;/li&gt;
&lt;li&gt;Fixing networking misconfigurations&lt;/li&gt;
&lt;li&gt;Understanding IAM policy conflicts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;💡 Lesson learned:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Debugging is where real learning happens.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🔄 &lt;strong&gt;What Changed for Me&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before this challenge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I knew &lt;em&gt;how&lt;/em&gt; to create resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I understand &lt;em&gt;how to design systems&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I deployed manually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I automate everything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I fixed issues manually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I build systems that prevent them&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;What’s Next?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is just the beginning.&lt;/p&gt;

&lt;p&gt;Next steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔹 Advanced Kubernetes (EKS deep dive)&lt;/li&gt;
&lt;li&gt;🔹 Multi-cloud Terraform deployments&lt;/li&gt;
&lt;li&gt;🔹 Policy-as-Code (OPA, Sentinel)&lt;/li&gt;
&lt;li&gt;🔹 Production-grade CI/CD pipelines&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;This challenge taught me something beyond Terraform:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Consistency beats intensity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Showing up every day—even when debugging for hours—made all the difference.&lt;/p&gt;

&lt;p&gt;If you’re starting your DevOps journey:&lt;br&gt;
👉 Don’t just watch tutorials&lt;br&gt;
👉 Build projects&lt;br&gt;
👉 Break things&lt;br&gt;
👉 Fix them with code&lt;/p&gt;

&lt;p&gt;That’s how you grow.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>beginners</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Mastering GitOps: Bridging Infrastructure and Application Delivery with Terraform &amp; ArgoCD</title>
      <dc:creator>Atul Vishwakarma</dc:creator>
      <pubDate>Tue, 28 Apr 2026 06:07:55 +0000</pubDate>
      <link>https://dev.to/vatul16/mastering-gitops-bridging-infrastructure-and-application-delivery-with-terraform-argocd-4385</link>
      <guid>https://dev.to/vatul16/mastering-gitops-bridging-infrastructure-and-application-delivery-with-terraform-argocd-4385</guid>
      <description>&lt;p&gt;After an intense journey through the &lt;strong&gt;#30DaysOfAWSTerraform challenge&lt;/strong&gt;, Day 29 stands out as one of the most transformative milestones so far.&lt;/p&gt;

&lt;p&gt;This wasn’t just about provisioning infrastructure anymore—it was about building a &lt;strong&gt;self-healing, automated, and production-grade GitOps workflow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If Infrastructure as Code (IaC) is the foundation, &lt;strong&gt;GitOps is the operating system&lt;/strong&gt; that keeps everything running smoothly.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔄 &lt;strong&gt;From IaC to GitOps: What Changed?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Until now, Terraform allowed me to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define infrastructure declaratively&lt;/li&gt;
&lt;li&gt;Provision resources consistently&lt;/li&gt;
&lt;li&gt;Avoid manual “ClickOps”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But GitOps takes it a step further:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git becomes the &lt;strong&gt;single source of truth&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Systems automatically &lt;strong&gt;reconcile drift&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Deployments become &lt;strong&gt;continuous and auditable&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 In short:&lt;br&gt;
&lt;strong&gt;IaC provisions infrastructure. GitOps keeps it correct.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🏗️ &lt;strong&gt;Architecture Overview&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This project bridges &lt;strong&gt;infrastructure provisioning&lt;/strong&gt; and &lt;strong&gt;application deployment&lt;/strong&gt; into one cohesive system.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;1. Infrastructure Repository (Terraform)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provisioning an &lt;strong&gt;Amazon EKS cluster&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Setting up networking (VPC, subnets)&lt;/li&gt;
&lt;li&gt;Installing &lt;strong&gt;ArgoCD via Helm&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Configuring storage (EBS CSI driver)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;2. Application Repository (Kubernetes Manifests)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend (UI layer)&lt;/li&gt;
&lt;li&gt;Backend (API layer)&lt;/li&gt;
&lt;li&gt;PostgreSQL database&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  ⚙️ &lt;strong&gt;How the Workflow Operates&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Terraform provisions the infrastructure (EKS + ArgoCD)&lt;/li&gt;
&lt;li&gt;ArgoCD connects to the application Git repository&lt;/li&gt;
&lt;li&gt;Kubernetes manifests define the desired application state&lt;/li&gt;
&lt;li&gt;ArgoCD continuously monitors and syncs changes&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  💡 The Magic:
&lt;/h3&gt;

&lt;p&gt;If anything changes outside Git (manual kubectl edits, scaling, etc.),&lt;br&gt;
&lt;strong&gt;ArgoCD automatically reverts it back to the desired state.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🔍 &lt;strong&gt;Key Concepts I Mastered&lt;/strong&gt;
&lt;/h2&gt;
&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;1. Terraform Helm Provider&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Instead of manually installing ArgoCD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"helm_release"&lt;/span&gt; &lt;span class="s2"&gt;"argocd"&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;"argocd"&lt;/span&gt;
  &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://argoproj.github.io/argo-helm"&lt;/span&gt;
  &lt;span class="nx"&gt;chart&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"argo-cd"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 This ensures ArgoCD itself is &lt;strong&gt;version-controlled and reproducible&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;2. The “App of Apps” Pattern&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Rather than deploying each service manually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;parent ArgoCD application&lt;/strong&gt; manages multiple child apps&lt;/li&gt;
&lt;li&gt;Each microservice is defined independently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✅ Result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scalable architecture&lt;/li&gt;
&lt;li&gt;Clean separation of concerns&lt;/li&gt;
&lt;li&gt;Easier multi-service management&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;3. Drift Detection &amp;amp; Self-Healing&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;This was the biggest “aha!” moment.&lt;/p&gt;

&lt;p&gt;I tested:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Manually modifying a deployment&lt;/li&gt;
&lt;li&gt;Changing image versions outside Git&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 Result:&lt;br&gt;
ArgoCD instantly detected the drift and &lt;strong&gt;reconciled it automatically&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 No manual rollback. No panic fixes. Just automation.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 &lt;strong&gt;What This Means in Real-World DevOps&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This project reflects how modern teams operate:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Traditional Approach&lt;/th&gt;
&lt;th&gt;GitOps Approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manual deployments&lt;/td&gt;
&lt;td&gt;Automated sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human error risk&lt;/td&gt;
&lt;td&gt;Self-healing systems&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drift goes unnoticed&lt;/td&gt;
&lt;td&gt;Drift auto-corrected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config scattered&lt;/td&gt;
&lt;td&gt;Git as single truth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🔐 &lt;strong&gt;Challenges &amp;amp; Lessons Learned&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ⚠️ Secrets Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes secrets are base64 encoded—not secure enough&lt;/li&gt;
&lt;li&gt;Next step: integrate &lt;strong&gt;AWS Secrets Manager&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ⚠️ State Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Local Terraform state is risky&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Plan: move to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;S3 backend&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DynamoDB locking&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ⚠️ Complexity Growth
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;GitOps introduces more moving parts&lt;/li&gt;
&lt;li&gt;But the trade-off is worth it for &lt;strong&gt;automation + reliability&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;What’s Next? (Final Stretch)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before wrapping up the challenge, I plan to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Implement remote state management&lt;/li&gt;
&lt;li&gt;✅ Secure secrets with AWS Secrets Manager&lt;/li&gt;
&lt;li&gt;✅ Enhance CI/CD integration&lt;/li&gt;
&lt;li&gt;✅ Explore advanced GitOps patterns&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Day 29 changed how I think about infrastructure.&lt;/p&gt;

&lt;p&gt;This isn’t just about provisioning resources anymore—it’s about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building &lt;strong&gt;autonomous systems&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Reducing &lt;strong&gt;human intervention&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Creating &lt;strong&gt;resilient platforms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 The goal is no longer just &lt;em&gt;“infrastructure that works”&lt;/em&gt;&lt;br&gt;
👉 It’s &lt;strong&gt;“infrastructure that fixes itself.”&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>aws</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Scaling Cloud Proficiency: Building a Production-Ready 3-Tier Architecture with Terraform</title>
      <dc:creator>Atul Vishwakarma</dc:creator>
      <pubDate>Mon, 27 Apr 2026 07:19:35 +0000</pubDate>
      <link>https://dev.to/vatul16/scaling-cloud-proficiency-building-a-production-ready-3-tier-architecture-with-terraform-11f6</link>
      <guid>https://dev.to/vatul16/scaling-cloud-proficiency-building-a-production-ready-3-tier-architecture-with-terraform-11f6</guid>
      <description>&lt;p&gt;As part of my &lt;em&gt;30 Days of AWS Terraform Challenge&lt;/em&gt;, Day 28 marked a significant milestone in my cloud engineering journey—designing and deploying a &lt;strong&gt;fully automated, production-grade 3-tier architecture on AWS using Terraform&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This project wasn’t just about provisioning resources. It was about &lt;strong&gt;thinking like a systems designer&lt;/strong&gt;—balancing scalability, security, and reliability.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌍 Why 3-Tier Architecture Matters
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;3-tier architecture&lt;/strong&gt; is a foundational pattern in modern cloud systems because it separates concerns into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Presentation Layer (Web Tier)&lt;/strong&gt; → Handles user requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application Layer (App Tier)&lt;/strong&gt; → Processes business logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Layer (DB Tier)&lt;/strong&gt; → Stores and manages data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ✅ Benefits:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Improved &lt;strong&gt;security&lt;/strong&gt; through isolation&lt;/li&gt;
&lt;li&gt;Better &lt;strong&gt;scalability&lt;/strong&gt; per tier&lt;/li&gt;
&lt;li&gt;Increased &lt;strong&gt;fault tolerance&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Easier &lt;strong&gt;maintenance &amp;amp; updates&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🏗️ Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Here’s how I implemented the architecture on AWS:&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 1. Custom VPC &amp;amp; Networking
&lt;/h3&gt;

&lt;p&gt;I created a &lt;strong&gt;custom Virtual Private Cloud (VPC)&lt;/strong&gt; with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public subnets → For Load Balancer&lt;/li&gt;
&lt;li&gt;Private subnets → For App + DB tiers&lt;/li&gt;
&lt;li&gt;Internet Gateway → Public access&lt;/li&gt;
&lt;li&gt;NAT Gateway → Secure outbound access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 This ensures:&lt;br&gt;
✔ Public entry is controlled&lt;br&gt;
✔ Backend remains private&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 2. High Availability Across AZs
&lt;/h3&gt;

&lt;p&gt;To eliminate single points of failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployed resources across &lt;strong&gt;2 Availability Zones&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Distributed compute and networking components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Result:&lt;br&gt;
✔ Application remains available even during AZ failures&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 3. Web Tier (Presentation Layer)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Application Load Balancer (ALB)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Handles incoming traffic&lt;/li&gt;
&lt;li&gt;Routes requests to application servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Acts as the &lt;strong&gt;only public entry point&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 4. Application Tier (Logic Layer)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;EC2 instances inside &lt;strong&gt;private subnets&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Managed using &lt;strong&gt;Auto Scaling Groups (ASG)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Features:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Horizontal scaling based on demand&lt;/li&gt;
&lt;li&gt;High availability&lt;/li&gt;
&lt;li&gt;Fault tolerance&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🔹 5. Database Tier (Data Layer)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Amazon RDS (MySQL/PostgreSQL)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Placed in &lt;strong&gt;private subnet group&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Accessible only from application tier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Ensures:&lt;br&gt;
✔ No public exposure&lt;br&gt;
✔ Strong data security&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Terraform Implementation
&lt;/h2&gt;

&lt;p&gt;Everything was provisioned using &lt;strong&gt;Terraform&lt;/strong&gt;, following a &lt;strong&gt;modular approach&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  📦 Modules Created:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;VPC Module&lt;/li&gt;
&lt;li&gt;Security Groups Module&lt;/li&gt;
&lt;li&gt;Compute (EC2 + ASG) Module&lt;/li&gt;
&lt;li&gt;RDS Module&lt;/li&gt;
&lt;li&gt;Load Balancer Module&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  💡 Why Modular Terraform?
&lt;/h3&gt;

&lt;p&gt;✔ Reusable across environments&lt;br&gt;
✔ Cleaner codebase&lt;br&gt;
✔ Easier debugging&lt;br&gt;
✔ Faster deployments&lt;/p&gt;

&lt;p&gt;👉 Write once → reuse everywhere&lt;/p&gt;




&lt;h2&gt;
  
  
  🔐 Security Best Practices Implemented
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Private subnets for app &amp;amp; DB&lt;/li&gt;
&lt;li&gt;Security group restrictions (least privilege)&lt;/li&gt;
&lt;li&gt;No direct DB exposure&lt;/li&gt;
&lt;li&gt;NAT for controlled outbound traffic&lt;/li&gt;
&lt;li&gt;Secrets managed via AWS Secrets Manager&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚧 Challenges &amp;amp; Troubleshooting
&lt;/h2&gt;

&lt;p&gt;This project wasn’t without hurdles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RDS parameter group configuration issues&lt;/li&gt;
&lt;li&gt;Terraform provider inconsistencies&lt;/li&gt;
&lt;li&gt;Networking misconfigurations&lt;/li&gt;
&lt;li&gt;Security group debugging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 These challenges were the &lt;strong&gt;real learning moments&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Key Learnings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔹 1. Design &amp;gt; Deployment
&lt;/h3&gt;

&lt;p&gt;Provisioning is easy. Designing a resilient system is the real skill.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 2. Security by Default 🔐
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Never expose databases publicly&lt;/li&gt;
&lt;li&gt;Always isolate layers&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🔹 3. Modularity is Power
&lt;/h3&gt;

&lt;p&gt;Terraform modules turn complex systems into manageable components.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 4. Hands-On &amp;gt; Theory
&lt;/h3&gt;

&lt;p&gt;Breaking things and fixing them teaches more than tutorials ever can.&lt;/p&gt;




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

&lt;p&gt;Day 28 felt like a &lt;strong&gt;turning point&lt;/strong&gt; in my journey.&lt;/p&gt;

&lt;p&gt;I moved from:&lt;br&gt;
➡️ Writing Terraform code&lt;br&gt;
➡️ To designing &lt;strong&gt;real-world cloud architectures&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This project reflects how modern systems are built:&lt;br&gt;
✔ Scalable&lt;br&gt;
✔ Secure&lt;br&gt;
✔ Fault-tolerant&lt;br&gt;
✔ Automated&lt;/p&gt;




&lt;h2&gt;
  
  
  🔮 What’s Next?
&lt;/h2&gt;

&lt;p&gt;Only 2 days left in this challenge! Up next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Final optimizations&lt;/li&gt;
&lt;li&gt;Advanced patterns&lt;/li&gt;
&lt;li&gt;Wrapping up the journey&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>cloudcomputing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building a Highly Available Web Architecture with Terraform</title>
      <dc:creator>Atul Vishwakarma</dc:creator>
      <pubDate>Mon, 20 Apr 2026 09:01:59 +0000</pubDate>
      <link>https://dev.to/vatul16/building-a-highly-available-web-architecture-with-terraform-38h1</link>
      <guid>https://dev.to/vatul16/building-a-highly-available-web-architecture-with-terraform-38h1</guid>
      <description>&lt;p&gt;As part of my &lt;em&gt;30 Days of AWS Terraform Challenge&lt;/em&gt;, Day 24 marked a major milestone in my journey—from provisioning basic infrastructure to designing a &lt;strong&gt;highly available, fault-tolerant, and scalable web architecture&lt;/strong&gt; using Terraform.&lt;/p&gt;

&lt;p&gt;This project pushed me to think like a &lt;strong&gt;Cloud Engineer&lt;/strong&gt;, not just a Terraform user.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌍 Why High Availability Matters
&lt;/h2&gt;

&lt;p&gt;In real-world production systems, downtime is not an option.&lt;/p&gt;

&lt;p&gt;A resilient architecture must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle failures gracefully&lt;/li&gt;
&lt;li&gt;Scale automatically with demand&lt;/li&gt;
&lt;li&gt;Maintain security best practices&lt;/li&gt;
&lt;li&gt;Ensure consistent performance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project brought all of these principles together.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The infrastructure I built follows a &lt;strong&gt;multi-tier, production-grade design&lt;/strong&gt; on AWS:&lt;/p&gt;

&lt;h3&gt;
  
  
  🔹 1. Application Load Balancer (ALB)
&lt;/h3&gt;

&lt;p&gt;The ALB acts as the &lt;strong&gt;entry point&lt;/strong&gt; for all incoming traffic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distributes traffic across multiple EC2 instances&lt;/li&gt;
&lt;li&gt;Spans multiple Availability Zones&lt;/li&gt;
&lt;li&gt;Ensures fault tolerance if one AZ fails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Result: Improved uptime and reliability&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 2. Auto Scaling Group (ASG)
&lt;/h3&gt;

&lt;p&gt;To make the system &lt;strong&gt;elastic&lt;/strong&gt;, I configured an Auto Scaling Group:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Defined &lt;strong&gt;min, max, and desired capacity&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Integrated &lt;strong&gt;CloudWatch metrics (CPU utilization)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scales &lt;strong&gt;out&lt;/strong&gt; during high traffic&lt;/li&gt;
&lt;li&gt;Scales &lt;strong&gt;in&lt;/strong&gt; during low usage&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Result: Performance + cost optimization&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 3. Private Subnet Architecture 🔐
&lt;/h3&gt;

&lt;p&gt;Instead of exposing servers directly to the internet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EC2 instances are deployed in &lt;strong&gt;private subnets&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Only the ALB resides in &lt;strong&gt;public subnets&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Result: Strong security posture (Zero direct public access)&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 4. NAT Gateway for Outbound Access
&lt;/h3&gt;

&lt;p&gt;Since private instances need internet access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NAT Gateways were deployed in each AZ&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OS updates&lt;/li&gt;
&lt;li&gt;Pulling Docker images&lt;/li&gt;
&lt;li&gt;External API calls&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Result: Secure outbound connectivity without compromising isolation&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Terraform Implementation
&lt;/h2&gt;

&lt;p&gt;The entire infrastructure was built using &lt;strong&gt;Infrastructure as Code (IaC)&lt;/strong&gt; with Terraform.&lt;/p&gt;

&lt;h3&gt;
  
  
  📦 Key Components:
&lt;/h3&gt;

&lt;h4&gt;
  
  
  🔸 Launch Templates
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Defined EC2 configuration&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Automated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker installation&lt;/li&gt;
&lt;li&gt;Application deployment (Django app)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  🔸 Auto Scaling Policies
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Connected with CloudWatch alarms&lt;/li&gt;
&lt;li&gt;Triggered scaling actions automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  🔸 Modular Design
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Separated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Networking&lt;/li&gt;
&lt;li&gt;Compute&lt;/li&gt;
&lt;li&gt;Security&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Improved readability and reusability&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Result: Clean, scalable, production-ready codebase&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Key Learnings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  💡 1. Fault Tolerance is Essential
&lt;/h3&gt;

&lt;p&gt;Deploying across multiple Availability Zones ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No single point of failure&lt;/li&gt;
&lt;li&gt;Continuous availability&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  💡 2. Automation Eliminates Drift
&lt;/h3&gt;

&lt;p&gt;Manually building this setup would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Be error-prone&lt;/li&gt;
&lt;li&gt;Lead to inconsistencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Terraform:&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
terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything becomes:&lt;br&gt;
✔ Repeatable&lt;br&gt;
✔ Version-controlled&lt;br&gt;
✔ Reliable&lt;/p&gt;




&lt;h3&gt;
  
  
  💡 3. Security First Mindset 🔐
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Private subnets for compute&lt;/li&gt;
&lt;li&gt;ALB as the only public entry&lt;/li&gt;
&lt;li&gt;NAT for controlled outbound access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 This is how real-world systems are designed&lt;/p&gt;




&lt;h3&gt;
  
  
  💡 4. Scalability is a Design Principle
&lt;/h3&gt;

&lt;p&gt;Instead of guessing capacity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Let metrics drive scaling decisions&lt;/li&gt;
&lt;li&gt;Build systems that adapt automatically&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚧 Challenges Faced
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Understanding ASG + ALB integration&lt;/li&gt;
&lt;li&gt;Debugging health checks&lt;/li&gt;
&lt;li&gt;Configuring correct security group rules&lt;/li&gt;
&lt;li&gt;Ensuring proper routing between subnets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Each issue improved my troubleshooting skills significantly&lt;/p&gt;




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

&lt;p&gt;This project was a &lt;strong&gt;turning point&lt;/strong&gt; in my Terraform journey.&lt;/p&gt;

&lt;p&gt;I moved from:&lt;br&gt;
➡️ Creating resources&lt;br&gt;
➡️ To designing &lt;strong&gt;resilient cloud systems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is what real DevOps engineering looks like.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔮 What’s Next?
&lt;/h2&gt;

&lt;p&gt;As I approach the final stretch of this challenge, I’m excited to explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advanced deployment strategies&lt;/li&gt;
&lt;li&gt;CI/CD integrations&lt;/li&gt;
&lt;li&gt;Multi-account architectures&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>devops</category>
      <category>terraform</category>
    </item>
  </channel>
</rss>
