<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dmitriy Trunov</title>
    <description>The latest articles on DEV Community by Dmitriy Trunov (@dmitriy_trunov_9a09a497b1).</description>
    <link>https://dev.to/dmitriy_trunov_9a09a497b1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3535387%2F758269ae-cfe3-444b-85ef-03ca4d2e1de9.png</url>
      <title>DEV Community: Dmitriy Trunov</title>
      <link>https://dev.to/dmitriy_trunov_9a09a497b1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmitriy_trunov_9a09a497b1"/>
    <language>en</language>
    <item>
      <title>I Gave Three Broken AWS Environments to an AI On-Call Engineer. Here's What AWS DevOps Agent Found</title>
      <dc:creator>Dmitriy Trunov</dc:creator>
      <pubDate>Sun, 11 Oct 2026 19:50:16 +0000</pubDate>
      <link>https://dev.to/dmitriy_trunov_9a09a497b1/i-gave-three-broken-aws-environments-to-an-ai-on-call-engineer-heres-what-aws-devops-agent-found-3lk4</link>
      <guid>https://dev.to/dmitriy_trunov_9a09a497b1/i-gave-three-broken-aws-environments-to-an-ai-on-call-engineer-heres-what-aws-devops-agent-found-3lk4</guid>
      <description>&lt;p&gt;It’s 2:00 AM. A CloudWatch alert triggers. The on-call engineer wakes up, opens five console tabs, and begins reconstructing the timeline of events from scratch.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;AWS DevOps Agent&lt;/strong&gt; is designed to handle that initial hour of painstaking analysis. AWS refers to it as a "frontier agent"- one that not only resolves incidents but prevents them before they occur. It analyzes CloudWatch metrics, CloudTrail data, VPC Flow Logs, RDS Performance Insights, and IaC stacks, subsequently providing a root-cause analysis and an action plan for remediation.&lt;/p&gt;

&lt;p&gt;I was curious to see how this works in practice, so I attended an AWS DevOps Agent workshop focused on incident investigation. The workshop consisted of four labs: one on system setup and three involving specially prepared environments with pre-injected faults. I attended this engaging event - organized by the &lt;strong&gt;AWS User Group 3City&lt;/strong&gt; community in collaboration with &lt;strong&gt;AWS&lt;/strong&gt;-some time ago.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;The AWS DevOps Agent console: "Resolve. Prevent. Improve."&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;What I broke&lt;/th&gt;
&lt;th&gt;What the agent concluded&lt;/th&gt;
&lt;th&gt;Time to root cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EC2 / Auto Scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ran a CPU-burn script on the ASG through SSM&lt;/td&gt;
&lt;td&gt;The CPU alarm had &lt;strong&gt;no actions attached&lt;/strong&gt; and the ASG had &lt;strong&gt;no scaling policy&lt;/strong&gt;, so the group couldn't scale out&lt;/td&gt;
&lt;td&gt;~10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Port scan + HTTP flood from my laptop&lt;/td&gt;
&lt;td&gt;A security group open to &lt;code&gt;0.0.0.0/0&lt;/code&gt; on 22/80/443/8080 since stack creation. It also found that &lt;strong&gt;3 of 4 earlier alarm firings were false positives&lt;/strong&gt; caused by S3 return traffic&lt;/td&gt;
&lt;td&gt;~20–25 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RDS MySQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;140 connections + a terrible &lt;code&gt;CROSS JOIN … ORDER BY RAND()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A runaway workload from the bastion host using the &lt;code&gt;admin&lt;/code&gt; user. It also spotted &lt;strong&gt;broken CloudWatch log export&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;~16 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In all three labs the agent did more than restate the alarm. It found the &lt;strong&gt;configuration gap underneath it&lt;/strong&gt;, tied the fix back to the &lt;strong&gt;CloudFormation stack&lt;/strong&gt; so the fix wouldn't become drift, and wrote a mitigation plan in five phases: &lt;em&gt;prepare → pre-validate → apply → post-validate → rollback&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: Setting it up takes about 5 minutes
&lt;/h2&gt;

&lt;p&gt;You create an &lt;strong&gt;Agent Space&lt;/strong&gt;. This is the boundary for what the agent can see and do. In the setup wizard you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name the space and optionally choose a response language (Russian, Japanese, Portuguese and others are available)&lt;/li&gt;
&lt;li&gt;let it auto-create a &lt;strong&gt;read role&lt;/strong&gt; for your primary AWS account&lt;/li&gt;
&lt;li&gt;optionally turn on &lt;strong&gt;Agent Actions&lt;/strong&gt;, a separate role for write actions. Every change needs your explicit approval.&lt;/li&gt;
&lt;li&gt;turn on the &lt;strong&gt;web app&lt;/strong&gt;, which is the operator UI your on-call team works in&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;p&gt;Once the space exists, you can add more &lt;strong&gt;capabilities&lt;/strong&gt;: secondary AWS accounts, Azure, GitHub/GitLab/Azure DevOps pipelines, Datadog/Dynatrace/New Relic/Splunk/Grafana telemetry, Slack/ServiceNow/PagerDuty, any &lt;strong&gt;MCP server&lt;/strong&gt;, and even &lt;strong&gt;remote agents over the A2A protocol&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 Full walkthrough: &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-1-setting-up-your-first-agent-space-2ib3"&gt;Part 1: Setting up AWS DevOps Agent&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: "Why didn't my Auto Scaling Group scale?"
&lt;/h2&gt;

&lt;p&gt;A Lambda used SSM Run Command to push one t3.micro instance to 100% CPU. The CloudWatch alarm went red, and the group stayed at one instance.&lt;/p&gt;

&lt;p&gt;About 1m48s in, the agent had its first finding: &lt;strong&gt;&lt;code&gt;ScalingPolicies: []&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;AlarmActions: []&lt;/code&gt;&lt;/strong&gt;. The alarm worked. It just wasn't wired to anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5brxb2q4i0nok7jkb1z5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5brxb2q4i0nok7jkb1z5.png" alt="First finding at +1m48s: no scaling policies on Lab2-ASG" width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ruled out &lt;strong&gt;CPU credit throttling&lt;/strong&gt;, since &lt;code&gt;CPUCreditBalance&lt;/code&gt; stayed at the t3.micro maximum of 288&lt;/li&gt;
&lt;li&gt;showed the spike &lt;strong&gt;wasn't user traffic&lt;/strong&gt;, because the ALB stayed at 0–1 req/min&lt;/li&gt;
&lt;li&gt;found the exact &lt;strong&gt;SSM Run Command ID&lt;/strong&gt; that started the CPU burn&lt;/li&gt;
&lt;li&gt;said plainly what it &lt;strong&gt;couldn't&lt;/strong&gt; do: CloudTrail was blocked in the workshop account, so it couldn't attribute the IAM principal&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;👉 &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-2-why-didnt-my-auto-scaling-group-scale-ma0"&gt;Part 2: EC2 CPU spike investigation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: Investigating a network attack using VPC Flow Logs
&lt;/h2&gt;

&lt;p&gt;I ran a script from my laptop that port-scanned the web server, enumerated admin pages, tried SSH logins and flooded it with HTTP requests.&lt;/p&gt;

&lt;p&gt;The agent rated it &lt;strong&gt;Severity: high&lt;/strong&gt;. Using VPC Flow Logs, it identified the source IP, listed the scanned ports, and confirmed that &lt;strong&gt;2,764 accepted packets on port 80&lt;/strong&gt; were the real attack.&lt;/p&gt;

&lt;p&gt;The part that stood out was &lt;strong&gt;historical context&lt;/strong&gt; I never asked for. Looking at earlier firings of the same alarm, it worked out that 3 of the 4 were &lt;strong&gt;benign S3 return traffic&lt;/strong&gt;, which means the alarm itself needs tuning.&lt;/p&gt;

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

&lt;p&gt;👉 &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-3-i-attacked-my-own-web-server-heres-what-the-ai-found-in-the-vpc-flow-2hdc"&gt;Part 3: Network security investigation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 4: RDS connection exhaustion, and where the agent contradicted itself
&lt;/h2&gt;

&lt;p&gt;A script on the bastion host opened about 140 connections against a &lt;code&gt;max_connections=150&lt;/code&gt; limit and pinned CPU with Cartesian joins.&lt;/p&gt;

&lt;p&gt;The investigation was excellent. It split the load using &lt;strong&gt;Performance Insights&lt;/strong&gt; into 108 sessions sitting in &lt;code&gt;SLEEP()&lt;/code&gt; and 38 AAS of the &lt;code&gt;CROSS JOIN&lt;/code&gt; query, and traced all of it to a single host and DB user.&lt;/p&gt;

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

&lt;p&gt;Then I asked follow-up questions in chat and found &lt;strong&gt;two contradictions&lt;/strong&gt; between the chat and the mitigation plan. The two disagreed on whether 150 is above or below the default &lt;code&gt;max_connections&lt;/code&gt;, and on whether &lt;code&gt;wait_timeout&lt;/code&gt; would help. Part 4 covers which one was right.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-4-rds-connection-exhaustion-and-why-you-should-still-check-the-ais-answers-2ai6"&gt;Part 4: RDS performance investigation&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I liked
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It answers why, not only what.&lt;/strong&gt; None of the root causes was simply "CPU was high". Each was a missing link: no scaling policy, a world-open security group, a host with no connection limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It knows about IaC.&lt;/strong&gt; Every mitigation plan warned that an API-only fix would &lt;strong&gt;drift from the CloudFormation stack&lt;/strong&gt;, and every one included a &lt;em&gt;code change specification&lt;/em&gt; to update the template as well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mitigation plans include rollback.&lt;/strong&gt; Every plan had prepare, pre-validate, apply, post-validate and rollback steps with copy-paste CLI commands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It reports its gaps.&lt;/strong&gt; Each investigation had an "Investigation gaps" section listing what it couldn't access (CloudTrail, GuardDuty, RDS log files).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It respects guardrails.&lt;/strong&gt; Its safe-deployment policy refused to auto-run destructive security group rule removals and marked them for a human to run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent-ready spec.&lt;/strong&gt; You can copy the remediation as a spec for a coding agent to implement.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What to watch out for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verify the chat answers against the investigation.&lt;/strong&gt; The structured findings were more careful than the conversational answers (see Part 4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM scope determines what it can find.&lt;/strong&gt; The agent can only investigate what its role and your SCPs allow. In the workshop account, CloudTrail lookups, GuardDuty and RDS log downloads were blocked, and those gaps showed up in the results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Investigations take time.&lt;/strong&gt; Expect 8–16 minutes. That's quick compared with a human starting at 2 AM, but it isn't instant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignore the "Unable to describe support level" error.&lt;/strong&gt; The workshop says it's expected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://aws.amazon.com/ru/devops-agent/pricing/" rel="noopener noreferrer"&gt;AWS DevOps Agent Pricing&lt;/a&gt;&lt;/strong&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Should you try it?
&lt;/h2&gt;

&lt;h2&gt;
  
  
  I'd put it in the on-call path as the first responder that hands a human a pre-built timeline, not as an autonomous fixer… yet
&lt;/h2&gt;




&lt;p&gt;&lt;em&gt;Screenshots are from my own run of the AWS DevOps Agent workshop on 3 October 2026. All resources were in a temporary workshop account that has since been deleted.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>aws</category>
      <category>devops</category>
    </item>
    <item>
      <title>AWS DevOps Agent, Part 4: RDS Connection Exhaustion, and Why You Should Still Check the AI's Answers</title>
      <dc:creator>Dmitriy Trunov</dc:creator>
      <pubDate>Sun, 11 Oct 2026 19:50:01 +0000</pubDate>
      <link>https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-4-rds-connection-exhaustion-and-why-you-should-still-check-the-ais-answers-2ai6</link>
      <guid>https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-4-rds-connection-exhaustion-and-why-you-should-still-check-the-ais-answers-2ai6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 4 of 4. &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/i-gave-three-broken-aws-environments-to-an-ai-on-call-engineer-heres-what-aws-devops-agent-found-3lk4"&gt;Overview&lt;/a&gt; · &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-1-setting-up-your-first-agent-space-2ib3"&gt;Setup&lt;/a&gt; · &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-2-why-didnt-my-auto-scaling-group-scale-ma0"&gt;EC2&lt;/a&gt; · &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-3-i-attacked-my-own-web-server-heres-what-the-ai-found-in-the-vpc-flow-2hdc"&gt;Network&lt;/a&gt; · &lt;strong&gt;RDS&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Too many connections" is one of the most common database incidents, and diagnosing it usually takes a lot of manual work: Performance Insights, &lt;code&gt;SHOW PROCESSLIST&lt;/code&gt;, parameter groups and CloudWatch, all in separate tabs. This was the most interesting lab in the workshop. The agent's investigation was the best of the three, and then the follow-up chat taught me something about how far to trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

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

&lt;p&gt;&lt;em&gt;Lab architecture (redrawn from the AWS DevOps Agent workshop)&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ecommerce-db&lt;/strong&gt;: RDS MySQL on &lt;strong&gt;db.t3.medium&lt;/strong&gt; (2 vCPU, 4 GB), with Performance Insights and Enhanced Monitoring, and &lt;code&gt;max_connections=150&lt;/code&gt; in a custom parameter group&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lab4-BastionHost&lt;/strong&gt;: the only host that can reach the database, accessed through &lt;strong&gt;SSM Session Manager&lt;/strong&gt; (no SSH keys)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lab4-RDS-HighConnections&lt;/strong&gt;: an alarm on &lt;code&gt;DatabaseConnections&lt;/code&gt; above 100&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the bastion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su - ec2-user
&lt;span class="nb"&gt;cat &lt;/span&gt;baseline_complete.txt         &lt;span class="c"&gt;# Connection Status: SUCCESS&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/scripts
./simulate-db-performance-issue.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;creates an &lt;code&gt;ecommerce&lt;/code&gt; database (10K customers, 5K products, 20K orders)&lt;/li&gt;
&lt;li&gt;starts &lt;strong&gt;100 concurrent inefficient queries&lt;/strong&gt; (full scans, complex JOINs)&lt;/li&gt;
&lt;li&gt;opens &lt;strong&gt;130 connections&lt;/strong&gt;, then 140, close to the 150 limit&lt;/li&gt;
&lt;li&gt;keeps the CPU busy with continuous heavy queries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The alarm fired within a couple of minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The investigation: about 16 minutes
&lt;/h2&gt;

&lt;p&gt;I triggered it at &lt;strong&gt;11:40:04&lt;/strong&gt; and the banner said &lt;em&gt;Investigation completed&lt;/em&gt; at &lt;strong&gt;11:56:03&lt;/strong&gt;.&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;Severity: high.&lt;/strong&gt; The summary:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ecommerce-db experienced connection exhaustion: &lt;code&gt;DatabaseConnections&lt;/code&gt; stepped from &lt;strong&gt;0 to ~148&lt;/strong&gt; (flat against the max_connections=150 ceiling) and &lt;code&gt;CPUUtilization&lt;/code&gt; pinned at &lt;strong&gt;~100%&lt;/strong&gt; in the same minute, causing "Too many connections" errors and elevated query latency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the cause:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A runaway, manually launched workload from the &lt;strong&gt;Lab4-BastionHost&lt;/strong&gt; client (10.10.20.201), authenticating as MySQL superuser &lt;strong&gt;&lt;code&gt;admin&lt;/code&gt;&lt;/strong&gt;, started right after an interactive login by principal &lt;code&gt;Participant&lt;/code&gt; at 09:36 UTC. It combined &lt;strong&gt;~108 concurrent &lt;code&gt;SELECT SLEEP(?)&lt;/code&gt; sessions&lt;/strong&gt; holding connections open with a pathological &lt;strong&gt;&lt;code&gt;customers CROSS JOIN customers … ORDER BY RAND()&lt;/code&gt;&lt;/strong&gt; query (~38 active sessions) pinning the 2 vCPUs at ~100%.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is close to what an experienced DBA would write. It used &lt;strong&gt;Performance Insights&lt;/strong&gt; to split the load into &lt;em&gt;"User sleep" (108 AAS)&lt;/em&gt; and &lt;em&gt;executing CROSS JOIN (38 AAS)&lt;/em&gt;. It traced the workload to &lt;strong&gt;one host, one DB user, and one interactive login&lt;/strong&gt;. It explained &lt;strong&gt;all three symptoms&lt;/strong&gt; (connection exhaustion, CPU and latency) with a single cause. It also listed what it &lt;strong&gt;ruled out&lt;/strong&gt;: no infrastructure, parameter group, security group or scaling change; not I/O-bound; not memory-bound; no lock contention; no gradual leak.&lt;/p&gt;

&lt;p&gt;The connections-and-CPU chart shows the incident in one view:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  It found a second problem I didn't plant
&lt;/h2&gt;

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

&lt;p&gt;Under &lt;strong&gt;Investigation gaps&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;RDS log file download blocked by IAM permissions (&lt;code&gt;rds:DownloadDBLogFilePortion&lt;/code&gt; denied to the agent role); &lt;strong&gt;the CloudWatch error log stopped ingesting after 2026-09-30 and the general/slowquery log groups were never created despite being enabled for export.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the log export was &lt;strong&gt;broken&lt;/strong&gt; even though the configuration said it was enabled. In a real incident you'd go looking for the slow query log and find nothing. The agent added this to the mitigation plan as its own item (&lt;em&gt;"Repair CloudWatch log export so future diagnosis is not hampered"&lt;/em&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The mitigation plan
&lt;/h2&gt;

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

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

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kill the runaway MySQL sessions from the Lab4-BastionHost client, stop the source process, and raise max_connections from 150 back toward the db.t3.medium engine default (~341).&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The steps are specific to RDS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;List&lt;/strong&gt; the offending sessions with &lt;code&gt;information_schema.PROCESSLIST WHERE HOST LIKE '10.10.20.201%'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kill&lt;/strong&gt; them with &lt;strong&gt;&lt;code&gt;CALL mysql.rds_kill(&amp;lt;id&amp;gt;)&lt;/code&gt;&lt;/strong&gt;, because on RDS you can't use plain &lt;code&gt;KILL&lt;/code&gt; for other users' threads&lt;/li&gt;
&lt;li&gt;A helpful warning: &lt;em&gt;"With connections near the 150 cap, you may need to connect using the RDS reserved superuser slot… if 'Too many connections' blocks login, briefly raise max_connections first."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop the source process&lt;/strong&gt; on the bastion through SSM. Without this, &lt;em&gt;"mysql.rds_kill alone will not hold; connections will be re-opened within seconds."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raise &lt;code&gt;max_connections&lt;/code&gt; to 341&lt;/strong&gt;. It's a dynamic parameter, so no reboot is needed. It also warns you to watch &lt;code&gt;FreeableMemory&lt;/code&gt; and &lt;code&gt;SwapUsage&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;p&gt;Then &lt;strong&gt;post-validate&lt;/strong&gt; (parameter value, connections dropping, alarm back to OK) and a &lt;strong&gt;rollback&lt;/strong&gt; command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent-ready spec
&lt;/h3&gt;

&lt;p&gt;At the end there's a &lt;strong&gt;"Next steps → Agent-ready spec"&lt;/strong&gt; block with &lt;strong&gt;Copy spec&lt;/strong&gt;. It's a list of change requirements written for a coding agent (or a developer) to implement in source code and IaC:&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Introduce bounded, pooled database connectivity (RDS Proxy or an app-side pool)&lt;/li&gt;
&lt;li&gt;Stop using the all-privileges &lt;code&gt;admin&lt;/code&gt; account, and cap per-user connections with &lt;code&gt;MAX_USER_CONNECTIONS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fix the Cartesian &lt;code&gt;ORDER BY RAND()&lt;/code&gt; query&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;max_connections&lt;/code&gt; durably in IaC&lt;/li&gt;
&lt;li&gt;Repair CloudWatch log export&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each item has &lt;strong&gt;acceptance criteria&lt;/strong&gt;, for example: &lt;em&gt;"A load test reproducing the 09:38 workload no longer trips the Lab4-RDS-HighConnections alarm."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the chat contradicted the investigation
&lt;/h2&gt;

&lt;p&gt;Next I asked the workshop's follow-up questions in chat.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1gozdtfrxmuwwgrken41.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1gozdtfrxmuwwgrken41.png" alt="Chat: what is causing the exhaustion (note the max_connections claim)" width="799" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The answers were fluent and mostly good. &lt;em&gt;"Should I increase max_connections or implement connection pooling?"&lt;/em&gt; got a clear &lt;strong&gt;pooling first&lt;/strong&gt; answer with sensible reasoning. But two claims &lt;strong&gt;contradict the agent's own mitigation plan&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Contradiction 1: is 150 above or below the default?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mitigation plan:&lt;/strong&gt; 150 is &lt;em&gt;"artificially low"&lt;/em&gt;. The db.t3.medium engine default is &lt;strong&gt;~341&lt;/strong&gt;, so raise it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat:&lt;/strong&gt; &lt;em&gt;"&lt;code&gt;max_connections=150&lt;/code&gt; was manually **raised above&lt;/em&gt;* the db.t3.medium default (~90–100)."*&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The plan is correct.&lt;/strong&gt; The RDS MySQL default is &lt;code&gt;{DBInstanceClassMemory/12582880}&lt;/code&gt;. With about 4 GiB that comes to roughly &lt;strong&gt;341&lt;/strong&gt;, so 150 is well &lt;em&gt;below&lt;/em&gt; the default. &lt;/p&gt;

&lt;h3&gt;
  
  
  ❌ Contradiction 2: will &lt;code&gt;wait_timeout&lt;/code&gt; help?
&lt;/h3&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngnxly0folvxy96y3mna.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fngnxly0folvxy96y3mna.png" alt="Chat: parameter group advice (note the wait_timeout claim)" width="799" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mitigation plan:&lt;/strong&gt; &lt;em&gt;"wait_timeout/interactive_timeout do **NOT&lt;/em&gt;* reap SLEEP() sessions (they count as active), idle-timeout reaping is not an effective lever here."*&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat:&lt;/strong&gt; &lt;em&gt;"&lt;code&gt;wait_timeout&lt;/code&gt;: this is **the big one&lt;/em&gt;* for your SLEEP() problem… Lower this significantly so idle sessions get auto-killed."* It ranked this as &lt;strong&gt;priority #1&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The plan is correct again.&lt;/strong&gt; &lt;code&gt;SELECT SLEEP(n)&lt;/code&gt; is an &lt;em&gt;executing query&lt;/em&gt;. In the processlist it shows &lt;code&gt;Command = Query&lt;/code&gt;, not &lt;code&gt;Command = Sleep&lt;/code&gt;. &lt;code&gt;wait_timeout&lt;/code&gt; only closes connections that are &lt;strong&gt;idle between statements&lt;/strong&gt;. The chat mixed up "connection in Sleep state" with "connection running SLEEP()". (In the previous answer it also told me to look for &lt;code&gt;command = 'Sleep'&lt;/code&gt; to find these sessions, which is the same mix-up.)&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ What the chat did well
&lt;/h3&gt;

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

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

&lt;p&gt;To be fair, the answer to &lt;strong&gt;"Would migrating to Aurora MySQL help?"&lt;/strong&gt; was well calibrated. It said the root cause is a &lt;em&gt;workload problem, not an engine limitation&lt;/em&gt;, that Aurora would not have prevented this incident, and it added: &lt;em&gt;"I haven't verified current Aurora MySQL connection-handling specifics… against AWS documentation. I'd want to confirm those before you make a migration decision."&lt;/em&gt; That is the kind of honesty I want from an agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I take from this
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;structured investigation&lt;/strong&gt; (timeline, findings, root cause and mitigation plan) is built from evidence: actual API calls, metrics and Performance Insights data. The &lt;strong&gt;chat&lt;/strong&gt; answers sound just as confident but rely more on general knowledge, and that's where the mistakes appeared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat the investigation output as the source of truth, and check chat answers against it&lt;/strong&gt;, especially before changing parameters in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🧠 &lt;strong&gt;Performance Insights used well&lt;/strong&gt;: it separated idle-holding sessions from CPU-burning sessions, which is the key to the right fix&lt;/li&gt;
&lt;li&gt;🎯 It traced the problem to &lt;strong&gt;one host, one DB user and one login session&lt;/strong&gt; without being told where to look&lt;/li&gt;
&lt;li&gt;🪵 It found &lt;strong&gt;broken log export&lt;/strong&gt;, a problem I didn't plant, that would hurt the next investigation&lt;/li&gt;
&lt;li&gt;🤖 The &lt;strong&gt;agent-ready spec&lt;/strong&gt; with acceptance criteria is a good handoff to development&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;The chat contradicted the investigation twice.&lt;/strong&gt; Verify before you act.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;That's the end of the series. For the overall verdict and what I'd tell a team considering AWS DevOps Agent, see the &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/i-gave-three-broken-aws-environments-to-an-ai-on-call-engineer-heres-what-aws-devops-agent-found-3lk4"&gt;overview post&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>aws</category>
      <category>database</category>
      <category>devops</category>
    </item>
    <item>
      <title>AWS DevOps Agent, Part 3: I Attacked My Own Web Server. Here's What the AI Found in the VPC Flow Logs.</title>
      <dc:creator>Dmitriy Trunov</dc:creator>
      <pubDate>Sun, 11 Oct 2026 19:49:41 +0000</pubDate>
      <link>https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-3-i-attacked-my-own-web-server-heres-what-the-ai-found-in-the-vpc-flow-2hdc</link>
      <guid>https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-3-i-attacked-my-own-web-server-heres-what-the-ai-found-in-the-vpc-flow-2hdc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 3 of 4. &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/i-gave-three-broken-aws-environments-to-an-ai-on-call-engineer-heres-what-aws-devops-agent-found-3lk4"&gt;Overview&lt;/a&gt; · &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-1-setting-up-your-first-agent-space-2ib3"&gt;Setup&lt;/a&gt; · &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-2-why-didnt-my-auto-scaling-group-scale-ma0"&gt;EC2&lt;/a&gt; · &lt;strong&gt;Network&lt;/strong&gt; · &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-4-rds-connection-exhaustion-and-why-you-should-still-check-the-ais-answers-2ai6"&gt;RDS&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-2-why-didnt-my-auto-scaling-group-scale-ma0"&gt;Part 2&lt;/a&gt; was an operations problem. This one is a security problem: a public web server gets scanned and flooded. Can AWS DevOps Agent work out who did it, how, and what to fix, using only VPC Flow Logs and CloudWatch?&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

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

&lt;p&gt;&lt;em&gt;Lab architecture (redrawn from the AWS DevOps Agent workshop)&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lab3-WebServer&lt;/strong&gt;: a public EC2 instance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC Flow Logs&lt;/strong&gt; going to CloudWatch Logs (&lt;code&gt;/aws/vpc/flowlogs/Lab3-networksec&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lab3-UnusualConnections&lt;/strong&gt;: an alarm on &lt;code&gt;NetworkPacketsIn&lt;/code&gt; with a sum above 500 per 5 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From &lt;strong&gt;my own laptop&lt;/strong&gt; I ran the workshop's &lt;code&gt;simulate-attack.sh&lt;/code&gt; against the server's public IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x simulate-attack.sh
./simulate-attack.sh &amp;lt;WEB_SERVER_PUBLIC_IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script runs four phases over about 2 minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Port scan&lt;/strong&gt; of 15 common ports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web enumeration&lt;/strong&gt; of admin and login pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH brute force&lt;/strong&gt; with common usernames&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection flood&lt;/strong&gt; of 450+ HTTP requests&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;About 5 minutes later, &lt;code&gt;Lab3-UnusualConnections&lt;/code&gt; went to &lt;strong&gt;In alarm&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting the investigation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Details:&lt;/strong&gt; &lt;em&gt;Suspicious network traffic detected from external source - need to analyze attack patterns and assess security posture&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting point:&lt;/strong&gt; &lt;em&gt;CloudWatch alarm Lab3-UnusualConnections triggered due to high incoming network traffic. Investigate source, attack methodology, and recommend security improvements.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workshop says to expect 8–12 minutes, because querying Flow Logs takes longer than reading metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The verdict: Severity high
&lt;/h2&gt;

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

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

&lt;p&gt;The summary had three layers, and that structure is what I'd want from a human analyst as well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; security group &lt;code&gt;sg-…&lt;/code&gt; exposes &lt;strong&gt;TCP 22/80/443/8080 to &lt;code&gt;0.0.0.0/0&lt;/code&gt;&lt;/strong&gt;. This was &lt;em&gt;baked into the CloudFormation template at stack creation… never tightened since&lt;/em&gt;. CloudTrail confirmed there were no later manual changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct trigger:&lt;/strong&gt; because port 80 was open to everyone, an external IP (&lt;strong&gt;my laptop&lt;/strong&gt;) ran a recon port scan and then a &lt;strong&gt;Layer-7 HTTP flood on TCP/80&lt;/strong&gt;: 2,764 accepted packets, about 80% of inbound traffic. It also found a &lt;strong&gt;similar flood from a different IP on 30 September&lt;/strong&gt;, three days before my test and before I had run anything, so someone else had found the same open host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contributing cause:&lt;/strong&gt; the &lt;code&gt;NetworkPacketsIn&lt;/code&gt; alarm &lt;strong&gt;also fires on benign AWS/S3 return traffic&lt;/strong&gt; to ephemeral ports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It described the overall pattern as &lt;em&gt;"opportunistic, rotating-IP internet scanning/flooding of an exposed public host, not a single persistent actor, and not a control-plane compromise."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The false-positive finding
&lt;/h2&gt;

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

&lt;p&gt;I only asked about the latest alarm. The agent went back through &lt;strong&gt;every&lt;/strong&gt; firing of &lt;code&gt;Lab3-UnusualConnections&lt;/code&gt; since 30 September and broke each one down:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;3 of the 4 'recurring spike' windows were dominated by AWS service IP ranges (52.217.x, 52.216.x, 16.182.x) sending large packet bursts to high ephemeral destination ports, the signature of legitimate S3/download return traffic, not an attack.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words, &lt;strong&gt;the alarm is mostly noise&lt;/strong&gt;. A plain packet-count threshold can't tell an &lt;code&gt;aws s3 cp&lt;/code&gt; apart from a flood. If an on-call engineer was paged for all four of those, they would soon start ignoring it, and that's how a real attack gets missed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings and evidence
&lt;/h2&gt;

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

&lt;p&gt;The findings list shows how the agent labels its own reasoning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔴 &lt;strong&gt;root cause&lt;/strong&gt;: the world-open security group since stack creation&lt;/li&gt;
&lt;li&gt;🟠 &lt;strong&gt;cause&lt;/strong&gt;: open ports enable unsolicited scans; benign S3 traffic triggers the alarm&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;hypothesis, ruled out&lt;/strong&gt;: &lt;em&gt;"Security group modified after creation to open SSH/HTTP/HTTPS"&lt;/em&gt;. CloudTrail showed it was never modified.&lt;/li&gt;
&lt;li&gt;🔵 &lt;strong&gt;symptom&lt;/strong&gt;: recurring short spikes since 30 September&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;gap&lt;/strong&gt;: &lt;em&gt;GuardDuty access denied by SCP&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;observations&lt;/strong&gt;: no misuse of instance-role credentials, VPC Flow Logs confirm the flood and scan, no control-plane changes&lt;/li&gt;
&lt;li&gt;⚪ &lt;strong&gt;topology&lt;/strong&gt;: instance, VPC, subnet, security group, flow log ID and log group, all mapped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping the ruled-out hypotheses in the list is useful. In a postmortem, &lt;em&gt;what it wasn't&lt;/em&gt; matters as much as &lt;em&gt;what it was&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mitigation plan, with guardrails
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3e99lgp6hj0sdies03js.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3e99lgp6hj0sdies03js.png" alt="Pre-validate steps, including the SSM check before removing port 22" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff4pvjnarzu0pt9h331sn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff4pvjnarzu0pt9h331sn.png" alt="Revoke steps blocked from automatic execution by the safe-deployment policy" width="800" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The plan: &lt;strong&gt;remove the &lt;code&gt;0.0.0.0/0&lt;/code&gt; rules for 22 and 8080 completely, and scope 80/443 to trusted clients or an ALB+WAF, then reconcile the CloudFormation stack so the fix lasts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pre-validate steps are careful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Save the current rule set as the rollback baseline&lt;/li&gt;
&lt;li&gt;Confirm VPC Flow Logs are active so you can see the effect of each removal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check recent ACCEPTed flows on each port&lt;/strong&gt; so you don't cut off a legitimate client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check that the instance is reachable through SSM Session Manager &lt;em&gt;before&lt;/em&gt; removing port 22&lt;/strong&gt;, so you don't lock yourself out&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more detail: the apply steps that &lt;strong&gt;revoke&lt;/strong&gt; security group rules were flagged &lt;em&gt;"Automated execution of this removal is denied by the safe-deployment deletion-operation policy, must be run manually by an operator."&lt;/em&gt; The agent can propose destructive changes, but it won't run them itself. That's the right default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow-up chat
&lt;/h2&gt;

&lt;p&gt;I worked through the workshop's suggested questions in chat.&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;"What specific ports were targeted?"&lt;/strong&gt; It returned the full scan list (1433, 139, 21, 5432, 3306, 9200, 23, 53, 135, 3389, 8080, 443), then pointed out that the &lt;strong&gt;attack that actually succeeded&lt;/strong&gt; was only on &lt;strong&gt;port 80&lt;/strong&gt; (2,764 packets) and &lt;strong&gt;port 22&lt;/strong&gt; (57 packets, &lt;em&gt;"too small to be a brute-force attempt"&lt;/em&gt;). It explicitly &lt;strong&gt;ruled out SYN flood and SSH brute force&lt;/strong&gt; as the attack type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How can I prevent similar attacks?"&lt;/strong&gt; Lock down the security group, put &lt;strong&gt;WAF with rate limiting&lt;/strong&gt; in front of 80/443, enable &lt;strong&gt;GuardDuty&lt;/strong&gt; (which was blocked by an SCP in this account), and fix the alarm's false-positive rate.&lt;/p&gt;

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

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

&lt;p&gt;&lt;strong&gt;"Should I implement AWS WAF?"&lt;/strong&gt; It said yes, and then added the catch: &lt;em&gt;"WAF attaches to CloudFront, ALB, or API Gateway, not directly to an EC2 instance… That's a bigger architectural change than just 'turning on WAF'."&lt;/em&gt; It suggested a path (ALB → WAF rate-based rule → security group only from the ALB → managed rule groups) and &lt;strong&gt;asked for confirmation before building anything&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🕵️ It turned Flow Logs into a clear attack story: the source IP, the phases, which ports were only probed and which were actually hit&lt;/li&gt;
&lt;li&gt;🔕 It found an &lt;strong&gt;alarm-quality problem&lt;/strong&gt; I hadn't asked about. 3 of 4 firings were S3 noise.&lt;/li&gt;
&lt;li&gt;🧪 It kept &lt;strong&gt;ruled-out hypotheses&lt;/strong&gt; visible, which is useful for audits and postmortems&lt;/li&gt;
&lt;li&gt;🛑 &lt;strong&gt;Destructive changes are blocked from auto-execution&lt;/strong&gt; by policy&lt;/li&gt;
&lt;li&gt;🧱 It noticed the misconfiguration came &lt;strong&gt;from the IaC template&lt;/strong&gt;, and the fix targets the template&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, in &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-4-rds-connection-exhaustion-and-why-you-should-still-check-the-ais-answers-2ai6"&gt;Part 4&lt;/a&gt;: an RDS MySQL instance that runs out of connections, and a case where the agent's chat answers contradicted its own mitigation plan.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>networking</category>
      <category>security</category>
    </item>
    <item>
      <title>AWS DevOps Agent, Part 2: Why Didn't My Auto Scaling Group Scale?</title>
      <dc:creator>Dmitriy Trunov</dc:creator>
      <pubDate>Sun, 11 Oct 2026 19:49:07 +0000</pubDate>
      <link>https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-2-why-didnt-my-auto-scaling-group-scale-ma0</link>
      <guid>https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-2-why-didnt-my-auto-scaling-group-scale-ma0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 2 of 4. &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/i-gave-three-broken-aws-environments-to-an-ai-on-call-engineer-heres-what-aws-devops-agent-found-3lk4"&gt;Overview&lt;/a&gt; · &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-1-setting-up-your-first-agent-space-2ib3"&gt;Setup&lt;/a&gt; · &lt;strong&gt;EC2&lt;/strong&gt; · &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-3-i-attacked-my-own-web-server-heres-what-the-ai-found-in-the-vpc-flow-2hdc"&gt;Network&lt;/a&gt; · &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-4-rds-connection-exhaustion-and-why-you-should-still-check-the-ais-answers-2ai6"&gt;RDS&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a common incident: CPU hits 100%, the alarm goes red, &lt;code&gt;MaxSize&lt;/code&gt; is 3, and the Auto Scaling Group still runs &lt;strong&gt;one&lt;/strong&gt; instance. I gave this scenario to AWS DevOps Agent to see how far it got without help.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

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

&lt;p&gt;&lt;em&gt;Lab architecture (source: AWS DevOps Agent workshop)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The workshop pre-deploys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lab2-ASG&lt;/strong&gt;: an Auto Scaling Group of t3.micro instances (min 1, max 3) behind &lt;strong&gt;Lab2-ALB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lab2-ASG-CPU-High&lt;/strong&gt;: a CloudWatch alarm on the ASG's average CPU above 60%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lab2-RemoteLoadTrigger&lt;/strong&gt;: a Lambda that uses &lt;strong&gt;SSM Run Command&lt;/strong&gt; to run a CPU-burn script on every instance for 5 minutes, with no SSH needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To start the incident, I opened the Lambda, created a test event with &lt;code&gt;{}&lt;/code&gt; as the payload, and clicked &lt;strong&gt;Test&lt;/strong&gt;. About 2–3 minutes later the alarm went from &lt;strong&gt;OK&lt;/strong&gt; to &lt;strong&gt;In alarm&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting the investigation
&lt;/h2&gt;

&lt;p&gt;In the web app, &lt;strong&gt;Start investigation&lt;/strong&gt; opens a short form. It also offers quick prompts such as &lt;em&gt;Latest alarm&lt;/em&gt;, &lt;em&gt;High CPU usage&lt;/em&gt; and &lt;em&gt;Error rate spike&lt;/em&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Investigation details:&lt;/strong&gt; &lt;em&gt;ASG CPU utilization spiked to near 100% during load testing - need to determine root cause and assess infrastructure scaling&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; &lt;code&gt;Workshop_ASG_CPU_Investigation&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Priority:&lt;/strong&gt; Medium (default)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ You may see &lt;em&gt;"Unable to describe support level"&lt;/em&gt; in the bottom-left corner. The workshop says this is expected and safe to ignore.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Watching it think
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Summary&lt;/strong&gt; tab updates live. Within seconds the agent had found the ASG and described its current focus:&lt;/p&gt;

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

&lt;p&gt;The &lt;strong&gt;Investigation timeline&lt;/strong&gt; tab shows the reasoning step by step. Each entry lists the tool calls behind it (&lt;code&gt;use_aws&lt;/code&gt;, &lt;code&gt;datetime&lt;/code&gt;), and you can expand them to see the actual API calls:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkh6sx4av7vg3ykp8s0sj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkh6sx4av7vg3ykp8s0sj.png" alt="First finding at +1m48s: no scaling policies on Lab2-AS" width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is what happened in the first two minutes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;What the agent did&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;+11s&lt;/td&gt;
&lt;td&gt;Checked the current time and listed Auto Scaling Groups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+34s&lt;/td&gt;
&lt;td&gt;Found &lt;code&gt;Lab2-ASG&lt;/code&gt;: t3.micro, Min=1/&lt;strong&gt;Max=3&lt;/strong&gt;/Desired=1, part of CloudFormation stack &lt;code&gt;devops-agent-combined&lt;/code&gt;, &lt;strong&gt;group metrics not enabled&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+1m32s&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;"Very revealing findings already"&lt;/em&gt;: &lt;strong&gt;&lt;code&gt;ScalingPolicies: []&lt;/code&gt;&lt;/strong&gt;, alarm has &lt;strong&gt;&lt;code&gt;AlarmActions: []&lt;/code&gt;&lt;/strong&gt;, and scaling history has &lt;strong&gt;only one event ever&lt;/strong&gt; (the initial launch)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+1m32s&lt;/td&gt;
&lt;td&gt;Started a &lt;strong&gt;sub-agent&lt;/strong&gt; (&lt;code&gt;alb-traffic-analysis&lt;/code&gt;) in parallel to check whether real traffic caused the spike&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;+1m48s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Finding:&lt;/strong&gt; &lt;em&gt;No Auto Scaling policies configured on Lab2-ASG&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So in under two minutes it had the core answer: &lt;strong&gt;the alarm fires, but nothing listens to it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Ruling out the alternatives
&lt;/h2&gt;

&lt;p&gt;A good investigation also eliminates other explanations, and the agent kept checking after its first finding:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When exactly did it happen?&lt;/strong&gt; About 0.4% CPU baseline for 2+ days, then a single anomaly. At 1-minute resolution: 08:15 ramp (avg 19%), &lt;strong&gt;08:20 peak (avg 81%, max 100%)&lt;/strong&gt;, 08:25 back to idle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was it CPU credit throttling?&lt;/strong&gt; No. &lt;em&gt;"CPUCreditBalance held at the t3.micro maximum of 288 throughout… the 100% CPU was genuine compute demand, not a burst-credit starvation artifact."&lt;/em&gt; This is the kind of t3-specific check a person can easily forget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Was it user traffic?&lt;/strong&gt; No. The ALB sub-agent showed &lt;code&gt;Lab2-ALB&lt;/code&gt; at a baseline of 0–1 req/min during the spike.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;So what caused it?&lt;/strong&gt; It found the &lt;strong&gt;SSM Run Command&lt;/strong&gt; (&lt;code&gt;AWS-RunShellScript&lt;/code&gt;, with the exact CommandId) that ran a &lt;code&gt;yes &amp;gt; /dev/null&lt;/code&gt; loop per vCPU for 300 s. That is the mechanism the workshop uses, and the agent identified it &lt;strong&gt;without being told&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;It also recorded a &lt;strong&gt;gap&lt;/strong&gt;: it couldn't tell &lt;em&gt;who&lt;/em&gt; sent that SSM command, because &lt;code&gt;cloudtrail lookup_events&lt;/code&gt; was blocked in the workshop account. It noted this is &lt;em&gt;"not material to the root cause… but needed for full accountability."&lt;/em&gt; I value an agent that says what it couldn't check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause
&lt;/h2&gt;

&lt;p&gt;At &lt;strong&gt;+10m22s&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;No Auto Scaling policy wired to the CPU alarm.&lt;/strong&gt; Lab2-ASG has no scaling policies attached (&lt;code&gt;ScalingPolicies: []&lt;/code&gt;). The alarm Lab2-ASG-CPU-High exists and would enter ALARM during a CPU spike, but has no AlarmActions configured, so it cannot trigger scale-out.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;The &lt;strong&gt;Root cause&lt;/strong&gt; tab separates &lt;strong&gt;Impact&lt;/strong&gt;, &lt;strong&gt;Root causes&lt;/strong&gt; and &lt;strong&gt;Investigation gaps&lt;/strong&gt;, and has &lt;strong&gt;Copy/Download&lt;/strong&gt; buttons, so you can paste it straight into a postmortem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mitigation plan
&lt;/h2&gt;

&lt;p&gt;Clicking &lt;strong&gt;Generate mitigation plan&lt;/strong&gt; took a few minutes and produced this:&lt;/p&gt;

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

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

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Attach a target-tracking scaling policy (CPU target 55%) to Lab2-ASG *and codify it in the devops-agent-combined CloudFormation template.&lt;/strong&gt;*&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every step comes with a ready-to-run command:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Prepare.&lt;/strong&gt; Save the current ASG config and the empty policy list as a rollback baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-validate.&lt;/strong&gt; Confirm there's no conflicting policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   aws autoscaling put-scaling-policy &lt;span class="nt"&gt;--auto-scaling-group-name&lt;/span&gt; Lab2-ASG &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--policy-name&lt;/span&gt; Lab2-ASG-TargetTracking-CPU55 &lt;span class="nt"&gt;--policy-type&lt;/span&gt; TargetTrackingScaling &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--estimated-instance-warmup&lt;/span&gt; 120 &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--target-tracking-configuration&lt;/span&gt; &lt;span class="s1"&gt;'{"PredefinedMetricSpecification":{"PredefinedMetricType":"ASGAverageCPUUtilization"},"TargetValue":55.0,"DisableScaleIn":false}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Post-validate.&lt;/strong&gt; Check the policy and the CloudWatch alarms it creates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback.&lt;/strong&gt; Remove the policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three details here are worth pointing out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Drift warning:&lt;/strong&gt; &lt;em&gt;"Making this change directly via the API registers as drift against CloudFormation stack devops-agent-combined and may be reverted on the next stack update."&lt;/em&gt; The agent knows the ASG is managed by IaC.&lt;/li&gt;
&lt;li&gt;ℹ️ &lt;strong&gt;Reactive scaling has lag:&lt;/strong&gt; &lt;em&gt;"For a burst as short as ~10 minutes, new capacity may only become healthy near the end."&lt;/em&gt; It suggests scheduled scaling or a higher &lt;code&gt;MinSize&lt;/code&gt; ahead of known load tests.&lt;/li&gt;
&lt;li&gt;💰 &lt;strong&gt;Cost note:&lt;/strong&gt; more instances under load means a higher EC2 bill.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The finished summary
&lt;/h2&gt;

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

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

&lt;p&gt;The final &lt;strong&gt;Summary&lt;/strong&gt; shows status &lt;strong&gt;complete&lt;/strong&gt;, &lt;strong&gt;Severity: low&lt;/strong&gt; (correct, since there was no user-facing impact: zero 5XX errors and no latency increase), and an &lt;strong&gt;annotated incident timeline&lt;/strong&gt; that marks the CPU ramp, the SSM command, the peak, the ALARM transition and the return to OK.&lt;/p&gt;

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

&lt;p&gt;Below that is a &lt;strong&gt;Code change specification&lt;/strong&gt;: add an &lt;code&gt;AWS::AutoScaling::ScalingPolicy&lt;/code&gt; to the &lt;code&gt;Lab2AutoScalingGroup&lt;/code&gt; logical resource, decide what to do with the now-redundant alarm, and reconsider whether t3.micro is the right size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Asking follow-up questions
&lt;/h2&gt;

&lt;p&gt;While the investigation runs you can ask questions in chat. For example: &lt;em&gt;"Why didn't my ASG scale during the CPU spike?"&lt;/em&gt; My question went into a &lt;strong&gt;Queued requests&lt;/strong&gt; list, and the investigation addressed it as part of the work instead of starting a separate thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⏱ &lt;strong&gt;About 10 minutes&lt;/strong&gt; to a root cause I would have accepted from a senior engineer&lt;/li&gt;
&lt;li&gt;🔍 It &lt;strong&gt;ruled out&lt;/strong&gt; credit throttling and user traffic with evidence, instead of only confirming the first hypothesis&lt;/li&gt;
&lt;li&gt;🧱 It recognised that the resource is &lt;strong&gt;managed by CloudFormation&lt;/strong&gt; and wrote the fix for the template, not only for the API&lt;/li&gt;
&lt;li&gt;🙋 It &lt;strong&gt;listed what it couldn't verify&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, in &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-3-i-attacked-my-own-web-server-heres-what-the-ai-found-in-the-vpc-flow-2hdc"&gt;Part 3&lt;/a&gt;, I attack a web server from my laptop and see whether the agent can work out what happened using VPC Flow Logs.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>aws</category>
      <category>devops</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>AWS DevOps Agent, Part 1: Setting Up Your First Agent Space</title>
      <dc:creator>Dmitriy Trunov</dc:creator>
      <pubDate>Sun, 11 Oct 2026 19:48:09 +0000</pubDate>
      <link>https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-1-setting-up-your-first-agent-space-2ib3</link>
      <guid>https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-1-setting-up-your-first-agent-space-2ib3</guid>
      <description>&lt;p&gt;Part 1 of 4. &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/i-gave-three-broken-aws-environments-to-an-ai-on-call-engineer-heres-what-aws-devops-agent-found-3lk4"&gt;Overview&lt;/a&gt; · &lt;strong&gt;Setup&lt;/strong&gt; · &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-2-why-didnt-my-auto-scaling-group-scale-ma0"&gt;EC2&lt;/a&gt; · &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-3-i-attacked-my-own-web-server-heres-what-the-ai-found-in-the-vpc-flow-2hdc"&gt;Network&lt;/a&gt; · &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/aws-devops-agent-part-4-rds-connection-exhaustion-and-why-you-should-still-check-the-ais-answers-2ai6"&gt;RDS&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS DevOps Agent is an AI agent that investigates operational incidents in your AWS (and multicloud) environment. Before it can investigate anything, you need to give it a scope. That scope is called an &lt;strong&gt;Agent Space&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post covers the setup I went through in the &lt;em&gt;AWS DevOps Agent Workshop for Incident Investigation&lt;/em&gt;. It takes about 5 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an Agent Space?
&lt;/h2&gt;

&lt;p&gt;An Agent Space is a &lt;strong&gt;logical container&lt;/strong&gt; that defines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;what the agent can see&lt;/strong&gt;, meaning which AWS accounts, telemetry tools, repositories and pipelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;what the agent can do&lt;/strong&gt;, meaning read-only investigation, plus optional write actions that need your approval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;who can use it&lt;/strong&gt;, through the web app that your operators work in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can have several Agent Spaces, for example one per product team or per environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Open the service
&lt;/h2&gt;

&lt;p&gt;In the AWS Console (region &lt;strong&gt;us-east-1&lt;/strong&gt;), search for &lt;strong&gt;DevOps Agent&lt;/strong&gt; and click &lt;strong&gt;Begin setup&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;The left nav has two items: &lt;strong&gt;Agent Spaces&lt;/strong&gt; and &lt;strong&gt;Capability Providers&lt;/strong&gt;. Capability providers are registered at the account level and then attached to individual spaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create the Agent Space
&lt;/h2&gt;

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

&lt;p&gt;The form has five sections:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;What I chose&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent Space details&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Name &lt;code&gt;enterprise-workshop-space&lt;/code&gt;, description&lt;/td&gt;
&lt;td&gt;Shown to your operators in the web app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS resource access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Auto-create a new DevOps Agent role&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;The &lt;strong&gt;read&lt;/strong&gt; role the agent uses to investigate the primary account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent Actions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enabled, auto-create role&lt;/td&gt;
&lt;td&gt;A &lt;strong&gt;separate&lt;/strong&gt; role for write actions. &lt;em&gt;"Every change requires your explicit approval before it runs."&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tags&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enable web app&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Auto-create a new DevOps Agent role&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;The role behind the operator web UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For each role you get three options: auto-create, assign an existing role, or &lt;strong&gt;create one from a policy template&lt;/strong&gt;. In production you'll likely want the template option so the role goes through your normal IAM review.&lt;/p&gt;

&lt;p&gt;There's also an optional &lt;strong&gt;Agent response language&lt;/strong&gt; setting. Findings and investigation output can come back in 17 languages:&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Step 3: The agent starts mapping your environment
&lt;/h2&gt;

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

&lt;p&gt;Right after creation, the banner says &lt;strong&gt;"DevOps Agent is Mapping entities: Analyzing resources and their relationships."&lt;/strong&gt; By default the agent builds a &lt;strong&gt;topology&lt;/strong&gt; of resources managed by CloudFormation, or of resources that carry tags you specify. That topology grows as it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;runs investigations&lt;/li&gt;
&lt;li&gt;learns from what you tell it in the web app&lt;/li&gt;
&lt;li&gt;gets new capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This topology is what lets it work out later that "this alarm is on this ASG, which is behind this target group, which belongs to this stack".&lt;/p&gt;

&lt;p&gt;The space page has tabs for &lt;strong&gt;Capabilities, Access, Configuration, Summary report and Environments&lt;/strong&gt;. On the Capabilities tab, the &lt;strong&gt;Primary source&lt;/strong&gt; account shows both a read-role status and an &lt;strong&gt;Actions role status&lt;/strong&gt;. Both should say &lt;em&gt;Valid&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Explore the capabilities
&lt;/h2&gt;

&lt;p&gt;This is where the agent gets more than a basic CloudWatch reader. Each capability type adds a new data source or a new way to interact:&lt;/p&gt;

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

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;What it adds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Secondary AWS accounts for cross-account investigations, and Azure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Sandbox&lt;/strong&gt; &lt;em&gt;(Preview)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Controls outbound network access and pre-installed packages for the agent's sandbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Telemetry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dynatrace, Datadog, New Relic, Splunk, Grafana&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pipeline&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GitHub, GitLab, Azure DevOps, so the agent can link incidents to code changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Communications&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slack, ServiceNow, PagerDuty (with bidirectional support)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MCP Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Any MCP server (OAuth 3LO, client credentials, or API key)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Remote Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Other agents over the &lt;strong&gt;A2A protocol&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Webhooks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Let third-party systems start investigations (HMAC or API-key auth)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;Capability Providers&lt;/strong&gt; page shows the full list. 14 of 17 were available to register in my account:&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;💡 The MCP and A2A options stood out to me. You can plug in your own internal tooling (a runbook service, a CMDB, a home-grown deploy tool) without waiting for AWS to build a native integration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: Open the web app
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;console&lt;/strong&gt; is for admin tasks: creating spaces, wiring integrations and managing permissions. Day-to-day incident work happens in the &lt;strong&gt;web app&lt;/strong&gt;, which you open with &lt;strong&gt;Launch via IAM&lt;/strong&gt; (the workshop calls this &lt;em&gt;Operator access&lt;/em&gt;).&lt;/p&gt;

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

&lt;p&gt;The web app gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;chat&lt;/strong&gt; with prompts such as &lt;em&gt;"Give me a health summary"&lt;/em&gt; (with a &lt;em&gt;Balanced&lt;/em&gt; mode selector)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incidents&lt;/strong&gt;, the investigation dashboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improvements&lt;/strong&gt;, proactive recommendations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Releases&lt;/strong&gt; &lt;em&gt;(Preview)&lt;/em&gt;, release-readiness checks on code changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifacts&lt;/strong&gt; and &lt;strong&gt;Topology&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents&lt;/strong&gt;, &lt;strong&gt;Knowledge&lt;/strong&gt; and &lt;strong&gt;Settings&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;Incident Response Dashboard&lt;/strong&gt; is where you start investigations. It also shows an &lt;strong&gt;Investigation hours used&lt;/strong&gt; counter, which is worth watching from a cost point of view:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;p&gt;By the end of setup you should have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ An Agent Space with primary account status &lt;strong&gt;Valid&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ The Actions role &lt;strong&gt;Valid&lt;/strong&gt;, if you enabled Agent Actions&lt;/li&gt;
&lt;li&gt;✅ The web app opening through &lt;em&gt;Launch via IAM&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;✅ An empty Incident Response Dashboard, ready for the first incident&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In &lt;a href="//02-ec2-cpu-spike.md"&gt;Part 2&lt;/a&gt; I break something on purpose: an Auto Scaling Group at 100% CPU that doesn't scale. Then I let the agent work out why.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>aws</category>
      <category>devops</category>
    </item>
    <item>
      <title>The SDLC is dead. Long live the AI-DLC!?</title>
      <dc:creator>Dmitriy Trunov</dc:creator>
      <pubDate>Tue, 22 Sep 2026 09:33:18 +0000</pubDate>
      <link>https://dev.to/dmitriy_trunov_9a09a497b1/the-sdlc-is-dead-long-live-the-ai-dlc-34bi</link>
      <guid>https://dev.to/dmitriy_trunov_9a09a497b1/the-sdlc-is-dead-long-live-the-ai-dlc-34bi</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fob7clvkc3taax446vu8v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fob7clvkc3taax446vu8v.png" alt=" " width="800" height="333"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;AI-DLC vs SDLC: What I Kept, What I Changed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Progress never stands still; it is constantly evolving, bringing changes to our daily work. Today, I want to describe the changes that have taken place in my own workflows. For years, we relied on the &lt;strong&gt;SDLC&lt;/strong&gt; (Software Development Life Cycle) approach; now, it is time to adopt the &lt;strong&gt;AI-DLC&lt;/strong&gt; approach.&lt;/p&gt;

&lt;p&gt;The first question is: what exactly is &lt;strong&gt;AI-DLC&lt;/strong&gt;? AI-DLC does not replace the traditional SDLC; instead, it shifts task execution from humans to &lt;strong&gt;AI&lt;/strong&gt; while retaining the human role of approving the results.&lt;/p&gt;

&lt;p&gt;I incorporated the &lt;a href="https://github.com/awslabs/aidlc-workflows" rel="noopener noreferrer"&gt;AWS-developed&lt;/a&gt; AI-DLC system into my own task-planning application—treating it as a core product feature rather than just a personal coding workflow. (After all, every self-respecting programmer should have their own to-do app😂) &lt;br&gt;
You articulate an "&lt;strong&gt;Intent&lt;/strong&gt;" in your own words; this is broken down into "&lt;strong&gt;Units&lt;/strong&gt;"—large, epic-level tasks with defined acceptance criteria and dependencies. These units are then executed via "&lt;strong&gt;Bolts&lt;/strong&gt;"—execution cycles lasting anywhere from a few hours to several days. At various points throughout the process, execution pauses to await human approval, and a clear history of changes (artifacts) is preserved in the target repository.&lt;/p&gt;

&lt;p&gt;For those considering whether to adopt this approach, the most interesting aspect isn't its novelty. What matters is how much of the traditional &lt;strong&gt;SDLC&lt;/strong&gt; remains virtually unchanged, despite the shift to this new model of task execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 16 stages are the old SDLC phases, renamed
&lt;/h2&gt;

&lt;p&gt;The catalog runs three phases — &lt;strong&gt;Inception → Construction (per unit) → Operations&lt;/strong&gt; — across sixteen stages. Every one of them is a classic SDLC activity, just regrouped:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SDLC activity&lt;/th&gt;
&lt;th&gt;AI-DLC stage(s)&lt;/th&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Discovery / requirements gathering&lt;/td&gt;
&lt;td&gt;Workspace Detection, Requirements Analysis, User Stories&lt;/td&gt;
&lt;td&gt;Inception&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planning / estimation&lt;/td&gt;
&lt;td&gt;Workflow Planning, Units Generation, Delivery Planning&lt;/td&gt;
&lt;td&gt;Inception&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System design&lt;/td&gt;
&lt;td&gt;Application Design, Functional Design, Infrastructure Design&lt;/td&gt;
&lt;td&gt;Inception / Construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-functional requirements&lt;/td&gt;
&lt;td&gt;NFR Requirements, NFR Design&lt;/td&gt;
&lt;td&gt;Construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implementation&lt;/td&gt;
&lt;td&gt;Code Generation Planning, Code Generation&lt;/td&gt;
&lt;td&gt;Construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Build and Test&lt;/td&gt;
&lt;td&gt;Construction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Release&lt;/td&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ops / observability&lt;/td&gt;
&lt;td&gt;Monitoring&lt;/td&gt;
&lt;td&gt;Operations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nothing was dropped. What changed is &lt;em&gt;who&lt;/em&gt; performs each stage and &lt;em&gt;what&lt;/em&gt; gates the move to the next one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I retained from the SDLC
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The sequence of stages itself&lt;/strong&gt;. The "requirements → design → build → test → deploy → monitor" flow remains linear for each unit of work. AI-DLC did not invent a new lifecycle model but replicated the old one, substituting the human actors involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human approval stages&lt;/strong&gt;. Moving to the next stage still requires approval of the previous one (analogous to code reviews or checkpoints in traditional SDLC), but this is now enforced at the system architecture level rather than merely through procedural compliance: a stage launches only after all preceding stages in its scope are complete; a conditional write in DynamoDB prevents state conflicts during concurrent executions; and no runner process contains code to set the "approved" status—this is possible only via an HTTP request initiated by a human.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process documentation&lt;/strong&gt;. The results of each stage are mirrored in Markdown format (within the aidlc-docs/&amp;lt;intent-slug&amp;gt;/... directory of the target repository) alongside an audit.md file, which logs executions, approvals, change requests, and skipped stages. These are the same artifacts and audit logs always required in SDLC management—only now they are generated automatically rather than created manually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Work decomposition structure&lt;/strong&gt;. Units of work—complete with acceptance criteria and dependencies—serve as epics; "Bolts" function as sprints with a clear Definition of Done: a Bolt cannot be closed if even one of its constituent units of work has an unapproved mandatory stage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separation of concerns&lt;/strong&gt;. Design and analysis stages operate in a read-only planning mode; file write permissions are granted exclusively to implementation stages (code_generation, build_and_test, deployment, monitoring). The design phase cannot silently refactor the repository while the document is being prepared—this is the same principle underlying the rule that "reviewers cannot merge their own pull requests."&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What has actually changed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Execution has shifted from humans to AI; the human role is now limited to approving results&lt;/strong&gt;. In a classic SDLC, a human writes the requirements document. Here, however, stages such as ask-executor force an LLM tool call to generate the document, while the human's task is narrowed down to reviewing and either accepting or rejecting the output. There are two types of executors: claude (autonomous execution of Claude within a specific intent's workspace—for stages requiring repository read/write operations) and ask (forcing a tool call via a configured provider—for stages where the output must be displayed as a form, such as clarifying questions or a list of proposed work units). Stage output can also be entered manually via the &lt;code&gt;PUT /stages/:id/output&lt;/code&gt; endpoint, allowing the entire process to function even without AI configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The lifecycle is initialized for each unit of work rather than once for the entire project&lt;/strong&gt;. The stage catalog is data-driven: creating an "Intent" triggers its own "Inception" stages, while accepting a "&lt;strong&gt;Unit&lt;/strong&gt;" launches its own "Construction" stages. Traditional SDLC phases are typically project-level rituals performed once; here, they are generated anew for each Intent and Unit. Furthermore, Units can be processed in parallel and independently, as a specific Unit's stages depend only on preceding stages within that same Unit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two artifact stores instead of one&lt;/strong&gt;. DynamoDB serves as the source of truth (authoritative store); this is necessary because the web application and an agent connected via MCP might write data simultaneously, and only a robust database supports conditional update operations. The Markdown files in the &lt;code&gt;aidlc-docs/&lt;/code&gt; directory constitute a read-only mirror that is updated after every transition. In a traditional SDLC, no such separation exists; the document itself serves as the record—the single source of truth.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Even the "implementation of AI-DLC" did not mean blindly following the specification.
&lt;/h2&gt;

&lt;p&gt;This warrants a separate section: implementing the AWS methodology entailed deliberate and documented deviations from it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Upstream AI-DLC&lt;/th&gt;
&lt;th&gt;This implementation&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;aidlc-state.md&lt;/code&gt; is the state&lt;/td&gt;
&lt;td&gt;DynamoDB is authoritative; markdown is a mirror&lt;/td&gt;
&lt;td&gt;Two writers (web app + MCP) plus conditional-update locking need a real store — a markdown file can't express a &lt;code&gt;ConditionExpression&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every stage runs through the coding agent&lt;/td&gt;
&lt;td&gt;Form-shaped stages run on the LLM provider directly (&lt;code&gt;ask&lt;/code&gt; executor)&lt;/td&gt;
&lt;td&gt;Keeps file-write authority away from stages that have no use for it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mob Elaboration (synchronous group review)&lt;/td&gt;
&lt;td&gt;Single-player: an editable unit list plus request-changes&lt;/td&gt;
&lt;td&gt;This app has profiles, not accounts — the intent of the practice survives, the "whole team" part isn't modelled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;33 stages across five phases&lt;/td&gt;
&lt;td&gt;16 stages across three&lt;/td&gt;
&lt;td&gt;The catalog is data, so growing it back is a data change, not new code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implicit approval authority&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;approvedBy&lt;/code&gt; records who acted, not that they were allowed to&lt;/td&gt;
&lt;td&gt;There's no authentication in this app, so approval is a record, not a claim&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The point is not that these deviations suit everyone, but rather that "implementing AI-DLC" is not a binary process. Any team adopting a known methodology will adapt it to their system's actual capabilities; documenting these changes—rather than letting them devolve into imperceptible process "drift"—is, in itself, a valuable SDLC discipline.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second layer of governance atop the first.
&lt;/h2&gt;

&lt;p&gt;The AWS AI-DLC methodology provided me with stages, modules, and an approval checkpoint. It differs from Anthropic’s approach (the AI-native SDLC playbook), which outlines its own six-step model (&lt;strong&gt;Planning → Design → Development → Testing → Deployment → Support&lt;/strong&gt;) based on three governance layers: Skills (advisory rules and policies), Hooks (deterministic runtime safeguards), and human approval stages. Comparing my implementation against this second model revealed gaps that the AWS specification alone did not address, and I resolved all four:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skills (Rules/Guidelines)&lt;/strong&gt;. Markdown-based policy files (e.g., security checklists or compliance rules) placed by the user in the target workspace; these are automatically attached as context to stages marked as relevant (nfr_design, infrastructure_design, deployment). They are purely advisory in nature, with a level of trust equal to that of any other context passed to the stage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hooks&lt;/strong&gt;. The AIDLC_POST_RUN_HOOK variable executes a custom shell command after the successful completion of the code_generation or build_and_test stages, but before the stage transitions to the awaiting_approval state. A non-zero exit code causes the stage to fail immediately—this serves as a deterministic counterpart to the advisory approach used by Skills and is the mechanism most similar to Claude Code hooks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-based verification extended to code-related stages&lt;/strong&gt;. The previously existing optional verification step—formerly limited to the requirements or design phases—now also analyzes diffs during the code_generation stage and the results (success/failure) of the build_and_test stage. These checks are purely advisory and never block the process, thereby upholding the inviolable principle that "no executor can approve their own work."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An evaluation suite.&lt;/strong&gt; Tests located in &lt;code&gt;apps/api/tests/*.eval.ts&lt;/code&gt; verify that stage prompts align with the intents defined in the test data (fixtures); this ensures that any changes to prompts or the directory structure—which might otherwise silently break Workspace Detection or Units Generation mechanisms—are caught during the CI phase rather than in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All these features are optional and disabled by default—including two that I developed but did not bring into full compliance with the specification: AIDLC_PARALLEL_WORKTREES assigns a separate Git worktree and branch to each unit to ensure complete build isolation, yet it intentionally omits the automatic merge step following approval that was envisioned in the original playbook design. Automatically merging a branch into the actual Git history is too critical an action to entrust to the stage runner without human oversight; Consequently, the branch remains available for the user to review and merge manually—reflecting the general principle that final approval always rests with a human. The &lt;code&gt;POST /aidlc/alerts&lt;/code&gt; endpoint closes the operational loop by creating a draft standard Intent based on a monitoring system alert; however, there is no automatic approval or execution involved here either: the generated Intent enters the pipeline at the workspace detection stage alongside other Intents and undergoes the same validation checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When creating something similar, it is more useful to view the situation not as a clash between two lifecycle models—SDLC and AI-DLC—but from a different perspective. AI-DLC is essentially the same as SDLC, except that the execution vector is redirected toward AI, while the validation vector is intentionally retained by humans. Control is maintained through structural mechanisms (such as blocks, checkpoints, and access control systems) rather than team discipline; after all, discipline cannot be relied upon when the work between checkpoints is performed by an agent.&lt;/p&gt;

&lt;p&gt;Everything else—stages, approval procedures, documentation, audit logs, and backlog structure—is carried over from the old model rather than reinvented. The methodology’s name changes, but the fundamental nature of the work remains largely the same.&lt;/p&gt;

&lt;p&gt;Are there any among you who have fully switched to &lt;strong&gt;AI-DLC&lt;/strong&gt;? What methodology do you use? Have you made any changes to the used &lt;strong&gt;AI-DLC&lt;/strong&gt; specifications?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>aidlc</category>
      <category>aws</category>
    </item>
    <item>
      <title>The Database I Deleted: Migrating an Agentic RAG App to AWS Serverless #3</title>
      <dc:creator>Dmitriy Trunov</dc:creator>
      <pubDate>Sat, 19 Sep 2026 19:54:30 +0000</pubDate>
      <link>https://dev.to/dmitriy_trunov_9a09a497b1/the-database-i-deleted-migrating-an-agentic-rag-app-to-aws-serverless-3-k0d</link>
      <guid>https://dev.to/dmitriy_trunov_9a09a497b1/the-database-i-deleted-migrating-an-agentic-rag-app-to-aws-serverless-3-k0d</guid>
      <description>&lt;h3&gt;
  
  
  Part 3 of 3 — bugs, numbers, and what generalizes
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/migrating-an-agentic-rag-app-to-aws-serverless-d2k"&gt;Part 1&lt;/a&gt; covered why an agentic RAG assistant moved off a single EC2 box onto AWS serverless, and the resulting architecture. &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/the-database-i-deleted-migrating-an-agentic-rag-app-to-aws-serverless-n18"&gt;Part 2&lt;/a&gt; walked through five decisions that didn't survive contact with AWS's fine print: a database that turned out to be a file, why there's no streaming, an auth design CloudFront can't do, what Bedrock batch inference can't give you, and where the vector text actually lives. This part is the payoff — what the migration found, what it cost, and what's still unverified.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Two bugs the migration found in the &lt;em&gt;existing&lt;/em&gt; system
&lt;/h2&gt;

&lt;p&gt;Re-deriving the corpus surfaced two problems that had nothing to do with AWS.&lt;/p&gt;

&lt;h3&gt;
  
  
  303 chunks that could never be retrieved
&lt;/h3&gt;

&lt;p&gt;The original derives a chunk id as &lt;code&gt;{repo}::{file_path}::{section}&lt;/code&gt;. Two chunks in one file can share a composed heading — a repeated "Usage", an "Example" under two parents. I measured it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;doc_id collisions (repo::file::section): 303   (of 24,775 chunks)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RRF dedupes by &lt;code&gt;doc_id&lt;/code&gt;. So 303 chunks were silently shadowing each other during fusion — one of each pair could never surface, and which one survived depended on iteration order. A pre-existing retrieval bug, invisible from the outside, sitting in a system whose evaluation numbers I'd published.&lt;/p&gt;

&lt;p&gt;Adding a per-file ordinal to the hash takes it to zero.&lt;/p&gt;

&lt;h3&gt;
  
  
  A chunk that could not be embedded at all
&lt;/h3&gt;

&lt;p&gt;Chunk sizes look fine at the median and awful in the tail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          chunks    p50     p90      p99      max
before    24,775    602   2,797   10,483  119,786   bytes
after     25,482    621   3,096    7,905    7,998
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 119 KB chunk is roughly 30,000 tokens. &lt;strong&gt;Titan Text Embeddings caps input at 8,192.&lt;/strong&gt; It would not have embedded — it would have failed, or been silently truncated, and simply been missing from the vector index.&lt;/p&gt;

&lt;p&gt;The fix splits on paragraph boundaries, with a hard fallback for unbroken tables and code blocks, because "never split mid-paragraph" is a nice principle right up until it leaves you with a chunk that no embedding model will accept.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rate limiter had to change shape, not providers
&lt;/h2&gt;

&lt;p&gt;The crawler paces itself against GitHub's quota and sleeps until reset when it runs low. That's correct for a local run that owns the whole crawl. Inside a Lambda it is wrong twice over:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sleeping burns billed duration&lt;/strong&gt;, and a GitHub reset window runs up to an hour — well past the 15-minute limit. The function gets killed mid-sleep and loses its work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Under a Map, each child sees only its own responses&lt;/strong&gt; while the quota is account-global. Every child independently concludes it has headroom. &lt;code&gt;maxConcurrency&lt;/code&gt; bounds concurrent &lt;em&gt;executions&lt;/em&gt;, not requests per hour.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the crawler now raises instead of sleeping, carrying the reset time, and the state machine catches it into a &lt;code&gt;Wait&lt;/code&gt; — which costs nothing and can span an hour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;crawl_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;wait_for_quota&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CrawlRepoRetryTask&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;crawl_repo&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RateLimitedError&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;result_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$.error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GITHUB_RATE_LIMIT_MODE&lt;/code&gt; keeps the sleeping behaviour for local runs. And there is a test asserting the &lt;em&gt;class name&lt;/em&gt; — Step Functions matches on the error name the runtime reports, so renaming the exception would silently break the Wait path with no import error to warn you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spend cap, ported and improved
&lt;/h2&gt;

&lt;p&gt;The original's proudest feature was an atomic budget reservation: not "read the total, compare, spend" — which races under concurrent traffic — but one &lt;code&gt;UPDATE ... WHERE ... RETURNING&lt;/code&gt; that Postgres serializes.&lt;/p&gt;

&lt;p&gt;That property transfers exactly onto a DynamoDB conditional write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;get_table&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;update_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PK_BUDGET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;          &lt;span class="c1"&gt;# window = today, UTC
&lt;/span&gt;    &lt;span class="n"&gt;UpdateExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ADD total_spent :amt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ConditionExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attribute_not_exists(total_spent) OR total_spent &amp;lt;= :headroom&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:amt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;amt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:headroom&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;amt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same one-round-trip check-and-deduct, single-digit milliseconds, zero idle cost. Verified against a real DynamoDB implementation (via &lt;code&gt;moto&lt;/code&gt;) rather than reasoned about — 40 concurrent requests against a cap allowing 5 reservations grant &lt;strong&gt;exactly 5&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;attribute_not_exists&lt;/code&gt; branch is not decoration: &lt;code&gt;ADD&lt;/code&gt; creates the attribute, but the condition still has to pass &lt;em&gt;before&lt;/em&gt; it exists. Without that clause, every new day starts permanently blocked.&lt;/p&gt;

&lt;p&gt;And one deliberate behaviour change, which is really a bug fix. The original cap was a &lt;strong&gt;lifetime&lt;/strong&gt; total. That means one scraper — or one enthusiastic afternoon — exhausts it forever, and every future visitor sees "budget used up" for the life of the demo. Keying the budget item by UTC date makes the window structural: yesterday's spend is simply a different item, nothing has to reset anything, and the worst case is bounded to one day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the interface had to become
&lt;/h2&gt;

&lt;p&gt;Streamlit is a WebSocket server holding per-session state. That doesn't survive contact with Lambda, so the UI became a static SPA on S3 behind CloudFront.&lt;/p&gt;

&lt;p&gt;It is three files — &lt;code&gt;index.html&lt;/code&gt;, &lt;code&gt;styles.css&lt;/code&gt;, &lt;code&gt;app.js&lt;/code&gt; — with no build step and no framework. That's a deliberate match to the rest of the architecture: nothing idles, few moving parts, CI syncs three files. The API is the real contract, so React later changes nothing server-side.&lt;/p&gt;

&lt;p&gt;The design has exactly one idea. The assistant answers two fundamentally different ways, and &lt;strong&gt;that distinction is the point of the project&lt;/strong&gt;, so colour and structure encode which mechanism answered: amber for exact metadata queries, teal for document retrieval, split down the bar when a question needed both. The same coding runs through the question index, so you can see &lt;em&gt;before&lt;/em&gt; clicking which path a question will take.&lt;/p&gt;

&lt;p&gt;I built it, then looked at it in a browser, which caught four things reasoning had not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.view { display: grid }&lt;/code&gt; beats the browser's &lt;code&gt;[hidden] { display: none }&lt;/code&gt;, so the History view rendered underneath the chat. Invisible in review, obvious in a screenshot.&lt;/li&gt;
&lt;li&gt;On mobile the question index came &lt;em&gt;before&lt;/em&gt; the input, so a phone user scrolled past fifteen questions to reach the box.&lt;/li&gt;
&lt;li&gt;The entry header crushed question titles to one word per line on narrow screens.&lt;/li&gt;
&lt;li&gt;IBM Plex &lt;strong&gt;Mono&lt;/strong&gt; gives every glyph a full em, so "25,482" rendered with a visible gap around the comma. No figure setting fixes that in a monospace face.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those are hard. All of them needed eyes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Original (EC2)&lt;/th&gt;
&lt;th&gt;Serverless&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Idle cost&lt;/td&gt;
&lt;td&gt;~$15/month&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$2/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At ~500 questions/month&lt;/td&gt;
&lt;td&gt;~$15/month&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$5–8/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployable artifact&lt;/td&gt;
&lt;td&gt;multi-GB image&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~7 MB zip&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime dependencies&lt;/td&gt;
&lt;td&gt;torch, sentence-transformers, minsearch, streamlit, pandas, psycopg, openai&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;boto3&lt;/strong&gt; + scrape/parse stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start&lt;/td&gt;
&lt;td&gt;n/a (always on)&lt;/td&gt;
&lt;td&gt;artifact download + init&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure as code&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;5 CDK stacks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ~7 MB bundle is the single most satisfying number. Dropping the two local torch models — Titan for embeddings, Amazon Rerank for reranking — is what turns a container image into a zip.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Unit prices are estimates and need re-checking before anyone quotes them.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I have not verified
&lt;/h2&gt;

&lt;p&gt;The original write-up was honest that its deep evaluation was scaffolded but unrun. This one has more to admit, and burying it would make everything above less trustworthy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Neither quality gate has produced a number.&lt;/strong&gt; And these are the two that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;routing gate&lt;/strong&gt; compares which tools fired, question by question, against the OpenAI baseline. It needs Bedrock model access to run.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;retrieval gate&lt;/strong&gt; re-measures hit-rate and MRR after swapping MiniLM + minsearch for Titan + S3 Vectors + FTS5. It needs a ground-truth set the batch pipeline generates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Until they run, the claim that this migration preserved answer quality is &lt;em&gt;unmeasured&lt;/em&gt;. The original's published retrieval numbers are fiction for this stack.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;is&lt;/em&gt; verified: the DynamoDB behaviour, against a real implementation, including the concurrency property above; the SQL port, against the real 279-project artifact (all 11 tools, with the leaderboard and top-projects rankings coming back genuinely different — which is the distinction the router exists to preserve); and the retrieval path end to end over the real 25,482-chunk corpus at 3–8 ms per keyword query.&lt;/p&gt;

&lt;h2&gt;
  
  
  The risk I'm most worried about
&lt;/h2&gt;

&lt;p&gt;Swapping OpenAI for Nova Pro is not a like-for-like substitution, and the place it will show is routing.&lt;/p&gt;

&lt;p&gt;The router makes a 12-way decision with genuinely subtle distinctions. "Top project score" and "leaderboard total score" are different numbers that must not be conflated. A person lookup is &lt;em&gt;required&lt;/em&gt; to chain into a follow-up document search, because a bare theme label is an uninformative answer. Those instructions were tuned against a stronger model.&lt;/p&gt;

&lt;p&gt;Here's why the obvious evaluation won't catch a regression: ask "who's the top scorer on the leaderboard?", have the model call &lt;code&gt;top_projects_by_score&lt;/code&gt; instead of &lt;code&gt;top_authors_by_total_score&lt;/code&gt;, and you get a fluent, confident, well-grounded-sounding answer built on &lt;strong&gt;the wrong number&lt;/strong&gt;. An LLM judge scoring answer-against-context will happily mark it RELEVANT. It &lt;em&gt;is&lt;/em&gt; consistent with its context. It's just answering a different question.&lt;/p&gt;

&lt;p&gt;Only the tool trace exposes that. Which is why the gate compares routing rather than relevance, and why the pass bar gets set before the eval runs rather than after.&lt;/p&gt;

&lt;h2&gt;
  
  
  What generalizes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Price the floor, not the feature.&lt;/strong&gt; Aurora Serverless v2 is the right answer to a lot of questions. It was the wrong answer to "where do I put 88 KB of read-only data that a pipeline rebuilds," and the only way to see that was to look at what the database actually held rather than what it was called.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check what your runtime can actually do before designing around it.&lt;/strong&gt; Python Lambda can't stream. CloudFront OAC can't sign a POST body. Bedrock batch can't constrain output. Each of those invalidated a design I'd already committed to on paper, and each took minutes to discover once I looked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A migration re-derives your data, which audits it.&lt;/strong&gt; Two real bugs in the original — 303 unreachable chunks and one chunk too large to embed — surfaced not from careful review but from rebuilding the corpus and measuring the result. If you migrate something, measure the thing you rebuilt against the thing you replaced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When two systems must agree, assert it in a test.&lt;/strong&gt; The vector filter and the SQL filter accepting the same fields is not something either system's own tests can catch. Neither is a Step Functions error-name match. Cross-system invariants need a test that exists specifically to hold them together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Say what you haven't verified.&lt;/strong&gt; A migration that "works" in the sense of compiling, synthesizing, and passing 142 tests is still a migration where no model has answered a real question. Those are very different claims, and only one of them is true here.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>rag</category>
      <category>serverless</category>
    </item>
    <item>
      <title>The Database I Deleted: Migrating an Agentic RAG App to AWS Serverless #2</title>
      <dc:creator>Dmitriy Trunov</dc:creator>
      <pubDate>Wed, 16 Sep 2026 16:43:58 +0000</pubDate>
      <link>https://dev.to/dmitriy_trunov_9a09a497b1/the-database-i-deleted-migrating-an-agentic-rag-app-to-aws-serverless-n18</link>
      <guid>https://dev.to/dmitriy_trunov_9a09a497b1/the-database-i-deleted-migrating-an-agentic-rag-app-to-aws-serverless-n18</guid>
      <description>&lt;h3&gt;
  
  
  Part 2 — the five decisions that fought back
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/migrating-an-agentic-rag-app-to-aws-serverless-d2k"&gt;Part 1&lt;/a&gt; covered why an agentic RAG assistant — built 284 LLM Zoomcamp capstone submissions — moved off a single EC2 box onto AWS serverless, and the resulting architecture: CloudFront and S3 for a static SPA, one Lambda behind a shared-secret header, Bedrock for inference/embeddings/reranking, S3 Vectors and a SQLite artifact for retrieval, DynamoDB for state, Step Functions for ingestion. This part is the five places where the obvious design turned out to be wrong.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick recap of the shape:&lt;/strong&gt; before, everything lived on one &lt;code&gt;t4g.small&lt;/code&gt; — Postgres, a Streamlit process holding two torch models in RAM, minsearch indexes, Grafana — idling at ~$15/month. After, nothing idles: a Lambda behind CloudFront calls Bedrock for inference and DynamoDB/S3/S3-Vectors for state, and an offline Step Functions pipeline does the ingestion. Below is why each piece of the "after" ended up where it did.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision 1: I deleted the database
&lt;/h2&gt;

&lt;p&gt;The obvious port of Postgres is Aurora Serverless v2 with the RDS Data API — HTTPS-based, so the Lambdas stay outside the VPC and you avoid a NAT gateway. I planned exactly that.&lt;/p&gt;

&lt;p&gt;Then I priced the floor. Aurora Serverless v2 at a 0.5-ACU minimum is &lt;strong&gt;~$43/month to sit there doing nothing&lt;/strong&gt; — three times what the whole EC2 box cost. Dropping to a 0-ACU floor helps, but the auto-pause interval is 300 seconds minimum and the resume takes ~15 seconds, or 30+ after a day idle. For a portfolio demo visited twice a week, "30 seconds before the first byte" &lt;em&gt;is&lt;/em&gt; the user experience.&lt;/p&gt;

&lt;p&gt;So I looked at what the database actually held, and the answer was embarrassing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;278 projects and a few thousand library rows. 88 KB. Rebuilt from scratch by the ingestion pipeline, and read-only at query time.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not a database workload. That is a file.&lt;/p&gt;

&lt;p&gt;The read path became a &lt;code&gt;projects.sqlite&lt;/code&gt; artifact — the projects and libraries tables, the corpus, and an FTS5 index over it — built by the pipeline, published to S3, and pulled into the Lambda's &lt;code&gt;/tmp&lt;/code&gt; on cold start. The write path (conversations, feedback, the spend ledger) went to DynamoDB. &lt;strong&gt;~$43/month → ~$1/month.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The part I didn't expect: it is also &lt;em&gt;better&lt;/em&gt;. Postgres full-text search has no IDF at all — &lt;code&gt;ts_rank_cd&lt;/code&gt; scores on term frequency and position, corpus-blind. SQLite's &lt;code&gt;bm25()&lt;/code&gt; does weight rare terms. The thing I moved to is closer to the minsearch TF-IDF I was replacing than the "proper" database would have been.&lt;/p&gt;

&lt;p&gt;And the original project made it cheap. Its own &lt;code&gt;schema.sql&lt;/code&gt; opens with a note that these queries already ran on SQLite before the Postgres migration, and that the SQL had been kept portable. Past me left a door open.&lt;/p&gt;

&lt;p&gt;Two things did need real work, not placeholder substitution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;github_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author_total_score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;author_total_score&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author_total_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;

&lt;span class="c1"&gt;-- SQLite: no DISTINCT ON. GROUP BY with MAX() returns the row that produced&lt;/span&gt;
&lt;span class="c1"&gt;-- the max, which is exactly the row DISTINCT ON would have kept.&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;github_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author_total_score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;author_total_score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;author_total_score&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;author_total_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;ARRAY_AGG(repo ORDER BY attempt)&lt;/code&gt; has no portable equivalent — SQLite only gained ordered aggregates in 3.44, and the Lambda runtime may ship older. That grouping moved into Python, which is duller and correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: no streaming, and the reason isn't performance
&lt;/h2&gt;

&lt;p&gt;I had planned a streaming Function URL. Then: &lt;strong&gt;Lambda response streaming is supported on Node.js managed runtimes only.&lt;/strong&gt; Python needs a custom runtime or the Lambda Web Adapter.&lt;/p&gt;

&lt;p&gt;I could have fought that. I didn't, because streaming buys less here than it looks. This router runs up to six turns of tool calls, plus a query rewrite, plus a rerank — streaming only helps the &lt;em&gt;final&lt;/em&gt; call, so the user waits through the entire tool chain either way.&lt;/p&gt;

&lt;p&gt;There's also a billing trap worth naming: a client that disconnects mid-stream &lt;strong&gt;still bills the full function duration and the full token spend&lt;/strong&gt;. On a public demo with a shared budget cap, that's a hole someone can walk through by closing tabs.&lt;/p&gt;

&lt;p&gt;Buffered JSON. Moving on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: the auth I designed doesn't work
&lt;/h2&gt;

&lt;p&gt;Plan: S3 + CloudFront with Origin Access Control, Lambda Function URL on &lt;code&gt;AuthType: AWS_IAM&lt;/code&gt;, OAC signing requests to the Lambda. Clean, no shared secrets.&lt;/p&gt;

&lt;p&gt;It does not work for POST. &lt;strong&gt;CloudFront signs the request but does not compute the body payload hash&lt;/strong&gt;, and Lambda rejects unsigned payloads. &lt;code&gt;/api/ask&lt;/code&gt; and &lt;code&gt;/api/feedback&lt;/code&gt; are both POSTs. Every one would fail with &lt;code&gt;InvalidSignatureException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;API Gateway solves auth — and reintroduces a ~29-second integration timeout that a six-turn loop can exceed.&lt;/p&gt;

&lt;p&gt;So: Function URL on &lt;code&gt;AuthType: NONE&lt;/code&gt;, with CloudFront attaching a shared secret header, and the Lambda comparing it with &lt;code&gt;secrets.compare_digest&lt;/code&gt;. Which leads to an asymmetry I find genuinely interesting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Lambda&lt;/strong&gt; reads the secret from Secrets Manager at cold start, because a Lambda environment variable is stored in plaintext in the CloudFormation template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront cannot.&lt;/strong&gt; Custom origin headers have no runtime secret reference. The value is resolved at deploy time into the distribution config, where anyone with console read access can see it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is inherent to the feature, not something I could engineer around. So I stopped pretending it was a credential and wrote down what it actually is: a proof-of-origin token. The rate limiter and the daily budget cap sit behind it precisely &lt;em&gt;because&lt;/em&gt; it is not a strong boundary. Both halves are now pinned by tests, so the weakness stays a known property rather than a surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 4: Bedrock batch can't do the thing the whole design rests on
&lt;/h2&gt;

&lt;p&gt;The original enforces a hard rule: only the live chat router makes synchronous LLM calls. Everything bulk — theme classification, ground-truth generation, RAG evaluation, judge scoring, about 11,000 requests — goes through the OpenAI Batch API, at roughly half price.&lt;/p&gt;

&lt;p&gt;Two discoveries, in increasing order of annoyance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Anthropic/OpenAI-style Message Batches API doesn't exist on Bedrock.&lt;/strong&gt; Fine — Bedrock has its own batch inference: JSONL to S3, &lt;code&gt;CreateModelInvocationJob&lt;/code&gt;, poll, JSONL back. Same discount, same shape, different API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Bedrock batch inference supports neither tool calling nor structured output.&lt;/strong&gt; That one hurts. Four of the five bulk stages relied on &lt;code&gt;text_format=&amp;lt;pydantic model&amp;gt;&lt;/code&gt; for guaranteed-schema-valid JSON.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So structure moved from a guarantee into a request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;json_instruction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The prompt fragment that stands in for structured output.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;bits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;one of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; -- &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;JSON_INSTRUCTION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and parsing became forgiving on the way back — fenced blocks, preambles, trailing explanations. Not because sloppiness is fine, but because &lt;strong&gt;discarding an otherwise-correct answer over a stray sentence means re-running a paid batch job&lt;/strong&gt;. A record that yields nothing parseable returns &lt;code&gt;None&lt;/code&gt; for a retry list rather than raising and killing the run.&lt;/p&gt;

&lt;p&gt;The live judge is unaffected: Converse's &lt;code&gt;toolConfig&lt;/code&gt; does constrain output. Two code paths, deliberately, and the reason is written at both call sites.&lt;/p&gt;

&lt;p&gt;The nice part: I kept the seam in one module. &lt;code&gt;_batch.py&lt;/code&gt; retained its function names and request shapes and became a translation layer onto Bedrock, so the prompts, parsing, and stage logic in four consumer modules never changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 5: what S3 Vectors should actually store
&lt;/h2&gt;

&lt;p&gt;The obvious design puts chunk text in vector metadata, so one query returns everything the answer needs.&lt;/p&gt;

&lt;p&gt;Two constraints argue against it, and both are unforgiving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filterable metadata is capped at &lt;strong&gt;2 KB per vector&lt;/strong&gt;, and keys are filterable &lt;em&gt;by default&lt;/em&gt;. The p90 chunk here is 2.8 KB — leaving &lt;code&gt;content&lt;/code&gt; filterable rejects most of the corpus.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nonFilterableMetadataKeys&lt;/code&gt; is fixed at index creation and &lt;strong&gt;cannot be changed&lt;/strong&gt;. Get it wrong and you rebuild.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the deciding argument was simpler. The SQLite artifact is already open in &lt;code&gt;/tmp&lt;/code&gt;. Hydrating text from it is a &lt;em&gt;local file read&lt;/em&gt;. Duplicating 31 MB of text into vector metadata means paying to write and store a second copy, and living against the 40 KB-per-vector total cap forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;S3 Vectors stores identity and filters. SQLite stores text.&lt;/strong&gt; Vector metadata is now a few hundred bytes.&lt;/p&gt;

&lt;p&gt;One correctness trap fell out of splitting retrieval across two systems, and it's the kind that no test of either half would catch: the S3 Vectors metadata filter and the SQLite &lt;code&gt;WHERE&lt;/code&gt; clause &lt;strong&gt;must accept exactly the same fields&lt;/strong&gt;. If they diverge, RRF fuses two candidate pools selected under different predicates, and the ranking is quiet wrong. There is now a test whose entire job is asserting those two tuples are equal.&lt;/p&gt;




&lt;p&gt;Five decisions were overridden by AWS's terms and conditions: a pricing floor, a runtime limit, a signing gap, the batch API's capabilities, and a metadata limit—rules that conflicted with my original plan.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>serverless</category>
      <category>rag</category>
    </item>
    <item>
      <title>Migrating an Agentic RAG App to AWS Serverless. #1</title>
      <dc:creator>Dmitriy Trunov</dc:creator>
      <pubDate>Fri, 11 Sep 2026 06:49:13 +0000</pubDate>
      <link>https://dev.to/dmitriy_trunov_9a09a497b1/migrating-an-agentic-rag-app-to-aws-serverless-d2k</link>
      <guid>https://dev.to/dmitriy_trunov_9a09a497b1/migrating-an-agentic-rag-app-to-aws-serverless-d2k</guid>
      <description>&lt;p&gt;&lt;em&gt;A &lt;a href="https://dev.to/dmitriy_trunov_9a09a497b1/two-ways-of-knowing-building-an-agentic-rag-explorer-for-llm-zoomcamp-capstones-3kpg"&gt;previous post&lt;/a&gt; covered building an agentic RAG router over **LLM Zoomcamp&lt;/em&gt;* capstone submissions. This series is about moving it off a single EC2 box and onto AWS serverless, where nearly every obvious choice turned out to be wrong, and the most valuable decision was deleting a database rather than migrating it. This part covers the starting point and the resulting architecture;&lt;/p&gt;




&lt;p&gt;The course on designing and creating a RAG project has concluded. The final project is working as expected. Now it's time to optimize and use the developed concept as a production system. In the previous article, I described creating an agent-based RAG router for processing LLM theses submitted at Zoomcamp. I decided to write a series of articles dedicated to migrating the project from a single EC2 server to a serverless AWS environment. This section examines the starting point and the resulting architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;Starting point and its drawbacks&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;The initial assistant is working. It answers fundamentally different types of questions about these repositories. &lt;br&gt;
For example, "What does this project do?" using a hybrid search of README files, and "various questions about performance statistics and libraries used in projects?" using a precise parameterized SQL query. The agent router chooses between them.&lt;/p&gt;

&lt;p&gt;In my capstone, it runs as six Docker Compose containers on a single t4g.small instance, deployed via SSH, with a 2GB swap file as a cheap safety net so that Postgres, Streamlit (which contains the Torch embedder and Torch cross-encoder in RAM), and Grafana can coexist on a 2GB RAM instance.&lt;/p&gt;

&lt;p&gt;This isn't bad for a capstone project. But it has properties I'd like to eliminate for a real system.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The corpus - 24,775 fragments, 115MB of JSON-is loaded and rebuilt into the process's RAM on each run.&lt;/li&gt;
&lt;li&gt;Two local machine learning models require a 1.7GB virtual environment and an image. Several gigabytes in size.&lt;/li&gt;
&lt;li&gt;There's no infrastructure as code at all. This approach is completely unsuitable for a real system. Deployment is done via a bash script.&lt;/li&gt;
&lt;li&gt;The cost of downtime is about $15 per month, regardless of whether anyone visits it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: rebuild it as a fully serverless AWS application using real infrastructure as code (IaC) as a learning path. All development is done using Amazon's own resources—Nova for output, Titan for embeddings, Amazon Rerank for reranking, and boto3—the only SDK. No OpenAI, no Anthropic.&lt;/p&gt;

&lt;p&gt;Here is what actually happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;Two architectures:&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Before — everything on one box, everything in RAM:&lt;/p&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    User["User"] --&amp;gt; ST["Streamlit :8501&amp;lt;br/&amp;gt;torch embedder + reranker&amp;lt;br/&amp;gt;24.8k chunks in RAM"]
    ST --&amp;gt; KW[("minsearch.Index&amp;lt;br/&amp;gt;keyword, in-process")]
    ST --&amp;gt; Vec[("minsearch.VectorSearch&amp;lt;br/&amp;gt;MiniLM, in-process")]
    ST --&amp;gt; PG[("Postgres 17&amp;lt;br/&amp;gt;projects · conversations&amp;lt;br/&amp;gt;feedback · budget")]
    ST --&amp;gt; OAI["OpenAI&amp;lt;br/&amp;gt;Responses + Batch API"]
    PG --&amp;gt; Graf["Grafana :3000"]
    Cache[("vector_index_cache.npz&amp;lt;br/&amp;gt;37MB on disk")] -.-&amp;gt; Vec

    subgraph EC2 ["one t4g.small — 6 containers, ~$15/mo idle"]
        ST
        KW
        Vec
        PG
        Graf
        Cache
    end

    style PG fill:#336791,color:#fff
    style EC2 fill:#f6f8fa,stroke:#8b949e&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;After — nothing idling, and the corpus is an artifact rather than a runtime cost:&lt;/p&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    User["User"] --&amp;gt; CF["CloudFront&amp;lt;br/&amp;gt;+ edge origin check"]
    CF --&amp;gt; S3W[("S3: static SPA&amp;lt;br/&amp;gt;3 files, no build step")]
    CF --&amp;gt; L["api Lambda&amp;lt;br/&amp;gt;~7MB zip · no VPC"]

    L --&amp;gt; BR["Bedrock&amp;lt;br/&amp;gt;Nova Pro / Nova Lite&amp;lt;br/&amp;gt;Titan · Rerank"]
    L --&amp;gt; SV[("S3 Vectors&amp;lt;br/&amp;gt;25,482 vectors&amp;lt;br/&amp;gt;identity + filters")]
    L --&amp;gt; SQ[("S3 artifact → /tmp&amp;lt;br/&amp;gt;projects.sqlite&amp;lt;br/&amp;gt;projects · chunks · FTS5")]
    L --&amp;gt; DDB[("DynamoDB&amp;lt;br/&amp;gt;conversations · feedback&amp;lt;br/&amp;gt;budget/day · rate limits")]

    SFN["Step Functions&amp;lt;br/&amp;gt;scrape · Map(crawl + Wait)&amp;lt;br/&amp;gt;embed · build artifact"] --&amp;gt; SV
    SFN --&amp;gt; SQ

    style BR fill:#232F3E,color:#fff
    style DDB fill:#4053D6,color:#fff
    style L fill:#0C8C7D,color:#fff&lt;/code&gt;&lt;/pre&gt;



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

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




</description>
      <category>ai</category>
      <category>aws</category>
      <category>serverless</category>
      <category>rag</category>
    </item>
    <item>
      <title>Two Ways of Knowing: Building an Agentic RAG Explorer for LLM Zoomcamp Capstones.</title>
      <dc:creator>Dmitriy Trunov</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:37:21 +0000</pubDate>
      <link>https://dev.to/dmitriy_trunov_9a09a497b1/two-ways-of-knowing-building-an-agentic-rag-explorer-for-llm-zoomcamp-capstones-3kpg</link>
      <guid>https://dev.to/dmitriy_trunov_9a09a497b1/two-ways-of-knowing-building-an-agentic-rag-explorer-for-llm-zoomcamp-capstones-3kpg</guid>
      <description>&lt;p&gt;&lt;em&gt;A technical deep-dive into an agentic RAG assistant built for the LLM Zoomcamp 2026 capstone — the architecture, five real production bugs, and what a race-condition stress test taught me about spend caps.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The problem with many GitHub repos&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The LLM Zoomcamp 2026 cohort produced over 300 capstone submissions — real RAG projects, each with its own GitHub repo, README, dependency manifest, and a whole course leaderboard score. Browsing that by hand doesn’t scale. So I built a chatbot over the whole corpus to obbtain the all themes, used technology, and library.&lt;/p&gt;

&lt;p&gt;_But the questions people actually ask about a corpus like this split cleanly into two kinds, and they need fundamentally different retrieval:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;”What does project X do? How does it implement reranking?” — a content question. Answer it by finding and grounding on the right README excerpt.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;”Which libraries are most common? What’s the top-scored project? Who’s the leaderboard’s top scorer?” — an aggregation question over the corpus’s structured metadata: score, votes, pass/fail, theme, declared dependencies, each author’s total course score._&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second category is the interesting one, because it’s exactly where naive RAG quietly breaks. Ask an LLM to “count” or “rank” over a pile of retrieved text chunks, and it will confidently produce a plausible-sounding wrong answer — not because the model is bad, but because counting isn’t a retrieval problem. It’s a database problem wearing a chat interface.&lt;/p&gt;

&lt;p&gt;And there’s a subtler trap hiding in that second category: “top-scored project” and “top of the leaderboard” sound like the same question. They aren’t. A project’s own score and its author’s total course score (homeworks + project + learning-in-public) are genuinely different numbers, and conflating them is a real, plausible failure mode — one I actually hit and had to fix (more on that below).&lt;/p&gt;

&lt;p&gt;Solving this well means routing each question to the right retrieval mechanism, not picking one strategy and forcing every question through it.&lt;/p&gt;

&lt;p&gt;Architecture: one router, two ways of knowing&lt;/p&gt;

&lt;p&gt;The ingestion side is what you’d expect from a RAG pipeline — scrape the listing pages, crawl every submitted repo’s markdown via the GitHub API, chunk by heading, cache everything to disk. Theme and tech-stack classification run as OpenAI Batch API jobs (bulk, offline, ~50% cheaper than synchronous calls) rather than one API call per project in the hot path.&lt;/p&gt;

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

&lt;p&gt;The interesting part is the runtime query path. Instead of one retrieval strategy, there’s an agentic router sitting in front of two completely different tools: &lt;code&gt;search_docs&lt;/code&gt; (hybrid keyword+vector search, RRF-fused, cross-encoder reranked, LLM-query-rewritten) and eleven parameterized, hand-written SQL functions-never LLM-generated SQL.&lt;/p&gt;

&lt;p&gt;A “which libraries are most common” question never touches the model’s judgment about counting. It runs &lt;code&gt;COUNT(*) … GROUP BY&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Why an agentic router, not “just RAG plus a calculator tool”?&lt;/p&gt;

&lt;p&gt;The tool-calling loop itself is unremarkable — it’s the same shape as any OpenAI function-calling agent: feed the model a question and a set of tool schemas, let it pick zero or more tools, feed the results back, repeat until it produces a final answer instead of another tool call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_turn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_turns&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;input_items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_tool_schemas&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;function_calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function_call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;function_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sources&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;all_sources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_calls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_calls_log&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# ... execute each function_call, append results, loop again
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What actually matters is what’s in those eleven tool schemas — specifically, how much implicit intent you have to make explicit before the model reliably picks it up. Here’s a real example from this project. &lt;code&gt;most_common_libraries&lt;/code&gt; takes an optional &lt;code&gt;filter_top_scored_n&lt;/code&gt; parameter that restricts the aggregation to the top-N scored projects instead of the whole corpus. The first version of its description just documented what the parameter did. Ask “what are the most common libraries among the top-scored projects?” and the model would call the tool — but leave &lt;code&gt;filter_top_scored_n&lt;/code&gt; unset, silently answering for the entire corpus instead of the subset the question actually asked about. The count was real, correctly computed, exactly matching the SQL — and still the wrong answer, because it answered a different question than the one asked.&lt;/p&gt;

&lt;p&gt;The fix wasn’t code. It was making the qualifier explicit in the tool description itself:&lt;/p&gt;

&lt;p&gt;IMPORTANT: if the question says ‘top-scored’, ‘top projects’, ‘best projects’, or similar, you MUST set &lt;code&gt;filter_top_scored_n&lt;/code&gt; — leaving it null answers for ALL projects instead, which is a different (wrong) answer to that question.&lt;/p&gt;

&lt;p&gt;Tool descriptions are prompts. Implicit qualifiers a human reader would catch instantly — ‘top-scored’, clearly means ‘restrict the scope’, — need to be spelled out just as explicitly as any other instruction, or the model will happily run the wrong exact query with complete confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Five bugs that actually happened:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything above is the design that looks right on a whiteboard. Here’s what broke in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. CUDA wheels on a code path that never touches a GPU&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every embedding call in this project forces &lt;code&gt;device=”cpu”&lt;/code&gt; explicitly, in code, everywhere — &lt;code&gt;sentence-transformers&lt;/code&gt; on CPU, &lt;code&gt;CrossEncoder&lt;/code&gt; reranking on CPU. And yet &lt;code&gt;uv sync&lt;/code&gt; was still resolving &lt;code&gt;torch&lt;/code&gt;’s full CUDA build by default: &lt;code&gt;cuda-bindings&lt;/code&gt;, &lt;code&gt;cuda-toolkit&lt;/code&gt;, half a dozen &lt;code&gt;nvidia-*&lt;/code&gt; packages — several gigabytes of GPU tooling for a code path that never runs on a GPU, on any platform, including ARM (the &lt;code&gt;cuda-toolkit&lt;/code&gt; extras aren’t gated to exclude &lt;code&gt;aarch64&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The fix is a standard &lt;code&gt;uv&lt;/code&gt; pattern most people don’t reach for until it bites them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;
&lt;span class="nn"&gt;[tool.uv.sources]&lt;/span&gt;
&lt;span class="py"&gt;torch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="py"&gt;index&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"pytorch-cpu"&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[[tool.uv.index]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"pytorch-cpu"&lt;/span&gt;
&lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://download.pytorch.org/whl/cpu"&lt;/span&gt;
&lt;span class="py"&gt;explicit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One subtlety: this silently did nothing until &lt;code&gt;torch&lt;/code&gt; was also listed as a direct dependency in &lt;code&gt;pyproject.toml&lt;/code&gt;, not just a transitive one pulled in via &lt;code&gt;sentence-transformers&lt;/code&gt;. Once it was explicit, re-locking dropped 18 CUDA/nvidia/triton packages from &lt;code&gt;uv.lock&lt;/code&gt;, and &lt;code&gt;uv sync&lt;/code&gt; actually uninstalled them from the local environment. Smaller image, faster builds, no wasted disk on a code path that was never going to touch a GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. A killed connection took the whole app down until restart&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The router keeps one Postgres connection open for the process’s entire lifetime — avoiding a fresh-connection-per-query cost for a chat app that might field hundreds of questions. That’s a reasonable optimization, and it worked fine until a killed backend connection (&lt;code&gt;psycopg.errors.AdminShutdown&lt;/code&gt;) took down every subsequent query_projects call until the whole process was manually restarted. One dead connection, and the assistant stopped answering any structured question for anyone.&lt;/p&gt;

&lt;p&gt;The fix was reconnect-and-retry-once logic wrapped around every SQL tool call. But there was a second, quieter bug hiding underneath it: every one of those SQL tools is a read-only &lt;code&gt;SELECT&lt;/code&gt; that never calls &lt;code&gt;.commit()&lt;/code&gt;, and Postgres’s default &lt;code&gt;autocommit=False&lt;/code&gt; means each one silently opens a transaction that never closes. Left alone, the shared connection accumulates “idle in transaction” state — which then blocks unrelated &lt;code&gt;DROP TABLE&lt;/code&gt; statements running elsewhere in the same database. It happened twice before the pattern was obvious. The real fix was one line: roll back (never commit — nothing was written) after every single tool call, closing the implicit transaction immediately instead of leaving it open indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Three judge false-negatives, one root cause&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The live chat has an LLM-as-judge step that scores every answer’s relevance against the context it was actually grounded in. Three separate times, it flagged a correct answer as unsupported, and all three traced back to the same underlying mistake: the judge was being shown a derived view of the grounding data, not the real thing.&lt;/p&gt;

&lt;p&gt;Round one: aggregation answers (no retrieved document chunks) were judged against a hardcoded empty context string, so every purely-SQL-grounded answer looked ungrounded by construction. Round two: after fixing that by building context from the &lt;code&gt;sources&lt;/code&gt; list, a &lt;code&gt;score&lt;/code&gt;-only field left the judge unable to verify an &lt;code&gt;author_total_score&lt;/code&gt; field the answer legitimately cited, because that field simply wasn’t in the sources view. Round three, the sharpest one: aggregation-only tool results (a library count, say) have no &lt;code&gt;github_url&lt;/code&gt; to become a &lt;code&gt;sources&lt;/code&gt; entry — correctly, there’s nothing to link to — but that left the judge with nothing at all to check a fully correct count against, so it flagged genuinely accurate answers as hallucinated.&lt;/p&gt;

&lt;p&gt;All three converged on the same fix: stop trying to derive judge context from a UI-facing view of the answer (the sources list), and instead hand the judge the actual tool-call results the answering model itself saw — the real data, not a lossy projection of it built for a different purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The budget-cap race condition&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Write on Medium&lt;br&gt;
This is the one I’m proudest of catching before it mattered in production, because the naive version looked completely reasonable.&lt;/p&gt;

&lt;p&gt;A public demo needs a hard spend cap — otherwise it’s a free, unlimited ChatGPT proxy to anyone with the URL. The first version did the obvious thing: read the total spent so far from Postgres, compare it to the cap in Python, and only call the LLM if there was room left.&lt;/p&gt;

&lt;p&gt;That’s a classic time-of-check-to-time-of-use race. Under concurrent traffic, every simultaneous request reads “total is under the cap” before any of them commits their own cost. With enough concurrent visitors, the real total can blow straight past the cap — a $0.05 limit could, in the worst case, cost many times that if enough requests land in the same window.&lt;/p&gt;

&lt;p&gt;The fix replaces “read, compare, spend” with one atomic Postgres operation — a conservative reservation made before the LLM call, not a check made after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;try_reserve_budget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RESERVATION_USD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                UPDATE budget_ledger
                SET total_spent = total_spent + %s
                WHERE id = 1 AND total_spent + %s &amp;lt;= %s
                RETURNING total_spent
                &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because a turn’s real cost isn’t known until after the LLM responds, this reserves a conservative fixed upper bound up front and trues it up to the real cost afterward (or fully refunds it if the call fails) — but the cap enforcement itself is a single atomic &lt;code&gt;UPDATE … WHERE … RETURNING&lt;/code&gt;, which Postgres serializes across concurrent transactions the same way it serializes any other concurrent row update. There’s no gap between “check” and “spend” for a race to land in.&lt;/p&gt;

&lt;p&gt;I didn’t just reason about this — I stress-tested it against the real database. Fifty concurrent reservation attempts against a $0.05 cap, with $0.01 reservations:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;concurrent attempts: 50&lt;br&gt;
succeeded reservations: 5&lt;br&gt;
final ledger total: $0.0500 (cap was $0.05)&lt;br&gt;
PASS: ledger never exceeded the cap under concurrency&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Exactly five succeeded. The ledger landed at exactly the cap, not a cent over. The naive read-then-check version would have let far more than five through under the same load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The disk-exhaustion saga that took two fixes to actually fix&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Deploying to a small (2GB RAM, 20GB disk) EC2 instance, &lt;code&gt;docker compose up — build&lt;/code&gt; failed with “no space left on device” mid-build. The instance had 8GB free at the time — that should have been plenty for one ~1.6GB Python image.&lt;/p&gt;

&lt;p&gt;The first diagnosis looked right: &lt;code&gt;docker-compose.yaml&lt;/code&gt; built five separate images from the identical Dockerfile (one per service — &lt;code&gt;init-db&lt;/code&gt;, &lt;code&gt;load-projects&lt;/code&gt;, &lt;code&gt;build-index&lt;/code&gt;, &lt;code&gt;streamlit&lt;/code&gt;, &lt;code&gt;dashboard&lt;/code&gt;), each independently unpacking a multi-GB layer in parallel. Giving all five services a shared &lt;code&gt;image:&lt;/code&gt; tag seemed like the obvious fix — one name instead of five.&lt;/p&gt;

&lt;p&gt;It wasn’t enough. The retry failed with the exact same error. Compose was still building and exporting the image once per service definition, just landing on the same final tag — five redundant multi-gigabyte export operations racing for disk, instead of five distinct named images. The tag was shared; the work wasn’t.&lt;/p&gt;

&lt;p&gt;The actual fix: only one service keeps a &lt;code&gt;build:&lt;/code&gt; block. The other four reference the same &lt;code&gt;image:&lt;/code&gt; with no &lt;code&gt;build:&lt;/code&gt; of their own. Compose’s build phase — triggered by &lt;code&gt;— build&lt;/code&gt; — runs to completion for every service that owns a &lt;code&gt;build:&lt;/code&gt; block before it starts any containers at all; &lt;code&gt;depends_on&lt;/code&gt; only orders container start, not the build phase. So by the time the other four services start, the image the first one built already exists on disk, and they just use it — no rebuild attempted, no redundant export.&lt;/p&gt;

&lt;p&gt;The lesson, generalized past Docker: when a fix makes the symptom go away sometimes but the same failure mode returns under slightly different conditions, look for whether you actually eliminated the redundant work, or just gave the redundant work a nicer name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluating an agentic router&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Retrieval-quality evaluation for a fixed single-strategy RAG pipeline is well-trodden: hit-rate, MRR, precision@k against a ground-truth question set. An agentic router adds a layer on top of that — it’s not just “did retrieval find the right chunk,” it’s “did the router pick the right tool.”&lt;/p&gt;

&lt;p&gt;This project runs two evaluations at different depths. The deep one — comparing &lt;code&gt;keyword&lt;/code&gt;/&lt;code&gt;vector&lt;/code&gt;/&lt;code&gt;hybrid&lt;/code&gt;/&lt;code&gt;hybrid+rerank&lt;/code&gt; retrieval strategies and two answer-generation prompts against an LLM-generated ground-truth set, via the Batch API — is fully scaffolded but wasn’t run for this write-up; it needs a real API key budget I hadn’t allocated yet. No numbers invented to fill that gap.&lt;/p&gt;

&lt;p&gt;The one that did run is faster and, in practice, more useful day to day: a 30-question smoke eval, running every question from the app’s own example-questions sidebar through the live assistant — real retrieval, real SQL, real LLM calls — judged the same way a live chat turn is judged. It’s a regression check on the router’s tool-routing, not a substitute for the deeper eval, and it re-runs in under three minutes:&lt;/p&gt;

&lt;p&gt;30 questions — RELEVANT: 30 PARTLY_RELEVANT: 0 NON_RELEVANT: 0 missing: 0&lt;/p&gt;

&lt;p&gt;relevance_score: 1.0 | total cost: $0.0248 | avg response time: 5.64s&lt;/p&gt;

&lt;p&gt;This eval set earned its keep twice during development — it’s what caught the &lt;code&gt;filter_top_scored_n&lt;/code&gt; bug above, and separately caught a one-shot Docker Compose loader container silently reverting an &lt;code&gt;author_total_score&lt;/code&gt; field to NULL after a stale image was reused. A fast, always-current smoke eval that’s genuinely wired into your deploy process catches real regressions a slower, less-frequently-run ground-truth suite won’t.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardening for a public demo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three small features made the difference between “a demo I can run locally” and “a demo I can actually hand someone a URL for”:&lt;/p&gt;

&lt;p&gt;A production flag that hides the ingestion trigger from public visitors — nobody browsing a public demo should be able to kick off a live re-crawl of 278 GitHub repos.&lt;br&gt;
The atomic budget cap described above.&lt;br&gt;
Answer-from-history caching: an exact-match repeat of a previously-asked question (unsurprisingly common when people click the same example question twice) answers instantly from the logged prior turn — no new LLM call, no new cost, and no budget reservation at all, since nothing is actually being spent.&lt;br&gt;
None of these are exotic. All three are the kind of thing that’s easy to skip when a project is “just a demo” — right up until it’s a public URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What generalizes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A few things from this build feel true beyond this specific project:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tool descriptions are prompts, not documentation. An implicit qualifier a human reader catches instantly — “top-scored” clearly means “restrict the scope” — has to be spelled out as explicitly as any other instruction, or the model will run the &lt;em&gt;wrong&lt;/em&gt; exact query with complete confidence and no visible error.&lt;/li&gt;
&lt;li&gt;A judge needs the real grounding data, not a UI-shaped view of it. Every judge false-negative in this project traced back to feeding the judge something derived from the actual answer-generation context, built for a different purpose (usually, rendering source links), rather than the real tool output the answering model itself saw.&lt;/li&gt;
&lt;li&gt;Anything with a spend cap under concurrent traffic needs an atomic reservation, not a read-then-check. This is true well beyond LLM cost caps — any shared, capped resource checked by “read the total, compare, then act” has the same race, and it’s worth stress-testing with real concurrent load, not just reasoning about it on paper.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is on &lt;a href="https://github.com/dim2e3/RAG-llm-zoomcamp-explorer" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;: &lt;/p&gt;

&lt;p&gt;Built as a capstone project for &lt;a href="https://github.com/DataTalksClub/llm-zoomcamp" rel="noopener noreferrer"&gt;[LLM Zoomcamp]&lt;/a&gt; 2026 cohort&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>programming</category>
      <category>python</category>
    </item>
    <item>
      <title>Begin your journey with Amazon Q Developer</title>
      <dc:creator>Dmitriy Trunov</dc:creator>
      <pubDate>Sun, 28 Sep 2025 19:07:04 +0000</pubDate>
      <link>https://dev.to/dmitriy_trunov_9a09a497b1/begin-your-journey-with-amazon-q-developer-4dp</link>
      <guid>https://dev.to/dmitriy_trunov_9a09a497b1/begin-your-journey-with-amazon-q-developer-4dp</guid>
      <description>&lt;p&gt;Last year, we heard too much about AI and AI technology. Names such as &lt;em&gt;ChatGPT, Claude&lt;/em&gt;, and &lt;em&gt;Deepseek&lt;/em&gt; are well-known in the internet community. Many people don't see themselves without their hints and help in everyday life. &lt;/p&gt;

&lt;p&gt;Almost all software developers are using tools like &lt;em&gt;Copilot&lt;/em&gt;, &lt;em&gt;Cursor&lt;/em&gt;, and &lt;em&gt;Windsurf&lt;/em&gt; in their working routines. However, in the development community, I encountered the ignorance of one tool that is worth paying attention to.&lt;br&gt;
This tool is named &lt;strong&gt;"Amazon Q Developer"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A former &lt;em&gt;Amazon&lt;/em&gt; product, "&lt;em&gt;CodeWhisperer&lt;/em&gt;", became a part of &lt;strong&gt;Amazon Q Developer&lt;/strong&gt;. It is a generative artificial intelligence (AI) powered conversational assistant that can help you understand, build, extend, and operate software applications. This product was made generally available on &lt;strong&gt;April 30, 2024&lt;/strong&gt;. Over the years of constant development and improvement, it has received many improvements and updates. It was full integration into the &lt;em&gt;AWS&lt;/em&gt; ecosystem, with major updates and milestones occurring throughout 2025. &lt;strong&gt;Amazon Q Developer&lt;/strong&gt; is built on &lt;em&gt;Amazon Bedrock&lt;/em&gt; and includes automated abuse detection implemented in &lt;em&gt;Amazon Bedrock&lt;/em&gt; to enforce safety, security, and the responsible use of AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon Q&lt;/strong&gt; was provided in two versions - &lt;strong&gt;Amazon Q CLI&lt;/strong&gt; and &lt;strong&gt;Amazon Q extension&lt;/strong&gt; for development tools such as &lt;em&gt;Visual Studio Code&lt;/em&gt; and &lt;em&gt;JetBrains IDE&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's start with Amazon Q Developer extension for free.
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;To do this, go to the website [AWS Skill Builder]&lt;a href="https://skillbuilder.aws/" rel="noopener noreferrer"&gt;https://skillbuilder.aws/&lt;/a&gt; and create a free &lt;strong&gt;AWS Builder ID&lt;/strong&gt; (no AWS account required).&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0tktg7b220c7hv1x2353.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0tktg7b220c7hv1x2353.png" alt="Create AWS SkillBuilder account" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Today, we are planning to start using &lt;strong&gt;Amazon Q&lt;/strong&gt; as an &lt;em&gt;IDE&lt;/em&gt; extension. I think you already have something installed on your computer. Download the extension for Visual Studio Code. Install the Amazon Q extension in VS Code.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1085wzzom1m5ozn961e0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1085wzzom1m5ozn961e0.png" alt="Install Amazon Q developer extentsion" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This procedure does not require you to have a &lt;em&gt;Builder ID&lt;/em&gt;. If you have not yet signed up for &lt;em&gt;Builder ID&lt;/em&gt;, you will have the opportunity to do so during the sign-in process.&lt;/p&gt;

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

&lt;p&gt;Choose the &lt;strong&gt;Amazon Q&lt;/strong&gt; icon in your &lt;em&gt;IDE&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The icon will be on the side of the interface by default.&lt;/p&gt;

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

&lt;p&gt;Follow the instructions in your browser to authenticate with &lt;em&gt;Builder ID&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;To begin using Amazon Q, choose the Amazon Q icon to chat with Amazon Q, or choose Amazon Q from the navigation bar at the bottom of your &lt;em&gt;IDE&lt;/em&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
Nowadays, many developers are bound by NDA rules and are concerned that their code may end up on the other side. I came across a situation where some very experienced Amazon Q users don't know some subtleties of these settings. Let's tune some extension settings.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This setting allows you to customize the ability to send tracking information in AWS&lt;/p&gt;

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

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

&lt;p&gt;Let's explore some additional settings related to context. This is the most important part of &lt;strong&gt;Amazon Q Developer&lt;/strong&gt; that allows us to use our own context to provide AI with the most relevant questions.&lt;br&gt;
The local workspace context created by these settings helps Amazon Q deliver more accurate answers and speed up the work capabilities. Amazon Q maintains the context of your conversation within a given session to inform future responses. You can ask follow-up questions or refer to previous questions and responses throughout the duration of your session. To start a new conversation with Amazon Q, open a new tab in the panel. You can open up to 10 tabs at a time. Amazon Q doesn't retain context across different conversations.&lt;/p&gt;

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

&lt;p&gt;Now you can ask Amazon Q questions, update your code, and initiate actions with quick commands all from the Amazon Q chat panel in your IDE.&lt;/p&gt;

&lt;p&gt;To start chatting with Amazon Q, choose the Amazon Q icon from the navigation bar in your IDE and enter your question in the text bar. To start chatting with Amazon Q in Visual Studio, choose View from the main menu and then choose Amazon Q chat.&lt;/p&gt;

&lt;p&gt;When you ask Amazon Q a question, it uses the current file that is open in your IDE as context, including the programming language and the file path.&lt;/p&gt;

&lt;p&gt;If Amazon Q includes code in its response, you can copy the code or insert it directly into your file by choosing Insert at cursor. Amazon Q might include inline references to its sources in its response. To view a list of sources, expand the Sources section at the bottom of a response.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
Chat commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can enter the following commands in the chat panel to access Amazon Q features, depending on your IDE.&lt;/p&gt;

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

&lt;p&gt;&lt;code&gt;/transform&lt;/code&gt; - Use this command to update the code language version of an entire project. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;/dev&lt;/code&gt; - Use this command to get an implementation plan to develop a feature. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;/test&lt;/code&gt; - Use this command to generate unit tests for your code.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/review&lt;/code&gt; - Use this command to review your codebase for security vulnerabilities and code quality issues. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;/doc&lt;/code&gt; - Use this command to generate READMEs for your project base.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/clear&lt;/code&gt; - Use this command to clear a current conversation. This removes all previous conversations from the chat panel and clears the context that Amazon Q has about your previous conversation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/compact&lt;/code&gt; - Use this command to compact your chat history when the context window approaches its capacity limit. This creates a concise summary of your conversation while preserving essential information.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/help&lt;/code&gt; - Use this command to see an overview of what Amazon Q can and can't do, example questions, and quick commands&lt;/p&gt;

&lt;p&gt;Try out the &lt;strong&gt;Amazon Q developer&lt;/strong&gt; capabilities and share your first impressions. &lt;/p&gt;

&lt;p&gt;In the next articles, I will try to contribute to you some advanced interaction techniques with &lt;strong&gt;Amazon Q Developer&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
