<?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: KithupaG</title>
    <description>The latest articles on DEV Community by KithupaG (@trenation).</description>
    <link>https://dev.to/trenation</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%2F1623578%2F2a7b831c-6be6-45f9-bee7-c18d3a997278.jpg</url>
      <title>DEV Community: KithupaG</title>
      <link>https://dev.to/trenation</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/trenation"/>
    <language>en</language>
    <item>
      <title>Building a Production Grade AWS Infrastructure Project (Part 3): IAM Terraform</title>
      <dc:creator>KithupaG</dc:creator>
      <pubDate>Mon, 20 Jul 2026 10:10:23 +0000</pubDate>
      <link>https://dev.to/trenation/building-a-production-grade-aws-infrastructure-project-part-3-iam-terraform-243a</link>
      <guid>https://dev.to/trenation/building-a-production-grade-aws-infrastructure-project-part-3-iam-terraform-243a</guid>
      <description>&lt;h2&gt;
  
  
  Building a Production Grade AWS Infrastructure Part 3: IAM Roles
&lt;/h2&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/trenation/building-a-production-grade-aws-infrastructure-project-part-2-containerization-pkg"&gt;previous post&lt;/a&gt;, I containerized the frontend and backend using Docker Compose with Postgres. Now that the app runs locally, it's time to start building the AWS infrastructure.&lt;/p&gt;

&lt;p&gt;But where do you even begin? AWS has a deny-by-default permissions model, meaning nothing works until you explicitly give it permission. That makes IAM the logical starting point, before you can spin up a VPC, launch an ECS task, or write to S3, you need roles and policies in place, otherwise all your services will fail from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IAM Mindset: Roles, not Users
&lt;/h2&gt;

&lt;p&gt;When I first started learning IAM, my mind automatically went to users, creating someone a username, password, access keys. I had to remind myself: this is infrastructure, not a team. In production, your services don't log in. They assume roles.&lt;/p&gt;

&lt;p&gt;The difference is important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IAM User&lt;/strong&gt; : person with a password and/or access keys&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM Role&lt;/strong&gt; : a set of permissions that a service can assume temporarily&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two are backed by the same core concept: an IAM identity. It's just that roles are designed for machines and services, not people. Since our app runs on AWS infrastructure, roles are the way to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  trust_policy_permissions: what?
&lt;/h2&gt;

&lt;p&gt;When you look at the role block in Terraform, the first thing you'll see is the trust policy:&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;trust_policy_permissions&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;TrustRoleAndServiceToAssume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"sts:TagSession"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
        &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;"ecs-tasks.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;TrustRoleAndServiceToAssume&lt;/code&gt; is just a friendly label given by the terraform module, it could be labeled anything. Like &lt;code&gt;TrustThisServicePlease&lt;/code&gt;. The important part is the actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;sts:AssumeRole&lt;/strong&gt; : This tells AWS to issue a temporary security credential (valid for about an hour by default) to the service specified in the principals block. It's how AWS allows one identity to borrow another identity's permissions temporarily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sts:TagSession&lt;/strong&gt; : More of an auditing tool. This attaches metadata tags to that session so you can trace which service, container, or user requested the credentials. Very useful for debugging later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now here's something beginners often get wrong — the principals block. I initially had:&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;type&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS"&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;changed&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
&lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ec2.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;changed&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="s2"&gt;"ecs-tasks.amazonaws.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Use &lt;code&gt;type = "Service"&lt;/code&gt; when the entity that needs the role is a service like EC2, ECS, or Lambda. Use &lt;code&gt;type = "AWS"&lt;/code&gt; when it's a user or account.&lt;/p&gt;

&lt;p&gt;I also removed the condition block:&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;condition&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringEquals"&lt;/span&gt;
    &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:ExternalId"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"some-secret-id"&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 block is specifically for external services, for example, if a third-party SaaS needs to access your AWS account. The ExternalId ensures that even if someone steals your role ARN, they can't use it without that secret ID. Since our application runs on ECS, which is an internal service, we don't need this.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why two roles? The ECS role architecture
&lt;/h2&gt;

&lt;p&gt;A common beginner mistake is to create a single role for everything. But if you're using ECS, there is a much better split:&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Task Execution Role (&lt;code&gt;app-execution-role&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This role belongs to the ECS agent, it's the AWS service itself. Its job is to bridge the gap between the container specification and the services ECS needs to start your app.&lt;/p&gt;

&lt;p&gt;Policies attached:&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;policies&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;getSecretsPostgres&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::aws:policy/AWSSecretsManagerClientReadOnlyAccess"&lt;/span&gt;
    &lt;span class="nx"&gt;pullImageFromECS&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AmazonECSTaskExecutionRolePolicy&lt;/strong&gt; : Allows the ECS agent to pull images from ECR and write logs to CloudWatch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWSSecretsManagerClientReadOnlyAccess&lt;/strong&gt; : Allows the ECS agent to read secrets from Secrets Manager&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of this as the valet parking guy: he parks your car (the container) in the right spot, but he doesn't drive it anywhere.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Task Role (&lt;code&gt;app-task-role&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This role belongs to the application itself, the code running inside the container. If your Node.js app needs to read an S3 bucket, send an email via SES, or access DynamoDB, this is where you attach those permissions.&lt;/p&gt;

&lt;p&gt;But here's the thing: right now, our application only talks to PostgreSQL via standard network drivers. It doesn't need IAM permissions.&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;policies&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Notice the empty policies block. I intentionally left it that way.&lt;/p&gt;

&lt;p&gt;If I didn't set a separate task role, the application would default to the host's IAM role (the EC2 instance's role), which could accidentally grant broad permissions. By explicitly setting an empty task role, we lock down the container: even if someone exploits a vulnerability in the Node application, there are zero permissions to leverage.&lt;/p&gt;

&lt;p&gt;Later, when the app grows and it needs to upload user avatars to S3, or send welcome emails via SES, I'll attach the appropriate policies.&lt;/p&gt;
&lt;h2&gt;
  
  
  How to decide on policies
&lt;/h2&gt;

&lt;p&gt;Settling on the right policies requires two questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. What AWS service does my app touch?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My app is a Node.js application backed by PostgreSQL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compute: ECS with Fargate (needs to pull images from ECR)&lt;/li&gt;
&lt;li&gt;Database: Amazon RDS (connection goes through the network, not IAM)&lt;/li&gt;
&lt;li&gt;Security: AWS Secrets Manager (to retrieve db credentials)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. What actions does my app need for each service?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pulling images from ECR? Read-only -&amp;gt; &lt;code&gt;AmazonEC2ContainerRegistryReadOnly&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Writing logs? Write -&amp;gt; &lt;code&gt;CloudWatchAgentServerPolicy&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The golden rule: the fewer permissions, the better. Start with nothing and add as you go. AWS provides plenty of managed policies, but if none fits perfectly, you can always write a custom one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Custom policies with data sources
&lt;/h2&gt;

&lt;p&gt;If you ever need a policy that doesn't exist in AWS's managed offerings, Terraform provides the &lt;code&gt;aws_iam_policy_document&lt;/code&gt; data source:&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;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can then reference &lt;code&gt;module.iam_policy.arn&lt;/code&gt; or build inline policies. The key thing is: write exactly what your app needs, nothing more.&lt;/p&gt;
&lt;h2&gt;
  
  
  The outputs file: exposing your roles
&lt;/h2&gt;

&lt;p&gt;Once the IAM roles are defined, your other modules can't see them without an outputs file. Think of it as an export statement.&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;output&lt;/span&gt; &lt;span class="s2"&gt;"execution_role_arn"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The ARN of the ECS execution role"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"task_role_arn"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The ARN of the ECS task role"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iam_role_task&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.arn&lt;/strong&gt; tells Terraform to fetch the resource name string generated by AWS once the role is created&lt;/li&gt;
&lt;li&gt;The outputs will be referenced in the ECS module later&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never output plaintext secrets&lt;/strong&gt; like database passwords. If you absolutely must, use &lt;code&gt;sensitive = true&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now our VPC module (which I'll cover in the next post) and the ECS module will be able to link to these IAM role ARNs programmatically, without copy-pasting random string IDs.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I learnt
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;IAM roles are for services, IAM users are for people — two sides of the same coin&lt;/li&gt;
&lt;li&gt;ECS needs two distinct roles: one for the ECS agent and one for the app&lt;/li&gt;
&lt;li&gt;Empty task policies are a feature: they keep the app in the most secure posture possible&lt;/li&gt;
&lt;li&gt;Export via outputs, never hardcode ARNs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Next up: The VPC module
&lt;/h2&gt;

&lt;p&gt;With IAM out of the way, the next logical step is setting up the VPC. All the networking, subnets, route tables, internet gateways. Needs to be defined before we can plug in RDS, ALB, or ECS.&lt;/p&gt;

&lt;p&gt;If you're learning IAM roles for the first time like me, what AWS service tripped you up the most? Drop a comment below!&lt;/p&gt;

&lt;p&gt;Want to follow along? The full code is available here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/KithupaG" rel="noopener noreferrer"&gt;
        KithupaG
      &lt;/a&gt; / &lt;a href="https://github.com/KithupaG/aws-production-infra" rel="noopener noreferrer"&gt;
        aws-production-infra
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A complete AWS themed production infrastructure
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;CloudNotes - Production Grade AWS Infrastructure&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A full-stack Note Taker application designed as a sandbox for building production-grade AWS infrastructure with Terraform. The app is fully containerized with Docker and orchestrated with Docker Compose, ready to be deployed across a high-availability AWS architecture.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Architecture&lt;/h2&gt;
&lt;/div&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;                        ┌──────────────────────────────────┐
                        │         AWS Cloud (Target)       │
                        │                                  │
                        │   Route53 ─── CloudFront ─── S3  │
                        │                    │             │
                        │                    ▼             │
                        │        ┌─────── ALB ───────┐     │
                        │        │                   │     │
                        │   ┌────▼────┐        ┌────▼────┐ │
                        │   │ EC2 ASG │        │ EC2 ASG │ │
                        │   │(Backend)│        │(Backend)│ │
                        │   └────┬────┘        └────┬────┘ │
                        │        │                  │      │
                        │        └────────┬─────────┘      │
                        │                 ▼                │
                        │           ┌──── RDS ────┐        │
                        │           │  PostgreSQL │        │
                        │           └─────────────┘        │
                        └──────────────────────────────────┘
Local (Docker Compose):
┌──────────┐    ┌────────────┐    ┌──────────────┐
│ Frontend │───▶│  Backend  │───▶│  PostgreSQL  │
│ (Nginx)  │    │ (Node.js)  │    │ (15-alpine)  │
│  :3000   │    │  :5001     │    │   :5432&lt;/code&gt;&lt;/pre&gt;…&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/KithupaG/aws-production-infra" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



</description>
      <category>terraform</category>
      <category>aws</category>
      <category>infrastructure</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Production Grade AWS Infrastructure Project (Part 2): Containerization</title>
      <dc:creator>KithupaG</dc:creator>
      <pubDate>Fri, 10 Jul 2026 16:53:22 +0000</pubDate>
      <link>https://dev.to/trenation/building-a-production-grade-aws-infrastructure-project-part-2-containerization-pkg</link>
      <guid>https://dev.to/trenation/building-a-production-grade-aws-infrastructure-project-part-2-containerization-pkg</guid>
      <description>&lt;h2&gt;
  
  
  The Pivot: Why Containerization First?
&lt;/h2&gt;

&lt;p&gt;After careful thought and consideration, I realized that building the VPC right away wasn't the most efficient path. Instead, I went back to the drawing board and decided to containerize the application first. This ensures that the environment is consistent before we start orchestrating it in the cloud.&lt;/p&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/trenation/building-a-production-grade-aws-infrastructure-project-part-1-5ceb"&gt;previous post&lt;/a&gt;, I introduced the project, the file structure, and the "why" behind this build. In this post, I'll be walking through the containerization process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Strategy
&lt;/h2&gt;

&lt;p&gt;Since the frontend and backend are two distinct applications with different tech stacks, I wrote two separate &lt;code&gt;Dockerfiles&lt;/code&gt; that align with the project structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Backend (Node.js)
&lt;/h3&gt;

&lt;p&gt;For the backend, the setup is relatively straightforward.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /usr/src/backend/&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev

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

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

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

&lt;/div&gt;


&lt;p&gt;Breaking it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Base Image:&lt;/strong&gt; I used &lt;code&gt;node:22-slim&lt;/code&gt; instead of the common alpine version. This is because the backend uses &lt;code&gt;sqlite3&lt;/code&gt;, which requires dependencies like &lt;code&gt;gcc&lt;/code&gt;, &lt;code&gt;g++&lt;/code&gt;, and &lt;code&gt;python&lt;/code&gt; to compile. The slim image handles these better than the ultra-minimal Alpine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencies:&lt;/strong&gt; I used &lt;code&gt;npm ci --omit=dev&lt;/code&gt;. This ignores development dependencies, keeping the production image as light as possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution:&lt;/strong&gt; We expose port 5000 and run the app using &lt;code&gt;node server.js&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  2. The Frontend (React + Nginx)
&lt;/h3&gt;

&lt;p&gt;The frontend utilizes a multi-stage build. This allows us to build the app in a Node environment and then serve the static files using Nginx.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: Build&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:22-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /usr/src/frontend/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# Stage 2: Serve&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;nginx:alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;server&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /usr/src/frontend/dist/ /usr/share/nginx/html&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; nginx.conf /etc/nginx/conf.d/default.conf&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 80&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["nginx", "-g", "daemon off;"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Why this approach?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight:&lt;/strong&gt; Nginx is significantly faster and smaller than keeping a Node server running just to serve static React files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse Proxy:&lt;/strong&gt; Since React is client-side, it doesn't "know" how to route API requests to a backend in a different container. We use Nginx to handle these routes via a custom &lt;code&gt;nginx.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD Friendly:&lt;/strong&gt; Building inside Docker means your CI/CD runner (like GitHub Actions) doesn't need to have Node installed. Everything happens inside the container.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Orchestrating with Docker Compose
&lt;/h2&gt;

&lt;p&gt;To manage the frontend, backend, and database simultaneously, I used Docker Compose.&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;note-taker&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src/backend&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&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;5001:5000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
      &lt;span class="na"&gt;PGHOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;database&lt;/span&gt;
      &lt;span class="na"&gt;PGUSER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;PGPASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;PGDATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notetaker&lt;/span&gt;
      &lt;span class="na"&gt;PGPORT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5432&lt;/span&gt;
      &lt;span class="na"&gt;PGSSL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-network&lt;/span&gt;

  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15-alpine&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production_db&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notetaker&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;ports&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;5432:5432"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db-data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-network&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&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;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;notetaker"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src/frontend&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;3000:80&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-network&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app-network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Key Features of this Compose file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Health Checks:&lt;/strong&gt; The backend waits for the database to be "healthy" (using &lt;code&gt;pg_isready&lt;/code&gt;) before starting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port Mapping:&lt;/strong&gt; I mapped the backend to 5001 and the frontend to 3000 to avoid conflicts with other local services like XAMPP or Docker Desktop defaults.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Networking:&lt;/strong&gt; All services share &lt;code&gt;app-network&lt;/code&gt;, allowing them to communicate via service names (e.g., the backend connects to &lt;code&gt;database:5432&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Nginx Configuration
&lt;/h2&gt;

&lt;p&gt;Without a custom nginx config, the frontend container would serve static files but have no idea where to send API requests. The browser makes requests to &lt;code&gt;/api/notes&lt;/code&gt; and &lt;code&gt;/health&lt;/code&gt;, but nginx doesn't know these belong to the backend.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://backend:5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/health&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://backend:5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/usr/share/nginx/html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The key pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;proxy_pass&lt;/code&gt;&lt;/strong&gt; forwards &lt;code&gt;/api/&lt;/code&gt; and &lt;code&gt;/health&lt;/code&gt; requests to the backend container using its service name (&lt;code&gt;backend:5000&lt;/code&gt;). Docker's built-in DNS resolves the name across containers on the same network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;try_files&lt;/code&gt;&lt;/strong&gt; handles React's client-side routing. Without it, refreshing on a route like &lt;code&gt;/notes&lt;/code&gt; would return a 404 because nginx would look for a file that doesn't exist. This directive falls back to &lt;code&gt;index.html&lt;/code&gt; and lets React handle the route.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Troubleshooting: The SSL Headache
&lt;/h2&gt;

&lt;p&gt;While running the backend, I hit a snag. The logs showed a connection failure:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Failed to connect to PostgreSQL database: The server does not support SSL connections"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  The Cause
&lt;/h3&gt;

&lt;p&gt;In my application code, I had logic that forced SSL if the environment was set to production:&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="nx"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PGSSL&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Since the Docker Compose environment was set to &lt;code&gt;NODE_ENV: production&lt;/code&gt;, the app was refusing any non-SSL connection from the Postgres container.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;I updated the logic in &lt;code&gt;db.js&lt;/code&gt; to be more granular. This allows me to explicitly toggle SSL off via environment variables even in production-mode containers:&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="nx"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PGSSL&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rejectUnauthorized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PGSSL&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rejectUnauthorized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;By setting &lt;code&gt;PGSSL: false&lt;/code&gt; in the Docker Compose file, the connection was finally successful.&lt;/p&gt;
&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Now that the application is fully containerized and running locally, the next step is to setup the IAM roles through a terraform script.&lt;/p&gt;



&lt;p&gt;Have you ever run into issues where "Production" environment flags caused local Docker testing to fail? How do you handle your SSL logic in dev vs. prod? Let me know in the comments!&lt;/p&gt;

&lt;p&gt;If you want to follow along on this project, here is my github repo&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/KithupaG" rel="noopener noreferrer"&gt;
        KithupaG
      &lt;/a&gt; / &lt;a href="https://github.com/KithupaG/aws-production-infra" rel="noopener noreferrer"&gt;
        aws-production-infra
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A complete AWS themed production infrastructure
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;aws-production-infra&lt;/h1&gt;

&lt;/div&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/KithupaG/aws-production-infra" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>docker</category>
      <category>webdev</category>
      <category>aws</category>
      <category>node</category>
    </item>
    <item>
      <title>Building a Production Grade AWS Infrastructure Project (Part 1)</title>
      <dc:creator>KithupaG</dc:creator>
      <pubDate>Wed, 20 May 2026 15:07:03 +0000</pubDate>
      <link>https://dev.to/trenation/building-a-production-grade-aws-infrastructure-project-part-1-5ceb</link>
      <guid>https://dev.to/trenation/building-a-production-grade-aws-infrastructure-project-part-1-5ceb</guid>
      <description>&lt;p&gt;I'm 16, self-taught, and currently studying for my AWS Solutions Architect Associate certification. Alongside the course, I'm building a production-grade AWS infrastructure from scrach and documenting the entire journey here&lt;/p&gt;

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

&lt;p&gt;Certs alone don't prove you can build anything. I want something real to show for it, So writing infrastruction that reflects how things are done in production and not just dwelling inside tutorial hell. Writing about it publicly keeps me accountable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is my stack?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloud: AWS&lt;/li&gt;
&lt;li&gt;Infrastructure as Code: Terraform (I decided to keep it modular with seperate dev and prod environments)&lt;/li&gt;
&lt;li&gt;CI/CD : Github Actions&lt;/li&gt;
&lt;li&gt;Containerization: Docker + Trivy (for container scanning)&lt;/li&gt;
&lt;li&gt;Application: A minimal PERN stack notes app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Heres what my folder structure looks like&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws-production-infra/&lt;br&gt;
├── modules/ # reusable Terraform modules&lt;br&gt;
│ ├── vpc/&lt;br&gt;
│ │ ├── main.tf&lt;br&gt;
│ │ ├── variables.tf&lt;br&gt;
│ │ ├── outputs.tf&lt;br&gt;
│ │ └── README.md&lt;br&gt;
│ ├── ec2-asg/&lt;br&gt;
│ │ ├── main.tf&lt;br&gt;
│ │ ├── variables.tf&lt;br&gt;
│ │ └── outputs.tf&lt;br&gt;
│ ├── alb/&lt;br&gt;
│ ├── rds/&lt;br&gt;
│ ├── s3-cdn/&lt;br&gt;
│ ├── iam/&lt;br&gt;
│ ├── security/&lt;br&gt;
│ └── monitoring/&lt;br&gt;
├── environments/&lt;br&gt;
│ ├── dev/&lt;br&gt;
│ │ ├── main.tf&lt;br&gt;
│ │ ├── variables.tf&lt;br&gt;
│ │ ├── terraform.tfvars&lt;br&gt;
│ │ └── backend.tf&lt;br&gt;
│ └── prod/&lt;br&gt;
├── app/&lt;br&gt;
│ ├── Dockerfile&lt;br&gt;
│ └── src/&lt;br&gt;
├── .github/&lt;br&gt;
│ └── workflows/&lt;br&gt;
│ ├── terraform-plan.yml&lt;br&gt;
│ └── docker-build.yml&lt;br&gt;
├── docs/&lt;br&gt;
│ ├── architecture.svg&lt;br&gt;
│ └── cost-estimate.md&lt;br&gt;
├── scripts/&lt;br&gt;
│ └── destroy-expensive.sh&lt;br&gt;
├── .gitignore&lt;br&gt;
└── README.md&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;They key decision I did here is seperating the &lt;code&gt;modules/&lt;/code&gt; folder from &lt;code&gt;environments/&lt;/code&gt;. Both environments call the same modules but with different variable values. Dev runs cheaper, single AZ resources purely for developing purposes while Prod runs Multi-AZ, larger instances in a more contained and secure setting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I've done so far&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The folder structure is set up and the application code is in the repo. I vibe-coded the entire PERN app to save time, and honestly the infrastructure is what its all about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My build order is&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;- Write all the terraform infrastructure first.&lt;/li&gt;
&lt;li&gt;- Write the Dockerfile and get Trivy scanning running in CI&lt;/li&gt;
&lt;li&gt;- Wire everything together with the GitHub actions deployment pipeline&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next post will cover building the VPC modules (building subnets, route tables, NAT gateway, and flow logs)&lt;/p&gt;

&lt;p&gt;Repo is here if you want to follow along: &lt;a href="https://github.com/KithupaG/aws-production-infra" rel="noopener noreferrer"&gt;https://github.com/KithupaG/aws-production-infra&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>learning</category>
      <category>aws</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Building a Simple Server Monitoring Script in My Homelab</title>
      <dc:creator>KithupaG</dc:creator>
      <pubDate>Wed, 27 Aug 2025 16:47:01 +0000</pubDate>
      <link>https://dev.to/trenation/building-a-simple-server-monitoring-script-in-my-homelab-4d97</link>
      <guid>https://dev.to/trenation/building-a-simple-server-monitoring-script-in-my-homelab-4d97</guid>
      <description>&lt;p&gt;*&lt;em&gt;Why monitoring matters *&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Monitoring matters because it provides a shared feedback loop, necessary visibility in the system metrics, performance, security, it also enables rapid issue detection, and continous improvement&lt;/p&gt;

&lt;p&gt;See my code in my github here:- &lt;a href="https://github.com/KithupaG/Homelab/blob/main/monitoring/server-stats.sh" rel="noopener noreferrer"&gt;https://github.com/KithupaG/Homelab/blob/main/monitoring/server-stats.sh&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I built it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I used functions to house the processes, adhering to the laws of writing code, inside those functions I housed the shell commands such as &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;free&lt;/code&gt;, &lt;code&gt;df&lt;/code&gt;, &lt;code&gt;ps&lt;/code&gt; etc to fetch the data and used &lt;code&gt;awk&lt;/code&gt; to tabularize the data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Demo output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fua3ojsivqlmrd2o6yat7.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.amazonaws.com%2Fuploads%2Farticles%2Fua3ojsivqlmrd2o6yat7.png" alt=" " width="533" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Future plan&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;logging to a file&lt;/strong&gt; → &lt;code&gt;server_stats.log&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;Output as &lt;strong&gt;JSON&lt;/strong&gt; so it integrates with Prometheus/ELK.
&lt;/li&gt;
&lt;li&gt;Connect to &lt;strong&gt;Grafana&lt;/strong&gt; and make dashboards.
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>devops</category>
      <category>beginners</category>
      <category>programming</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
