<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Pawan's Tech Blog]]></title><description><![CDATA[Pawan's Tech Blog]]></description><link>https://thepawan.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1745228800070/33cf05f2-cb8e-49aa-ad46-dab320e3ec2e.png</url><title>Pawan&apos;s Tech Blog</title><link>https://thepawan.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 18 Apr 2026 10:23:36 GMT</lastBuildDate><atom:link href="https://thepawan.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Migrating GitLab Runners from EKS Fargate to EKS Auto Mode: A 40% Cost Reduction Journey]]></title><description><![CDATA[The Problem: Fargate Costs Were Adding Up
For over a year, we ran our GitLab Runners on Amazon EKS with AWS Fargate. The serverless approach was appealing—no nodes to manage, automatic scaling, and a ]]></description><link>https://thepawan.dev/migrating-gitlab-runners-from-eks-fargate-to-eks-auto-mode-a-40-cost-reduction-journey</link><guid isPermaLink="true">https://thepawan.dev/migrating-gitlab-runners-from-eks-fargate-to-eks-auto-mode-a-40-cost-reduction-journey</guid><dc:creator><![CDATA[Pawan Sawalani]]></dc:creator><pubDate>Tue, 24 Mar 2026 11:50:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767949597033/18bcbe7d-fbff-4918-88c9-c8627cf9d64b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/67db3328954e01d80347c10f/ca68bea3-7b4f-4569-91ef-bf49db8b2c3e.png" alt="" style="display:block;margin:0 auto" />

<h2>The Problem: Fargate Costs Were Adding Up</h2>
<p>For over a year, we ran our GitLab Runners on Amazon EKS with AWS Fargate. The serverless approach was appealing—no nodes to manage, automatic scaling, and a simple mental model. Each CI/CD job spun up as a Fargate pod, ran its tests, and disappeared.</p>
<p>But as our engineering team grew and pipeline frequency increased, the monthly bill told a different story:</p>
<table>
<thead>
<tr>
<th>Component</th>
<th>Monthly Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Fargate vCPU hours</td>
<td>~$115</td>
</tr>
<tr>
<td>Fargate memory hours</td>
<td>~$28</td>
</tr>
<tr>
<td>EKS control plane</td>
<td>~$74</td>
</tr>
<tr>
<td>NAT Gateway</td>
<td>~$77</td>
</tr>
<tr>
<td>Supporting infrastructure</td>
<td>~$90</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td><strong>~$385/month</strong></td>
</tr>
</tbody></table>
<p>The Fargate compute alone was costing us <strong>$143/month</strong> for what amounted to intermittent CI/CD workloads. Our pipelines ran maybe 4-6 hours of actual compute per day, yet we were paying premium serverless pricing for every second.</p>
<h3>Why Fargate Becomes Expensive for CI/CD</h3>
<p>Fargate pricing in eu-west-1:</p>
<ul>
<li><p><strong>$0.04048</strong> per vCPU per hour</p>
</li>
<li><p><strong>$0.004445</strong> per GB memory per hour</p>
</li>
</ul>
<p>For a typical CI job requesting 2 vCPU and 4GB memory running for 15 minutes:</p>
<ul>
<li><p>Fargate cost: ~$0.034 per job</p>
</li>
<li><p>Equivalent spot instance (m6a.large): ~$0.007 per job</p>
</li>
</ul>
<p>That's nearly <strong>5x more expensive</strong> than spot instances for the same workload.</p>
<hr />
<h2>Why Run GitLab Runners on Kubernetes (EKS)?</h2>
<p>Before diving into our solution, it's worth understanding why we chose Kubernetes in the first place. There are several ways to run GitLab Runners on AWS:</p>
<h3>Option 1: EC2 Instances (Docker/Shell Executor)</h3>
<p>The traditional approach—run GitLab Runner directly on EC2 instances using the Docker or Shell executor.</p>
<p><strong>Pros:</strong></p>
<ul>
<li><p>Simple to set up and understand</p>
</li>
<li><p>Full control over the environment</p>
</li>
<li><p>Works with existing EC2 knowledge</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p><strong>Manual scaling</strong>: You manage autoscaling groups, launch templates, lifecycle hooks</p>
</li>
<li><p><strong>Resource waste</strong>: Instances run 24/7 or you build complex scaling logic</p>
</li>
<li><p><strong>Docker-in-Docker issues</strong>: Security concerns with privileged containers</p>
</li>
<li><p><strong>Maintenance burden</strong>: OS patching, Docker updates, runner upgrades are your responsibility</p>
</li>
<li><p><strong>No bin-packing</strong>: Each instance typically runs one job at a time (or complex queue management)</p>
</li>
</ul>
<h3>Option 2: ECS with Fargate or EC2</h3>
<p>Amazon ECS offers container orchestration without Kubernetes complexity.</p>
<p><strong>Pros:</strong></p>
<ul>
<li><p>Native AWS integration</p>
</li>
<li><p>Fargate provides serverless containers</p>
</li>
<li><p>Simpler than Kubernetes for basic use cases</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p><strong>No native GitLab integration</strong>: GitLab Runner doesn't have an ECS executor—you'd need custom solutions</p>
</li>
<li><p><strong>Task definition management</strong>: More complex than Kubernetes pods for dynamic workloads</p>
</li>
<li><p><strong>Limited ecosystem</strong>: Fewer community tools and patterns compared to Kubernetes</p>
</li>
<li><p><strong>Vendor lock-in</strong>: ECS is AWS-specific; Kubernetes skills transfer across clouds</p>
</li>
</ul>
<h3>Option 3: EKS with Kubernetes Executor (Our Choice)</h3>
<p>GitLab Runner's Kubernetes executor is purpose-built for CI/CD on Kubernetes.</p>
<p><strong>Pros:</strong></p>
<ul>
<li><p><strong>Native GitLab integration</strong>: First-class support in GitLab Runner</p>
</li>
<li><p><strong>Automatic pod lifecycle</strong>: Each job gets a fresh pod, automatic cleanup</p>
</li>
<li><p><strong>Bin-packing</strong>: Multiple jobs share nodes efficiently</p>
</li>
<li><p><strong>Karpenter/Auto Mode</strong>: Intelligent, automatic node provisioning in seconds</p>
</li>
<li><p><strong>Ecosystem benefits</strong>: Helm charts, operators, monitoring tools</p>
</li>
<li><p><strong>Portability</strong>: Same configuration works on any Kubernetes cluster</p>
</li>
<li><p><strong>Security</strong>: Pod security standards, network policies, IRSA for AWS access</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p><strong>Kubernetes complexity</strong>: Learning curve if you're new to K8s</p>
</li>
<li><p><strong>More moving parts</strong>: Nodes, pods, services vs. just EC2 instances</p>
</li>
</ul>
<h3>Why EKS Won for Us</h3>
<p>The deciding factors were:</p>
<ol>
<li><p><strong>GitLab's Kubernetes executor is mature</strong>: Handles job isolation, artifact management, and service containers natively</p>
</li>
<li><p><strong>Karpenter changes everything</strong>: No more managing autoscaling groups—Karpenter provisions exactly what you need in seconds</p>
</li>
<li><p><strong>Cost efficiency through bin-packing</strong>: Multiple CI jobs share a single node, maximizing utilization</p>
</li>
<li><p><strong>Team skills</strong>: We already run production workloads on Kubernetes</p>
</li>
<li><p><strong>Future flexibility</strong>: Easy to migrate to another cloud or on-premises if needed</p>
</li>
</ol>
<hr />
<h2>Why EKS Auto Mode with Spot Over Standard EKS with Fargate?</h2>
<p>This is the key architectural decision. Both approaches run on EKS, but they have fundamentally different characteristics.</p>
<h3>EKS with Fargate: The Serverless Promise</h3>
<p>Fargate abstracts away nodes entirely. You define pod specs, and AWS handles the compute.</p>
<p><strong>How Fargate works for GitLab Runners:</strong></p>
<pre><code class="language-plaintext">Job triggered → GitLab Runner creates pod → Fargate provisions microVM → Job runs → Pod terminates
</code></pre>
<p><strong>Fargate Advantages:</strong></p>
<ul>
<li><p>Zero node management</p>
</li>
<li><p>Per-second billing</p>
</li>
<li><p>Strong isolation (each pod is a separate microVM)</p>
</li>
<li><p>No capacity planning</p>
</li>
<li><p>Automatic security patching</p>
</li>
</ul>
<p><strong>Fargate Disadvantages for CI/CD:</strong></p>
<ul>
<li><p><strong>Premium pricing</strong>: 20-40% more expensive than on-demand EC2, 5x more than Spot</p>
</li>
<li><p><strong>No Spot support</strong>: Can't use Spot instances with Fargate (as of 2024)</p>
</li>
<li><p><strong>Cold starts</strong>: Every job spins up a new microVM (30-60 seconds)</p>
</li>
<li><p><strong>No node reuse</strong>: Can't cache Docker layers, npm packages, or Maven artifacts on disk</p>
</li>
<li><p><strong>Resource limits</strong>: 4 vCPU / 30GB memory maximum per pod</p>
</li>
<li><p><strong>No DaemonSets</strong>: Can't run node-level agents (though less relevant for CI/CD)</p>
</li>
</ul>
<h3>EKS Auto Mode with Spot: Managed Karpenter</h3>
<p>Auto Mode gives you the operational simplicity of Fargate with the flexibility and cost of EC2.</p>
<p><strong>How Auto Mode works for GitLab Runners:</strong></p>
<pre><code class="language-plaintext">Job triggered → GitLab Runner creates pod → Karpenter provisions Spot node (if needed) → Job runs on shared node → Pod terminates → Node consolidates when empty
</code></pre>
<p><strong>Auto Mode Advantages:</strong></p>
<ul>
<li><p><strong>Spot pricing</strong>: 60-70% cheaper than on-demand, 80%+ cheaper than Fargate</p>
</li>
<li><p><strong>Intelligent provisioning</strong>: Karpenter selects optimal instance types in seconds</p>
</li>
<li><p><strong>Node reuse</strong>: Multiple jobs share warm nodes—no cold starts</p>
</li>
<li><p><strong>Diverse instance pools</strong>: Specify many instance types for Spot availability</p>
</li>
<li><p><strong>Managed Karpenter</strong>: AWS handles installation, upgrades, and security patches</p>
</li>
<li><p><strong>Consolidation</strong>: Automatically terminates unused nodes</p>
</li>
</ul>
<p><strong>Auto Mode Considerations:</strong></p>
<ul>
<li><p><strong>Spot interruptions</strong>: 2-minute warning when instances are reclaimed (mitigated by diverse instance types)</p>
</li>
<li><p><strong>Some node awareness needed</strong>: You configure NodePools, though it's simpler than managing node groups</p>
</li>
<li><p><strong>Shared security model</strong>: Multiple pods share a node (use Pod Security Standards)</p>
</li>
</ul>
<h3>Cost Comparison: Real Numbers</h3>
<p>For our workload (~100 CI jobs/day, average 15 minutes each):</p>
<table>
<thead>
<tr>
<th>Approach</th>
<th>Monthly Compute Cost</th>
<th>Explanation</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Fargate</strong></td>
<td>~$143</td>
<td>Premium per-second billing, every job is a new microVM</td>
</tr>
<tr>
<td><strong>On-Demand EC2</strong></td>
<td>~$85</td>
<td>Better bin-packing, but still paying full price</td>
</tr>
<tr>
<td><strong>Spot EC2</strong></td>
<td>~$45</td>
<td>60-70% discount, excellent bin-packing with Karpenter</td>
</tr>
<tr>
<td><strong>Reserved</strong></td>
<td>~$55</td>
<td>Requires commitment, doesn't scale to zero</td>
</tr>
</tbody></table>
<p>Spot on Auto Mode delivered <strong>68% compute savings</strong> compared to Fargate.</p>
<h3>When to Choose Each</h3>
<p><strong>Choose Fargate when:</strong></p>
<ul>
<li><p>Security requires microVM isolation per job</p>
</li>
<li><p>Workloads are unpredictable or very low volume</p>
</li>
<li><p>Team lacks Kubernetes experience</p>
</li>
<li><p>Simplicity is worth the cost premium</p>
</li>
</ul>
<p><strong>Choose Auto Mode with Spot when:</strong></p>
<ul>
<li><p>Cost optimization is a priority</p>
</li>
<li><p>You run enough jobs to benefit from node reuse</p>
</li>
<li><p>You can tolerate occasional Spot interruptions (GitLab retries automatically)</p>
</li>
<li><p>Team is comfortable with basic Kubernetes concepts</p>
</li>
</ul>
<hr />
<h2>The Solution: EKS Auto Mode with Spot Instances</h2>
<p>In late 2024, AWS announced EKS Auto Mode—a fully managed experience that handles node provisioning, scaling, and lifecycle management automatically. Unlike traditional EKS where you manage node groups or install Karpenter yourself, Auto Mode includes:</p>
<ul>
<li><p><strong>Built-in Karpenter</strong>: AWS manages the Karpenter installation and upgrades</p>
</li>
<li><p><strong>Managed node classes</strong>: Pre-configured, secure node templates</p>
</li>
<li><p><strong>Automatic scaling</strong>: Nodes spin up in seconds when pods are pending</p>
</li>
<li><p><strong>Spot instance support</strong>: Native integration with EC2 Spot for massive cost savings</p>
</li>
</ul>
<p>This was exactly what we needed—the operational simplicity approaching Fargate with the cost efficiency of EC2 Spot.</p>
<hr />
<h2>Why CDK Instead of CLI Commands?</h2>
<p>AWS published an excellent blog post titled <em>"Streamline your containerized CI/CD with GitLab Runners and Amazon EKS Auto Mode"</em> that walks through setting up GitLab Runners on Auto Mode using CLI commands. It's a great tutorial for getting started.</p>
<p>However, for production infrastructure, we chose AWS CDK (Infrastructure as Code) instead. Here's why:</p>
<h3>1. Reproducibility</h3>
<pre><code class="language-bash"># CLI approach - hope you documented everything
eksctl create cluster --name gitlab-runners --version 1.34 ...
kubectl apply -f nodepool.yaml
helm install gitlab-runner ...
</code></pre>
<p>vs.</p>
<pre><code class="language-typescript">// CDK approach - the code IS the documentation
const cluster = new eks.Cluster(this, 'GitLabRunners', {
  version: eks.KubernetesVersion.V1_34,
  defaultCapacityType: eks.DefaultCapacityType.AUTOMODE,
  // Every configuration decision is captured here
});
</code></pre>
<p>With CDK, our entire cluster configuration is version-controlled. Six months from now, when someone asks "why did we configure Karpenter consolidation this way?", the answer is in the Git history.</p>
<h3>2. Multi-Environment Consistency</h3>
<p>We run separate clusters for development, staging, and production CI/CD. CDK lets us define the infrastructure once and deploy it consistently:</p>
<pre><code class="language-typescript">// Same stack, different environments
new GitLabRunnersStack(app, 'Dev', { env: 'development' });
new GitLabRunnersStack(app, 'Prod', { env: 'production' });
</code></pre>
<p>With CLI commands, you're copying and pasting between environments, inevitably introducing drift.</p>
<h3>3. Dependency Management</h3>
<p>Our GitLab Runner stack depends on:</p>
<ul>
<li><p>An existing VPC with specific subnets</p>
</li>
<li><p>IAM roles with IRSA (IAM Roles for Service Accounts)</p>
</li>
<li><p>S3 buckets for build cache</p>
</li>
<li><p>Secrets Manager for runner tokens</p>
</li>
</ul>
<p>CDK handles these dependencies elegantly:</p>
<pre><code class="language-typescript">const cacheBucket = new s3.Bucket(this, 'RunnerCache', {
  lifecycleRules: [{ expiration: cdk.Duration.days(30) }],
});

const runnerRole = new iam.Role(this, 'RunnerRole', {
  assumedBy: new iam.FederatedPrincipal(
    cluster.openIdConnectProvider.openIdConnectProviderArn,
    // IRSA trust policy automatically configured
  ),
});

runnerRole.addToPolicy(new iam.PolicyStatement({
  actions: ['s3:GetObject', 's3:PutObject'],
  resources: [cacheBucket.arnForObjects('*')],
}));
</code></pre>
<h3>4. Safer Updates</h3>
<p>When we needed to change the Karpenter NodePool configuration (more on that later), CDK gave us:</p>
<ul>
<li><p><code>cdk diff</code> to preview changes before applying</p>
</li>
<li><p>CloudFormation rollback if something went wrong</p>
</li>
<li><p>A clear audit trail of what changed and when</p>
</li>
</ul>
<h3>5. Integration with Existing Infrastructure</h3>
<p>Our CDK codebase already manages VPCs, databases, and other AWS resources. Adding the GitLab Runners stack meant it automatically inherited:</p>
<ul>
<li><p>Consistent tagging policies</p>
</li>
<li><p>Security group rules</p>
</li>
<li><p>Monitoring and alerting configuration</p>
</li>
<li><p>Cost allocation tags</p>
</li>
</ul>
<hr />
<h2>The Implementation</h2>
<h3>Cluster Configuration</h3>
<pre><code class="language-typescript">const cluster = new eks.Cluster(this, 'GitLabRunnersCluster', {
  clusterName: `gitlab-runners-${environment}`,
  version: eks.KubernetesVersion.V1_34,
  kubectlLayer: new KubectlV34Layer(this, 'KubectlLayer'),
  
  // This is the magic - Auto Mode handles everything
  defaultCapacityType: eks.DefaultCapacityType.AUTOMODE,
  
  // Use existing VPC
  vpc: ec2.Vpc.fromLookup(this, 'Vpc', { vpcId: config.vpcId }),
  vpcSubnets: [{ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }],
  
  // Enable cluster logging for debugging
  clusterLogging: [
    eks.ClusterLoggingTypes.API,
    eks.ClusterLoggingTypes.AUDIT,
    eks.ClusterLoggingTypes.SCHEDULER,
  ],
});
</code></pre>
<h3>NodePool for Spot Instances</h3>
<p>The key to cost savings is the Karpenter NodePool configuration:</p>
<pre><code class="language-typescript">const spotNodePool = new eks.KubernetesManifest(this, 'SpotNodePool', {
  cluster,
  manifest: [{
    apiVersion: 'karpenter.sh/v1',
    kind: 'NodePool',
    metadata: { name: 'gitlab-spot' },
    spec: {
      template: {
        metadata: {
          labels: { 'node-type': 'spot' }
        },
        spec: {
          nodeClassRef: {
            group: 'eks.amazonaws.com',
            kind: 'NodeClass',
            name: 'default'  // Use Auto Mode's managed node class
          },
          requirements: [
            {
              key: 'node.kubernetes.io/instance-type',
              operator: 'In',
              values: [
                // Diverse instance types for spot availability
                'm6a.large', 'm6a.xlarge',
                'm7a.large', 'm7a.xlarge',
                'c6a.large', 'c6a.xlarge',
                'c7a.large', 'c7a.xlarge',
              ]
            },
            {
              key: 'karpenter.sh/capacity-type',
              operator: 'In',
              values: ['spot']  // Spot instances only
            },
          ],
        }
      },
      disruption: {
        consolidationPolicy: 'WhenEmpty',
        consolidateAfter: '5m',
        budgets: [{ nodes: '100%' }]  // Allow consolidation of empty nodes
      }
    }
  }]
});
</code></pre>
<h3>GitLab Runner Helm Chart</h3>
<pre><code class="language-typescript">const gitlabRunner = cluster.addHelmChart('GitLabRunner', {
  chart: 'gitlab-runner',
  repository: 'https://charts.gitlab.io',
  namespace: 'gitlab',
  values: {
    gitlabUrl: 'https://gitlab.com',
    runnerToken: runnerToken.secretValue.unsafeUnwrap(),
    concurrent: 20,
    runners: {
      config: `
        [[runners]]
          executor = "kubernetes"
          [runners.kubernetes]
            namespace = "gitlab"
            cpu_request = "2"
            memory_request = "4Gi"
            [runners.kubernetes.node_selector]
              node-type = "spot"
            [runners.kubernetes.pod_annotations]
              karpenter.sh/do-not-disrupt = "true"
      `
    }
  }
});
</code></pre>
<hr />
<h2>The Results</h2>
<p>After migrating and decommissioning the old Fargate cluster:</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Before (Fargate)</th>
<th>After (Auto Mode)</th>
<th>Change</th>
</tr>
</thead>
<tbody><tr>
<td>Monthly compute</td>
<td>$143</td>
<td>~$45</td>
<td><strong>-68%</strong></td>
</tr>
<tr>
<td>Total monthly</td>
<td>$385</td>
<td>~$230</td>
<td><strong>-40%</strong></td>
</tr>
<tr>
<td>Annual savings</td>
<td>-</td>
<td>~$1,860</td>
<td>-</td>
</tr>
</tbody></table>
<p>The dramatic compute savings come from:</p>
<ol>
<li><p><strong>Spot pricing</strong>: 60-70% cheaper than on-demand, and 80%+ cheaper than Fargate</p>
</li>
<li><p><strong>Efficient bin-packing</strong>: Multiple CI jobs share the same node</p>
</li>
<li><p><strong>Right-sized instances</strong>: Karpenter picks the optimal instance type for pending pods</p>
</li>
</ol>
<hr />
<h2>Lessons Learned (The Hard Way)</h2>
<h3>Lesson 1: Understanding Karpenter Disruption Budgets</h3>
<p>Our first deployment used settings designed to prevent job disruption:</p>
<pre><code class="language-yaml">disruption:
  consolidationPolicy: WhenEmpty
  consolidateAfter: 5m
  budgets:
    - nodes: '0'  # WRONG - this blocks ALL consolidation!
</code></pre>
<p>We thought <code>nodes: '0'</code> meant "don't evict nodes with running pods." What it actually means is "you can disrupt ZERO nodes at any time"—which completely blocks consolidation, even for empty nodes.</p>
<p><strong>The symptom</strong>: After CI jobs completed, nodes with only DaemonSet pods (CloudWatch agent, GuardDuty agent) would never terminate. We ended up with 14 orphaned nodes running indefinitely, burning money.</p>
<p><strong>The fix</strong>: Use <code>nodes: '100%'</code> to allow Karpenter to consolidate empty nodes:</p>
<pre><code class="language-yaml">disruption:
  consolidationPolicy: WhenEmpty      # Only consolidate when NO workload pods
  consolidateAfter: 5m                # Wait 5 minutes after becoming empty
  budgets:
    - nodes: '100%'                   # Allow consolidation of all empty nodes
</code></pre>
<p><strong>How job protection actually works:</strong></p>
<ul>
<li><p><code>WhenEmpty</code> policy: Karpenter ignores DaemonSet pods when determining if a node is "empty"—a node with only CloudWatch/GuardDuty agents IS considered empty</p>
</li>
<li><p><code>do-not-disrupt</code> annotation: Pods with this annotation prevent their node from being consolidated</p>
</li>
<li><p>Combined effect: Running CI jobs are protected by the annotation, but nodes scale down within 5 minutes of jobs completing</p>
</li>
</ul>
<h3>Lesson 2: Add the <code>do-not-disrupt</code> Annotation</h3>
<p>For explicit job protection, annotate your job pods:</p>
<pre><code class="language-yaml">[runners.kubernetes.pod_annotations]
  karpenter.sh/do-not-disrupt = "true"
</code></pre>
<p>This tells Karpenter: "Never consolidate a node while this pod is running." When the CI job completes and the pod terminates, the annotation goes with it, and the node becomes eligible for consolidation.</p>
<h3>Lesson 3: Avoid Burstable Instances for CI/CD Job Pods</h3>
<p>We initially included t3/t3a instances in our NodePool for <strong>CI job pods</strong>. Bad idea.</p>
<p>CI/CD workloads (especially Maven/Gradle builds) are CPU-intensive and will exhaust the burst credits quickly. Once credits are gone, you're throttled to 20% baseline CPU, and your 10-minute build becomes a 50-minute build.</p>
<p><strong>Use m-series or c-series instances</strong> that provide consistent CPU performance for job pods:</p>
<pre><code class="language-yaml">requirements:
  - key: 'node.kubernetes.io/instance-type'
    operator: In
    values:
      # Good: Fixed-performance instances for CI job pods
      - 'm6a.large', 'm6a.xlarge'
      - 'c6a.large', 'c6a.xlarge'
      # Bad: Burstable instances for CI jobs (avoid these)
      # - 't3.large', 't3a.xlarge'  # DON'T USE for job pods
</code></pre>
<blockquote>
<p><strong>Note</strong>: This advice applies to the <strong>Spot NodePool</strong> where CI jobs run. We <em>do</em> use a <code>t3a.medium</code> for the GitLab Runner manager itself—the lightweight, always-on process that coordinates jobs. The runner manager barely uses any CPU (it's just polling GitLab for jobs and creating pods), so it stays well within its burst credits. In fact, we downsized the runner manager from a <code>c7a.medium</code> to a <code>t3a.medium</code> during the migration, saving an additional ~$40/month on that single instance alone. The key distinction: <strong>burstable is fine for control plane workloads, not for compute-heavy CI jobs.</strong></p>
</blockquote>
<h3>Lesson 4: Node Labels Must Match Job Selectors</h3>
<p>The Auto Mode NodePool must include labels that match your job pod's <code>nodeSelector</code>:</p>
<pre><code class="language-yaml"># NodePool template
metadata:
  labels:
    node-type: spot  # Must match...

# GitLab Runner config
[runners.kubernetes.node_selector]
  node-type = "spot"  # ...this selector
</code></pre>
<p>If these don't match, Karpenter won't provision nodes for your jobs—pods will stay pending forever.</p>
<h3>Lesson 5: Diverse Instance Types for Spot Availability</h3>
<p>Don't just specify one or two instance types. Spot capacity varies by instance type and availability zone. More options = higher chance of getting capacity:</p>
<pre><code class="language-yaml">values:
  - 'm6a.large', 'm6a.xlarge', 'm6a.2xlarge'
  - 'm7a.large', 'm7a.xlarge', 'm7a.2xlarge'
  - 'c6a.large', 'c6a.xlarge', 'c6a.2xlarge'
  - 'c7a.large', 'c7a.xlarge', 'c7a.2xlarge'
</code></pre>
<p>Karpenter will automatically select from available capacity at the best price.</p>
<hr />
<h2>Drawbacks and Considerations</h2>
<p>No solution is perfect. Here are the trade-offs:</p>
<h3>1. Spot Interruptions</h3>
<p>Spot instances can be reclaimed with 2 minutes notice. For CI/CD:</p>
<ul>
<li><p><strong>Mitigated by</strong>: Diverse instance types (Karpenter will find alternatives)</p>
</li>
<li><p><strong>Mitigated by</strong>: Most CI jobs are under 30 minutes</p>
</li>
<li><p><strong>Mitigated by</strong>: GitLab automatically retries failed jobs</p>
</li>
<li><p><strong>Reality</strong>: We've seen &lt;5% interruption rate in eu-west-1</p>
</li>
</ul>
<h3>2. Cold Start Latency</h3>
<p>New nodes take 45-90 seconds to provision vs. 30-60 seconds for Fargate pods. In practice, this is negligible because:</p>
<ul>
<li><p>Karpenter keeps nodes running for <code>consolidateAfter</code> duration</p>
</li>
<li><p>Subsequent jobs reuse warm nodes</p>
</li>
<li><p>Only the first job after idle periods sees the delay</p>
</li>
</ul>
<h3>3. Increased Complexity</h3>
<p>Auto Mode is simpler than self-managed Karpenter, but still more complex than Fargate:</p>
<ul>
<li><p>You need to understand NodePools and disruption settings</p>
</li>
<li><p>Debugging requires node-level visibility occasionally</p>
</li>
<li><p>More configuration knobs to tune</p>
</li>
</ul>
<h3>4. Cost Visibility</h3>
<p>With Fargate, costs are per-pod and easy to attribute. With shared nodes, cost allocation becomes fuzzier. AWS Cost Explorer shows EC2 costs, but not which CI jobs caused them.</p>
<h3>5. Shared Node Security</h3>
<p>Multiple CI jobs share the same node. If you run untrusted code:</p>
<ul>
<li><p>Use Pod Security Standards (restricted profile)</p>
</li>
<li><p>Consider separate NodePools for different trust levels</p>
</li>
<li><p>Or stick with Fargate for strict isolation</p>
</li>
</ul>
<hr />
<h2>When to Stick with Fargate</h2>
<p>Despite our migration, Fargate is still the right choice for:</p>
<ul>
<li><p><strong>Low-volume CI/CD</strong>: If you run &lt;10 pipelines/day, Fargate's simplicity wins</p>
</li>
<li><p><strong>Strict isolation requirements</strong>: Each Fargate pod is a separate microVM</p>
</li>
<li><p><strong>Unpredictable workloads</strong>: Fargate scales to zero perfectly</p>
</li>
<li><p><strong>Teams without Kubernetes expertise</strong>: Fargate abstracts more complexity</p>
</li>
<li><p><strong>Running untrusted code</strong>: MicroVM isolation is stronger than pod isolation</p>
</li>
</ul>
<hr />
<h2>Conclusion</h2>
<p>Migrating from EKS Fargate to EKS Auto Mode with Spot instances reduced our CI/CD infrastructure costs by 40%. The key enablers were:</p>
<ol>
<li><p><strong>EKS Auto Mode</strong>: Managed Karpenter without the operational burden</p>
</li>
<li><p><strong>Spot instances</strong>: 70% cheaper than on-demand, 80%+ cheaper than Fargate</p>
</li>
<li><p><strong>CDK Infrastructure as Code</strong>: Reproducible, version-controlled, and safe to update</p>
</li>
</ol>
<p>The migration wasn't without challenges—understanding Karpenter's disruption budgets took some debugging. But with the right configuration, we now have a cost-effective, reliable CI/CD infrastructure that scales automatically.</p>
<p><strong>Key takeaways:</strong></p>
<ul>
<li><p>Use <code>WhenEmpty</code> with <code>budgets: [{ nodes: '100%' }]</code> for proper consolidation</p>
</li>
<li><p>Add <code>do-not-disrupt</code> annotations to protect running jobs</p>
</li>
<li><p>Avoid burstable instances (t3/t3a) for CPU-intensive CI workloads</p>
</li>
<li><p>Specify diverse instance types for Spot availability</p>
</li>
</ul>
<p>If your Fargate bill is growing and you're comfortable with Kubernetes, EKS Auto Mode is worth serious consideration. The 40% savings we achieved compound quickly, and the operational overhead is minimal thanks to AWS managing the hard parts.</p>
<hr />
<p><em>Have questions about this migration? Found a better approach? I'd love to hear from you.</em></p>
<hr />
<h2>Resources</h2>
<ul>
<li><p><a href="https://aws.amazon.com/blogs/containers/streamline-your-containerized-ci-cd-with-gitlab-runners-and-amazon-eks-auto-mode/">AWS Blog: Streamline your containerized CI/CD with GitLab Runners and Amazon EKS Auto Mode</a></p>
</li>
<li><p><a href="https://docs.aws.amazon.com/eks/latest/userguide/automode.html">EKS Auto Mode Documentation</a></p>
</li>
<li><p><a href="https://karpenter.sh/docs/concepts/disruption/">Karpenter Disruption Documentation</a></p>
</li>
<li><p><a href="https://docs.gitlab.com/runner/executors/kubernetes/">GitLab Runner Kubernetes Executor</a></p>
</li>
<li><p><a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_eks-readme.html">AWS CDK EKS Module</a></p>
</li>
<li><p><a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-best-practices.html">EC2 Spot Best Practices</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[I'm Now an AWS Community Builder — Here's What That Means]]></title><description><![CDATA[A few days ago, I received an email I'd been hoping for: I've been accepted into the AWS Community Builders program under the Dev Tools category.
In this post, I want to share what the program is, why]]></description><link>https://thepawan.dev/i-m-now-an-aws-community-builder-here-s-what-that-means</link><guid isPermaLink="true">https://thepawan.dev/i-m-now-an-aws-community-builder-here-s-what-that-means</guid><category><![CDATA[AWS]]></category><category><![CDATA[AWS Community Builder]]></category><category><![CDATA[DevSecOps]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[PCI DSS]]></category><category><![CDATA[Cloud Computing]]></category><dc:creator><![CDATA[Pawan Sawalani]]></dc:creator><pubDate>Fri, 06 Mar 2026 10:36:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67db3328954e01d80347c10f/5a975dcf-1156-40aa-a08e-99642b67a72b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A few days ago, I received an email I'd been hoping for: I've been accepted into the <strong>AWS Community Builders</strong> program under the <strong>Dev Tools</strong> category.</p>
<p>In this post, I want to share what the program is, why I applied, and what I plan to contribute over the coming year.</p>
<h2>What is the AWS Community Builders Program?</h2>
<p>For those unfamiliar, AWS Community Builders is an invite-only program by Amazon Web Services that connects cloud enthusiasts, content creators, and practitioners with AWS product teams and other community members. It's designed for people who are actively building on AWS and sharing their knowledge — through blogs, talks, open source contributions, or community engagement.</p>
<p>As a member, you get access to private Slack channels with AWS engineers and product teams, early visibility into upcoming services and features (under NDA), AWS credits, mentorship, and a community of like-minded builders from around the world.</p>
<h2>Why I Applied</h2>
<p>I've been building on AWS for nearly a decade now. What started as setting up basic EC2 instances has evolved into designing and managing multi-account AWS organisations — complete with PCI DSS/NIST/ISO/SOC2 compliance, and a security-first DevSecOps culture.</p>
<p>For most of that journey, I was heads-down building. But over the past year or two, I've started writing more — documenting the things I've learned, the decisions I've made, and the problems I've solved. This blog has been a big part of that shift.</p>
<p>When I came across the Community Builders application, it felt like a natural next step. I wanted to move from building in isolation to building in the open — learning from others, getting feedback, and contributing back to a community that's given me so much through the years (blog posts, re:Invent talks, open source tools, and countless Stack Overflow answers).</p>
<h2>What I Plan to Share</h2>
<p>Being part of the Community Builders program is a commitment to keep sharing and learning. Here are the areas I'm most excited to write and talk about:</p>
<h3>Containerisation Journeys</h3>
<p>I've been deeply involved in migrating workloads from EC2 to containers. This involves evaluating ECS vs. EKS, designing Helm charts, building reusable CI/CD components for container workflows, setting up preview environments, and figuring out observability with CloudWatch Application Signals and Container Insights. There's no shortage of real-world lessons to share here.</p>
<h3>Security-First Cloud Architecture</h3>
<p>Building for regulated industries means every architectural decision has a compliance dimension. I want to share practical patterns for achieving PCI DSS/ISO/NIST/SOC2 compliance on AWS — not just the theory, but the actual implementation details that are hard to find in documentation.</p>
<h3>AI-Powered DevSecOps</h3>
<p>This is where things get really interesting. I've been building AI agents that automate parts of the DevSecOps workflow — from vulnerability triage and remediation suggestions, to compliance monitoring, to intelligent pipeline analysis. The combination of AWS Bedrock, LangGraph, and MCP servers opens up powerful possibilities.</p>
<h3>Kubernetes in Production</h3>
<p>I have operated EKS clusters across multiple environments. From cluster upgrades and Karpenter autoscaling to running CI/CD runners on EKS with Spot instances (achieving significant cost savings), there's a lot of practical Kubernetes content I plan to share.</p>
<h2>Looking Ahead</h2>
<p>I'm genuinely excited about this. The AWS Community Builders program isn't just a badge — it's an opportunity to connect with people who care about the same things I do: building reliable, secure, well-architected systems on the cloud.</p>
<p>If any of these topics resonate with you, I'd love to connect. You can find me here on the blog, on <a href="https://www.linkedin.com/in/pawan-sawalani-b59129a4/">LinkedIn</a>, or reach out directly.</p>
<p>Here's to a great year of building, sharing, and learning together.</p>
<p><em>— Pawan</em></p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Level Up Your Cloud Security: My Playbook for DevSecOps Acceleration with AWS LZA]]></title><description><![CDATA[Introduction: The Quest for Secure and Agile Cloud Operations
Let's be honest, scaling cloud operations is exciting, but keeping everything secure and agile as you grow? That’s where the real challenge begins. Juggling multiple AWS accounts, ensuring...]]></description><link>https://thepawan.dev/level-up-your-cloud-security-my-playbook-for-devsecops-acceleration-with-aws-lza</link><guid isPermaLink="true">https://thepawan.dev/level-up-your-cloud-security-my-playbook-for-devsecops-acceleration-with-aws-lza</guid><category><![CDATA[AWS]]></category><category><![CDATA[DevSecOps]]></category><category><![CDATA[cloud security]]></category><category><![CDATA[automation]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Pawan Sawalani]]></dc:creator><pubDate>Tue, 13 May 2025 19:53:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747165724582/a495e37a-cad7-4288-ae3b-6b82777be991.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction-the-quest-for-secure-and-agile-cloud-operations">Introduction: The Quest for Secure and Agile Cloud Operations</h2>
<p>Let's be honest, scaling cloud operations is exciting, but keeping everything secure and agile as you grow? That’s where the real challenge begins. Juggling multiple AWS accounts, ensuring consistent security policies, and empowering developers without opening Pandora's Box – it’s a familiar story for many of us in the tech trenches.</p>
<p>In our organization, we hit a point where the sheer complexity of managing our expanding AWS footprint was becoming a bottleneck. We were grappling with ensuring consistent security baselines across new projects and maintaining governance without stifling the very innovation the cloud promises. We needed a better way to establish a secure foundation, one that could keep pace with our DevSecOps ambitions. This wasn't just about adding more tools; it was about fundamentally rethinking our approach to cloud platform management. The ad-hoc solutions and manual interventions that worked for a handful of accounts were clearly not sustainable as we scaled. This realization pushed us to look for a more structured, automated, and inherently secure way to manage our AWS estate.</p>
<p>That's when we discovered the AWS Landing Zone Accelerator (LZA). And let me tell you, it wasn't just another tool; it was a pivotal shift in how we approached cloud governance and security. This blog post is my story – our story – of how LZA didn't just help us build a secure baseline, but how it became a powerful accelerator for our DevSecOps practices. We'll dive into what LZA is, the tangible benefits we've seen, and why I believe it's a critical enabler for any organization serious about secure, scalable cloud operations. The journey to LZA was driven by a clear need to move beyond reactive firefighting to proactive, strategic platform building.</p>
<p>Whether you're a cloud architect designing resilient infrastructures, a security engineer fortifying defenses, a developer aiming for faster, secure deployments, or just curious about taming cloud complexity, I think you'll find some valuable takeaways here. The challenges we faced are common, and the solutions LZA offers address fundamental aspects of cloud maturity.</p>
<h2 id="heading-the-multi-account-tightrope-why-managing-aws-at-scale-needs-a-safety-net">The Multi-Account Tightrope: Why Managing AWS at Scale Needs a Safety Net</h2>
<p>As your AWS footprint grows, so does the complexity. What starts as a manageable handful of accounts can quickly morph into a sprawling ecosystem. Without a robust strategy, you're walking a tightrope. The allure of agility and innovation that draws us to the cloud can be quickly hampered if the underlying management of that environment doesn't keep pace.</p>
<p>We certainly felt this pressure. One of the first major hurdles we encountered was <strong>inconsistent security postures</strong>. Each new account or project, often spun up with the best intentions to meet urgent business needs, risked becoming an island, potentially drifting from our organization's core security standards. Ensuring every team adhered to the same critical security configurations, like encryption standards or network access controls, became a constant, manual battle. This inconsistency wasn't just a theoretical risk; it translated into real vulnerabilities and increased our audit burden.  </p>
<p>Then there was the <strong>governance overhead</strong>. Manually enforcing governance policies, managing Identity and Access Management (IAM) at scale, and ensuring compliance across dozens of accounts? It’s a recipe for burnout and, worse, security gaps. Our central security and operations teams were stretched thin, trying to keep up with the demands of a rapidly expanding environment. The complexity of IAM, in particular, became a significant challenge, with the potential for over-privileged roles or inconsistent access patterns across accounts.  </p>
<p>This operational burden directly led to <strong>slow provisioning and innovation drag</strong>. The time it took to provision new, secure environments for development teams started to hinder our agility. What should have been a quick turnaround to support a new initiative often involved lengthy manual setup and verification processes. Instead of accelerating innovation, our foundational setup was becoming a drag, a source of frustration for developers eager to build and deploy.  </p>
<p>Now, don't get me wrong, a multi-account strategy is an AWS best practice for good reasons – resource isolation, security boundaries, simplified billing, and limiting the blast radius of any potential security incident are all crucial. We understood these benefits and were committed to them. But the advantages can quickly be overshadowed by the operational nightmare of managing it all without the right framework. The very structure designed to enhance security and organization can, ironically, introduce new complexities if not managed properly.  </p>
<p>Before LZA, we were investing significant engineering effort into simply maintaining the status quo, building custom scripts, and performing manual checks to keep our multi-account environment somewhat consistent. It felt like we were constantly playing catch-up, reacting to issues rather than proactively building a secure and scalable platform. This reactive mode is antithetical to a DevSecOps culture, which thrives on proactivity and automation. The time spent on these manual, foundational tasks was time not spent on embedding security deeper into our development lifecycles or exploring new ways to innovate securely. This realization was a key driver in our search for a more comprehensive solution.</p>
<h2 id="heading-enter-aws-landing-zone-accelerator-our-foundation-for-secure-innovation">Enter AWS Landing Zone Accelerator: Our Foundation for Secure Innovation</h2>
<p>So, what exactly is this AWS Landing Zone Accelerator or LZA? Think of it as an architectural blueprint and an automation engine, designed by AWS, to help you deploy a secure, resilient, and scalable multi-account AWS environment, fast. It’s not just about creating accounts; it’s about establishing a comprehensive cloud foundation aligned with AWS best practices and numerous global compliance frameworks, such as NIST, CMMC, and HIPAA, depending on the configuration. This alignment provides a significant head start for organizations in regulated industries.  </p>
<p>Several key characteristics define LZA and how it operates. Crucially, LZA is provided as an open-source project built using the AWS Cloud Development Kit (AWS CDK). This is a massive win because it means your entire foundational environment – networking, security services, account structures – is defined as code. This Infrastructure as Code (IaC) approach is fundamental to achieving automation, version control, and repeatability, which are cornerstones of modern cloud management and DevOps practices.  </p>
<p>It's often recommended to deploy AWS Control Tower as your foundational landing zone and then enhance it with LZA. AWS Control Tower provides an easy way to set up and govern a new, secure, multi-account AWS environment with baseline guardrails. LZA then builds upon this, offering a powerful, highly customizable solution across a vast array of AWS services (over 35, in fact!) for managing more complex environments and specific compliance needs. This layered approach allows organizations to start with Control Tower's simplicity and then graduate to LZA's advanced capabilities as their requirements evolve.  </p>
<p>You manage LZA through a simplified set of configuration files, typically written in YAML. These files allow you to define and manage various aspects of your multi-account environment, including foundational networking topology with Amazon Virtual Private Clouds (VPCs), AWS Transit Gateways, and AWS Network Firewall, as well as security services like AWS Config Managed Rules and AWS Security Hub. This configuration-driven approach abstracts away much of the underlying complexity, allowing for powerful customizations without necessarily requiring deep coding expertise for every adjustment.  </p>
<p>When we first deployed LZA in our organization, the immediate impact was profound. Suddenly, we had a robust, secure baseline established across our accounts, almost out-of-the-box. This wasn't just a minor improvement; it was a turning point for us. The consistency and pre-configured security controls, such as centralized logging, identity and access management configurations, and network security setups, gave us a level of confidence we hadn't had before. The ability to manage this foundation as code, using the AWS CDK, was the real 'aha!' moment for our engineering teams. It aligned perfectly with our DevOps mindset and immediately clicked. The transparency and control offered by an open-source, CDK-based solution meant we could understand, customize, and truly <em>own</em> our cloud foundation, rather than treating it as an opaque managed service.  </p>
<p>Beyond the technical achievements, we saw several key benefits:</p>
<ul>
<li><p><strong>Speed and Efficiency:</strong> Setting up new, secure accounts and environments went from weeks of manual toil to a streamlined, automated process. This dramatically reduced the labor overhead and lead time associated with onboarding new projects or teams.  </p>
</li>
<li><p><strong>Built-in Security &amp; Compliance:</strong> Knowing that our foundation was aligned with AWS Well-Architected principles and designed to support various compliance frameworks gave our security and Governance, Risk, and Compliance (GRC) teams immense peace of mind. LZA provides the foundational infrastructure from which additional complementary solutions can be integrated to meet specific compliance goals.  </p>
</li>
<li><p><strong>Scalability:</strong> LZA is built for scale. We knew that as we grew, our foundational governance and security would scale with us, not become a bottleneck. The architecture supports managing and governing a multi-account environment suitable for highly-regulated workloads and complex compliance requirements.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747165336539/f30f7f8a-ca35-45e6-8702-a816ae6b837f.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-lza-the-devsecops-supercharger">LZA: The DevSecOps Supercharger</h2>
<p>For us, LZA wasn't just about better infrastructure management; it was a direct catalyst for accelerating our DevSecOps adoption. DevSecOps, at its heart, is about integrating security into every phase of the development lifecycle, making it a shared responsibility across development, security, and operations teams. LZA provides the secure and automated playground for this to happen effectively. It addresses the foundational layer, ensuring that the environment where DevSecOps practices are applied is itself secure, consistent, and manageable. This allows teams to focus on application-level security and agile delivery, rather than constantly wrestling with the underlying infrastructure.  </p>
<h3 id="heading-a-shifting-security-left-effortlessly">A. Shifting Security Left, Effortlessly</h3>
<p>One of the core tenets of DevSecOps is "shifting security left" – addressing security concerns as early as possible in the development lifecycle, ideally from the moment developers start coding. LZA embodies this principle at the foundational level. Instead of bolting on security later or discovering misconfigurations in production, LZA provisions environments with pre-configured security services like AWS Security Hub, Amazon GuardDuty, AWS Config rules, AWS Network Firewall, and robust IAM policies from day one.  </p>
<p><strong>Our Experience:</strong> We found that LZA naturally pushed our security considerations earlier into the development lifecycle. Developers receive accounts that already have baseline security measures, detective controls, and preventative guardrails (like SCPs) in place. This significantly reduces the risk of insecure configurations slipping through due to oversight or lack of awareness. For example, network configurations are established with security in mind, and default IAM roles are designed with least privilege.  </p>
<p><strong>Impact:</strong> This proactive stance means fewer security vulnerabilities make it to later stages of development or, worse, into production. This saves us significant time and effort in remediation, reduces the cost of fixing bugs (which increases the later they are found), and ultimately lowers our risk profile. The platform itself becomes an enabler of secure development, rather than an obstacle.  </p>
<h3 id="heading-b-automation-as-a-force-multiplier">B. Automation as a Force Multiplier</h3>
<p>DevOps (and by extension, DevSecOps) thrives on automation. LZA brings extensive automation to the often-manual and error-prone process of setting up and managing a multi-account AWS foundation. Being built on AWS CDK, the entire LZA deployment and configuration update process can be managed through AWS CodePipeline. This means changes to your core infrastructure—like adding new security controls, modifying network routes, or updating SCPs—are deployed in a consistent, auditable, and repeatable manner.  </p>
<p><strong>Our Experience:</strong> The automation LZA brought to provisioning and managing our foundational environment freed up significant engineering time. Our platform team, which was previously bogged down in manual setup and troubleshooting, could now focus on higher-value tasks like developing new platform capabilities or supporting development teams more directly. Our development teams, in turn, received the resources they needed much faster, accelerating their own workflows.  </p>
<p><strong>Impact:</strong> This level of automation not only boosts speed but also drastically reduces the risk of human error – a common source of security misconfigurations. When the "right way" to configure something is the automated way, consistency and adherence to standards improve dramatically. This automated, stable base is critical for then building automated application security testing and deployment pipelines on top.  </p>
<h3 id="heading-c-security-as-code-in-practice">C. Security as Code in Practice</h3>
<p>The principle of "Security as Code" means treating your security configurations with the same rigor as your application code – versioning it, testing it, and automating its deployment. LZA makes this a reality for your cloud foundation. With LZA, security policies, IAM roles and permissions, network configurations (like VPCs and firewall rules), and compliance guardrails are defined in configuration files (YAML) and deployed via the AWS CDK. Even complex IAM setups, including federation with identity providers and granular permission sets, can be managed this way.  </p>
<p><strong>Our Experience:</strong> For us, being able to define and version our security posture as code was a huge win. It simplified audits immensely because the "as-is" state of our security configurations could be easily compared against the "to-be" state defined in code. It made rollbacks safer and more predictable if a change had unintended consequences. Most importantly, it fostered better collaboration between our security, operations, and even development teams because the "rules of the road" were clearly codified and accessible.</p>
<p><strong>Impact:</strong> This approach aligns perfectly with GitOps workflows, where the Git repository becomes the single source of truth for your infrastructure and security configuration. Changes go through pull requests, reviews, and automated pipeline deployments, bringing a new level of discipline and transparency to security management. This dramatically reduces configuration drift and enhances the overall auditability of the environment.  </p>
<h3 id="heading-d-centralized-governance-that-empowers-not-restricts">D. Centralized Governance that Empowers, Not Restricts</h3>
<p>Effective governance in a DevSecOps world isn't about locking everything down; it's about establishing clear guardrails that allow teams to innovate safely within well-defined boundaries. LZA provides the tools for this centralized governance. Through its deep integration with AWS Organizations, Service Control Policies (SCPs), and centralized logging and monitoring (via services like AWS CloudTrail, AWS Config, AWS Security Hub, and Amazon GuardDuty), LZA gives you a holistic view and control over your entire AWS environment.  </p>
<p><strong>Our Experience:</strong> We use LZA-managed SCPs to enforce critical security boundaries—for example, restricting the use of certain AWS Regions or denying access to specific services that don't align with our security policies. This is done at the organizational unit (OU) level, providing broad enforcement without micromanaging individual accounts. Centralized logging, with logs from all accounts aggregated into a dedicated Log Archive account, has also been invaluable for security monitoring, threat detection, and incident response.  </p>
<p><strong>Impact:</strong> This centralized approach ensures consistency and compliance across the organization, while still allowing development teams the autonomy they need within those well-defined boundaries. It’s about enabling speed with safety.Developers can experiment and deploy resources, confident that the foundational guardrails are in place to prevent egregious errors or policy violations. This "trust but verify" model, enabled by strong automated controls, is key to fostering agility in a DevSecOps context.  </p>
<p>The synergy of these elements—shifting security left for the platform, automating foundational controls, codifying security policies, and enabling intelligent governance—creates an environment where DevSecOps principles aren't just aspirations but are actively supported and reinforced by the underlying cloud infrastructure. This holistic impact is what truly accelerates DevSecOps maturity.</p>
<h2 id="heading-our-lza-journey-key-wins-and-real-world-impact">Our LZA Journey: Key Wins and Real-World Impact</h2>
<p>Beyond the general DevSecOps acceleration, I want to share some specific, tangible wins we experienced after implementing LZA. These are the results that really brought its value home for us, transforming how we operate and innovate on AWS.</p>
<p><strong>Drastic Reduction in Secure Environment Provisioning Time</strong> One of the most immediate and impactful wins was the dramatic reduction in time to provision new, secure development and test environments. What used to take our platform team days, sometimes even weeks, of manual configuration, cross-team approvals, and painstaking checks, now happens in a fraction of that time, fully automated. I'd estimate we cut down provisioning time by over 70% for a standard project environment. This wasn't just about speed; it was about consistency. Every new environment now adheres to our security baseline automatically, thanks to LZA's IaC approach. This agility has been a massive boost for our project teams, allowing them to get started on new initiatives much faster.  </p>
<p><strong>Enhanced Security Posture &amp; Compliance Readiness</strong> Our security team sleeps better at night, and I'm not exaggerating! The consistent application of security controls—like pre-configured Security Groups, Network ACLs, centralized AWS Network Firewalls, and integration with services like Amazon GuardDuty for threat detection and AWS Security Hub for a unified view of security alerts—has significantly improved our overall security posture. Furthermore, automated compliance checks via AWS Config rules, orchestrated by LZA, provide continuous monitoring against our defined standards. When audit season comes around, we're far more prepared because much of the evidence is automatically gathered, and our configurations are codified, versioned, and easily auditable. This has streamlined our interactions with auditors and reduced the stress associated with compliance reporting.  </p>
<p><strong>Developer Empowerment &amp; Increased Velocity</strong> Perhaps counterintuitively for a governance tool, LZA actually empowered our developers. By providing them with secure, pre-approved environments and clear, automated guardrails (through SCPs and detective controls), they could innovate faster without the constant fear of accidental misconfiguration or unintentional policy violation. The "safe sandbox" LZA creates has boosted their velocity and encouraged experimentation. They understand the boundaries, and within those boundaries, they have the freedom to operate. This has fostered a more positive relationship between development and security teams, as security is seen more as an enabler than a blocker.  </p>
<p><strong>A Lesson Learned or Pro-Tip:</strong> One lesson we learned is the importance of investing time upfront in understanding and customizing the LZA configuration files (the YAML files) to truly match your organization's specific needs. While the defaults provided by LZA are excellent and align with general best practices, tailoring aspects like your specific network design, OU structure, or fine-grained IAM permission sets early on pays huge dividends in the long run. Don't just deploy and forget; treat your LZA configuration as a living part of your infrastructure that you iterate on as your needs evolve. This iterative approach ensures the landing zone remains aligned with your business and technical requirements.  </p>
<p><strong>My Opinionated Stance:</strong> From my perspective, LZA isn't just a 'nice-to-have' for organizations serious about AWS; it's rapidly becoming a foundational necessity for anyone looking to scale securely and embrace DevSecOps. The initial learning curve is there, yes—understanding the configuration files and the CDK structure takes some effort. But the long-term benefits in terms of security, operational efficiency, governance, and developer enablement far outweigh that initial investment. The shift from manual, reactive management to an automated, proactive, code-driven approach to our cloud foundation has been a game-changer.</p>
<h2 id="heading-getting-your-hands-on-lza-its-more-accessible-than-you-think">Getting Your Hands on LZA: It's More Accessible Than You Think</h2>
<blockquote>
<p>GitHub repo - <a target="_blank" href="https://github.com/awslabs/landing-zone-accelerator-on-aws">https://github.com/awslabs/landing-zone-accelerator-on-aws</a></p>
</blockquote>
<p>If you're thinking this sounds powerful but perhaps overwhelmingly complex to implement, there's good news. You're likely not starting from absolute zero, especially if you're already using or considering AWS Control Tower.</p>
<p>As mentioned, LZA is designed to enhance an AWS Control Tower setup. Control Tower lays down the initial multi-account structure and baseline guardrails, providing a guided, user-friendly way to get started with a well-architected environment. LZA then comes in to add layers of advanced customization, more granular security controls, sophisticated networking configurations, and alignment with specific, often stringent, compliance frameworks. So, if you have Control Tower, you have a solid launching pad for LZA.  </p>
<p>A huge plus is that LZA is an open-source project, available on GitHub under the <code>awslabs</code> organization. You can find the code, explore how it works, see how it's structured, and understand the underlying automation. This transparency is invaluable. It means a community is building around it, sharing best practices, configurations, and solutions to common challenges. Being open source also means you're not locked into a proprietary black box; you have the ability to understand and, if necessary, adapt the solution.  </p>
<p>Because it's built on the AWS Cloud Development Kit (CDK), if your team has experience with common programming languages like TypeScript or Python (the primary languages supported by CDK), they can understand, manage, and even extend the LZA codebase. This is a significant advantage over purely template-based solutions (like raw CloudFormation) or GUI-driven configurations, as it allows you to apply software development best practices to your infrastructure management. This accessibility of the codebase can also help bridge the skill gap between infrastructure and development teams, fostering better collaboration.  </p>
<p>Ready to explore further? AWS provides extensive documentation, including an Implementation Guide, a Solution Overview, and sample configurations that can help you get started. The GitHub repository itself is a goldmine of information, containing not just the source code but also issue trackers where you can see ongoing development, community discussions, and known challenges. These resources can significantly flatten the learning curve.  </p>
<p>While LZA automates a tremendous amount, deploying and customizing it effectively does require an investment in learning and planning. It's not a magic button, but it <em>is</em> a powerful accelerator. You'll need to understand your organization's specific security, networking, and compliance requirements to tailor the LZA configuration files effectively. For us, the effort invested upfront in planning and understanding LZA's capabilities was well worth the outcome in terms of long-term stability, security, and operational efficiency. The move towards managing our foundational infrastructure as code with LZA has democratized access to what was previously a very complex and specialized domain, allowing more of our team to contribute to and understand our cloud platform.</p>
<h2 id="heading-conclusion-building-a-secure-agile-future-with-lza-powered-devsecops">Conclusion: Building a Secure, Agile Future with LZA-Powered DevSecOps</h2>
<p>Our journey with AWS Landing Zone Accelerator has been transformative. It provided the secure, automated, and governed foundation we desperately needed to scale our AWS environment effectively. More importantly, it has been a powerful catalyst for our DevSecOps maturity, enabling us to integrate security more deeply and efficiently into our cloud operations and development lifecycles.</p>
<p>By baking in security from the start, automating foundational configurations, enabling security as code, and providing robust centralized governance, LZA has allowed us to move faster, more securely, and with greater confidence. The shift from a reactive, often manual approach to a proactive, automated, and code-driven paradigm for our cloud foundation has unlocked new levels of agility and resilience. It has allowed us to focus more on innovation and less on the undifferentiated heavy lifting of managing a complex multi-account environment.</p>
<p>In today's cloud landscape, speed and security are not mutually exclusive – they are prerequisites for success. Tools like AWS LZA are vital in bridging that gap, turning complex challenges into manageable, automated processes. It exemplifies a broader industry trend towards codifying and automating all aspects of IT infrastructure, with security as an integral component from the outset.</p>
<p>If your organization is navigating the complexities of a multi-account AWS environment and striving to accelerate your DevSecOps adoption, I wholeheartedly recommend taking a serious look at the AWS Landing Zone Accelerator. It certainly changed the game for us, and I believe it can do the same for you. Start by exploring the AWS documentation and the GitHub repository – your future, more secure and agile cloud self will thank you. The initial investment in learning and configuration will pay dividends in the form of a more robust, compliant, and innovation-friendly cloud platform.</p>
]]></content:encoded></item><item><title><![CDATA[Automating AWS EC2 CloudWatch Agent Monitoring & Email Alerting with Lambda and CDK]]></title><description><![CDATA[Ensuring that the CloudWatch Agent is installed and running on all EC2 instances is crucial for complete observability. In this guide, we’ll explore why monitoring the CloudWatch Agent matters, why you should alert on missing/stopped agents, and how ...]]></description><link>https://thepawan.dev/automating-aws-ec2-cloudwatch-agent-monitoring-and-email-alerting-with-lambda-and-cdk</link><guid isPermaLink="true">https://thepawan.dev/automating-aws-ec2-cloudwatch-agent-monitoring-and-email-alerting-with-lambda-and-cdk</guid><category><![CDATA[AWS]]></category><category><![CDATA[aws lambda]]></category><category><![CDATA[aws-cdk]]></category><category><![CDATA[#CloudWatch]]></category><category><![CDATA[DevSecOps]]></category><dc:creator><![CDATA[Pawan Sawalani]]></dc:creator><pubDate>Fri, 02 May 2025 13:59:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746194272137/15497aa6-2c78-4b37-b718-5ac76e94f60b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ensuring that the <strong>CloudWatch Agent</strong> is installed and running on all EC2 instances is crucial for complete observability. In this guide, we’ll explore <strong>why monitoring the CloudWatch Agent matters</strong>, <strong>why you should alert on missing/stopped agents</strong>, and <strong>how to implement an automated checker</strong> using an AWS Lambda function. We’ll then walk through deploying the solution with <strong>AWS CDK (TypeScript)</strong> for a repeatable setup. The language will be friendly and the approach hands-on, so you can follow along easily.</p>
<h2 id="heading-why-cloudwatch-agent-monitoring-is-important-for-ec2"><strong>Why CloudWatch Agent Monitoring is Important for EC2</strong></h2>
<p>AWS EC2 instances by default only send a limited set of metrics to CloudWatch (CPU, network, etc.). The <strong>CloudWatch Agent</strong> extends this by collecting additional system metrics and logs from inside the instance . For example, the CloudWatch Agent can report <strong>memory usage, disk utilization, detailed OS metrics</strong>, and even custom application metrics, which are not available through the default EC2 monitoring . It also streams system logs (and custom log files) to CloudWatch Logs for centralized logging .</p>
<p>By running the CloudWatch Agent on your servers, you get a <strong>much more comprehensive view of instance health</strong>. Memory consumption, disk space, swap usage, and application logs are critical for diagnosing issues, and the CloudWatch Agent gathers these with minimal effort. In short, <strong>CloudWatch Agent monitoring is vital</strong> because it ensures that you’re not flying blind on important metrics and logs that go beyond the basic EC2 data.</p>
<h2 id="heading-why-you-need-alerts-for-missing-or-stopped-agents"><strong>Why You Need Alerts for Missing or Stopped Agents</strong></h2>
<p>If the CloudWatch Agent is <strong>missing or stopped</strong> on an EC2 instance, you effectively lose visibility into that instance’s detailed metrics and logs. This is a serious blind spot: imagine a scenario where an application is consuming all memory on a server, but you have no CloudWatch metrics or logs to alert you because the agent that collects them isn’t running. By the time you realize there’s a problem, it might be too late to prevent an outage.</p>
<p>Setting up <strong>alerts for CloudWatch Agent status</strong> ensures that you are notified as soon as an agent isn’t running when it should be. This proactive alerting allows your team to quickly remediate the issue (install or restart the agent) before it impacts monitoring or operations. It’s essentially <strong>monitoring your monitoring</strong> – a safety net that catches misconfigurations or failures in the telemetry pipeline. In real-world use, this kind of alert can save hours of troubleshooting during incidents, because you’ll immediately know if lack of metrics is due to an agent issue. It also helps maintain <strong>compliance</strong> with any internal policies that <strong>all instances must have monitoring active</strong>. The bottom line: if the CloudWatch Agent stops, you want to know right away so you can fix it and restore full visibility.</p>
<h2 id="heading-building-an-automated-cloudwatch-agent-status-checker-lambda-ssm"><strong>Building an Automated CloudWatch Agent Status Checker (Lambda + SSM)</strong></h2>
<p>To automatically detect and alert on CloudWatch Agent issues, we’ll build a <strong>Python AWS Lambda function</strong> that runs on a schedule. This function will use <strong>AWS Systems Manager (SSM)</strong> to remotely check each EC2 instance and verify the CloudWatch Agent’s status. If the agent is not installed or not running on any instance, the Lambda will send an <strong>email alert (via Amazon SES)</strong> in Markdown format summarizing the problem. Here’s how it works:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746193311231/16a21ef3-b41e-4d0c-a141-3b7e4705560c.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-discovering-ec2-instances-via-ssm"><strong>Discovering EC2 Instances via SSM</strong></h3>
<p>First, the Lambda function needs to know <strong>which EC2 instances to check</strong>. We leverage AWS Systems Manager for this, since SSM can enumerate instances that have the SSM Agent running. Using the SSM API describe_instance_information (or its boto3 equivalent), the Lambda can list all managed instances. We typically filter this to instances that are currently online with SSM (PingStatus = “Online”) . This ensures we target only instances that are up and have the SSM agent available to run commands. You could also filter by tags (for example, only check instances with a specific tag like Monitoring=true if you don’t want to cover every instance), but the key is that SSM gives us a reliable inventory of instances to probe.</p>
<p>In Python (boto3), this might look like:</p>
<pre><code class="lang-python">ssm = boto3.client(<span class="hljs-string">'ssm'</span>)
<span class="hljs-comment"># Get all online managed instances</span>
response = ssm.describe_instance_information(
    Filters=[{ <span class="hljs-string">'Key'</span>: <span class="hljs-string">'PingStatus'</span>, <span class="hljs-string">'Values'</span>: [<span class="hljs-string">'Online'</span>] }]
)
instances = [info[<span class="hljs-string">'InstanceId'</span>] <span class="hljs-keyword">for</span> info <span class="hljs-keyword">in</span> response[<span class="hljs-string">'InstanceInformationList'</span>]]
</code></pre>
<p>This collects the list of EC2 instance IDs that we will check. (We assume the SSM agent is installed on your instances – which is true for most modern AWS Linux/Windows AMIs – otherwise SSM can’t run commands on them.)</p>
<h3 id="heading-checking-cloudwatch-agent-status-with-ssm-run-command"><strong>Checking CloudWatch Agent Status with SSM Run Command</strong></h3>
<p>For each instance, the Lambda uses <strong>SSM Run Command</strong> to execute a pre-built document called <strong>“AmazonCloudWatch-ManageAgent”</strong>. AWS provides this Systems Manager document to manage the CloudWatch Agent (install, configure, or query its status). We’ll invoke it with the <strong>“status” action</strong>, which tells the agent to report its current status . In effect, this SSM command asks the CloudWatch Agent (via the SSM agent on the instance) whether it’s running, and if so, returns details like the running status and version.</p>
<p>The Lambda uses ssm.send_command for this. For example:</p>
<pre><code class="lang-python">cmd_response = ssm.send_command(
    InstanceIds=[instance_id],
    DocumentName=<span class="hljs-string">'AmazonCloudWatch-ManageAgent'</span>,
    Parameters={
        <span class="hljs-string">'action'</span>: [<span class="hljs-string">'status'</span>],
        <span class="hljs-string">'mode'</span>: [<span class="hljs-string">'ec2'</span>]
    }
)
command_id = cmd_response[<span class="hljs-string">'Command'</span>][<span class="hljs-string">'CommandId'</span>]
</code></pre>
<p>A few notes on this command: We specify the action as “status” and mode “ec2” (since these are EC2 instances, not on-premises). We target one instance at a time here by ID (you could target multiple in one command, but handling results is simpler per instance). The response gives us a CommandId which we’ll use to retrieve the execution output.</p>
<p>Under the hood, the <strong>AmazonCloudWatch-ManageAgent</strong> document will run the amazon-cloudwatch-agent-ctl command on the instance to get the agent status. If the CloudWatch Agent is running, the output will be a small JSON snippet indicating "status": "running" along with the start time and version . If the agent is <strong>stopped (not running)</strong>, the JSON will say "status": "stopped" . In cases where the agent is not installed at all, the SSM command might report an error or simply that the service isn’t running (which effectively is the same outcome — it’s not running). We’ll handle those cases as “not running” as well, since either way the instance isn’t being monitored by the agent.</p>
<h3 id="heading-retrieving-and-parsing-the-command-results"><strong>Retrieving and Parsing the Command Results</strong></h3>
<p>SSM Run Command is asynchronous, so after sending the command we need to retrieve the results. We use ssm.get_command_invocation with the Command ID and instance ID to get the output. One important detail here: the <strong>AmazonCloudWatch-ManageAgent</strong> document may consist of multiple steps/plugins internally, so we should specify the <strong>Plugin Name</strong> corresponding to the status action when fetching the results. Otherwise, the API might throw an “InvalidPluginName” error if it doesn’t know which step’s output to return . In our case, the plugin (step) name is “status” (since we invoked the status action).</p>
<p>So, the Lambda will do something like:</p>
<pre><code class="lang-python"><span class="hljs-comment"># (It’s a good practice to wait a few seconds or poll until the command is finished)</span>
result = ssm.get_command_invocation(
    CommandId=command_id,
    InstanceId=instance_id,
    PluginName=<span class="hljs-string">'status'</span>  <span class="hljs-comment"># specify the 'status' step output</span>
)
output_text = result.get(<span class="hljs-string">'StandardOutputContent'</span>, <span class="hljs-string">''</span>)
</code></pre>
<p>The StandardOutputContent will contain the JSON string output from the agent status command. For example, it might be:</p>
<pre><code class="lang-json">{ <span class="hljs-attr">"status"</span>: <span class="hljs-string">"running"</span>, <span class="hljs-attr">"starttime"</span>: <span class="hljs-string">"2025-04-01T12:00:00"</span>, <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.300257.0"</span> }
</code></pre>
<p>We parse this JSON in the Lambda (e.g., using Python’s json.loads) to easily inspect the fields:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> json
<span class="hljs-keyword">if</span> output_text:
    data = json.loads(output_text)
    agent_status = data.get(<span class="hljs-string">'status'</span>, <span class="hljs-string">'unknown'</span>)
<span class="hljs-keyword">else</span>:
    agent_status = <span class="hljs-string">'unknown'</span>
</code></pre>
<p>Now, for each instance we have agent_status which will be "running" if the CloudWatch Agent is OK. If the agent is stopped or not installed, we might get "stopped" or no output. We treat any status other than “running” as a problem that needs alerting. (If the SSM command itself failed to execute, we also consider that as the agent not running, since we couldn’t confirm it’s active.)</p>
<p>We can also grab the agent version from the output (the version field) if we want to include it in the report. This could be useful to see what version is running or if an outdated version might be an issue.</p>
<h3 id="heading-formatting-the-markdown-alert-and-sending-email-via-ses"><strong>Formatting the Markdown Alert and Sending Email via SES</strong></h3>
<p>Once the Lambda has checked all instances, it will compile a list of any instances that <strong>need attention</strong> (i.e., where the CloudWatch Agent is missing or stopped). The alert email will be composed in <strong>Markdown format</strong> for clarity. For example, the message body might look like:</p>
<ul>
<li><p><strong>i-0123456789abcdef (WebServer1)</strong> – CloudWatch Agent is <strong>STOPPED</strong> (not running)</p>
</li>
<li><p><strong>i-0fedcba9876543210 (DatabaseServer)</strong> – CloudWatch Agent is <strong>NOT INSTALLED</strong></p>
</li>
</ul>
<p>Each bullet highlights the instance (by ID and maybe Name tag if we fetch it via EC2 API for friendliness) and the issue. We use bold text and other Markdown features to make it easy to read in the email. In our Python code, we might assemble this as a string with newline-separated - list items.</p>
<p>Finally, the Lambda uses <strong>Amazon SES</strong> to send out the email. We can use the ses.send_email API, specifying the <strong>From address</strong> (which must be a verified SES identity) and the <strong>To address</strong>(es) for the recipients. We put our markdown-formatted message in the email body. Typically, we send it as a simple text email (many email clients won’t render Markdown, but the formatting ensures it’s still human-readable). Optionally, we could convert the Markdown to HTML and send an HTML email for nicer formatting, but that adds complexity – sending it as plain text Markdown is straightforward and effective.</p>
<p>For example:</p>
<pre><code class="lang-python">ses = boto3.client(<span class="hljs-string">'ses'</span>)
email_body = <span class="hljs-string">"## CloudWatch Agent Alert\nThe following instances have issues:\n"</span> + <span class="hljs-string">"\n"</span>.join(problem_lines)
ses.send_email(
    Source=ALERT_FROM_ADDRESS,
    Destination={<span class="hljs-string">'ToAddresses'</span>: [ALERT_TO_ADDRESS]},
    Message={
        <span class="hljs-string">'Subject'</span>: {<span class="hljs-string">'Data'</span>: <span class="hljs-string">'⚠️ AWS CloudWatch Agent Alert'</span>},
        <span class="hljs-string">'Body'</span>: {<span class="hljs-string">'Text'</span>: {<span class="hljs-string">'Data'</span>: email_body}}
    }
)
</code></pre>
<p>In the above snippet, problem_lines is a list of strings like the bullet points shown earlier. We included a warning emoji in the subject for visibility, and used a Markdown header “## CloudWatch Agent Alert” in the body as a title. You can customize the content as you see fit (include timestamps, agent versions, suggestions to reinstall, etc.).</p>
<p><strong>Note:</strong> Before the Lambda can actually send emails, you’ll need to <strong>verify the sender email (or domain) in SES</strong> and possibly the recipient as well (if your SES is in sandbox mode). We’ll touch on that in the deployment steps, but it’s an important prerequisite to avoid email delivery issues.</p>
<p>With the Lambda function logic explained, let’s move on to deploying this setup using AWS CDK for a clean, infrastructure-as-code deployment.</p>
<h2 id="heading-step-by-step-deployment-with-aws-cdk-typescript"><strong>Step-by-Step Deployment with AWS CDK (TypeScript)</strong></h2>
<p>We will use the AWS Cloud Development Kit (CDK) in TypeScript to deploy the Lambda function, its scheduling, and the necessary permissions. This allows us to define the entire stack in code and easily repeat it in different environments. Below are the main steps:</p>
<ol>
<li><p><strong>Define the Lambda Function and Code:</strong> In your CDK application, create a Lambda function resource. For example, use new lambda.Function(...) in your Stack, specifying the runtime (Python 3.x), handler (the entry point in your Python code), and code (pointing to the directory or file with your Lambda code). Include any necessary <strong>environment variables</strong> for the function. Common env vars might be the ALERT_EMAIL_TO (recipient address) and ALERT_EMAIL_FROM (sender address), and perhaps a filter tag for instances if you want that configurable. For instance:</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">const</span> monitorFn = <span class="hljs-keyword">new</span> lambda.Function(<span class="hljs-built_in">this</span>, <span class="hljs-string">'AgentMonitorFunction'</span>, {
   runtime: lambda.Runtime.PYTHON_3_9,
   handler: <span class="hljs-string">'index.handler'</span>,
   code: lambda.Code.fromAsset(path.join(__dirname, <span class="hljs-string">'../lambda'</span>)), <span class="hljs-comment">// your code directory</span>
   environment: {
     ALERT_EMAIL_TO: <span class="hljs-string">'ops-team@example.com'</span>,
     ALERT_EMAIL_FROM: <span class="hljs-string">'no-reply@mycompany.com'</span>
     <span class="hljs-comment">// ... any other configuration</span>
   }
 });
</code></pre>
<p> Make sure the ALERT_EMAIL_FROM is an address or domain verified in SES. (You can verify emails via the SES console or CLI; CDK won’t auto-verify it for you.)  </p>
</li>
<li><p><strong>Assign IAM Permissions to the Lambda:</strong> The function needs permissions to use SSM, EC2 (optional), and SES. You can attach these permissions by adding IAM policy statements or using managed policies:</p>
<ul>
<li><p><em>SSM Permissions:</em> Allow actions like ssm:DescribeInstanceInformation, ssm:SendCommand, and ssm:GetCommandInvocation. You can scope the SendCommand permission to the specific SSM document ARN for <strong>AmazonCloudWatch-ManageAgent</strong> if you like, or use a broader permission (for simplicity, many will just allow ssm:* on resources *, but least privilege is recommended). These let the Lambda list instances and execute the status commands.</p>
</li>
<li><p><em>EC2 Permissions:</em> If your code looks up EC2 instance tags (e.g., to get the Name tag for friendlier alerts), allow ec2:DescribeInstances (or ec2:DescribeTags). This is optional but useful for enriching alert info.</p>
</li>
<li><p><em>SES Permissions:</em> Allow ses:SendEmail (or ses:SendRawEmail) on your SES identity. You can scope it to the Resource of your SES identity ARN. This permission enables the Lambda to actually send the email.</p>
<p>  In CDK, you can attach a policy like:</p>
<pre><code class="lang-typescript">  monitorFn.addToRolePolicy(<span class="hljs-keyword">new</span> iam.PolicyStatement({
    actions: [
      <span class="hljs-string">"ssm:DescribeInstanceInformation"</span>,
      <span class="hljs-string">"ssm:SendCommand"</span>,
      <span class="hljs-string">"ssm:GetCommandInvocation"</span>,
      <span class="hljs-string">"ec2:DescribeInstances"</span>,
      <span class="hljs-string">"ses:SendEmail"</span>
    ],
    resources: [<span class="hljs-string">"*"</span>]
  }));
</code></pre>
<p>  Here we grant access to the necessary actions across all resources for brevity. In a production environment, tighten the resources scope if possible (for example, restrict SES to your specific identity ARN, and SSM to the target instance ARNs or the document name). The Lambda’s execution role now has the needed powers to do its job.  </p>
</li>
</ul>
</li>
<li><p><strong>Schedule the Lambda with EventBridge (CloudWatch Events):</strong> We want the Lambda to run periodically (for example, once a day or every hour, depending on how quickly you want to catch issues). In CDK, create an EventBridge Rule to trigger the Lambda on a schedule. For example:</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">const</span> rule = <span class="hljs-keyword">new</span> events.Rule(<span class="hljs-built_in">this</span>, <span class="hljs-string">'ScheduleRule'</span>, {
   schedule: events.Schedule.cron({ minute: <span class="hljs-string">'0'</span>, hour: <span class="hljs-string">'*/6'</span> })  <span class="hljs-comment">// every 6 hours, for instance</span>
 });
 rule.addTarget(<span class="hljs-keyword">new</span> targets.LambdaFunction(monitorFn));
</code></pre>
<p> This will invoke our monitorFn Lambda on the defined schedule (here it’s every 6 hours; you can adjust cron or use Schedule.rate(Duration.days(1)) for daily, etc.). CDK will handle the permissions so EventBridge can invoke the Lambda. By scheduling it, we ensure the CloudWatch Agent check runs regularly without human intervention.</p>
</li>
<li><p><strong>Deploy and Verify:</strong> Synthesize and deploy the CDK stack (cdk deploy). Once deployed, check the AWS Console:</p>
<ul>
<li><p>Verify that the Lambda function is created, and the environment variables are set correctly.</p>
</li>
<li><p>Verify that the EventBridge rule is in place and targeting the Lambda.</p>
</li>
<li><p>In the <strong>SES Console</strong>, make sure the ALERT_EMAIL_FROM address (or its domain) is verified (you should have done this before deployment or you can do it now). If you are in the SES <strong>sandbox</strong>, also verify the ALERT_EMAIL_TO recipient or move out of sandbox to send to arbitrary emails.</p>
</li>
<li><p>You can run a quick test by manually invoking the Lambda (e.g., via the Lambda console or CLI) to see if it sends an email. Check your inbox (including spam) for the alert message. It might say that all instances are fine (if none were stopped) or list any issues it found.</p>
</li>
</ul>
</li>
<li><p><strong>Operational Considerations:</strong> After deployment, your <strong>automated monitoring</strong> is in place. Going forward, whenever the CloudWatch Agent is not running on an instance, the Lambda will fire an email alert to your team. Ensure that your team knows how to respond (e.g., reinstall or start the agent on the affected instance). You might also consider integrating the alert with a ticketing system or an SNS topic (instead of direct SES emails) if that suits your operations better. The solution is highly customizable – for example, you could extend the Lambda to automatically attempt to restart the agent by running the SSM document with action: restart when it detects an issue, in addition to sending an alert.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>By implementing this automated monitoring, you gain peace of mind that your <strong>CloudWatch Agents are continuously monitored</strong> just like the rest of your infrastructure. The Lambda + SSM approach effectively asks each instance “Hey, is your CloudWatch Agent OK?” on a schedule, and immediately notifies you if the answer is no. This proactive alerting brings real-world benefits: you’ll catch missing or crashed agents early, <strong>before</strong> they lead to missing metrics or logs during a crucial moment. In practice, this means more reliable monitoring, faster troubleshooting, and a more robust AWS environment.</p>
<p>In summary, we covered why CloudWatch Agent is important for EC2 monitoring and why you should alert on any gaps. We then built a Lambda function that checks agent status using SSM (leveraging the same AWS-recommended commands you could run manually ) and sends out markdown-styled email reports. Finally, we deployed the whole stack using AWS CDK, making it easy for DevOps and platform engineers to set up in their own accounts.</p>
<p><strong>Real-world motivation:</strong> Think of this as a watchdog for your watchdog. It’s a simple investment of time that can pay off big by ensuring your monitoring infrastructure remains healthy. No one wants to discover during an outage that the reason you have no metrics is because the monitoring agent was down – with this solution, such surprises are a thing of the past. By implementing CloudWatch Agent alerting, you’re moving your ops culture toward one of <strong>preventative monitoring</strong> and greater reliability.</p>
]]></content:encoded></item><item><title><![CDATA[Alerting for AWS EC2 Instances Not Managed by SSM Using AWS Config and CDK]]></title><description><![CDATA[Monitoring your EC2 instances to ensure they’re managed by AWS Systems Manager (SSM) is crucial for security, compliance, and smooth operations. In this post, we’ll explain why SSM-managed instances are so important, how instances can become “unmanag...]]></description><link>https://thepawan.dev/alerting-for-aws-ec2-instances-not-managed-by-ssm-using-aws-config-and-cdk</link><guid isPermaLink="true">https://thepawan.dev/alerting-for-aws-ec2-instances-not-managed-by-ssm-using-aws-config-and-cdk</guid><category><![CDATA[AWS]]></category><category><![CDATA[aws lambda]]></category><category><![CDATA[CDK]]></category><category><![CDATA[AWS Config]]></category><category><![CDATA[AWS Systems Manager]]></category><dc:creator><![CDATA[Pawan Sawalani]]></dc:creator><pubDate>Sat, 19 Apr 2025 11:31:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745061920892/980268fc-6804-416c-80ea-bbae445515ce.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Monitoring your EC2 instances to ensure they’re managed by AWS Systems Manager (SSM) is crucial for security, compliance, and smooth operations. In this post, we’ll explain why SSM-managed instances are so important, how instances can become “unmanaged,” and walk through a step-by-step guide to <strong>automatically alert you about any EC2 instance that isn’t managed by SSM</strong>. We’ll use a combination of AWS Config, Amazon EventBridge, AWS Lambda, and Amazon SES – all provisioned with the AWS CDK in TypeScript – to build an automated compliance alert system. By the end, you’ll have a CDK stack that detects non-compliant EC2 instances and emails you their details, helping you uphold DevSecOps best practices and maintain AWS environment hygiene.</p>
<h1 id="heading-why-ensure-ec2-instances-are-managed-by-ssm"><strong>Why Ensure EC2 Instances Are Managed by SSM</strong></h1>
<p>AWS Systems Manager (SSM) is a service that allows you to <strong>automate operational tasks, manage instances at scale, and perform detailed system-level monitoring</strong> for EC2 instances . When your instances are SSM-managed (often called “managed instances”), you unlock a host of benefits:</p>
<p>• <strong>Security &amp; Patch Management:</strong> SSM lets you automate patching and apply updates to your instances, keeping them secure. With SSM’s Patch Manager and Automation documents, you can ensure instances receive timely security patches and configuration updates. Instances not in SSM may miss critical updates, leaving vulnerabilities unaddressed.</p>
<p>• <strong>Compliance &amp; Auditing:</strong> Many compliance frameworks (e.g., NIST CSF) and internal policies require centralized management of servers. By utilizing AWS Systems Manager, you ensure EC2 instances are continuously monitored for security compliance, patch management, software inventory, and regulatory adherence . This helps maintain a secure and compliant environment by centralizing and streamlining EC2 management .</p>
<p>• <strong>Operational Efficiency:</strong> SSM provides tools like Run Command, Session Manager, and Inventory. These enable you to remotely execute commands, manage configurations, and track software inventory across all your instances. If an instance isn’t SSM-managed, you lose these capabilities for that instance, making troubleshooting and administration harder.</p>
<p>• <strong>Consistency:</strong> SSM-managed instances can be part of Automation workflows and State Manager associations to enforce desired states. Unmanaged instances might drift from your standard configurations.</p>
<p>In short, an EC2 instance managed by SSM (i.e., a “managed instance”) is <strong>any instance that has been configured for Systems Manager</strong> . Ensuring all instances are managed means you have consistent control and visibility, which is key to good DevSecOps practices.</p>
<h1 id="heading-how-ec2-instances-become-unmanaged"><strong>How EC2 Instances Become “Unmanaged”</strong></h1>
<p>Even in well-run environments, it’s possible for EC2 instances to fall out of SSM management and become “unmanaged.” Here are common scenarios:</p>
<p>• <strong>SSM Agent Not Running:</strong> The AWS SSM Agent is the software on the instance that communicates with Systems Manager. If this agent is not installed, has been accidentally uninstalled, or is stopped/disabled, the instance will not report to SSM. According to AWS Config’s managed rule documentation, an instance is flagged NON_COMPLIANT if the instance is running but the SSM Agent is stopped or terminated . In other words, a running EC2 with no active SSM agent is considered unmanaged.</p>
<p>• <strong>Missing IAM Role/Permissions:</strong> For an EC2 instance to be managed by SSM, it must have an IAM instance profile with the necessary permissions (typically the <strong>AmazonSSMManagedInstanceCore</strong> policy). If an instance was launched without this IAM role or the role’s policies were removed, the SSM agent cannot connect to the Systems Manager service. The instance will then drop out of SSM’s managed instances list.</p>
<p>• <strong>Network Misconfiguration:</strong> SSM Agent needs network access to communicate with AWS endpoints. If the instance has no internet access or no VPC endpoints for SSM, it may fail to connect. For example, an EC2 in a private subnet without the required VPC endpoints (or NAT) for SSM will be unable to register, effectively unmanaged.</p>
<p>• <strong>Outdated or Misconfigured Agent:</strong> In some cases, an outdated SSM agent might crash or behave unexpectedly, or the instance might be misconfigured (e.g., firewall blocking SSM traffic). This can cause previously managed instances to stop reporting.</p>
<p>• <strong>Instances Launched Outside Standard Processes:</strong> You might have automation to auto-register instances with SSM, but if someone launches an EC2 instance outside of those processes (for instance, in a new account or region without SSM setup), that instance might come up unmanaged.</p>
<p>It’s important to catch these situations. Unmanaged instances won’t receive your automated patches or configurations, and they won’t show up in SSM Inventory or Compliance views. This could lead to security gaps or operational blind spots. Our goal is to set up an automated alert whenever an instance becomes unmanaged so that you can remediate it (for example, reinstall the agent, fix the IAM role, or shut down the instance if it’s not approved).</p>
<h1 id="heading-how-can-we-detect-unmanaged-instances-automatically"><strong>How can we detect unmanaged instances automatically?</strong></h1>
<p>AWS offers a managed AWS Config rule exactly for this: <strong>“EC2 instances managed by Systems Manager.”</strong> This Config rule (EC2_INSTANCE_MANAGED_BY_SSM) evaluates all EC2 instances and checks if each is managed by SSM. The rule is marked <strong>NON_COMPLIANT if an EC2 instance is running and the SSM Agent is stopped or not communicating</strong> . We will use this rule as the trigger for our alerting system.</p>
<h1 id="heading-solution-overview-and-architecture"><strong>Solution Overview and Architecture</strong></h1>
<p>To automate alerts for unmanaged EC2 instances, we’ll use the following AWS services working in tandem:</p>
<p>• <strong>AWS Config Managed Rule:</strong> We’ll enable the <em>ec2-instance-managed-by-systems-manager</em> rule, which flags any running EC2 that isn’t SSM-managed as non-compliant.</p>
<p>• <strong>Amazon EventBridge (CloudWatch Events):</strong> We’ll create an EventBridge rule to listen for compliance change events from AWS Config. Specifically, it will catch events when the above Config rule evaluates an instance as NON_COMPLIANT. By filtering the event, we ensure we react only to instances becoming non-compliant (unmanaged).</p>
<p>• <strong>AWS Lambda Function:</strong> The EventBridge rule will trigger a Lambda function. This function will gather details about the offending EC2 instance (like its instance ID and Name tag) and send out an alert email.</p>
<p>• <strong>Amazon Simple Email Service (SES):</strong> Our Lambda will use Amazon SES to send the actual email notification to specified recipients. We’ll use SES because it’s reliable and designed for such use cases. (SNS could be an alternative for simple text notifications, but SES allows a direct email with customizable content.)</p>
<p>• <strong>AWS CDK (TypeScript):</strong> We’ll implement all the infrastructure above using the AWS CDK, making it easy to deploy repeatably. This includes the Config rule, EventBridge rule, Lambda function code, and necessary permissions and SES setup.</p>
<p>Below is a conceptual diagram of the architecture:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745061087213/fb74388c-ec99-4b1e-ba65-04069688dde7.png" alt class="image--center mx-auto" /></p>
<p><em>Event-driven architecture: AWS Config (via EventBridge) triggers a Lambda Function when an instance becomes non-compliant, and the Lambda sends an email using Amazon SES.</em></p>
<p>In this flow, AWS Config continuously evaluates instances against the SSM management rule. If an instance fails (becomes unmanaged), EventBridge immediately triggers the Lambda. The Lambda logs the event (CloudWatch Logs) and sends an email via SES. Using CDK ensures this entire setup is defined as code, version-controlled, and can be deployed in multiple environments – a big win for DevSecOps practices.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><strong>DevSecOps Tip:</strong> Automating compliance checks and alerts is a best practice to maintain cloud hygiene. As AWS itself notes, <em>having automation to manage server configuration and compliance helps companies save time, improve availability, and lower the risks associated with security</em> . Instead of manual audits, this solution provides continuous monitoring and instant notification, so you can address issues proactively.</div>
</div>

<p>Now, let’s dive into the step-by-step implementation.</p>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before we start building, make sure you have the following:</p>
<p>• <strong>AWS Account &amp; CLI Access:</strong> You’ll need an AWS account with permissions to create the resources (Config rules, Lambda, SES, etc.). Configure AWS CLI or AWS CDK with appropriate credentials (e.g., Admin or a role with necessary rights).</p>
<p>• <strong>AWS CDK Toolkit:</strong> Install the AWS CDK if you haven’t already. For example, via npm: npm install -g aws-cdk. This post uses CDK v2 with TypeScript.</p>
<p>• <strong>AWS Config Enabled:</strong> Ensure AWS Config is enabled in the region you’re deploying to. You should have a Configuration Recorder and Delivery Channel set up (with an S3 bucket for config data). If not, you can enable AWS Config in the console by choosing a bucket/role. (CDK can create these too, but it’s outside our main scope.)</p>
<p>• <strong>SES Email Verification:</strong> Decide which email address (or domain) will be used as the sender for alerts, and which address will receive the alerts. In SES, verify the sender email (and recipient as well, if your SES is in sandbox mode). <em>In SES, you need to verify both the sender and recipient email addresses to ensure a secure and controlled email-sending environment</em> when in sandbox. We’ll assume an email (e.g., <a target="_blank" href="mailto:alerts@example.com">alerts@example.com</a>) is verified to send, and you have access to an inbox to receive the notifications.</p>
<p>With that out of the way, let’s start building our solution with CDK!</p>
<h3 id="heading-step-1-initialize-a-new-aws-cdk-project"><strong>Step 1: Initialize a New AWS CDK Project</strong></h3>
<p>First, set up a new CDK project for our infrastructure:</p>
<p>1. <strong>Create a project directory</strong> (e.g., aws-ssm-alert) and initialize a CDK TypeScript app:</p>
<pre><code class="lang-bash">mkdir aws-ssm-alert &amp;&amp; <span class="hljs-built_in">cd</span> aws-ssm-alert
cdk init app --language typescript
</code></pre>
<p>This will generate a baseline CDK project with TypeScript. You’ll see files like package.json, cdk.json, a /bin folder with the app’s entry point, and a /lib folder with a stack class.</p>
<p>2. <strong>Explore the project structure:</strong> After init, your project should look like:</p>
<pre><code class="lang-plaintext">aws-ssm-alert/
├── bin/
│   └── aws-ssm-alert.ts        # CDK app entry point
├── lib/
│   └── aws-ssm-alert-stack.ts  # Your main stack definition
├── package.json
├── cdk.json
├── tsconfig.json
└── etc...
</code></pre>
<p>The CDK app in bin/aws-ssm-alert.ts instantiates the stack defined in lib/aws-ssm-alert-stack.ts. We will be writing our infrastructure code in the stack class.</p>
<p>3. <strong>Install dependencies:</strong> The template already includes aws-cdk-lib and constructs in package.json. Run npm install if it wasn’t run automatically. Also, ensure the CDK libraries are up to date (you can use npm install aws-cdk-lib@latest if needed).</p>
<p>4. <strong>Bootstrap (if necessary):</strong> If you haven’t used CDK in this AWS account/region before, run cdk bootstrap to set up the necessary CDK resources (this creates a CDK toolkit stack with an S3 bucket for assets, etc.). This only needs to be done once per account/region.</p>
<p>With the CDK project ready, we can start defining our AWS resources in code.</p>
<h3 id="heading-step-2-define-the-aws-config-rule-ssm-managed-instances"><strong>Step 2: Define the AWS Config Rule (SSM Managed Instances)</strong></h3>
<p>The core of our alerting system is the AWS Config rule that checks for SSM-managed instances. AWS provides a managed rule for this, so we don’t have to write custom logic – we just need to enable it in our account via CDK.</p>
<p>Open the stack file (lib/aws-ssm-alert-stack.ts) and add the following code to create the AWS Config rule:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> config <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-config'</span>;
<span class="hljs-comment">// ... other imports ...</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AwsSsmAlertStack <span class="hljs-keyword">extends</span> Stack {
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">scope: Construct, id: <span class="hljs-built_in">string</span>, props?: StackProps</span>) {
    <span class="hljs-built_in">super</span>(scope, id, props);

    <span class="hljs-comment">// AWS Config rule to check if EC2 instances are managed by SSM</span>
    <span class="hljs-keyword">const</span> ssmManagedRule = <span class="hljs-keyword">new</span> config.ManagedRule(<span class="hljs-built_in">this</span>, <span class="hljs-string">'SSMManagedInstancesRule'</span>, {
      identifier: config.ManagedRuleIdentifiers.EC2_INSTANCE_MANAGED_BY_SSM,
      configRuleName: <span class="hljs-string">'ec2-instance-managed-by-systems-manager'</span>,
      description: <span class="hljs-string">'Checks if EC2 instances are managed by AWS Systems Manager (SSM).'</span>
    });
    <span class="hljs-comment">// ... (we will add more resources below) ...</span>
  }
}
</code></pre>
<p>A few notes on this code:</p>
<p>• <code>ManagedRuleIdentifiers.EC2_INSTANCE_MANAGED_BY_SSM</code> is a constant for the managed rule identifier. This corresponds to the AWS Config rule that <em>“checks whether the Amazon EC2 instances in your account are managed by AWS Systems Manager”</em> .</p>
<p>• We set configRuleName to <code>ec2-instance-managed-by-systems-manager</code>. This is the display name of the rule (the one you’ll see in the AWS Config console). AWS’s documentation notes that the rule identifier and name differ for this rule ; using the documented name avoids confusion.</p>
<p>• The rule has no additional parameters – it will evaluate all EC2 instances in the account/region by default. The rule triggers on configuration changes (such as an instance starting or an agent status change).</p>
<p>When this rule finds a non-compliant instance (SSM agent off), it will report NON_COMPLIANT to AWS Config. However, just creating the rule doesn’t send any alerts by itself – that’s where the next components come in.</p>
<blockquote>
<p><strong>Important:</strong> Make sure AWS Config is active. If AWS Config is not yet enabled in this region, deploy a Configuration Recorder and Delivery Channel (or enable via console) <strong>before</strong> deploying this rule. Without an active recorder, the rule won’t run.</p>
</blockquote>
<h3 id="heading-step-3-create-the-lambda-function-to-send-alerts"><strong>Step 3: Create the Lambda Function to Send Alerts</strong></h3>
<p>Next, we’ll create a Lambda function that will be triggered on non-compliance events and send an email alert via SES. We’ll implement the function in Python for simplicity.</p>
<p><strong>Lambda Function Code (Python):</strong> Create a new directory (e.g., lambda) in the project, and inside it create a file ssm_alert_<a target="_blank" href="http://function.py">function.py</a> with the following code:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
<span class="hljs-keyword">import</span> boto3

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lambda_handler</span>(<span class="hljs-params">event, context</span>):</span>
    <span class="hljs-comment"># Extract relevant details from the AWS Config event</span>
    detail = event.get(<span class="hljs-string">'detail'</span>, {})
    instance_id = detail.get(<span class="hljs-string">'resourceId'</span>)
    region = detail.get(<span class="hljs-string">'awsRegion'</span>)
    account = detail.get(<span class="hljs-string">'awsAccountId'</span>)
    rule_name = detail.get(<span class="hljs-string">'configRuleName'</span>)
    compliance = detail.get(<span class="hljs-string">'newEvaluationResult'</span>, {}).get(<span class="hljs-string">'complianceType'</span>)

    <span class="hljs-comment"># Default values if data is missing</span>
    instance_name = <span class="hljs-string">"Unknown"</span>

    <span class="hljs-comment"># Get the EC2 instance's Name tag (if available) for a human-friendly identifier</span>
    <span class="hljs-keyword">if</span> instance_id <span class="hljs-keyword">and</span> region:
        ec2 = boto3.client(<span class="hljs-string">'ec2'</span>, region_name=region)
        <span class="hljs-keyword">try</span>:
            resp = ec2.describe_instances(InstanceIds=[instance_id])
            reservations = resp.get(<span class="hljs-string">'Reservations'</span>, [])
            <span class="hljs-keyword">if</span> reservations:
                tags = reservations[<span class="hljs-number">0</span>][<span class="hljs-string">'Instances'</span>][<span class="hljs-number">0</span>].get(<span class="hljs-string">'Tags'</span>, [])
                <span class="hljs-keyword">for</span> tag <span class="hljs-keyword">in</span> tags:
                    <span class="hljs-keyword">if</span> tag[<span class="hljs-string">'Key'</span>] == <span class="hljs-string">'Name'</span>:
                        instance_name = tag[<span class="hljs-string">'Value'</span>]
                        <span class="hljs-keyword">break</span>
        <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
            print(<span class="hljs-string">f"Error fetching instance name for <span class="hljs-subst">{instance_id}</span>: <span class="hljs-subst">{e}</span>"</span>)

    <span class="hljs-comment"># Compose the email subject and body</span>
    subject = <span class="hljs-string">f"Alert: EC2 instance <span class="hljs-subst">{instance_id}</span> is NOT managed by SSM"</span>
    body_text = (
        <span class="hljs-string">f"EC2 instance <span class="hljs-subst">{instance_id}</span> (Name: <span class="hljs-subst">{instance_name}</span>) in account <span class="hljs-subst">{account}</span>, region <span class="hljs-subst">{region}</span> "</span>
        <span class="hljs-string">f"is <span class="hljs-subst">{compliance}</span> with AWS Systems Manager compliance (rule: <span class="hljs-subst">{rule_name}</span>).\n"</span>
        <span class="hljs-string">"This means the instance is not managed by AWS Systems Manager (SSM). "</span>
        <span class="hljs-string">"Please check the instance's SSM agent and IAM role to restore management."</span>
    )

    <span class="hljs-comment"># Send the email via Amazon SES</span>
    ses = boto3.client(<span class="hljs-string">'ses'</span>, region_name=region)
    response = ses.send_email(
        Source=os.environ[<span class="hljs-string">'SENDER_EMAIL'</span>],
        Destination={<span class="hljs-string">'ToAddresses'</span>: [os.environ[<span class="hljs-string">'ALERT_EMAIL'</span>]]},
        Message={
            <span class="hljs-string">'Subject'</span>: {<span class="hljs-string">'Data'</span>: subject},
            <span class="hljs-string">'Body'</span>: {<span class="hljs-string">'Text'</span>: {<span class="hljs-string">'Data'</span>: body_text}}
        }
    )
    print(<span class="hljs-string">f"SES send_email response: <span class="hljs-subst">{response[<span class="hljs-string">'MessageId'</span>]}</span>"</span>)
    <span class="hljs-keyword">return</span> {<span class="hljs-string">"status"</span>: <span class="hljs-string">"sent"</span>, <span class="hljs-string">"messageId"</span>: response[<span class="hljs-string">'MessageId'</span>]}
</code></pre>
<p>Let’s break down what this function does:</p>
<p>• It expects the event to be an AWS Config compliance change event. It parses out details like instance_id, AWS region, AWS account, the rule_name, and the compliance status. These are nested under event['detail'].</p>
<p>• It then uses the EC2 API (describe_instances) to get the instance’s tags, searching for the <strong>Name tag</strong>. This is optional but makes the alert more informative (e.g., “Instance i-0123456789 (Name: <strong>WebServer1</strong>) is not managed by SSM”). If anything goes wrong or the tag isn’t found, we default to “Unknown”.</p>
<p>• Next, it composes an email message. We use environment variables SENDER_EMAIL and ALERT_EMAIL (we will set these in the CDK) to avoid hard-coding addresses. The email includes the instance ID, name, account, region, and a brief note that it’s not managed by SSM.</p>
<p>• Finally, it uses <strong>boto3</strong> (AWS SDK for Python) to call ses.send_email() with the given subject and body. The result includes a MessageId which we log for reference.</p>
<p>This is a straightforward function – essentially, gather info and send an email. Using SES through boto3 in Lambda is a common pattern (just ensure your region supports SES and the emails are verified).</p>
<p>Now we need to integrate this Lambda function into our CDK stack:</p>
<p>In the CDK stack code (aws-ssm-alert-stack.ts), add the Lambda function resource:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> lambda <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-lambda'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">'path'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> iam <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-iam'</span>;
<span class="hljs-comment">// ... (make sure to import these at top)</span>

  <span class="hljs-comment">// Within the AwsSsmAlertStack constructor, after defining ssmManagedRule:</span>

  <span class="hljs-comment">// Lambda function to handle non-compliance events and send email alerts</span>
  <span class="hljs-keyword">const</span> alertFunction = <span class="hljs-keyword">new</span> lambda.Function(<span class="hljs-built_in">this</span>, <span class="hljs-string">'SSMAlertFunction'</span>, {
    runtime: lambda.Runtime.PYTHON_3_9,
    handler: <span class="hljs-string">'ssm_alert_function.lambda_handler'</span>,
    code: lambda.Code.fromAsset(path.join(__dirname, <span class="hljs-string">'../lambda'</span>)),  <span class="hljs-comment">// assumes code is in lambda/ directory</span>
    environment: {
      <span class="hljs-string">'SENDER_EMAIL'</span>: <span class="hljs-string">'alerts@example.com'</span>,      <span class="hljs-comment">// replace with your verified sender</span>
      <span class="hljs-string">'ALERT_EMAIL'</span>: <span class="hljs-string">'admin@example.com'</span>         <span class="hljs-comment">// replace with your recipient email</span>
    }
  });

  <span class="hljs-comment">// Grant permissions to the Lambda to describe EC2 instances and send SES email</span>
  alertFunction.addToRolePolicy(<span class="hljs-keyword">new</span> iam.PolicyStatement({
    actions: [<span class="hljs-string">'ec2:DescribeInstances'</span>],
    resources: [<span class="hljs-string">'*'</span>]  <span class="hljs-comment">// describing is read-only; cannot easily restrict by resource ID</span>
  }));
  alertFunction.addToRolePolicy(<span class="hljs-keyword">new</span> iam.PolicyStatement({
    actions: [<span class="hljs-string">'ses:SendEmail'</span>, <span class="hljs-string">'ses:SendRawEmail'</span>],
    resources: [<span class="hljs-string">'*'</span>]  <span class="hljs-comment">// allow sending email via SES from any identity (we will restrict via verified identity in SES itself)</span>
  }));
</code></pre>
<p>Key points about this CDK code:</p>
<p>• We package the Lambda code using lambda.Code.fromAsset. This will zip the contents of the lambda directory (which contains our ssm_alert_<a target="_blank" href="http://function.py">function.py</a>) and deploy it. Make sure the path is correct relative to your CDK project structure.</p>
<p>• We set the runtime to Python 3.9 (you can use 3.10 or 3.8 as supported) and the handler to ssm_alert_function.lambda_handler (file name and function name).</p>
<p>• In environment, plug in the verified SES sender and the desired recipient. <strong>Use your actual emails</strong> here: for instance, if you verified <a target="_blank" href="mailto:ops-team@mycompany.com">ops-team@mycompany.com</a> in SES, set SENDER_EMAIL to that. For ALERT_EMAIL, you can use a distribution list or your email (and ensure it’s verified if in sandbox).</p>
<p>• We then add IAM permissions for the Lambda:</p>
<p>• ec2:DescribeInstances so the function can look up instance tags. (We scope to all resources; you could theoretically scope to the specific instance ARN if the event provided it, but since any instance ID could come through, we allow all. This action is read-only.)</p>
<p>• ses:SendEmail and ses:SendRawEmail so the function can send emails via SES. We allow all resources (*) which means it can use any verified identity in this account. For tighter security, you could specify the ARN of the SES identity (your verified email/domain), but that can vary by region. Simplicity is fine here, given that only our function has this role.</p>
<p>By default, the Lambda gets a basic execution role that allows CloudWatch Logs, so it can write logs (the print statements) to CloudWatch.</p>
<p>Now we have the Config rule and a Lambda function to handle alerts. Next, we connect them with EventBridge.</p>
<h3 id="heading-step-4-set-up-an-eventbridge-rule-to-trigger-the-lambda"><strong>Step 4: Set Up an EventBridge Rule to Trigger the Lambda</strong></h3>
<p>AWS Config emits events on state changes of rules. We’ll create an EventBridge rule (formerly CloudWatch Events rule) that listens for our Config rule becoming NON_COMPLIANT and invokes the Lambda function.</p>
<p>Add the following to the stack code:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> events <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-events'</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> targets <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-cdk-lib/aws-events-targets'</span>;
<span class="hljs-comment">// ... (ensure these imports are present)</span>

  <span class="hljs-comment">// Within the constructor, after defining alertFunction:</span>

  <span class="hljs-comment">// EventBridge rule to catch Config rule non-compliance and trigger the Lambda</span>
  <span class="hljs-keyword">const</span> configNonComplianceRule = <span class="hljs-keyword">new</span> events.Rule(<span class="hljs-built_in">this</span>, <span class="hljs-string">'ConfigNonComplianceRule'</span>, {
    description: <span class="hljs-string">'Triggers on EC2 SSM Managed Instances rule NON_COMPLIANT evaluations'</span>,
    eventPattern: {
      source: [<span class="hljs-string">'aws.config'</span>],
      detailType: [<span class="hljs-string">'Config Rules Compliance Change'</span>],
      detail: {
        <span class="hljs-comment">// Only target our specific Config rule by name:</span>
        configRuleName: [ <span class="hljs-string">'ec2-instance-managed-by-systems-manager'</span> ],
        <span class="hljs-comment">// Only trigger when the new evaluation is NON_COMPLIANT</span>
        newEvaluationResult: {
          complianceType: [ <span class="hljs-string">'NON_COMPLIANT'</span> ]
        }
      }
    }
  });
  <span class="hljs-comment">// Set the Lambda as the target for the EventBridge rule</span>
  configNonComplianceRule.addTarget(<span class="hljs-keyword">new</span> targets.LambdaFunction(alertFunction));
</code></pre>
<p>Let’s explain the event pattern:</p>
<p>• <strong>source</strong>: We filter events from aws.config only.</p>
<p>• <strong>detailType</strong>: We specifically look at “Config Rules Compliance Change” events. AWS Config generates this event type when a resource’s compliance status changes for a Config rule .</p>
<p>• <strong>detail.configRuleName</strong>: We restrict the rule name to our rule (“ec2-instance-managed-by-systems-manager”). This ensures we don’t catch compliance changes from other, unrelated Config rules you might have. (If you want one Lambda to handle multiple rules, you could list multiple names here.)</p>
<p>• <strong>detail.newEvaluationResult.complianceType</strong>: We only care about transitions <em>to</em> NON_COMPLIANT. This way, when an instance becomes non-compliant (unmanaged), we trigger the Lambda. We ignore transitions to COMPLIANT or other states. By filtering on the complianceType, we handle only the “went non-compliant” direction .</p>
<p>With this pattern, whenever AWS Config flags an instance as not managed by SSM, an event will match and our alertFunction will be invoked. The event data (as we saw in the Lambda code) includes the instance ID, etc.</p>
<p>We’ve now defined all the main components in our CDK stack: the Config rule, the EventBridge rule, and the Lambda with SES permissions. The last piece is making sure SES is configured to allow our emails to be sent.</p>
<h3 id="heading-step-5-configure-amazon-ses-for-email-sending"><strong>Step 5: Configure Amazon SES for Email Sending</strong></h3>
<p>Amazon SES (Simple Email Service) will actually deliver our alert emails. We need to ensure SES is set up properly:</p>
<p>• <strong>Verify the Sender Email:</strong> In the AWS SES console, verify the email address you intend to send from (the one we set as SENDER_EMAIL in the Lambda environment). SES will send a verification link to that email; once you confirm it, the email is registered as a verified identity in SES. If you have a domain verified in SES, you could use an address on that domain as well.</p>
<p>• <strong>Verify the Recipient Email (if needed):</strong> If your SES is in the <em>sandbox</em> (the default for new accounts), you must also verify any recipient email address. Verify the address that you set as ALERT_EMAIL. (In production SES – after you request a sending limit increase – you can send to any address. But initially, sandbox mode restrictions apply.)</p>
<p>• <strong>Region Consideration:</strong> Note which AWS region you verified the identities in. SES identities are regional. Make sure the Lambda uses the same region for the SES client. In our code, we used region_name=event['detail']['awsRegion'] to initialize the boto3 SES client to the region where the Config event came from (which should be your deployment region). If your SES is in a different region, you might want to explicitly set the region or adjust accordingly. It’s simplest to deploy everything in one region.</p>
<p>If you haven’t used SES before, the AWS documentation provides guidance on verifying identities . The key point is that SES must know you own the sender (and for sandbox, the receiver). After verification, our Lambda’s ses.send_email call will be able to send the message.</p>
<blockquote>
<p>📝 <strong>Tip:</strong> You can test SES sending manually by using the “Send Test Email” feature in the SES console with your verified identities, just to ensure email delivery works, before relying on the Lambda.</p>
</blockquote>
<p>Now our infrastructure and configurations are all set. Let’s deploy and see it in action.</p>
<h3 id="heading-step-6-deploy-the-cdk-stack-and-test-the-setup"><strong>Step 6: Deploy the CDK Stack and Test the Setup</strong></h3>
<p>With our CDK code ready, we can deploy it to AWS:</p>
<p>1. <strong>Synthesize the CloudFormation template:</strong> Run cdk synth in your project directory. This will compile your TypeScript and output the CloudFormation template. Check that there are no errors and the resources (Config rule, Lambda, EventBridge rule, IAM policies) are present in the synthesized output.</p>
<p>2. <strong>Deploy to AWS:</strong> Run cdk deploy. You’ll be asked to confirm IAM changes (since we’re creating roles and a Config rule that involves AWS Config service). Type “y” to approve. CDK will then create the stack in your AWS account.</p>
<p>• The deployment will output progress. It should create the Config rule, Lambda function (and an S3 asset upload for the code), the EventBridge rule, etc.</p>
<p>• Once complete, note any output or confirmations. There might not be custom outputs, but you can verify in the AWS console that the resources are now live:</p>
<p>• In AWS Config console, under <strong>Rules</strong>, you should see <strong>ec2-instance-managed-by-systems-manager</strong> with a green check if all instances are compliant (or a red exclamation if not).</p>
<p>• In the Lambda console, the function <strong>SSMAlertFunction</strong> should exist with the environment variables set.</p>
<p>• In EventBridge (or CloudWatch Events) console, a rule <strong>ConfigNonComplianceRule</strong> should be present targeting the Lambda.</p>
<p>• In IAM, the Lambda’s role should have the SES and EC2 read permissions we defined.</p>
<p>3. <strong>Test the functionality:</strong> Now the fun part – validate that the alert works. There are a couple of ways to test:</p>
<p>• <strong>Immediate Compliance Check:</strong> As soon as the Config rule is active, it will evaluate your instances. If you <em>already have an unmanaged EC2 instance</em>, the rule should flag it. AWS Config might periodically re-evaluate or evaluate on state change. To force a quick evaluation, you can go to the AWS Config console, select the rule, and trigger a re-evaluation (or stop/start an instance to generate a config change).</p>
<p>• <strong>Simulate a Non-Compliant Instance:</strong> If all your instances are compliant, you can simulate a violation. For example, take a test EC2 instance and try to break its SSM management:</p>
<p>• If it’s Windows or Linux with SSM agent running, you could stop the SSM agent service on that instance (for a quick test). On Amazon Linux, sudo systemctl stop amazon-ssm-agent will do it. Within a few minutes, AWS Config should detect the agent is not responding.</p>
<p>• Alternatively, remove the IAM instance profile from the instance (or create a new instance without an IAM role for SSM). The agent will lose access and should drop off.</p>
<p>• <strong>Wait for AWS Config Evaluation:</strong> AWS Config will mark the instance as NON_COMPLIANT on the next evaluation cycle or config change trigger. When that happens, EventBridge should catch it and invoke the Lambda. This is usually quite fast (near real-time on config change).</p>
<p>• <strong>Check for Email:</strong> Monitor the recipient inbox (and possibly spam folder, just in case) for the alert email. The email subject should start with “Alert: EC2 instance i-XXXX is NOT managed by SSM”. The body will list the instance ID and name, account, region, and advise to check the agent/role.</p>
<p>• <strong>Verify CloudWatch Logs:</strong> If you don’t see an email, go to CloudWatch Logs for the Lambda (/aws/lambda/SSMAlertFunction) to troubleshoot. You should see logs of the event and any errors. Common issues could be SES not sending (check that the email is verified and in correct region) or permissions.</p>
<p>If everything is set up correctly, you should receive the notification email shortly after an instance becomes unmanaged. Success! 🎉 You’ve got an automated watcher on your EC2 instances’ SSM status.</p>
<h1 id="heading-conclusion"><strong>Conclusion</strong></h1>
<p>By following this guide, you have implemented an automated alert system for unmanaged EC2 instances using AWS Systems Manager and AWS CDK. This solution ensures that if any EC2 in your environment loses SSM management (whether due to agent issues or misconfiguration), you’ll promptly get an email alert with the details. The combination of AWS Config and EventBridge provides a powerful event-driven way to monitor compliance in near real-time, and using Lambda+SES gives a flexible notification mechanism.</p>
<p>This setup isn’t just about notifications – it’s about <strong>embracing DevSecOps principles</strong>. We’ve automated a compliance check and integrated it into your operations workflow, reducing the need for manual monitoring. Such automation helps maintain infrastructure hygiene and frees engineers to focus on proactive improvements rather than reactively putting out fires. <em>As noted earlier, automation of compliance checks saves time and lowers risk</em> , which is exactly what we achieved here.</p>
<p>You can extend this approach further by adding automatic remediation (for example, triggering a Systems Manager Automation document to reinstall the agent or notify via Slack or create a ServiceNow ticket, etc.). AWS Config Rules even support automatic remediation actions via SSM documents , which could be a next step.</p>
<p>In production environments, it’s highly recommended to adopt this kind of guardrail. It provides quick visibility into potential misconfigurations (like someone launching an instance without proper IAM role or an agent crash going unnoticed). With AWS CDK, you also have this infrastructure as code, making it easy to version control and deploy to multiple accounts or regions as needed.</p>
<p><strong>In summary</strong>, by setting up AWS alerts for unmanaged EC2 instances, you bolster your cloud environment’s security and compliance posture. We encourage you to integrate this solution (and others like it for different compliance rules) into your environments. It’s a small investment of time that pays off with continuous, hands-off monitoring and peace of mind knowing no EC2 instance will silently drift out of management without you knowing. Happy automating and stay secure!</p>
]]></content:encoded></item><item><title><![CDATA[Goodbye Bastion, Hello Zero-Trust: Our Journey to Simplified RDS Access]]></title><description><![CDATA[Connecting to a private AWS database shouldn’t feel like hacking through a jungle of jump boxes and VPNs. In our team’s early days, though, that was our reality. This post is a candid look at how we improved the developer experience and security of a...]]></description><link>https://thepawan.dev/goodbye-bastion-hello-zero-trust-our-journey-to-simplified-rds-access</link><guid isPermaLink="true">https://thepawan.dev/goodbye-bastion-hello-zero-trust-our-journey-to-simplified-rds-access</guid><category><![CDATA[AWS]]></category><category><![CDATA[AWS RDS]]></category><category><![CDATA[AWS SSM]]></category><category><![CDATA[Databases]]></category><category><![CDATA[DevSecOps]]></category><dc:creator><![CDATA[Pawan Sawalani]]></dc:creator><pubDate>Wed, 09 Apr 2025 11:04:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744196503145/0f71ad25-2aa0-4252-8b9f-edcfa09fb7d1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Connecting to a private AWS database shouldn’t feel like hacking through a jungle of jump boxes and VPNs. In our team’s early days, though, that was our reality. This post is a candid look at how we improved the developer experience and security of accessing Amazon RDS databases – moving from an old-school Windows bastion (jump box) to AWS’s shiny new Verified Access, and finally landing on a surprisingly simple solution with AWS Systems Manager Session Manager. We’ll cover what worked, what didn’t, and how you can set up a smooth, secure database access workflow no matter your experience level.</p>
<h2 id="heading-background-the-old-bastion-setup-rdp-into-rds"><strong>Background: The Old Bastion Setup (RDP into RDS)</strong></h2>
<p>Not long ago, our developers accessed private RDS databases by <strong>RDP-ing into a Windows “bastion” host</strong> in AWS. This bastion was an EC2 instance in a public subnet acting as a jump box. Team members would Remote Desktop into it, then use database GUI tools (like SQL Server Management Studio or pgAdmin) <strong>installed on that bastion</strong> to connect to the actual RDS instances in private subnets. It was the traditional solution to avoid exposing databases directly, but it came with plenty of headaches:</p>
<ul>
<li><p><strong>Clunky User Experience:</strong> Engineers couldn’t use their own machines or preferred tools directly. They had to operate via a remote Windows desktop, often suffering lag and limited clipboard sharing. It felt like working through a periscope rather than directly on your workstation.</p>
</li>
<li><p><strong>Security Risks:</strong> The bastion needed an open RDP port (3389) accessible (albeit restricted by IP). This inherently increases risk – if the security group was misconfigured or an exploit found in RDP, our private DB network could be exposed . With more remote work, the chances of someone poking a hole in the firewall for convenience grew .</p>
</li>
<li><p><strong>Maintenance Burden:</strong> A Windows server requires constant care – OS patching, user account management, and even handling RDP license limits if multiple people use it . We had to keep the DB client software up-to-date on the bastion too. All this ops overhead for a box that didn’t do any “real” work, except letting us in.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744194817214/ca73fa4c-a6ba-4c02-b7fa-e8bcbf246748.png" alt class="image--center mx-auto" /></p>
<p><em>Figure: Traditional approach using a bastion host (an EC2 jump box in a public subnet) to reach a private Amazon RDS database. Developers’ traffic goes from the corporate network (or internet) to the bastion, then onward to the database. This requires opening RDP/SSH access to the bastion, which introduces management overhead and potential security exposure.</em></p>
<p>It was clear this setup didn’t scale well for our growing team. We wanted a way to <strong>connect to RDS directly from our laptops</strong>, without that clumsy remote hop – but still keeping the databases locked down from the internet. VPN was one option, but managing a full-blown VPN client and infrastructure felt heavy. In late 2024, AWS announced something that caught our attention as a possible answer.</p>
<h2 id="heading-trying-aws-verified-access-for-direct-database-connectivity"><strong>Trying AWS Verified Access for Direct Database Connectivity</strong></h2>
<p>When AWS released <strong>Verified Access (AVA)</strong>, it sounded like a game-changer. AWS Verified Access is a service built on zero-trust principles that lets users connect securely to internal applications without a VPN . Initially it was only for web (HTTP) apps, but as of <a target="_blank" href="https://aws.amazon.com/blogs/networking-and-content-delivery/aws-verified-access-support-for-non-http-resources-is-now-generally-available/">re:Invent 2024</a>, it expanded to support non-HTTP endpoints – including RDS databases . The promise was <strong>VPN-less, policy-controlled access</strong> to private resources, with fine-grained checks on each connection (user identity, device security posture, etc.). For our use case, the appeal was huge:</p>
<ul>
<li><p>Engineers could run their favorite database GUI <em>directly on their laptop</em> and connect to the RDS endpoint as if they were in the office network. No more RDP hop – <strong>better user experience</strong> and productivity.</p>
</li>
<li><p>Security would actually <em>improve</em>: Verified Access would evaluate every login attempt against security policies (who you are, whether your device is trusted, etc.), only then broker a connection . It’s based on “never trust, always verify” principles, meaning even if someone somehow got credentials, if they weren’t on an approved device or didn’t meet policy, access would be denied.</p>
</li>
<li><p>We could eliminate the exposed bastion entirely. Verified Access acts as a managed gatekeeper in AWS’s cloud, so <strong>no need for an open port</strong> in our VPC for RDP or SSH.</p>
</li>
</ul>
<p>Setting up AWS Verified Access for our databases involved a few pieces. First, we needed to integrate it with our SSO identity provider (AWS IAM Identity Center in our case) as a “trust provider”. This let Verified Access confirm our engineers’ identities via SSO login. Next, we created a Verified Access instance and defined an <strong>endpoint for our RDS</strong>. AWS now allows an RDS instance (or cluster or proxy) to be a target for Verified Access . We then set up an access policy – in our test, we kept it simple: allow members of our engineering SSO group who passed MFA. Verified Access can get very granular (checking device OS, patch level, etc.), but we started basic just to get it working.</p>
<p>One critical component was deploying the <strong>AWS Verified Access client</strong> (also called the <em>Connectivity Client</em>) on our laptops . This is a small app that runs on the user’s machine to facilitate the connection. It <strong>encrypts and funnels traffic from the laptop to AWS Verified Access</strong>, including attaching the user’s identity and device info, so that AWS can decide if the traffic is allowed . In essence, it’s like a smart VPN client but application-specific and ephemeral. We installed the client, and it prompted us to log in via our SSO in a browser. Once authenticated, the client established a secure tunnel to AWS.</p>
<p>From a user standpoint, after launching the Verified Access client and logging in, they could open their database tool (say, <strong>DBeaver or DataGrip</strong>), and connect to the database’s endpoint (we used the regular RDS hostname) on the default port. The Verified Access client transparently routed that connection through AWS to our VPC. It really felt like magic the first time – my pgAdmin on my MacBook connected to a Postgres in a private subnet without any SSH tunnels or VPN, and with AWS handling the security behind the scenes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744194906600/0a8770c7-536f-42ca-8d9d-366774085c95.jpeg" alt class="image--center mx-auto" /></p>
<p><em>Figure: AWS Verified Access.</em></p>
<p><strong>Initial benefits we observed:</strong></p>
<ul>
<li><p><strong>Night-and-day UX improvement:</strong> Everyone could use their own IDE/GUI, with native performance. Running queries or browsing tables was as snappy as if on a local network.</p>
</li>
<li><p><strong>No more shared jump box:</strong> Each engineer authenticated individually via SSO. There was no single chokepoint server to maintain or that could be compromised to gain broader access – Verified Access only let <strong>that one user’s session</strong> through, and only to the specific database endpoint we configured.</p>
</li>
<li><p><strong>Auditing and control:</strong> Verified Access logs every access request. We could enforce multi-factor auth and even device compliance (e.g., only allow up-to-date company laptops). It’s true zero-trust: every new connection is verified against policies rather than implicitly trusted once on a VPN.</p>
</li>
</ul>
<h3 id="heading-the-downsides-of-verified-access-in-practice"><strong>The Downsides of Verified Access in Practice</strong></h3>
<p>This pilot with AWS Verified Access was promising, but as we dug deeper and scaled it out, we hit some challenges that made us reconsider relying on it long-term:</p>
<ul>
<li><p><strong>Client Software Limitations:</strong> Since it was a new service, the <strong>Verified Access connectivity client</strong> had a few rough edges. It was only available for Windows and Mac at first – our one engineer on Linux was out of luck . (AWS hinted Linux support was coming, but it wasn’t there yet.) Additionally, the client lacked a friendly GUI; we had to configure it by dropping a JSON config file onto the machine (no simple one-click setup) . This was manageable for our tech-savvy team, but not exactly polished.</p>
</li>
<li><p><strong>Complexity of Policies:</strong> Writing policies in AWS Verified Access uses AWS Cedar (a policy language). It’s powerful but introduced a learning curve. Simple policies were fine, but anything custom required understanding a new syntax and debugging in a new console. For a small team, this felt like overkill just to allow database access for devs.</p>
</li>
<li><p><strong>Cost Concerns:</strong> Perhaps the biggest factor – <strong>cost</strong>. AWS Verified Access is a managed service you pay for per application endpoint and per hour. In our case, each private RDS we wanted to enable access to counted as an application endpoint. The pricing in our region came out to about <strong>$0.27 per hour per app</strong> plus a small per-GB data charge . That means roughly $200 per month <em>for each database</em>. In a dev/test/prod scenario with multiple databases, we were looking at several hundreds of dollars monthly just for this convenience. Compared to a simple EC2 bastion (which might be ~$50 or less per month) it was an order of magnitude more expensive. As a startup, that was hard to justify beyond initial testing.</p>
</li>
<li><p><strong>Operational Maturity:</strong> Being a very new service, we encountered a few hiccups – occasional client disconnects and once an identity sync issue that blocked a login until we reset the client. AWS support was helpful, but it reminded us that we were early adopters on the bleeding edge. We had to ask: did we want to be pioneers here, or use something more battle-tested?</p>
</li>
</ul>
<p>Weighing these downsides, we decided to explore alternatives. We loved the idea of ditching the bastion and having direct access, but maybe there was a simpler way to get there without the cost and complexity of Verified Access. It turned out, the solution was something we already had at our fingertips in AWS.</p>
<h2 id="heading-switching-gears-to-aws-ssm-session-manager"><strong>Switching Gears to AWS SSM Session Manager</strong></h2>
<p>After our trial with Verified Access, we took a step back and reexamined the problem. We wanted <strong>secure, easy access to private RDS from our laptops</strong>, and we wanted to minimize infrastructure and maintenance. AWS actually provides a feature for secure remote access that we had used before for shell access: <strong>AWS Systems Manager Session Manager</strong> (SSM Session Manager). Could we use it for database access? The answer was yes – and it was surprisingly straightforward.</p>
<p>AWS Session Manager lets you open a shell or tunnel to an EC2 instance <strong>without any SSH keys or open ports</strong>, by using an SSM Agent installed on the instance . What many don’t realize is that Session Manager can also handle <strong>port forwarding</strong>. <a target="_blank" href="https://aws.amazon.com/blogs/database/securely-connect-to-an-amazon-rds-or-amazon-ec2-database-instance-remotely-with-your-preferred-gui/">In late 2022</a>, AWS added the ability to forward traffic not just to the instance itself, but <em>through</em> the instance to another host – essentially an SSH tunnel-like capability, but over the SSM channel . This is perfect for our use case: we can use a lightweight EC2 instance as a private relay to the database, and Session Manager will securely connect our laptop to that instance and pipe the traffic to the RDS.</p>
<p>Here’s how we built our Session Manager solution, step by step, and how it addressed our needs:</p>
<h3 id="heading-1-setting-up-a-small-ec2-tunnel-instance"><strong>1. Setting Up a Small EC2 “Tunnel” Instance</strong></h3>
<p>First, we launched a tiny EC2 instance in the same VPC and private subnet as our RDS. (We jokingly call this our “bastion”, but it’s not accessible like a traditional one – no inbound access at all.) Important details for this instance:</p>
<ul>
<li><p><strong>Instance Type &amp; OS:</strong> We chose an Amazon Linux 2 t4g.nano (very cheap, ~$4/month). Amazon Linux comes with the SSM Agent pre-installed, which saved setup time.</p>
</li>
<li><p><strong>SSM IAM Role:</strong> We attached the <strong>AmazonSSMManagedInstanceCore</strong> IAM policy via an instance role. This grants the instance permission to communicate with the SSM service. With this, the SSM Agent on the instance can register itself and receive Session Manager connection requests. (No SSH keys needed at all – authentication will be handled by IAM and SSM.)</p>
</li>
<li><p><strong>Security Groups:</strong> The instance’s security group was locked down. We did not allow any inbound ports from anywhere (not even SSH from our IP). We only allowed outbound traffic. Specifically, outbound rules allowed HTTPS (port 443) so the agent could reach SSM’s endpoints, and allowed outbound to the RDS’s port. The RDS’s security group in turn allowed inbound from this instance’s security group on the database port. This way, the EC2 can talk to the database internally, but nothing external can talk to the EC2.</p>
</li>
<li><p><strong>Networking:</strong> We gave the instance access to the internet <strong>only via an SSM VPC endpoint</strong> (and a VPC endpoint for EC2 messages), instead of a NAT gateway. This is an optional step, but it means the SSM Agent traffic goes through a private VPC endpoint to AWS, which is more secure and avoids NAT data charges. (If you skip VPC endpoints, the agent will use the NAT to reach the Systems Manager API, which is fine but costs a bit more and traverses the internet.)</p>
</li>
</ul>
<p>At this point, we had an <strong>SSM-managed instance</strong> in the private subnet. Think of it as a potential one-to-one replacement of the old bastion – except it’s not exposed to the world at all. Now we needed to actually <em>use</em> it to reach the database from our laptops.</p>
<h3 id="heading-2-starting-a-session-manager-port-forward"><strong>2. Starting a Session Manager Port Forward</strong></h3>
<p>AWS provides a CLI command to open a Session Manager session. Instead of a normal shell session, we will start a <strong>port forwarding session</strong>. Here’s an example command we use (in a Bash script on our laptops) to connect to one of our PostgreSQL databases:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Variables for clarity</span>
INSTANCE_ID=<span class="hljs-string">"i-0123456789abcdef0"</span>   <span class="hljs-comment"># The EC2 instance acting as our SSM tunnel</span>
RDS_ENDPOINT=<span class="hljs-string">"mydatabase.cluster-abcdefghijkl.us-east-1.rds.amazonaws.com"</span>
DB_PORT=5432

aws ssm start-session \
  --target <span class="hljs-string">"<span class="hljs-variable">$INSTANCE_ID</span>"</span> \
  --document-name <span class="hljs-string">"AWS-StartPortForwardingSessionToRemoteHost"</span> \
  --parameters <span class="hljs-string">"host=<span class="hljs-variable">$RDS_ENDPOINT</span>,portNumber=<span class="hljs-variable">$DB_PORT</span>,localPortNumber=<span class="hljs-variable">$DB_PORT</span>"</span>
</code></pre>
<p>Let’s break down what this does:</p>
<ul>
<li><p>aws ssm start-session: This initiates an SSM Session Manager session from our machine. (Make sure you’ve configured your AWS CLI with credentials/SSO that have permission to use Session Manager on that instance.)</p>
</li>
<li><p>--target: The ID of the EC2 instance we launched. This tells AWS which instance’s SSM Agent should handle the session.</p>
</li>
<li><p>--document-name "AWS-StartPortForwardingSessionToRemoteHost": This is an AWS-provided session document that knows how to set up port forwarding to a specified remote host. It’s essentially a pre-built SSM action for tunneling.</p>
</li>
<li><p>--parameters "host=...,portNumber=...,localPortNumber=...": Here we provide the RDS host and port we want to reach, and which local port to use on our laptop. In our example, we set host to the RDS endpoint DNS name, portNumber to 5432 (the DB’s port), and localPortNumber also to 5432. This means <strong>the SSM Agent on the EC2 will open a connection to mydatabase...:5432 (our RDS)</strong>, and forward that back through the session to <a target="_blank" href="http://localhost:5432"><strong>localhost:5432</strong></a> <strong>on our laptop</strong> .</p>
</li>
</ul>
<p>When we run this command, a few things happen behind the scenes:</p>
<ul>
<li><p>The AWS CLI calls the SSM service, which in turn signals the SSM Agent on our instance to start a port forwarding session. Because our instance can reach the RDS internally, it successfully connects to the database’s host and port.</p>
</li>
<li><p>The CLI also starts a local proxy listening on the specified localPortNumber (5432). You’ll see output like “Starting session with SessionId …” and “Port 5432 opened for session … Waiting for connections…” . This means everything is set – the tunnel is up and idle, waiting for you to connect.</p>
</li>
<li><p>We keep that terminal running (the session stays active). Now on our <strong>local machine</strong>, we can connect to <a target="_blank" href="http://localhost:5432">localhost:5432</a> and it will actually reach the RDS through the tunnel.</p>
</li>
</ul>
<p>At this point, the experience is exactly like using Verified Access (or a VPN). I can fire up my database client on my laptop, but now I point it to 127.0.0.1:5432 (or a <a target="_blank" href="http://localhost">localhost</a> alias), with the usual database credentials. <strong>Boom – I’m connected to the private RDS</strong>. The Session Manager tunnel carries all the traffic. From the database’s perspective, it sees a connection coming from the EC2 instance’s IP (since that instance is acting as the client on its behalf). From my perspective, it feels local.</p>
<p>One great aspect of Session Manager is that all of this is done using my AWS IAM credentials. If I’m authenticated with AWS (for example via AWS SSO login or access keys), I don’t need to juggle any SSH keys or bastion passwords. Permissions to use Session Manager can be controlled via IAM policies (for instance, only allow certain IAM roles to start sessions to that instance). And every session is logged in AWS CloudTrail (and even Session Manager can be set to log full console output to S3/CloudWatch if needed). So we <strong>gained auditability</strong> without much effort – an improvement over the old bastion where RDP logins were somewhat opaque.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744195290039/c92498f5-cf13-4dea-b892-c0659c4c0a3b.png" alt class="image--center mx-auto" /></p>
<p><em>Figure: Using AWS Systems Manager Session Manager to create a secure tunnel from a client to an RDS database via a private EC2 instance. The EC2 “bastion” lives in a private subnet with</em> <strong><em>no inbound ports</em></strong> <em>open. The Session Manager agent on it connects out to AWS, allowing authorized users to start an encrypted session. This lets us forward a local port on our laptop to the remote database securely.</em></p>
<p><strong>Cost impact:</strong> Remember the cost comparison that motivated us? Here’s how it played out:</p>
<ul>
<li><p>The Session Manager approach requires a small EC2 instance running 24/7. Our t4g.nano plus storage costs about <strong>$5 per month</strong>. We could even stop it out of hours, but at that price it’s not worth the hassle.</p>
</li>
<li><p>Session Manager itself doesn’t cost extra; it’s a feature of AWS Systems Manager. There is no hourly charge for sessions, and data transfer is minimal (just the database traffic which we’d have anyway; it might incur tiny charges if it goes through a NAT or VPC endpoint, but those are pennies).</p>
</li>
<li><p>Versus Verified Access, which would have been around <strong>$0.27/hour</strong> each for our databases (≈$200/month per DB) , the savings are enormous. Even factoring in the old Windows bastion cost (say ~$50/month), Session Manager is an order of magnitude cheaper. Essentially, we got nearly the same functionality for <strong>almost no cost</strong> in our AWS bill.</p>
</li>
</ul>
<h3 id="heading-3-smoothing-the-workflow-making-it-easy-for-engineers"><strong>3. Smoothing the Workflow (Making it Easy for Engineers)</strong></h3>
<p>Running a long CLI command to start the tunnel was fine for us, but we wanted to make this as seamless as possible – especially for new engineers who might not be AWS CLI wizards. We took a couple of steps to streamline usage on our laptops:</p>
<ul>
<li><p><strong>Bash Script &amp; Alias:</strong> We wrapped the aws ssm start-session command in a simple shell script (<a target="_blank" href="http://connect-db.sh">connect-db.sh</a>) and put it in our team’s internal toolkit repository. It accepts the environment or database name as an argument, so it knows which instance and host to target. For example: <a target="_blank" href="http://connect-db.sh">connect-db.sh</a> prod reporting-db would fetch the appropriate instance ID and DB host from a config and run the above command. Developers can alias this in their shell, so bringing up the tunnel is one short command away. Each script execution opens a new terminal window with the session (so we remember to close it when done).</p>
</li>
<li><p><strong>Auto-Connect on macOS (Launch Agent):</strong> For those frequently connecting to a dev database, we created a Launch Agent on macOS to <strong>automatically start the tunnel at login</strong>. This uses a .plist file in ~/Library/LaunchAgents. Here’s a snippet of what that looks like:</p>
</li>
</ul>
<pre><code class="lang-xml"><span class="hljs-comment">&lt;!-- ~/Library/LaunchAgents/com.mycompany.ssm-tunnel.plist --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">plist</span> <span class="hljs-attr">version</span>=<span class="hljs-string">"1.0"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">dict</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>Label<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>com.mycompany.ssm-tunnel<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>ProgramArguments<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">array</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>/usr/local/bin/aws<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>ssm<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>start-session<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>--target<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>i-0123456789abcdef0<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>--document-name<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>AWS-StartPortForwardingSessionToRemoteHost<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>--parameters<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>host=mydatabase.cluster-abcdefghijkl.us-east-1.rds.amazonaws.com,portNumber=5432,localPortNumber=5432<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">array</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>RunAtLoad<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">true</span>/&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>KeepAlive<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">true</span>/&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>StandardOutPath<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>/tmp/ssm-tunnel.log<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>StandardErrorPath<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>/tmp/ssm-tunnel.err<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">dict</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">plist</span>&gt;</span>
</code></pre>
<ul>
<li><p>In plain English, this Launch Agent definition does: when I log in, run the AWS CLI session manager command with the given parameters. The RunAtLoad means start it automatically, and KeepAlive means if it crashes or the session drops, launchd will restart it. We log output to /tmp for debugging. After loading this (launchctl load -w ~/Library/LaunchAgents/com.mycompany.ssm-tunnel.plist), the developer gets a persistent tunnel in the background. They can now connect to the DB anytime without even thinking about the tunnel – it’s just there. (We set KeepAlive so that if the session times out after inactivity, it will try to reconnect. One caveat: Session Manager sessions do have a max duration, a few hours, so the agent will reconnect a few times a day in the background.)</p>
</li>
<li><p><strong>Using SSH Config (alternate method, which we used eventually):</strong> Another neat trick is to use the SSH client as a wrapper for Session Manager. This might sound odd since we said “no SSH”, but it’s just leveraging the SSH command as a convenient way to manage tunnels. By adding an entry in ~/.ssh/config that calls the Session Manager proxy, one can bring up a tunnel with a simple ssh invocation. For example:</p>
</li>
</ul>
<pre><code class="lang-apache"><span class="hljs-attribute">Host</span> rds-tunnel
  <span class="hljs-attribute">HostName</span> i-<span class="hljs-number">0123456789</span>abcdef<span class="hljs-number">0</span>
  <span class="hljs-attribute">User</span> ec<span class="hljs-number">2</span>-user
  <span class="hljs-attribute">ProxyCommand</span> aws ssm start-session --target %h --document-name AWS-StartPortForwardingSessionToRemoteHost --parameters <span class="hljs-string">"host=mydatabase.cluster-abcdefghijkl.us-east-1.rds.amazonaws.com,portNumber=5432,localPortNumber=5432"</span>
</code></pre>
<ul>
<li>With such an entry, running ssh -N rds-tunnel will trigger the AWS CLI to start the session (the %h is replaced with the instance ID as HostName). The -N flag tells SSH not to execute a remote command (since we aren’t actually going to log in; we just want the tunnel). This is a bit of a hack and still requires the AWS CLI, but some GUI tools can invoke SSH tunnels this way as well.</li>
</ul>
<h3 id="heading-4-results-a-happy-team-with-secure-access"><strong>4. Results: A Happy Team with Secure Access</strong></h3>
<p>Once we rolled out the Session Manager solution, feedback from the team was very positive. It achieved what we wanted:</p>
<ul>
<li><p><strong>Greatly improved UX:</strong> Just like with Verified Access, engineers use their local tools and don’t have to maintain a remote VM workspace. Whether it’s a newbie using a point-and-click SQL client or a veteran automating a psql script, they run it from their machine as if the database were local. Onboarding a new engineer to access the DB is as simple as: “Install AWS CLI (or our helper script), run this command, and you’re in.”</p>
</li>
<li><p><strong>Tight Security (no more open holes):</strong> We completely shut down the old bastion. No RDP, no SSH – nothing is exposed. The EC2 instance is invisible from the internet. Session Manager uses an encrypted TLS connection initiated from the inside, and requires the user to auth with AWS. This <strong>removed a major attack surface</strong>. As AWS’s own best practices note, Session Manager eliminates the need for bastion hosts or open inbound ports . We also benefit from audit logs; we can see which user opened a session at what time in AWS CloudTrail, and even log the I/O if we wanted to inspect what commands are run (for shell sessions).</p>
</li>
<li><p><strong>Low Maintenance:</strong> The EC2 tunnel instance is about as low-touch as it gets. Amazon Linux 2 applies security patches on reboot easily; we can also bake an AMI periodically with updates if we ever needed to replace it. The SSM Agent updates itself automatically via AWS Systems Manager. There are no user accounts or keys on this instance to manage. In fact, the instance runs with no human login at all. If we want to administer it, we’d use Session Manager to get a shell. This dramatically reduces the admin overhead compared to the old Windows bastion that needed active user management and patching. And unlike Verified Access, there’s no separate client software for us to deploy to everyone – just the ubiquitous AWS CLI.</p>
</li>
<li><p><strong>Cost Savings:</strong> We already calculated the stark difference – on the order of maybe $10/month vs $200-$600/month for our scenario. Over a year, that’s thousands saved, which matters for our budget. We’re effectively paying only for a tiny instance and using an AWS service that’s <em>free</em> (covered by the fact we use AWS in general). For larger orgs, the cost argument might be different, but for us this was a huge win.</p>
</li>
<li><p><strong>Room for Expansion:</strong> With this setup, if we add more databases or even other internal services (e.g., an ElastiCache Redis, or an internal HTTP service), we have options. We can either use the same EC2 as a multi-purpose tunnel (starting separate sessions for different targets as needed), or create more instances if we want isolation per environment. Since it’s so cheap, spinning up one per environment or per service is not an issue. Session Manager even allows tunneling RDP or SSH if we ever needed GUI or console access to an instance – it’s versatile.</p>
</li>
</ul>
<h2 id="heading-conclusion-lessons-learned-and-tips"><strong>Conclusion: Lessons Learned and Tips</strong></h2>
<p>Our journey from a clunky bastion to a modern access solution taught us a few valuable lessons:</p>
<ul>
<li><p><strong>“New and shiny” isn’t always “better for us.”</strong> AWS Verified Access is a powerful service and no doubt the future for many zero-trust network scenarios. If we had strict device compliance requirements or a larger enterprise setup, its policy-based access and deep integration with corporate identity might be worth the cost. But in our case, the simpler Session Manager approach covered 90% of our needs at a fraction of the complexity and cost. It was a reminder that tried-and-true tools can sometimes beat bleeding-edge solutions, depending on the context.</p>
</li>
<li><p><strong>User experience matters, but balance it with security and cost.</strong> We were determined to improve UX for our engineers, and we did – moving away from the old jump box improved quality of life significantly. However, we had to also consider security (ensuring the new solution wasn’t trading one risk for another) and cost. We found a sweet spot where UX, security, and cost were all satisfied. Whenever you introduce a new access method, evaluate it holistically: how will users feel about it, is it actually secure, and does it justify the expense?</p>
</li>
<li><p><strong>AWS Session Manager is underrated.</strong> Many engineers know Session Manager as “that thing you can use instead of SSH to get a console”. But its port forwarding capability is a game-changer for scenarios like database access. It enabled us to implement a lightweight <strong>bastion-as-a-service</strong> without maintaining complex infrastructure. If you’re still using old bastion hosts or SSH tunnels, give Session Manager a serious look – it can simplify your life. As AWS’s security blog notes, Session Manager can eliminate the need for bastions and open ports while still giving you necessary access .</p>
</li>
<li><p><strong>Automation makes perfect.</strong> Once you set up a solution like this, invest a bit of time to script it and integrate it into your team’s workflows. Our use of Launch Agents and simple CLI wrappers means nobody is fumbling with long commands or forgetting to start their tunnel. New hires get a smooth experience from day one (“Just run this script and you’re connected”). Little quality-of-life improvements go a long way in adoption of a new tool.</p>
</li>
</ul>
<p>In the end, our team now connects to our databases securely, quickly, and with minimal fuss. We retired the fragile old Windows jump box and significantly cut down our attack surface. We also saved money, which is always a nice bonus. And when AWS improves Verified Access (maybe a fully managed client, Linux support, lower costs?), we’ll be ready to re-evaluate it. But for now, <strong>Session Manager has become our go-to solution</strong> for remote access to cloud resources.</p>
<p>If you’re in a similar boat – juggling bastions, VPNs, or pondering AWS Verified Access – I hope our story helps you find the approach that works best for you. Sometimes the solution is hiding in plain sight (in our case, in the AWS CLI we were already using). Happy connecting, and may all your database queries be speedy and secure!</p>
]]></content:encoded></item><item><title><![CDATA[Why Security-Conscious Startups Need DevSecOps from Day One]]></title><description><![CDATA[I vividly remember a night early in my career when I jolted awake at 3 AM to a barrage of Slack notifications. Our company’s app was under attack – a database had been left exposed, and an attacker was starting to poke around our customer data. We we...]]></description><link>https://thepawan.dev/why-security-conscious-startups-need-devsecops-from-day-one</link><guid isPermaLink="true">https://thepawan.dev/why-security-conscious-startups-need-devsecops-from-day-one</guid><category><![CDATA[Landing Zone Accelerator]]></category><category><![CDATA[Devops]]></category><category><![CDATA[DevSecOps]]></category><category><![CDATA[AWS]]></category><category><![CDATA[#Amazon GuardDuty]]></category><category><![CDATA[aws-inspector]]></category><category><![CDATA[Platform Engineering ]]></category><dc:creator><![CDATA[Pawan Sawalani]]></dc:creator><pubDate>Wed, 26 Mar 2025 22:41:51 GMT</pubDate><content:encoded><![CDATA[<p>I vividly remember a night early in my career when I jolted awake at 3 AM to a barrage of Slack notifications. Our company’s app was under attack – a database had been left exposed, and an attacker was starting to poke around our customer data. We were lucky to catch it in time, but that sleepless night was a wake-up call for me. It drove home a lesson I carry to this day: if you’re a security-conscious startup (and you should be), you need to embed security practices from day one. In other words, you need DevSecOps at the core of your startup’s DNA from the very beginning.</p>
<p>My name is Pawan. As a Lead DevSecOps Engineer at Prommt with 8+ years in AWS, platform engineering, and cloud security, I’ve seen firsthand how proactive security can make or break a young company. In this post, I want to share why integrating DevSecOps early isn’t a “nice-to-have” but a must-have for startups — and how it’s helped me build safer, faster-moving teams. I’ll also sprinkle in some personal war stories (including how we shaping DevSecOps workflow at Prommt) to illustrate the impact. So grab a coffee, and let’s dive in.</p>
<p><strong>The Startup Security Blind Spot</strong></p>
<p>Startups thrive on moving fast and innovating. “Move fast and break things,” right? The problem is, <em>what</em> breaks isn’t always just your code – it could be your security. In the rush to ship features and impress investors, it’s easy for a small team to push off security tasks. I’ve heard founders say, “We’ll worry about security after we get our first 100k users,” only to realize (sometimes painfully) that a single security mishap can <strong>stop</strong> them from ever getting to that milestone.</p>
<p>The truth is, even early-stage startups are juicy targets for cyber attackers. You might think, “We’re too small, who would target us?” But attackers often see startups as low-hanging fruit – less likely to have strong defenses, but still holding valuable data. A single breach can wreck your reputation and destroy user trust overnight. It might even scare off potential investors or partners. (As someone who’s had to field panicked investor calls after a security incident, I can tell you that nothing derails a promising pitch faster than news of a breach.)</p>
<p>So what’s the solution? It’s not to slow down or become risk-averse – it’s to build security into your fast-paced workflow. That’s exactly what DevSecOps is about. By weaving security into development and operations from day one, you mitigate that blind spot. Instead of a mad scramble to patch holes after an incident, you’re preventing those holes from appearing in the first place. Think of it as vaccinating your product against common exploits and misconfigurations. You still move fast, but you’re not flying blind.</p>
<p><strong>DevSecOps on a Startup Budget: Working Smarter with Automation</strong></p>
<p>One pushback I often hear is, “We’re just a tiny startup – we can’t afford a full security team or expensive tools yet.” The good news is, you don’t need a dedicated army of security engineers to start practicing DevSecOps. In fact, adopting DevSecOps early can save your startup time and money in the long run by catching issues early (when they’re cheapest to fix) and by reducing the likelihood of costly breaches.</p>
<p>DevSecOps isn’t about buying fancy appliances; it’s a mindset and a set of practices. For a startup, that means <strong>working smarter with the resources you have</strong>. Here are a few practical ways we’ve implemented DevSecOps on a lean budget:</p>
<p>• <strong>Automate Repetitive Checks:</strong> Use open-source or built-in tools to automate security tests in your CI/CD (Continuous Integration/Continuous Deployment) pipeline. For example, you can run static code analysis to catch vulnerable code, dependency scans to find risky libraries, and secret scanners to ensure no API keys slip into your repos. These can run on every code commit or pull request. In one of my previous roles, after we set up automated dependency scanning, we caught and fixed dozens of vulnerabilities <em>before</em> they ever made it to production – saving us from potential exploits down the road.</p>
<p>• <strong>Leverage Cloud Security Features:</strong> If you’re on AWS (as many startups are), take advantage of the free or low-cost security features from day one. AWS has services like GuardDuty (for threat detection), Config (for policy compliance checks), and CloudTrail (for auditing), to name a few. Turning these on early creates a safety net. I like to set up simple alerts for things like “unexpected public S3 bucket” or unusual login locations. It’s amazing how a few well-placed guardrails can prevent the classic rookie mistakes (like accidentally leaving storage buckets open to the world).</p>
<p>• <strong>Infrastructure as Code &amp; Safe Defaults:</strong> Define your infrastructure in code and put it in version control. By doing this, you can enforce best practices (like secure configurations) by default. For instance, our team uses Terraform to spin up cloud resources with security built-in – only necessary ports open, proper encryption enabled, least-privilege access roles, etc. Developers don’t have to think about those details because the infrastructure code and templates handle it. This way, security isn’t a one-off effort; it’s consistently applied every time we deploy.</p>
<p>The key is <strong>automation and consistency</strong>. As a startup, you want your small team focused on building the product, not doing tedious manual security reviews for each release. DevSecOps lets you automate those reviews. It’s like having a tireless security assistant who checks everything in the background while your team concentrates on core work.</p>
<p>By investing a bit of time to set up these automated checks and processes early, you avoid much bigger headaches (and costs) later. Trust me, spending an afternoon to script a security test is way better than spending a week doing damage control after a hack.</p>
<p><strong>Security Is a Team Sport: Building a Security-First Culture</strong></p>
<p>Tools and automation are fantastic, but DevSecOps is more than just tools – it’s about culture. One of the biggest benefits of adopting DevSecOps early is the ability to instill a <strong>security-first culture</strong> from the get-go. In a startup, culture is formed quickly by the founding team’s values and habits. If security is part of that DNA, it becomes a shared responsibility rather than “someone else’s problem.”</p>
<p>In practical terms, a security-first culture means developers, ops, and even product folks all recognize that security is part of their job. It doesn’t mean everyone becomes a security expert, but it does mean everyone cares about doing the right thing. Here are a few ways to nurture this culture:</p>
<p>• <strong>Lead by Example:</strong> As a tech leader or founder, your attitude towards security sets the tone. If you treat security as an important, enabling part of building software (e.g. asking “How can we make this feature secure by design?”) rather than as a hindrance, the team will follow. In my teams, I make a point to celebrate when someone proactively finds and fixes a security issue. It’s as worthy of praise as shipping a new feature.</p>
<p>• <strong>Empower and Educate:</strong> Give your team the knowledge and tools to act on security. At Prommt, we started “DevSync“, where Developer, Infra and ops get together and discuss/demo any new security enhancements and how it can be used in our web-app to improve the security even more. That knowledge paid off when we later caught a potential DDoS attack <em>before</em> it went live, just because we embedded the security enhancement discussed/demoed in our <em>DevSync</em>.</p>
<p>• <strong>Integrate, Don’t Silo:</strong> Avoid the old-school model of a separate security team that only swoops in at the end. In a startup, you probably don’t even have a separate security team – and that’s okay. Make security part of your development process. If you do have a security specialist (like me at Prommt), embed them in the design and sprint cycles rather than keeping them on the sidelines. When devs and ops see security folks as partners rather than gatekeepers, they’re more likely to engage us early to get things right.</p>
<p>By fostering this collaborative mindset early, you reduce friction later on. I’ve seen the contrast: in one organization that embraced DevSecOps from day one, developers would literally call me over to double-check their approach to storing passwords or to brainstorm the safest way to implement a feature. In another company that bolted on security much later, every security fix felt like pulling teeth – developers were defensive and saw it as “extra work.” The difference was night and day, and it all came down to culture.</p>
<p>Plus, a security-first culture is something you can proudly share with investors and customers. It shows that your startup is mature and trustworthy beyond its years. In the long run, that reputation can be as valuable as any feature on your roadmap.</p>
<p><strong>Our DevSecOps Journey at Prommt: Integrating AWS Security Services into Our CI/CD Pipeline</strong></p>
<p>Let me share a concrete example from my current role. When I joined Prommt as the Lead DevSecOps Engineer, we were a rapidly scaling startup in the payments industry. Given our responsibility for handling sensitive financial transactions, we understood immediately that security had to be integral, not an afterthought. Early on, we made deliberate choices to embed robust security measures like AWS WAF, Inspector, AWS Landing Zone, and GuardDuty directly into our development lifecycle through our CI/CD processes.</p>
<p>What does this integration look like in practice? Here are a few highlights:</p>
<p>• <strong>AWS Landing Zone as a Secure Foundation</strong>: We adopted AWS Landing Zone to establish a secure, multi-account AWS environment right from the start. This provided us a standardized baseline for managing account governance, access controls, and compliance across the organization, reducing operational overhead and security risks significantly.</p>
<p>• <strong>Security Embedded CI/CD Pipelines</strong>: Every code commit automatically triggers our CI/CD pipeline, which includes security checks leveraging SonarQube, Automated Security Helper (ASH) and AWS Inspector. If critical issues are detected, deployments are halted immediately, providing developers clear and actionable remediation guidance. Catching vulnerabilities early means fewer risks make it into production.</p>
<p>• <strong>Real-time Threat Detection with AWS GuardDuty</strong>: We integrated AWS GuardDuty into our platform to provide continuous threat detection and monitoring. GuardDuty analyzes logs and network activities across our AWS accounts, automatically identifying suspicious activities such as unauthorized access attempts or unusual API calls. This proactive approach helps us quickly pinpoint and address security incidents before they escalate.</p>
<p>• <strong>Web Application Firewall (AWS WAF)</strong>: To protect our web-facing services, we implemented AWS WAF at the platform level. WAF provides robust protection against common web exploits such as SQL injection and cross-site scripting (XSS). By integrating WAF rules directly into our deployment process, each new web application or microservice is immediately shielded from known threats, significantly reducing our attack surface.</p>
<p>Integrating these AWS security tools delivered substantial benefits:</p>
<p>• <strong>Streamlined Security Operations</strong>: With AWS Landing Zone and GuardDuty in place, we've significantly cut down on manual security management tasks. Our security team spends less time firefighting and more time improving proactive security measures.</p>
<p>• <strong>Enhanced Developer Confidence</strong>: Embedding security directly within CI/CD pipelines reduces the stress associated with deployments. Developers appreciate the rapid feedback provided by SonarQube, Automated Security Helper (ASH), AWS Inspector and GuardDuty, knowing that potential issues are flagged immediately, allowing them to address security as part of their normal workflow.</p>
<p>• <strong>Simplified Compliance Audits</strong>: As a payments-focused startup regularly audited for compliance (PCI-DSS, GDPR), the standardization offered by AWS Landing Zone and the continuous protection of GuardDuty and WAF makes audit processes smoother and less disruptive. Our security practices are transparent and consistently enforced, satisfying auditors and stakeholders alike.</p>
<p>Perhaps most importantly, integrating AWS security services has significantly boosted team morale. Developers at Prommt now spend less time worrying about hidden vulnerabilities or security misconfigurations and more time delivering secure, quality features to our customers. Security has genuinely become an empowering aspect of their everyday workflow.</p>
<p>The Prommt story is just one way to implement DevSecOps early, but it shows that even a small team can build powerful safeguards into their workflow. The key takeaway is to <strong>embed security and automation into the fabric of your development process</strong>. Whether it’s a full-blown IDP or simply a well-tuned CI/CD pipeline with security gates, that integration will pay dividends as you scale.</p>
<p><strong>The Payoff: Trust, Speed, and Peace of Mind</strong></p>
<p>Embracing DevSecOps from day one isn’t just about avoiding disasters – it can actually <strong>fuel your startup’s growth</strong>. When security is baked in early, you’re not constantly putting out fires. You can ship features faster and more confidently because the team isn’t bogged down by last-minute security scrambles or emergency patching.</p>
<p>There’s also a clear business upside. Customers and investors might not see all the under-the-hood work, but they <em>do</em>notice the outcomes: your product is reliable, there’s no news of data leaks, and you can speak confidently about your security practices. This builds trust. I’ve been in due diligence meetings where a startup’s security posture was a deciding factor for an investment. Being able to say “Yes, we have automated security testing, infrastructure guardrails, and a security-aware culture from day one” can literally <strong>secure</strong> the deal (pun intended). In a crowded market, a strong security story sets you apart – it signals that you’re not just moving fast, you’re moving fast <em>and</em> safe.</p>
<p>Finally, consider the human factor: peace of mind. Launching a startup is stressful enough without wondering if today is the day you’ll get hacked or accidentally expose user data. Knowing that you’ve put a DevSecOps foundation in place – even if it’s not perfect – helps everyone sleep a little better at night. I can attest that it’s a great feeling when weeks go by without any 3 AM security emergencies. Your on-call engineers will thank you when they’re not waking up to critical pager alerts every other week.</p>
<p><strong>Conclusion</strong></p>
<p>Security doesn’t have to be the enemy of innovation. In fact, when done right, it’s a catalyst for sustainable innovation. By adopting DevSecOps from day one, you’re investing in your startup’s long-term resilience. The payoff comes in many forms: fewer security incidents, faster delivery, happier developers, and greater trust from users and investors.</p>
<p>If you’re a startup founder or engineer, here are a few actionable takeaways to get started:</p>
<p>1. <strong>Start Small, But Start Now:</strong> Pick one or two security practices and integrate them into your development process <em>this week</em>. For example, enable an automated dependency scan in your build, or add a step in code review to check for basic security issues. You don’t have to do everything at once – the important part is to begin.</p>
<p>2. <strong>Automate the Basics:</strong> Identify common security “gotchas” (like leaked credentials, open admin ports, or outdated libraries) and use scripts or tools to check for them continuously. Set up alerts for the critical ones. Early automation yields big benefits with minimal effort.</p>
<p>3. <strong>Educate &amp; Involve the Team:</strong> Share at least one security tip or lesson in your next team meeting. Encourage questions about security when designing features. Maybe even host a casual threat modeling session over pizza. Make security a normal part of the conversation, not a taboo topic.</p>
<p>4. <strong>Leverage the Community and Tools:</strong> You’re not alone in this. There are plenty of free resources, open-source tools, and communities (DevSecOps forums, blogs, etc.) where you can learn tips tailored for startups. Don’t reinvent the wheel – borrow proven ideas and adapt them to your needs.</p>
<p>In the end, DevSecOps is about building a company that can move fast <strong>and</strong> confidently. I’ve been on both sides – the frantic firefighting mode and the smooth, secure delivery mode – and I can’t recommend the proactive approach enough. Security-conscious startups set themselves up for success by treating DevSecOps as a day-one priority.</p>
<p>Thanks for reading! If you have questions or want to share your own startup security stories, drop a comment – I’d love to hear your experiences.</p>
]]></content:encoded></item></channel></rss>