<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Simple AWS</title>
    <description>A biweekly deep-dive newsletter for engineers and architects building on AWS. Stop copying cloud solutions, start understanding them.</description>
    
    <link>https://newsletter.simpleaws.dev/</link>
    <atom:link href="https://rss.beehiiv.com/feeds/IlfGrX4LIG.xml" rel="self"/>
    
    <lastBuildDate>Wed, 16 Sep 2026 03:36:55 +0000</lastBuildDate>
    <pubDate>Sat, 12 Sep 2026 03:01:30 +0000</pubDate>
    <atom:published>2026-09-12T03:01:30Z</atom:published>
    <atom:updated>2026-09-16T03:36:55Z</atom:updated>
    
      <category>Programming</category>
      <category>Software Engineering</category>
      <category>Technology</category>
    <copyright>Copyright 2026, Simple AWS</copyright>
    
    <image>
      <url>https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/publication/logo/ed281676-a90b-4ea4-8399-5bd595846499/Simple_AWS_logo_round.png</url>
      <title>Simple AWS</title>
      <link>https://newsletter.simpleaws.dev/</link>
    </image>
    
    <docs>https://www.rssboard.org/rss-specification</docs>
    <generator>beehiiv</generator>
    <language>en-us</language>
    <webMaster>support@beehiiv.com (Beehiiv Support)</webMaster>

      <item>
  <title>How to Optimize AI Agents on AWS</title>
  <description>Improve AI agents on AWS with Bedrock Advanced Prompt Optimization, AgentCore recommendations, batch evaluations, and A/B tests of task quality and cost.</description>
  <link>https://newsletter.simpleaws.dev/p/how-to-optimize-ai-agents-on-aws</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/how-to-optimize-ai-agents-on-aws</guid>
  <pubDate>Sat, 12 Sep 2026 03:01:30 +0000</pubDate>
  <atom:published>2026-09-12T03:01:30Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Artificial Intelligence]]></category>
    <category><![CDATA[Operations]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">Suppose you ask an agent to move an appointment to Tuesday afternoon. It replies with a friendly confirmation, the right time, and the right address. But the scheduling system still has your original appointment. Or perhaps the agent moves the appointment correctly, but forgets to tell you the location.</p><p class="paragraph" style="text-align:left;">Both interactions need improvement, but the work required is different for each failure mode. One involves the agent&#39;s actions, the other involves how it explains their result.</p><p class="paragraph" style="text-align:left;">We&#39;ll use this hypothetical scheduling agent to work through <b>agent optimization on AWS</b>. Our goal is to get the agent to complete more requests correctly, and to do it with as little work as possible on our side. <a class="link" href="https://docs.aws.amazon.com/bedrock/latest/userguide/advanced-prompt-optimization-how.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">Amazon Bedrock Advanced Prompt Optimization</a> and <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/optimization.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">Amazon Bedrock AgentCore Optimization</a> are going to help us automate different parts of the process.</p><hr class="content_break"><p class="paragraph" style="text-align:left;"><i>This article is sponsored by Inngest</i></p><h2 class="heading" style="text-align:left;" id="dont-let-agent-failures-blow-the-bu">Don&#39;t let agent failures blow the budget.</h2><div class="image"><img alt="Inngest advertisement: Long-running agents fail. Don’t restart from zero." class="image__image" style="border-radius:0px 0px 0px 0px;border-style:solid;border-width:0px 0px 0px 0px;box-sizing:border-box;border-color:#E5E7EB;" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/c096e88a-32ad-47f8-af8a-788a039095a2/Frame_2.png?t=1787964019"/></div><p class="paragraph" style="text-align:left;">Agents, like all of us, often need to recover from failure. But they shouldn&#39;t start from scratch.</p><p class="paragraph" style="text-align:left;">Inngest&#39;s durable execution checkpoints your agents every step of the way.</p><p class="paragraph" style="text-align:left;">So your agents can pick up where they left off, and avoid doubling up on token & compute spend.<br><br>Inngest offers a generous free tier and a sleek local dev server for testing.</p><div class="button" style="text-align:center;"><a target="_blank" rel="noopener nofollow noreferrer" class="button__link" style="" href="https://inngest.link/simple-aws-nl?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws"><span class="button__text" style=""> Build durably with Inngest </span></a></div><hr class="content_break"><h2 class="heading" style="text-align:left;" id="choose-the-behavior-you-want-to-imp">Choose the behavior you want to improve</h2><p class="paragraph" style="text-align:left;">You always need to start with a failure mode you can describe precisely. “The agent isn&#39;t reliable” doesn&#39;t tell you what to change. Instead, “The agent creates a second appointment when asked to reschedule the existing one” gives you something to investigate, and a clear goal to optimize towards.</p><p class="paragraph" style="text-align:left;">From there you need to pick examples where that happened, and read the session trace: the record of the model&#39;s outputs, tool calls, and returned results. Use that to investigate precisely what went wrong. Did the agent choose the wrong operation? Did it send the wrong appointment ID? Did the correct operation return an error? Each of these causes will require a different fix. AWS&#39;s <a class="link" href="https://aws.amazon.com/blogs/machine-learning/debugging-production-agents-with-amazon-bedrock-agentcore-observability/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">agent debugging guide</a> dives a bit deeper into the topic.</p><p class="paragraph" style="text-align:left;">For our scheduling example, the first experiment we need to run depends on what the trace shows:</p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="33%"><p class="paragraph" style="text-align:left;">Observed failure</p></th><th class="bh__table_header" width="33%"><p class="paragraph" style="text-align:left;">Improvement to investigate</p></th><th class="bh__table_header" width="33%"><p class="paragraph" style="text-align:left;">Outcome to check</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">The agent chooses the <code>create_appointment</code> tool instead of <code>reschedule_appointment</code>.</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Clarify tool descriptions or action-selection instructions.</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">The intended appointment is moved without creating a duplicate.</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Rescheduling succeeds, but the reply omits the location.</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Improve the instructions for the confirmation.</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">The reply includes the correct day, time, and location.</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">The correct operation fails inside the scheduling service.</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Repair the failing implementation or dependency.</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">The operation completes with valid inputs.</p></td></tr></table></div><p class="paragraph" style="text-align:left;">This is part of what makes agent optimization difficult. Changing one instruction can affect several decisions, and the same agent can behave differently across repeated attempts. You need enough of the execution history to understand where the failure began, so you can know what to do next and how to measure if it&#39;s improving. <a class="link" href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">Anthropic&#39;s agent evaluation guide</a> makes the useful distinction between the conversation transcript and the final state of the environment. Use both.</p><p class="paragraph" style="text-align:left;">Once you know precisely what you want to optimize, you&#39;ll know which of these AWS services can help you.</p><h2 class="heading" style="text-align:left;" id="optimize-a-model-task-with-bedrock-">Optimize a model task with Bedrock Advanced Prompt Optimization</h2><p class="paragraph" style="text-align:left;"><a class="link" href="https://docs.aws.amazon.com/bedrock/latest/userguide/advanced-prompt-optimization-how.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">Amazon Bedrock Advanced Prompt Optimization</a>, or AdvPO, takes a prompt template, example inputs, and an evaluation method. It runs the examples through your selected models, scores the responses, and uses that feedback to rewrite the template. Then it repeats the process, and keeps improving.</p><p class="paragraph" style="text-align:left;">That removes the manual cycle of editing a prompt, running the same examples, comparing responses, and deciding what to try next. You still supply the task and the definition of a good result, it does its best to change the task so its results approach what you define as “good”.</p><p class="paragraph" style="text-align:left;">For our agent&#39;s incomplete confirmations, AdvPO is a sensible starting point. We can isolate the model call that turns a scheduling result into a message, and focus on that. The input already contains the new appointment details, and the output should communicate them accurately.</p><h3 class="heading" style="text-align:left;" id="give-the-optimizer-useful-examples-">Give the optimizer useful examples and feedback</h3><p class="paragraph" style="text-align:left;">The input file is JSONL, with one record per prompt template. Each record contains its examples and evaluation configuration. Here&#39;s an example record, it&#39;s expanded for readability but in the actual file it would be a single line. This record contains one prompt template and only two evaluation samples. Add representative samples inside evaluationSamples; add more JSONL lines only when optimizing additional templates.</p><div class="codeblock"><pre><code>&#123;
  &quot;version&quot;: &quot;bedrock-2026-05-14&quot;,
  &quot;templateId&quot;: &quot;appointment-confirmation&quot;,
  &quot;promptTemplate&quot;: &quot;Write a confirmation for this appointment operation.\nRequest: &#123;&#123;request&#125;&#125;\nResult: &#123;&#123;result&#125;&#125;&quot;,
  &quot;steeringCriteria&quot;: [
    &quot;When rescheduling succeeds, include the appointment day, time, and location.&quot;,
    &quot;Do not claim rescheduling succeeded when the result reports failure.&quot;,
    &quot;Use only facts provided in the request and result.&quot;
  ],
  &quot;evaluationSamples&quot;: [
    &#123;
      &quot;inputVariables&quot;: [
        &#123;&quot;request&quot;: &quot;Move my appointment to Tuesday afternoon.&quot;&#125;,
        &#123;&quot;result&quot;: &quot;status=rescheduled; day=Tuesday; time=14:00; location=Central office&quot;&#125;
      ],
      &quot;referenceResponse&quot;: &quot;Your appointment has been moved to Tuesday at 14:00 at the Central office.&quot;
    &#125;,
    &#123;
      &quot;inputVariables&quot;: [
        &#123;&quot;request&quot;: &quot;Move my appointment to Tuesday afternoon.&quot;&#125;,
        &#123;&quot;result&quot;: &quot;status=failed; reason=no matching slot available&quot;&#125;
      ],
      &quot;referenceResponse&quot;: &quot;I couldn&#39;t reschedule your appointment because no matching slot was available.&quot;
    &#125;
  ]
&#125;
</code></pre></div><p class="paragraph" style="text-align:left;">The <code>&#123;&#123;request&#125;&#125;</code> and <code>&#123;&#123;result&#125;&#125;</code> placeholders receive their values from each sample. Each variable occupies its own object in <code>inputVariables</code>. Reference responses are optional, though highly recommended, since they give the evaluator a concrete example of the desired answer and thus a way to understand what a good answer looks like. You can find the complete schema at the <a class="link" href="https://docs.aws.amazon.com/bedrock/latest/userguide/advanced-prompt-optimization-input.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">input documentation</a>.</p><p class="paragraph" style="text-align:left;">It&#39;s important to include examples of failures. If every sample contains a successful reschedule, you give the optimizer little reason to distinguish between confirming an action and explaining that it couldn&#39;t happen. Add representative variations, including cases the existing prompt already handles well, and keep some examples aside for checking the result afterward (this is called a held-out set in Machine Learning jargon).</p><p class="paragraph" style="text-align:left;">AdvPO provides three <a class="link" href="https://docs.aws.amazon.com/bedrock/latest/userguide/advanced-prompt-optimization-evaluation.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">evaluation approaches</a>: natural-language steering criteria (which is what the example above uses), a custom LLM judge with your own rubric, or a Lambda function that computes a score. A judge is useful for semantic requirements such as an accurate, complete explanation. Lambda is useful when code can check exact fields, values, or structured output. I wrote about an experiment we ran at work using a <a class="link" href="https://caylent.com/blog/automating-prompt-iterations-with-amazon-bedrock-advanced-prompt-optimization?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">Lambda evaluator</a>.</p><p class="paragraph" style="text-align:left;">Choose based on what factors make an answer wrong. A beautifully written confirmation containing the wrong appointment time must score poorly, so anything that measures how well-written it is won&#39;t work for this case. Remember that the optimizer follows the feedback you give it.</p><p class="paragraph" style="text-align:left;">If part of your template already works well, <a class="link" href="https://docs.aws.amazon.com/bedrock/latest/userguide/advanced-prompt-optimization-selective.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">selective optimization</a> lets you mark editable sections with <code>&lt;advpo:optimize&gt;</code>. Content outside those blocks is preserved, and the returned prompt has the tags removed.</p><h3 class="heading" style="text-align:left;" id="run-the-job-and-use-the-result">Run the job and use the result</h3><p class="paragraph" style="text-align:left;">Before submitting, make sure your identity has the right permissions to manage optimization jobs, invoke the selected models, and read and write the S3 locations. The S3 bucket where you put the JSONL file must be in the job&#39;s AWS Region. Custom Lambda evaluation and KMS encryption add their respective permissions, of course. Here&#39;s the list of <a class="link" href="https://docs.aws.amazon.com/bedrock/latest/userguide/advanced-prompt-optimization-prereqs.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">prerequisites</a>, which also describe cross-Region inference used during optimization.</p><p class="paragraph" style="text-align:left;">In the Bedrock console:</p><ol start="1"><li><p class="paragraph" style="text-align:left;">Open <b>Advanced Prompt Optimization</b> and create a job.</p></li><li><p class="paragraph" style="text-align:left;">Select your target model or models.</p></li><li><p class="paragraph" style="text-align:left;">Upload the JSONL input or provide its S3 location.</p></li><li><p class="paragraph" style="text-align:left;">Choose the S3 output location and submit.</p></li></ol><p class="paragraph" style="text-align:left;">The API equivalent is <code>CreateAdvancedPromptOptimizationJob</code>. If you&#39;re trying to improve the prompt, start with your current model. You can also do a model comparison, in that case include the current model and candidate replacements. The service supports up to five models, ten templates per job, and 100 examples per template. <a class="link" href="https://docs.aws.amazon.com/bedrock/latest/userguide/advanced-prompt-optimization-quotas.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">Current limits and supported Regions</a> are important to check before preparing a larger input.</p><p class="paragraph" style="text-align:left;">The <a class="link" href="https://docs.aws.amazon.com/bedrock/latest/userguide/advanced-prompt-optimization-results.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">results</a> contain optimized templates for the target models, sample scores, cost estimates, and time to first token. TTFT measures when a streamed response begins. For an agent, also measure how long it takes to provide useful information and complete the requested action. Read the changed responses alongside the scores, then compare the original and selected template on the examples you held aside (your holdout set, in ML jargon).</p><p class="paragraph" style="text-align:left;">There are two different costs to consider for AdvPO. The optimization job consumes inference for generating responses, judging them, and rewriting prompts, plus Lambda usage if you choose it. The resulting prompt then has its own recurring inference cost when you use it, which might be higher than the cost of the original prompt if the change that improved the response made your prompt longer. AdvPO has no separate service charge beyond its underlying usage. Check <a class="link" href="https://aws.amazon.com/bedrock/pricing/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">Bedrock pricing</a> for how much models cost.</p><p class="paragraph" style="text-align:left;">AdvPO <a class="link" href="https://docs.aws.amazon.com/bedrock/latest/userguide/advanced-prompt-optimization-advanced-topics.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">evaluates the supplied examples independently</a>. It can see conversation history or tool results you provide, but it doesn&#39;t execute those earlier actions. Our confirmation examples let us test how the model describes a supplied scheduling result. The agent still needs to produce that result correctly, throughout its full execution. That&#39;s where AgentCore Optimization comes into play.</p><h2 class="heading" style="text-align:left;" id="improve-agent-behavior-with-agent-c">Improve agent behavior with AgentCore Optimization</h2><p class="paragraph" style="text-align:left;">AgentCore Optimization starts with the agent&#39;s current configuration and session traces. Its <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/optimization-recommendations.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">recommendations</a> propose changes to system prompts or tool descriptions, with explanations of the changes.</p><p class="paragraph" style="text-align:left;">Let&#39;s go back to the case of our agent selecting the <code>create_appointment</code> tool when the user asked to reschedule. If the descriptions make both tools sound like reasonable choices for the action you&#39;re asking the agent to take, it&#39;s going to get confused about which to pick. A description should make the distinction clear: one creates a new appointment, the other changes an existing appointment identified by its ID. We need to improve that.</p><p class="paragraph" style="text-align:left;"><a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/recommendations-tool-description.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">Tool-description recommendations</a> analyze selection patterns directly. You provide names, descriptions, and traces, and receive proposed descriptions with explanations. In the <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-get-started-cli.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">AgentCore CLI</a>, use <code>--type tool-description</code> and provide each description with <code>--tools &quot;name:description&quot;</code>. This mode omits the evaluator parameter.</p><p class="paragraph" style="text-align:left;">System-prompt recommendations work differently. You supply one numerical evaluator to define the direction of improvement, and that&#39;s what AgentCore uses to determine whether it&#39;s making progress on improving the response. <code>Builtin.GoalSuccessRate</code> is a good fit for task completion, and <code>Builtin.Helpfulness</code> fits more open-ended interaction quality. You can also use a custom evaluator, which can express a requirement specific to your agent. The result is revised prompt text and an explanation. AWS&#39;s <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/recommendations-system-prompt.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">system-prompt guide</a> documents these choices in more depth.</p><h3 class="heading" style="text-align:left;" id="generate-a-candidate-from-relevant-">Generate a candidate from relevant sessions</h3><p class="paragraph" style="text-align:left;">You can use CloudWatch to get all the necessary data from your agent&#39;s past sessions. Configure AgentCore Observability and Transaction Search, invoke your agent, and let the telemetry arrive. You can also supply captured OpenTelemetry-compatible spans from tests, so you don&#39;t need a production deployment before you can generate recommendations. See the <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/optimization-prereqs.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">setup requirements</a> and <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/optimization-recommendations.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">supported trace sources</a> for more info on how to set this up.</p><p class="paragraph" style="text-align:left;">To improve on failures in the agent&#39;s overall instructions, the following example illustrates a system-prompt recommendation using seven days of traces. It assumes an existing <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-get-started-cli.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">AgentCore CLI</a> project. Replace <code>MyAgent</code> and the file path with your project&#39;s runtime and current prompt.</p><div class="codeblock"><pre><code>agentcore run recommendation \
  --type system-prompt \
  --run improve_scheduling \
  --runtime MyAgent \
  --evaluator Builtin.GoalSuccessRate \
  --prompt-file ./system-prompt.txt \
  --lookback 7 \
  --wait</code></pre></div><p class="paragraph" style="text-align:left;"><code>--wait</code> waits for the asynchronous recommendation job. Replace <code>RECOMMENDATION_ID</code> in the following command with the job ID returned above to retrieve the proposed prompt and its explanation:</p><div class="codeblock"><pre><code>agentcore view recommendation RECOMMENDATION_ID --json
</code></pre></div><p class="paragraph" style="text-align:left;">Per the <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/bedrock-agentcore-limits.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">service quotas</a>, you can send up to 20 sampled sessions per recommendation and a 20,000-character prompt limit. This means that providing a long lookback doesn&#39;t mean every session was analyzed. You need to choose evidence that represents the behavior you want to improve, not send every session and hope for the best.</p><p class="paragraph" style="text-align:left;">Read the proposed change before applying it (especially in prod). If a recommendation resolves ambiguity by changing what the agent is allowed to do, it hasn&#39;t necessarily preserved your intended behavior, even if technically it did improve on the specific issue you&#39;re trying to fix. The candidate prompt should still express the task you actually want performed.</p><h3 class="heading" style="text-align:left;" id="test-the-changed-agent">Test the changed agent</h3><p class="paragraph" style="text-align:left;">Apply the candidate to a separate agent version, so you can A/B test and roll back easily if needed. You can deploy another endpoint, or use a <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/configuration-bundles.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">configuration bundle</a>: an immutable version of settings such as the prompt, model ID, and tool descriptions. Bundles are optional, but I recommend them. Remember that your agent must <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/configuration-bundles-runtime.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">read and apply the configuration</a>, just creating one won&#39;t replace a prompt hard-coded in your application.</p><p class="paragraph" style="text-align:left;">Run the original and candidate agents on the same scenarios, including straightforward rescheduling, ambiguous appointment requests, and unavailable slots. Start each trial with fresh conversation state and independent or reset appointment data and slot availability. Otherwise, the first run can change the task faced by the second.</p><p class="paragraph" style="text-align:left;">Check the resulting appointment state and confirmation. Include cases that previously worked so you can make sure that the change that helped one behavior didn&#39;t break something else. If the difference isn&#39;t clear, repeat the relevant trials and compare outcomes across attempts.</p><p class="paragraph" style="text-align:left;">AgentCore <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/batch-evaluations.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">batch evaluation</a> scores recorded sessions and returns aggregate results. However, it doesn&#39;t take a revised prompt and rerun your agent. Generate fresh sessions with the candidate first, since rescoring the original sessions would only tell you about the original agent. The SDK&#39;s <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/dataset-evaluations.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">dataset runners</a> can handle invocation, telemetry collection, and evaluation together, though that interface is in public preview as of writing this.</p><p class="paragraph" style="text-align:left;">Then you should confirm a change under live usage, and for that <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/ab-testing.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">AgentCore A/B testing</a> splits Gateway traffic between the original version and your new, hopefully improved one. Traffic is sticky by session, keeping every message in a conversation on the same version, so users won&#39;t perceive changes in quality within the same session. You can run this for a while and then compare the sessions from both versions.</p><p class="paragraph" style="text-align:left;">The <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/ab-testing-target-based.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">experiment results</a> that you can see include per-evaluator changes and statistical uncertainty. Based on this you should check whether the improvement is large enough to matter and whether another relevant measure regressed. With this you&#39;ll have enough information to know whether you should deploy the new version or not.</p><p class="paragraph" style="text-align:left;">You can also do this for agents hosted outside AgentCore Runtime, but they&#39;ll require <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/ab-testing-3p-agents.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">instrumentation, reachable Gateway targets, and Gateway tracing</a> to associate scored sessions with variants. That&#39;s useful when you want managed experiments around an existing agent, but it is setup work to account for.</p><p class="paragraph" style="text-align:left;">Recommendations, batch evaluation, and A/B testing are generally available. Recommendation generation has no separate charge, you pay for new evaluations and the underlying resources. If you use batch evaluation, you get a 25% discount from standard evaluation rates. Live experiments consume Gateway, evaluation, and execution resources at their normal rate, with telemetry billed separately. The <a class="link" href="https://aws.amazon.com/bedrock/agentcore/pricing/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-optimize-ai-agents-on-aws" target="_blank" rel="noopener noreferrer nofollow">AgentCore pricing page</a> has the current rates and billing details.</p><h2 class="heading" style="text-align:left;" id="when-to-use-either-service-and-when">When to use either service, and when to use both</h2><p class="paragraph" style="text-align:left;">The choice follows the work you need to do:</p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="25%"><p class="paragraph" style="text-align:left;">Your situation</p></th><th class="bh__table_header" width="25%"><p class="paragraph" style="text-align:left;">Start with</p></th><th class="bh__table_header" width="25%"><p class="paragraph" style="text-align:left;">Why</p></th><th class="bh__table_header" width="25%"><p class="paragraph" style="text-align:left;">Still check</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">A defined model task needs better responses.</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">AdvPO</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Examples and a score can guide repeated template improvements.</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Whether the new template works on unfamiliar inputs and inside the agent.</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">You&#39;re comparing Bedrock models.</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">AdvPO</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">You can compare candidates with prompts optimized for each.</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Complete-task quality, time, and expense.</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Traces show weak instructions or confusion between tools.</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">AgentCore recommendations</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">The candidate changes are informed by actual session behavior.</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Fresh runs with the proposed configuration.</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">An AdvPO candidate looks promising and you need managed agent experiments.</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Both</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">AdvPO produces the candidate, AgentCore evaluates the agent as a whole.</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Whether the full agent improves enough to make it worth adopting these changes.</p></td></tr></table></div><p class="paragraph" style="text-align:left;">For example, you might use AdvPO to improve the appointment confirmation in our example agent, and to compare its current model with a less expensive candidate. After selecting a promising template/model combination, you integrate it into the agent. Then you run complete scheduling scenarios with both versions, and use AgentCore&#39;s live experiment if you need evidence from real user sessions.</p><p class="paragraph" style="text-align:left;">You don&#39;t need an AgentCore recommendation job just to validate an AdvPO candidate. Nor do you need AdvPO after AgentCore has already produced a useful recommendation. If your existing test setup can establish the relevant outcomes, AdvPO can be enough for the optimization work. If traces point to unclear tool descriptions, AgentCore can take you from that evidence through a proposed change and its validation.</p><h2 class="heading" style="text-align:left;" id="conclusion">Conclusion</h2><p class="paragraph" style="text-align:left;">For a model task you can express through examples and a scoring method, start with Bedrock Advanced Prompt Optimization. For changes informed by agent sessions, use AgentCore recommendations and its evaluation tools. Combine them when you need both prompt/model exploration and managed experiments around the complete agent.</p><p class="paragraph" style="text-align:left;">Measure the outputs, and make sure you&#39;re not regressing behavior in other scenarios. Also, measure the cost, since it may change significantly. Use A/B testing to test the new version with live data, then promote it if it&#39;s better.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>Creating an AI Agent in 3 Clicks with Amazon Bedrock AgentCore Harness</title>
  <description>Create an AI agent with Amazon Bedrock AgentCore Harness, then manage prompts, tools, memory, versions, endpoints, observability, and export to Python.</description>
      <enclosure url="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/9d79e58b-1ab1-41f1-a756-be71f5d54198/image.png" length="251173" type="image/png"/>
  <link>https://newsletter.simpleaws.dev/p/amazon-bedrock-agentcore-harness</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/amazon-bedrock-agentcore-harness</guid>
  <pubDate>Sat, 29 Aug 2026 00:47:21 +0000</pubDate>
  <atom:published>2026-08-29T00:47:21Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Artificial Intelligence]]></category>
    <category><![CDATA[Architecture]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">An AI agent consists of a model (which you can call via <a class="link" href="https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=creating-an-ai-agent-in-3-clicks-with-amazon-bedrock-agentcore-harness" target="_blank" rel="noopener noreferrer nofollow">Amazon Bedrock</a>), a prompt, a set of tools that you can make available via <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=creating-an-ai-agent-in-3-clicks-with-amazon-bedrock-agentcore-harness" target="_blank" rel="noopener noreferrer nofollow">AgentCore Gateway</a>, and a loop that manages the model and tool calls, which can be implemented with <a class="link" href="https://github.com/strands-agents?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=creating-an-ai-agent-in-3-clicks-with-amazon-bedrock-agentcore-harness" target="_blank" rel="noopener noreferrer nofollow">Strands SDK</a> and that can run in <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=creating-an-ai-agent-in-3-clicks-with-amazon-bedrock-agentcore-harness" target="_blank" rel="noopener noreferrer nofollow">AgentCore Runtime</a>. You probably want to add some auth with <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/identity.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=creating-an-ai-agent-in-3-clicks-with-amazon-bedrock-agentcore-harness" target="_blank" rel="noopener noreferrer nofollow">AgentCore Identity</a>, memory with <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/memory.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=creating-an-ai-agent-in-3-clicks-with-amazon-bedrock-agentcore-harness" target="_blank" rel="noopener noreferrer nofollow">AgentCore Memory</a>, and set up proper observability with <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/observability.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=creating-an-ai-agent-in-3-clicks-with-amazon-bedrock-agentcore-harness" target="_blank" rel="noopener noreferrer nofollow">AgentCore Observability</a> sending logs and traces to <a class="link" href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/WhatIsCloudWatch.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=creating-an-ai-agent-in-3-clicks-with-amazon-bedrock-agentcore-harness" target="_blank" rel="noopener noreferrer nofollow">Amazon CloudWatch</a>. I can throw 10 more keywords at you, but I think you get the idea: Agents have a lot of moving parts, and AWS offers a bunch of services and features that you need to combine in the right way. Or you can read Simple AWS and learn about AgentCore harness 😁.</p><p class="paragraph" style="text-align:left;"><a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/harness.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=creating-an-ai-agent-in-3-clicks-with-amazon-bedrock-agentcore-harness" target="_blank" rel="noopener noreferrer nofollow">Amazon Bedrock AgentCore harness</a> is a capability of AgentCore, released on June 17, 2026, which acts as an opinionated way to create AI agents in the easiest way possible. You create a Harness resource and describe the agent through configuration: its model, instructions, tools, skills, memory, environment, and execution limits. AgentCore runs the loop inside an isolated AgentCore Runtime session and connects it to the configured capabilities, just like you would if you set everything up manually.</p><hr class="content_break"><p class="paragraph" style="text-align:left;"><i>This article is sponsored by Inngest</i></p><h2 class="heading" style="text-align:left;" id="dont-let-agent-failures-blow-the-bu">Don&#39;t let agent failures blow the budget.</h2><div class="image"><img alt="Inngest advertisement: Long-running agents fail. Don’t restart from zero." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/c096e88a-32ad-47f8-af8a-788a039095a2/Frame_2.png?t=1787964019"/></div><p class="paragraph" style="text-align:left;">Agents, like all of us, often need to recover from failure. But they shouldn&#39;t start from scratch.</p><p class="paragraph" style="text-align:left;">Inngest&#39;s durable execution checkpoints your agents every step of the way.</p><p class="paragraph" style="text-align:left;">So your agents can pick up where they left off, and avoid doubling up on token & compute spend.<br><br>Inngest offers a generous free tier and a sleek local dev server for testing.</p><div class="button" style="text-align:center;"><a target="_blank" rel="noopener nofollow noreferrer" class="button__link" style="" href="https://www.inngest.com/?utm_medium=newsletter&utm_source=beehiiv&utm_campaign=creator-growth-Q32026&utm_content=simple-aws-08282026"><span class="button__text" style=""> Build durably with Inngest </span></a></div><hr class="content_break"><h2 class="heading" style="text-align:left;" id="creating-an-agent-with-agent-core-h">Creating an Agent With AgentCore Harness</h2><p class="paragraph" style="text-align:left;">Let&#39;s start by creating an agent. We’ll build an AWS release researcher: You can ask it about an AWS service or feature, and it will use AgentCore Browser to check official AWS pages before returning the launch status, a short explanation, and its sources.</p><p class="paragraph" style="text-align:left;">Open the Amazon Bedrock AgentCore console, navigate to <b>Harnesses</b>, click the <b>Quick create harness</b> dropdown, and click <b>Advanced create harness</b>. You can also click <b>Quick create harness</b> and that will give you a harness in 3 clicks, like I promised in the title, but let&#39;s take the scenic route.</p><div class="image"><img alt="Amazon Bedrock AgentCore Harness console with the Quick create Harness menu expanded and Advanced create Harness highlighted." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/e685d282-4238-4653-bde6-375e4211ad9c/image.png?t=1787957313"/></div><p class="paragraph" style="text-align:left;">Give the Harness a name such as <code>aws_release_researcher</code>, pick a model such as Sonnet 4.6, and add this system prompt:</p><div class="codeblock"><pre><code>You are an AWS release research assistant.

Use the browser to consult official AWS documentation and AWS blogs.

For every answer:
1. State the current launch status.
2. Explain what changed.
3. Include the source URLs you used.
4. Say when the available evidence does not support a claim.

Do not use third-party sources.</code></pre></div><p class="paragraph" style="text-align:left;">Inside Tools, toggle Browser tool and select <b>AgentCore Browser Tool</b>. Keep everything else as is, you can play with it later (I know you will!).</p><p class="paragraph" style="text-align:left;"><i>Security FYI: AgentCore needs an execution role for the Harness. That role is assumed inside the Runtime session and needs permission to invoke your selected model and use the capabilities attached to the Harness. The default configuration is to create a new role for this. You can change it under Permissions if you&#39;re mindful about security, or keep it as it is for this test.</i></p><p class="paragraph" style="text-align:left;">Click <b>Create Harness</b> and wait for it to be ready. Once it&#39;s created, click the yellow <b>Test Harness</b> button on the top right, and you&#39;ll get redirected to the Harness playground interface. You can interact with your agent here. Try asking something that requires current information, such as:</p><div class="codeblock"><pre><code>When did Amazon Bedrock AgentCore Harness become generally available,
and what was added at GA?</code></pre></div><p class="paragraph" style="text-align:left;">AgentCore starts an isolated Runtime session, sends the prompt and Browser tool definition to the model, runs the Browser when the model requests it, returns the results to the model, and streams the final answer back to the console. Notice you did not write the orchestration loop or deploy an agent container, all of that is handled for you. </p><div class="image"><img alt="AgentCore Harness playground shows an AWS release research question, Browser tool trace, generated answer, and model, system prompt and memory configuration." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/9d79e58b-1ab1-41f1-a756-be71f5d54198/image.png?t=1787963584"/></div><p class="paragraph" style="text-align:left;">The console is the quickest way to understand the experience, but the same agent can be created and invoked through the SDK. The following example uses an existing IAM execution role and an Amazon Bedrock model ID supplied through environment variables, and sets up everything else using Python and boto3:</p><div class="codeblock"><pre><code>import os
import time
import uuid
import boto3

REGION = os.getenv(&quot;AWS_REGION&quot;, &quot;us-west-2&quot;)
ROLE_ARN = os.environ[&quot;HARNESS_EXECUTION_ROLE_ARN&quot;]
MODEL_ID = os.environ[&quot;BEDROCK_MODEL_ID&quot;]

SYSTEM_PROMPT = &quot;&quot;&quot;
You are an AWS release research assistant.

Use the browser to consult official AWS documentation and AWS blogs.

For every answer:
1. State the current launch status.
2. Explain what changed.
3. Include the source URLs you used.
4. Say when the available evidence does not support a claim.

Do not use third-party sources.
&quot;&quot;&quot;.strip()

control = boto3.client(
    &quot;bedrock-agentcore-control&quot;,
    region_name=REGION,
)

runtime = boto3.client(
    &quot;bedrock-agentcore&quot;,
    region_name=REGION,
)

created = control.create_harness(
    harnessName=&quot;aws_release_researcher&quot;,
    executionRoleArn=ROLE_ARN,
    model=&#123;
        &quot;bedrockModelConfig&quot;: &#123;
            &quot;modelId&quot;: MODEL_ID,
        &#125;
    &#125;,
    systemPrompt=[
        &#123;
            &quot;text&quot;: SYSTEM_PROMPT,
        &#125;
    ],
    tools=[
        &#123;
            &quot;type&quot;: &quot;agentcore_browser&quot;,
            &quot;name&quot;: &quot;browser&quot;,
        &#125;
    ],
    maxIterations=12,
    timeoutSeconds=300,
)

harness = created[&quot;harness&quot;]

while harness[&quot;status&quot;] == &quot;CREATING&quot;:
    time.sleep(3)
    harness = control.get_harness(
        harnessId=harness[&quot;harnessId&quot;],
    )[&quot;harness&quot;]

if harness[&quot;status&quot;] != &quot;READY&quot;:
    raise RuntimeError(
        f&quot;Harness creation failed: &#123;harness.get(&#39;failureReason&#39;, &#39;unknown error&#39;)&#125;&quot;
    )

response = runtime.invoke_harness(
    harnessArn=harness[&quot;arn&quot;],
    runtimeSessionId=str(uuid.uuid4()),
    messages=[
        &#123;
            &quot;role&quot;: &quot;user&quot;,
            &quot;content&quot;: [
                &#123;
                    &quot;text&quot;: (
                        &quot;When did Amazon Bedrock AgentCore Harness become &quot;
                        &quot;generally available, and what was added at GA?&quot;
                    )
                &#125;
            ],
        &#125;
    ],
)

for event in response[&quot;stream&quot;]:
    if &quot;contentBlockDelta&quot; in event:
        delta = event[&quot;contentBlockDelta&quot;].get(&quot;delta&quot;, &#123;&#125;)
        if &quot;text&quot; in delta:
            print(delta[&quot;text&quot;], end=&quot;&quot;, flush=True)

    elif &quot;runtimeClientError&quot; in event:
        raise RuntimeError(event[&quot;runtimeClientError&quot;][&quot;message&quot;])</code></pre></div><p class="paragraph" style="text-align:left;">This code snippet creates the resource, waits for it, and prints the response stream. Notice that there is no Strands <code>Agent</code>, no tool dispatcher, no loop that reads <code>toolUse</code> blocks, and no container entrypoint. Those things are entirely defined by the harness, though you can override them if you want. </p><h2 class="heading" style="text-align:left;" id="agent-as-configuration">Agent as Configuration</h2><p class="paragraph" style="text-align:left;">The whole point of harness is not really the defaults and the three clicks, but rather the ability to define an agent through configuration. The model, prompt, tools, memory, environment, and limits are properties of the Harness, passed as parameters to a simple <code>create_harness</code> function call, instead of constants buried inside an agent application.</p><p class="paragraph" style="text-align:left;">That separates the agent definition from the mechanism used to create it. Here is a CloudFormation version of the same release researcher:</p><div class="codeblock"><pre><code>AWSTemplateFormatVersion: &quot;2010-09-09&quot;
Description: AWS release research agent built with AgentCore Harness

Parameters:
  HarnessExecutionRoleArn:
    Type: String

  BedrockModelId:
    Type: String

Resources:
  ReleaseResearcherHarness:
    Type: AWS::BedrockAgentCore::Harness
    Properties:
      HarnessName: aws_release_researcher
      ExecutionRoleArn: !Ref HarnessExecutionRoleArn

      Model:
        BedrockModelConfig:
          ModelId: !Ref BedrockModelId
          Temperature: 0.2

      SystemPrompt:
        - Text: |
            You are an AWS release research assistant.

            Use the browser to consult official AWS documentation
            and AWS blogs.

            For every answer:
            1. State the current launch status.
            2. Explain what changed.
            3. Include the source URLs you used.
            4. Say when the available evidence does not support a claim.

            Do not use third-party sources.

      Tools:
        - Type: agentcore_browser
          Name: browser

      Memory:
        ManagedMemoryConfiguration:
          Strategies:
            - SEMANTIC
            - SUMMARIZATION
          EventExpiryDuration: 30

      MaxIterations: 12
      TimeoutSeconds: 300

      Tags:
        - Key: application
          Value: aws-release-researcher
        - Key: managed-by
          Value: cloudformation</code></pre></div><p class="paragraph" style="text-align:left;">This template defines the model, its instructions, the tool it can use, how memory is managed, and how long the agent can keep working on one request. CloudFormation creates the Harness, and AgentCore turns those properties into a Runtime, a managed loop, an isolated session environment, and the connections needed by the configured capabilities.</p><p class="paragraph" style="text-align:left;">AgentCore Browser, Code Interpreter, Gateway, and remote MCP tools can be connected to the managed loop. Skills can be added with reusable instructions, scripts, references, and files. Memory can be managed by the Harness, supplied through an existing AgentCore Memory resource, or disabled. A custom environment can add dependencies and command-line tools while the orchestration remains managed. All of that is done as configuration, and Harness takes care of translating it into implementation details.</p><p class="paragraph" style="text-align:left;">You can also change the configuration for a single invocation. Suppose we want a shorter answer from the release researcher without changing the deployed Harness:</p><div class="codeblock"><pre><code>response = runtime.invoke_harness(
    harnessArn=HARNESS_ARN,
    runtimeSessionId=str(uuid.uuid4()),
    systemPrompt=[
        &#123;
            &quot;text&quot;: (
                &quot;You are an AWS release research assistant. &quot;
                &quot;Use only official AWS sources. &quot;
                &quot;Return at most three paragraphs and include the URLs.&quot;
            )
        &#125;
    ],
    maxIterations=6,
    messages=[
        &#123;
            &quot;role&quot;: &quot;user&quot;,
            &quot;content&quot;: [
                &#123;
                    &quot;text&quot;: &quot;Explain what AgentCore Harness manages.&quot;
                &#125;
            ],
        &#125;
    ],
)</code></pre></div><p class="paragraph" style="text-align:left;">The override applies to that invocation. The Harness resource uses its existing configuration for every other call. You can use this to test another supported model or tool set without creating a second harness.</p><p class="paragraph" style="text-align:left;"><i>Security FYI: This flexibility also creates a permission boundary. A caller who can override model fields, tools, skills, or instructions can materially change what the agent accesses and how it behaves. The API you expose to invoke the harness should remove fields the caller should never control and allowlist the values that remain.</i></p><p class="paragraph" style="text-align:left;">AgentCore Harness has no separate service charge. You&#39;re billed the regular price for model inference, Runtime consumption, managed Memory, Browser, Gateway, Code Interpreter, storage, data transfer, CloudWatch, and anything else you use. The configuration is also the right place to add tags and limits so those costs can be attributed and bounded. For Bedrock calls, instead of a base model ID use an inference profile with the tags you want. Harness tags propagate to the managed Runtime, Runtime endpoint, and managed Memory it creates. Tag separately created resources, such as Gateway, EFS, S3, and existing Memory resources, independently:</p><div class="codeblock"><pre><code># Inside your AWS::BedrockAgentCore::Harness resource
MaxIterations: 12
MaxTokens: 8000
TimeoutSeconds: 300

Tags:
  - Key: application
    Value: aws-release-researcher
  - Key: environment
    Value: production
  - Key: owner
    Value: platform-team
  - Key: cost-center
    Value: genai</code></pre></div><p class="paragraph" style="text-align:left;">To use these tags in Cost Explorer and billing reports, you still need to activate their keys as user-defined cost allocation tags in AWS Billing and Cost Management.</p><h2 class="heading" style="text-align:left;" id="release-lifecycle-of-a-harness">Release Lifecycle of a Harness</h2><p class="paragraph" style="text-align:left;">I showed you how easy it is to create the first Harness. But after five prompt changes, two model experiments, and a new tool, you&#39;ll probably have a hard time keeping track of what it does. That&#39;s where versions and endpoints become useful.</p><p class="paragraph" style="text-align:left;">Suppose version 1 of our release researcher reports the launch date and source links, but users also want to know about regional availability, and a warning when an AWS page still contains stale preview language. We can test those new instructions through an invocation override, and iterate on them until we get the right prompt. Then we update the Harness configuration.</p><p class="paragraph" style="text-align:left;">That update creates a new immutable Harness version. Version 1 still contains the original model, prompt, tools, memory, environment, and limits. Version 2 contains the updated definition, with our changes.</p><p class="paragraph" style="text-align:left;">Harness endpoints decide which version receives requests. The automatically created <code>DEFAULT</code> endpoint always follows the latest version, and you can create named endpoints where you control which version they point to.</p><p class="paragraph" style="text-align:left;">For production, I recommend you create a named endpoint instead of sending traffic through <code>DEFAULT</code>:</p><div class="codeblock"><pre><code>aws bedrock-agentcore-control create-harness-endpoint \
  --harness-id &quot;$HARNESS_ID&quot; \
  --endpoint-name &quot;production&quot; \
  --target-version &quot;1&quot; \
  --description &quot;Production release researcher&quot;</code></pre></div><p class="paragraph" style="text-align:left;">You can update the Harness several times while <code>production</code> remains on version 1. After version 2 passes your tests, move the endpoint to point to it:</p><div class="codeblock"><pre><code>aws bedrock-agentcore-control update-harness-endpoint \
  --harness-id &quot;$HARNESS_ID&quot; \
  --endpoint-name &quot;production&quot; \
  --target-version &quot;2&quot; \
  --description &quot;Promote version 2&quot;</code></pre></div><p class="paragraph" style="text-align:left;">If the new version behaves badly, you can point the endpoint back to version 1, thus rolling back the change. You don&#39;t need to keep track of the old prompt or remember which model settings you used back then, version 1 contains all of that.</p><p class="paragraph" style="text-align:left;">This is how you should be iterating on a Harness:</p><p class="paragraph" style="text-align:left;"><b>Override one setting → inspect the session → evaluate the result → update the Harness → create a new version → move the endpoint</b></p><p class="paragraph" style="text-align:left;">Do this for every change. Remember that every single configuration item can significantly alter how the agent behaves. A simple prompt change can lead the model to select a different tool. A new skill can change the procedure it follows. Picking a different model can affect output quality, latency and cost. It&#39;s great that none of those changes require an application rebuild, but all of them can change what users see and what downstream systems receive, so they must be treated as new application versions.</p><h2 class="heading" style="text-align:left;" id="observability-and-evaluations-for-h">Observability and Evaluations for Harness</h2><p class="paragraph" style="text-align:left;">Harness sends traces, logs, and metrics for model calls, tool calls, memory operations, and shell commands to AgentCore Observability and CloudWatch. You need to enable CloudWatch Transaction Search before those traces are visible, then decide which dashboards, alerts, retention settings, and cost reports your team will use.</p><p class="paragraph" style="text-align:left;">Harness also gives you an observability dashboard where you can monitor your agents, pulling the same data that CloudWatch shows you, and with links to easily access more detailed information in CloudWatch.</p><div class="image"><img alt="AgentCore Harness observability dashboard showing five sessions and invocations, ten memory retrievals, and Runtime, Memory, Gateway, Browser and Code Interpreter metrics." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/93d6a3fd-ccd8-43d6-b471-0a324576ffd7/image.png?t=1787963671"/></div><p class="paragraph" style="text-align:left;">AgentCore Evaluations can score sessions and compare behavior across changes. Batch evaluations can run the same dataset against multiple versions, while the broader optimization capabilities can generate prompt or tool-description recommendations and test variants. The service can run the evaluation, but you still need to provide representative cases and a threshold that means something for the application.</p><p class="paragraph" style="text-align:left;">For the release researcher, that dataset could contain questions about launches with clean documentation, launches whose status changed from preview to GA, and services with conflicting or stale pages. An evaluation that checks only whether the final answer is fluent and well written will miss the failures this agent was created to prevent: stale information confidently presented as current.</p><h2 class="heading" style="text-align:left;" id="the-limits-of-agent-core-harness">The Limits of AgentCore Harness</h2><p class="paragraph" style="text-align:left;">Harness is easy to use because it makes several decisions for you. It runs an AWS-managed, Strands-powered agent loop with the configuration points AgentCore exposes. You can select models, provide instructions, attach tools and skills, configure memory and the environment, and set execution limits. The main limitation is that you cannot replace the loop with any control flow you want.</p><p class="paragraph" style="text-align:left;">If you need another agent framework, graph or workflow orchestration inside the agent, custom hooks or middleware, bidirectional streaming, or direct control over how each reasoning step proceeds, you&#39;ll need to define your agent in the old-fashioned way, and run it directly in AgentCore Runtime. In those cases, orchestration is part of your application rather than reusable plumbing around it, so Harness&#39;s sensible defaults to implement it no longer apply to your case.</p><p class="paragraph" style="text-align:left;">You do have some room between a completely managed Harness and a completely custom agent. Inline functions let the client execute individual tool calls, or AWS Step Functions can place a Harness inside a larger deterministic workflow with branching, retries, approvals, and non-agent steps. This way the reasoning loop remains managed and easy to build, while the wider process stays explicit and hyper-configurable.</p><p class="paragraph" style="text-align:left;"><i>Security FYI: Harness can&#39;t advise you on whether the configured behavior is safe for your application. The agent&#39;s execution assumes an IAM role you provide, but you still need to scope that role to the models, tools, credentials, and resources the agent requires. Skill repositories and S3 locations are treated as trusted content, but you probably want to scope this down. Invocation overrides can change provider settings and available tools, and it&#39;s your job to prevent this from happening. The caller and the Harness may both be authenticated while the requested action is still something your application should reject. You can set very good controls with </i><a class="link" href="https://newsletter.simpleaws.dev/p/layered-authorization-ai-agents-amazon-bedrock-agentcore?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=creating-an-ai-agent-in-3-clicks-with-amazon-bedrock-agentcore-harness" target="_blank" rel="noopener noreferrer nofollow"><i>Layered Authorization for AI Agents using Amazon Bedrock AgentCore</i></a>.</p><p class="paragraph" style="text-align:left;">Memory needs the same level of attention. Managed memory is convenient and can be easily enabled through the Harness, but actor IDs, retention, retrieval, privacy, and deletion remain application decisions (i.e. your decisions). The service can remember that <code>user-123</code> prefers short summaries, but your application must ensure that whomever initiated an agent invocation is actually allowed to read and write memory for <code>user-123</code>.</p><h2 class="heading" style="text-align:left;" id="exporting-a-harness">Exporting a Harness</h2><p class="paragraph" style="text-align:left;">AgentCore can turn an existing Harness into editable Python source code using Strands. This gives you a practical path from a configuration-managed agent to a code-based agent that can run in AgentCore Runtime, without recreating the whole thing by hand.</p><p class="paragraph" style="text-align:left;">The export includes model settings, tools, skills, memory, execution limits, context truncation, filesystem mounts, and authorization configuration into the generated agent. Currently the only available export target is Python with Strands, with either a <code>CodeZip</code> or container build. AWS currently documents additional frameworks as coming soon; no release date or additional language support is confirmed here.</p><p class="paragraph" style="text-align:left;">Our release researcher was created in the console, so to export it via the AgentCore CLI we need to use its ARN:</p><div class="codeblock"><pre><code>agentcore export harness \
  --arn &quot;$HARNESS_ARN&quot; \
  --target-agent-name &quot;AWSReleaseResearcherRuntime&quot; \
  --build CodeZip</code></pre></div><p class="paragraph" style="text-align:left;">If the Harness was created inside the same AgentCore CLI project, you can use its name instead:</p><div class="codeblock"><pre><code>agentcore export harness \
  --name &quot;aws_release_researcher&quot; \
  --target-agent-name &quot;AWSReleaseResearcherRuntime&quot;</code></pre></div><p class="paragraph" style="text-align:left;">If you want to use a container-based Runtime, you can change the build type while exporting it:</p><div class="codeblock"><pre><code>agentcore export harness \
  --arn &quot;$HARNESS_ARN&quot; \
  --target-agent-name &quot;AWSReleaseResearcherRuntime&quot; \
  --build Container</code></pre></div><p class="paragraph" style="text-align:left;">The CLI writes the generated agent under <code>app/</code> and adds it to the AgentCore project as a normal Runtime. Before touching the code, read the generated <code>EXPORT_NOTES.md</code>. That file lists configuration the exporter could not carry over automatically, explains why it needs attention, and tells you what to change. Once those items are resolved, deploy the generated agent to AgentCore Runtime by running <code>agentcore deploy</code>.</p><p class="paragraph" style="text-align:left;">The generated agent is ordinary source code from this point forward. You can change the Strands loop, add middleware, integrate another Python library, implement custom tool behavior, or restructure how the agent reasons. You also own its tests, dependencies, packaging, security updates, deployment, and future maintenance.</p><p class="paragraph" style="text-align:left;">Keep in mind that you don&#39;t need to export a Harness simply because it has reached production. Versions, named endpoints, observability, evaluations, managed memory, and infrastructure-as-code support are all available while the agent remains a Harness. Export should be used when you need to configure something that Harness doesn&#39;t support.</p><h2 class="heading" style="text-align:left;" id="conclusion">Conclusion</h2><p class="paragraph" style="text-align:left;">AgentCore Harness and the AWS console give you the fastest path to an agent, with just three clicks. Advanced create harness lets you set the values for every configuration item. The SDK and CloudFormation expose the same agent as a resource you can automate and review. Invocation overrides let you test changes without mutating it. Immutable versions and named endpoints let you release those changes deliberately. Export gives you the source code when you need to change something that is not supported in Harness&#39;s configuration.</p><p class="paragraph" style="text-align:left;">You still need to build the parts that are specific to your application. The prompt needs a clear job. Tools need correct behavior and narrow permissions. Memory needs proper user scoping. Evaluations need representative cases. The application still needs an interface, access controls, budgets, and someone who owns the result. And you still need to do the security work.</p><p class="paragraph" style="text-align:left;">Harness lets you separate the what from the how. You define via configuration every element of your agent, and Harness takes care of how to implement each element and how to tie everything together. Additionally, it gives you sufficient control that you can take an agent to production and keep managing it through Harness, with versions, endpoints and invocation overrides. You don&#39;t exactly outgrow Harness, but you might hit the limits of its configurations, and at that point you can export it and continue as code.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>How to Orchestrate AWS Lambda Preflight Checks Across Accounts and Regions with Kestra</title>
  <description>Build a Kestra workflow that invokes Lambda across AWS accounts and Regions, validates readiness results, requests approval, and updates a change ticket.</description>
      <enclosure url="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/08a424f8-110f-41e8-bdc9-abc64baef422/image.png" length="1917984" type="image/png"/>
  <link>https://newsletter.simpleaws.dev/p/kestra-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/kestra-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions</guid>
  <pubDate>Wed, 26 Aug 2026 22:42:31 +0000</pubDate>
  <atom:published>2026-08-26T22:42:31Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Serverless]]></category>
    <category><![CDATA[Operations]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;"><i>This article is sponsored by Kestra.</i></p><p class="paragraph" style="text-align:left;">Say you have a service that runs in three production environments, each in a different AWS account and Region. Before a release, you need to know whether every environment is healthy. For each account you can call a small Lambda function that reads the value of a <span style="color:#188038;">release-readiness</span> CloudWatch metric, and you&#39;ll know whether the environment in that account is ready. The problem we&#39;re going to discuss is how to call all three functions across different accounts, and aggregate the responses into a single result that answers whether we can release or not.</p><h2 class="heading" style="text-align:left;" id="introduction-to-kestra">Introduction to Kestra</h2><p class="paragraph" style="text-align:left;">We’re going to use <a class="link" href="https://fandf.co/4xTnbzq?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow">Kestra</a>, a declarative orchestration platform with an <a class="link" href="https://fandf.co/3SrmSfY?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow">Apache 2.0-licensed Open Source Edition</a> that can be self-hosted. For our solution, Kestra is going to act as the orchestration control plane. We&#39;re going to assume there are Lambda functions in different AWS accounts, which report whether the environment in that account is ready for release. Kestra is going to invoke them, verify that every required environment reports ready, and pause for a reviewer to approve the handoff. To build this we’ll adapt Kestra’s public <a class="link" href="https://fandf.co/4hMnqaH?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow">AWS Lambda Blueprint</a>, replacing its fixed set of invocations with a multi-account release runbook.</p><p class="paragraph" style="text-align:left;">Kestra workflows are defined in YAML. This is what a basic runbook looks like, to invoke an AWS Lambda function in different accounts:</p><div class="codeblock"><pre><code>tasks:
  - id: run_checks
    type: io.kestra.plugin.core.flow.ForEach
    values: &quot;&#123;&#123; vars.targets | keys &#125;&#125;&quot;
    tasks:
      - id: invoke
        type: io.kestra.plugin.aws.lambda.Invoke
        region: &quot;&#123;&#123; vars.targets[taskrun.value].region &#125;&#125;&quot;
        stsRoleArn: &quot;&#123;&#123; vars.targets[taskrun.value].role_arn &#125;&#125;&quot;
        functionArn: &quot;&#123;&#123; vars.targets[taskrun.value].function_arn &#125;&#125;&quot;</code></pre></div><p class="paragraph" style="text-align:left;"><span style="color:#188038;">vars.targets</span> will be a map containing the Region, role ARN, and function ARN for each production environment. <span style="color:#188038;">ForEach</span> runs the nested <span style="color:#188038;">Invoke</span> task once for every target, and <span style="color:#188038;">taskrun.value</span> identifies the target being processed in the current iteration.</p><p class="paragraph" style="text-align:left;">That loop is the center of the Kestra runbook we&#39;re building. Before we expand it into its complete form, let&#39;s define the AWS Lambda function that determines whether an environment is ready for release or not.</p><h2 class="heading" style="text-align:left;" id="defining-the-readiness-function">Defining the Readiness function</h2><p class="paragraph" style="text-align:left;">Our Readiness function is a regular Lambda function, which reads a value from the <span style="color:#188038;">release-readiness</span> CloudWatch metric and returns <span style="color:#188038;">passed: true</span> or <span style="color:#188038;">passed: false</span> depending on that metric&#39;s value. It&#39;s deployed in three different AWS accounts:</p><ul><li><p class="paragraph" style="text-align:left;">Account <span style="color:#188038;">111122223333</span>, in region <span style="color:#188038;">us-east-1</span> </p></li><li><p class="paragraph" style="text-align:left;">Account <span style="color:#188038;">444455556666</span>, in region <span style="color:#188038;">eu-west-1</span> </p></li><li><p class="paragraph" style="text-align:left;">Account <span style="color:#188038;">777788889999</span>, in region <span style="color:#188038;">ap-southeast-2</span> </p></li></ul><p class="paragraph" style="text-align:left;">Here&#39;s what the code looks like, more or less. I&#39;m not going to show you how to deploy it, let&#39;s just assume it&#39;s deployed.</p><div class="codeblock"><pre><code>from datetime import datetime, timedelta, timezone

import boto3

cloudwatch = boto3.client(&quot;cloudwatch&quot;)

NAMESPACE = &quot;Company/Release&quot;
METRIC_NAME = &quot;release-readiness&quot;
LOOKBACK_MINUTES = 5


def lambda_handler(event, context):
    required = (&quot;changeId&quot;, &quot;releaseId&quot;, &quot;executionId&quot;, &quot;target&quot;)

    if not isinstance(event, dict):
        raise ValueError(&quot;The invocation payload must be an object&quot;)

    missing = [
        field
        for field in required
        if not isinstance(event.get(field), str) or not event[field]
    ]
    if missing:
        raise ValueError(f&quot;Missing or invalid fields: &#123;&#39;, &#39;.join(missing)&#125;&quot;)

    # Exclude the current, potentially incomplete one-minute period.
    end = datetime.now(timezone.utc).replace(second=0, microsecond=0)

    response = cloudwatch.get_metric_data(
        MetricDataQueries=[
            &#123;
                &quot;Id&quot;: &quot;readiness&quot;,
                &quot;MetricStat&quot;: &#123;
                    &quot;Metric&quot;: &#123;
                        &quot;Namespace&quot;: NAMESPACE,
                        &quot;MetricName&quot;: METRIC_NAME,
                    &#125;,
                    &quot;Period&quot;: 60,
                    &quot;Stat&quot;: &quot;Minimum&quot;,
                    &quot;Unit&quot;: &quot;Count&quot;,
                &#125;,
                &quot;ReturnData&quot;: True,
            &#125;
        ],
        StartTime=end - timedelta(minutes=LOOKBACK_MINUTES),
        EndTime=end,
        ScanBy=&quot;TimestampDescending&quot;,
    )

    series = response[&quot;MetricDataResults&quot;][0]

    if series.get(&quot;StatusCode&quot;) != &quot;Complete&quot;:
        raise RuntimeError(
            f&quot;CloudWatch returned &#123;series.get(&#39;StatusCode&#39;, &#39;no status&#39;)&#125;&quot;
        )

    if not series.get(&quot;Values&quot;):
        raise RuntimeError(
            &quot;No release-readiness datapoint was found &quot;
            &quot;in the last five completed minutes&quot;
        )

    value = series[&quot;Values&quot;][0]
    observed_at = series[&quot;Timestamps&quot;][0].astimezone(timezone.utc)

    return &#123;
        &quot;schemaVersion&quot;: &quot;1&quot;,
        &quot;changeId&quot;: event[&quot;changeId&quot;],
        &quot;releaseId&quot;: event[&quot;releaseId&quot;],
        &quot;executionId&quot;: event[&quot;executionId&quot;],
        &quot;target&quot;: event[&quot;target&quot;],
        &quot;functionVersion&quot;: context.function_version,
        &quot;requestId&quot;: context.aws_request_id,
        &quot;passed&quot;: value == 1,
        &quot;message&quot;: f&quot;&#123;METRIC_NAME&#125; returned &#123;value:g&#125;&quot;,
        &quot;observedAt&quot;: observed_at.isoformat(),
    &#125;</code></pre></div><p class="paragraph" style="text-align:left;">Our Readiness function returns an object like this:</p><div class="codeblock"><pre><code>&#123;
 &quot;schemaVersion&quot;: &quot;1&quot;,
 &quot;changeId&quot;: &quot;CHG-2037&quot;,
 &quot;releaseId&quot;: &quot;2026.08.20&quot;,
 &quot;executionId&quot;: &quot;2nP8wQ5kF4rM7cV1&quot;,
 &quot;target&quot;: &quot;prod_us_east_1&quot;,
 &quot;functionVersion&quot;: &quot;42&quot;,
 &quot;requestId&quot;: &quot;328145a1-7b4c-4d72-9010-43d47b9d05a4&quot;,
 &quot;passed&quot;: true,
 &quot;message&quot;: &quot;release-readiness returned 1&quot;,
 &quot;observedAt&quot;: &quot;2026-08-20T16:42:08+00:00&quot;
&#125;</code></pre></div><p class="paragraph" style="text-align:left;">The <span style="color:#188038;">changeId</span>, <span style="color:#188038;">releaseId</span> and <span style="color:#188038;">executionId</span> fields let the flow verify that the response belongs to the current request. <span style="color:#188038;">functionVersion</span> is the Lambda function version that was executed, <span style="color:#188038;">requestId</span> can be matched to the Lambda invocation, and <span style="color:#188038;">observedAt</span> records the timestamp of the CloudWatch metric datapoint used for the readiness decision. <span style="color:#188038;">passed</span> is the value the workflow will eventually combine across all three targets.</p><p class="paragraph" style="text-align:left;">In this case we&#39;re deploying the same code to all three environments, to keep things simple. A more interesting case would be if each environment required checking different information to determine whether it&#39;s ready or not for a release. For example, each account could belong to a dedicated deployment for a tenant, where each may have different configurations, or be running different versions of our infrastructure, or custom variations. In that case, we&#39;d need a customized Lambda function for each environment. So long as they&#39;re Lambda functions and they return the same response format, we can call them and process them without changing the Kestra flow, it doesn&#39;t matter what they do to calculate the value of <span style="color:#188038;">passed</span>.</p><h2 class="heading" style="text-align:left;" id="calling-the-readiness-function-in-e">Calling the Readiness function in every AWS account</h2><p class="paragraph" style="text-align:left;">Here&#39;s where Kestra comes into play. This is based on Kestra&#39;s public <a class="link" href="https://fandf.co/4hMnqaH?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow">AWS Lambda Blueprint</a>, which already gives us the basic <span style="color:#188038;">Invoke</span> task to call an AWS Lambda function. For the runbook that we need to solve this particular problem, the Region, IAM role, and function ARN are going to take different values per environment. What we&#39;re going to do is keep those values together in a target map and let Kestra loop over it. This is what our Kestra runbook looks like:</p><div class="codeblock"><pre><code>id: multi_account_lambda_preflight
namespace: company.platform

inputs:
 - id: change_id
   type: STRING
   required: true
   validator: &#39;^[A-Za-z0-9][A-Za-z0-9._-]&#123;0,126&#125;$&#39;

 - id: release_id
   type: STRING
   required: true

variables:
 targets:
   prod_us_east_1:
     region: us-east-1
     role_arn: arn:aws:iam::111122223333:role/KestraPreflightInvoker
     function_arn: arn:aws:lambda:us-east-1:111122223333:function:release-readiness:42

   prod_eu_west_1:
     region: eu-west-1
     role_arn: arn:aws:iam::444455556666:role/KestraPreflightInvoker
     function_arn: arn:aws:lambda:eu-west-1:444455556666:function:release-readiness:17

   prod_ap_southeast_2:
     region: ap-southeast-2
     role_arn: arn:aws:iam::777788889999:role/KestraPreflightInvoker
     function_arn: arn:aws:lambda:ap-southeast-2:777788889999:function:release-readiness:9

tasks:
 - id: run_checks
   type: io.kestra.plugin.core.flow.ForEach
   values: &quot;&#123;&#123; vars.targets | keys &#125;&#125;&quot;
   tasks:
     - id: invoke
       type: io.kestra.plugin.aws.lambda.Invoke
       region: &quot;&#123;&#123; vars.targets[taskrun.value].region &#125;&#125;&quot;
       stsRoleArn: &quot;&#123;&#123; vars.targets[taskrun.value].role_arn &#125;&#125;&quot;
       stsRoleSessionName: &quot;kestra-&#123;&#123; execution.id &#125;&#125;&quot;
       functionArn: &quot;&#123;&#123; vars.targets[taskrun.value].function_arn &#125;&#125;&quot;
       functionPayload:
         changeId: &quot;&#123;&#123; inputs.change_id &#125;&#125;&quot;
         releaseId: &quot;&#123;&#123; inputs.release_id &#125;&#125;&quot;
         executionId: &quot;&#123;&#123; execution.id &#125;&#125;&quot;
         target: &quot;&#123;&#123; taskrun.value &#125;&#125;&quot;
       timeout: PT2M</code></pre></div><p class="paragraph" style="text-align:left;">The value for the key <span style="color:#188038;">values</span> inside <span style="color:#188038;">tasks</span> gets expanded into three keys from <span style="color:#188038;">targets</span>, which is where we defined our invocation targets (the Lambda functions, each in a separate AWS account). Since our task is of type <span style="color:#188038;">ForEach</span>, Kestra creates one iteration for each key, and <span style="color:#188038;">taskrun.value</span> inside the payload identifies the environment in the current iteration. The <span style="color:#188038;">Invoke</span> task uses that key to select the corresponding Region, role, and function ARN. This way, we define the invocation once, define each of our targets, and have the invocation be executed for all targets.</p><p class="paragraph" style="text-align:left;">The payload also includes the change id and release id (parameters we would want to set to keep track of what we&#39;re trying to release), the Kestra execution id, and the aforementioned target. The Lambda function will return those same values with its result, so the Kestra flow can reject a response that belongs to another target or execution.</p><h3 class="heading" style="text-align:left;" id="giving-kestra-access-to-each-accoun"><span style="color:#434343;">Giving Kestra access to each account</span></h3><p class="paragraph" style="text-align:left;">Notice that we&#39;re not adding AWS credentials anywhere here. We give an identity to the Kestra worker, which needs  <span style="color:#188038;">sts:AssumeRole</span> permission, and then Kestra&#39;s<a class="link" href="https://fandf.co/4xv3kqF?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow"> Invoke task</a> uses the AWS SDK default credential chain to assume the role that we defined for it in <span style="color:#188038;">stsRoleArn</span>.</p><p class="paragraph" style="text-align:left;">Each target role we define for tasks must have a trust relationship with the identity we give the Kestra worker, and in this case the target roles need to allow <span style="color:#188038;">lambda:InvokeFunction</span> on the readiness function in their respective accounts. Separate from this we have the Lambda execution role, which gives the function permission to read the local CloudWatch metric without giving Kestra direct access to that data.</p><div class="image"><img alt="Kestra release-readiness workflow invokes Lambda in three AWS accounts and Regions, validates CloudWatch-based results, aggregates readiness, requests human approval, and hands off to a change system." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/08a424f8-110f-41e8-bdc9-abc64baef422/image.png?t=1787783614"/></div><p class="paragraph" style="text-align:left;">Each function ARN above ends in a numbered version. A<a class="link" href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-versions.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow"> published Lambda version</a> is an immutable snapshot of the function’s code and most of its configuration. This flow deliberately uses qualified ARNs so the function can return <span style="color:#188038;">context.function_version</span> and Kestra can compare it with the configured version suffix. An unqualified ARN would invoke <span style="color:#188038;">$LATEST</span>, and it would work if we change the validation a bit, but it&#39;s always a good idea to pin versions.</p><p class="paragraph" style="text-align:left;">With this, Kestra can now invoke one readiness function per environment and keep all three responses in the same execution. However, those responses still need a common format before the workflow can combine them into a single result.</p><h2 class="heading" style="text-align:left;" id="validating-each-lambda-response">Validating each Lambda response</h2><p class="paragraph" style="text-align:left;">There are many things that can go wrong in the invocation of our Readiness Lambda functions. Here&#39;s how we want to treat them.</p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><td class="bh__table_cell" width="50%"><p class="paragraph" style="text-align:center;"><b>Outcome</b></p></td><td class="bh__table_cell" width="50%"><p class="paragraph" style="text-align:center;"><b>What the runbook should do</b></p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="50%"><p class="paragraph" style="text-align:left;">Role assumption or Lambda API request fails</p></td><td class="bh__table_cell" width="50%"><p class="paragraph" style="text-align:left;">Stops because the environment was not checked</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="50%"><p class="paragraph" style="text-align:left;">Lambda returns <span style="color:#188038;">FunctionError</span></p></td><td class="bh__table_cell" width="50%"><p class="paragraph" style="text-align:left;">Stops because the function produced no usable result</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="50%"><p class="paragraph" style="text-align:left;">Lambda returns malformed or mismatched JSON</p></td><td class="bh__table_cell" width="50%"><p class="paragraph" style="text-align:left;">Stops because the response cannot be trusted</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="50%"><p class="paragraph" style="text-align:left;">Lambda returns valid JSON with <span style="color:#188038;">passed: false</span></p></td><td class="bh__table_cell" width="50%"><p class="paragraph" style="text-align:left;">Records the response, then rejects the combined preflight</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="50%"><p class="paragraph" style="text-align:left;">Lambda returns valid JSON with <span style="color:#188038;">passed: true</span></p></td><td class="bh__table_cell" width="50%"><p class="paragraph" style="text-align:left;">Records the response and waits for the other environments</p></td></tr></table></div><p class="paragraph" style="text-align:left;">A failed STS or Lambda API request causes the <span style="color:#188038;">Invoke</span> task to fail. Handler and runtime failures are different, a synchronous Lambda invocation can still return HTTP 200, with <span style="color:#188038;">FunctionError</span> indicating that the function failed. Kestra’s <span style="color:#188038;">Invoke</span> task checks that field and fails the task. A normal handler return, whether <span style="color:#188038;">passed</span> is true or false, produces a successful invocation task, and it&#39;s up to the flow to inspect the payload.</p><p class="paragraph" style="text-align:left;">To do that, we need to add the following tasks after <span style="color:#188038;">invoke</span>, inside <span style="color:#188038;">run_checks.tasks</span>:</p><div class="codeblock"><pre><code>     - id: validate_response
       type: io.kestra.plugin.core.execution.Assert
       conditions:
         - &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).schemaVersion == &#39;1&#39; &#125;&#125;&quot;
         - &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).changeId == inputs.change_id &#125;&#125;&quot;
         - &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).releaseId == inputs.release_id &#125;&#125;&quot;
         - &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).executionId == execution.id &#125;&#125;&quot;
         - &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).target == taskrun.value &#125;&#125;&quot;
         - &gt;-
           &#123;&#123;
             fromJson(read(outputs.invoke[taskrun.value].uri)).functionVersion ==
             (vars.targets[taskrun.value].function_arn | split(&#39;:&#39;) | last)
           &#125;&#125;
         - &gt;-
           &#123;&#123;
             (
               fromJson(read(outputs.invoke[taskrun.value].uri))
               | jq(&#39;((.requestId | type) == &quot;string&quot;) and
                     (.requestId | length &gt; 0) and
                     ((.passed | type) == &quot;boolean&quot;) and
                     ((.message | type) == &quot;string&quot;) and
                     (.message | length &gt; 0) and
                     ((.observedAt | type) == &quot;string&quot;) and
                     (.observedAt | length &gt; 0)&#39;)
               | first
             ) == true
           &#125;&#125;
       errorMessage: &quot;Invalid preflight response for &#123;&#123; taskrun.value &#125;&#125;&quot;

     - id: record_result
       type: io.kestra.plugin.core.output.OutputValues
       values:
         target: &quot;&#123;&#123; taskrun.value &#125;&#125;&quot;
         region: &quot;&#123;&#123; vars.targets[taskrun.value].region &#125;&#125;&quot;
         functionArn: &quot;&#123;&#123; vars.targets[taskrun.value].function_arn &#125;&#125;&quot;
         functionVersion: &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).functionVersion &#125;&#125;&quot;
         requestId: &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).requestId &#125;&#125;&quot;
         passed: &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).passed &#125;&#125;&quot;
         message: &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).message &#125;&#125;&quot;
         observedAt: &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).observedAt &#125;&#125;&quot;</code></pre></div><p class="paragraph" style="text-align:left;">This new <span style="color:#188038;">validate_response</span> task that we&#39;ve just added compares the values returned by each <span style="color:#188038;">Invoke</span> task with the current Kestra flow inputs and execution. It also checks the function version and the types of the fields that the rest of the workflow will use.</p><p class="paragraph" style="text-align:left;">The assertion deliberately accepts <span style="color:#188038;">passed: false</span> as a valid value. That response means the function ran correctly and found that its environment was not ready. We want Kestra to record the result, then let the aggregator decide whether the release can continue (spoiler: it can&#39;t, but we&#39;re separating concerns).</p><p class="paragraph" style="text-align:left;">After these validations, we have one response from each environment. Kestra can now compare those responses with the target inventory and determine whether we can proceed with the release or not.</p><h2 class="heading" style="text-align:left;" id="combining-the-responses-into-one-re">Combining the responses into one release decision</h2><p class="paragraph" style="text-align:left;">We have the individual responses, now we need to aggregate them into a single answer and determine if we can release or not. To do that, we&#39;re going to add these tasks to our Kestra flow after <span style="color:#188038;">run_checks</span>:</p><div class="codeblock"><pre><code> - id: collect_results
   type: io.kestra.plugin.core.output.OutputValues
   values:
     results: &gt;-
       &#123;&#123; outputs.record_result | values | jq(&#39;map(.values) | sort_by(.target)&#39;) | first &#125;&#125;

 - id: require_all_to_pass
   type: io.kestra.plugin.core.execution.Assert
   conditions:
     - &quot;&#123;&#123; (vars.targets | keys | length) &gt; 0 &#125;&#125;&quot;
     - &gt;-
       &#123;&#123;
         (outputs.collect_results.values.results | length) ==
         (vars.targets | keys | length)
       &#125;&#125;
     - &gt;-
       &#123;&#123;
         (
           outputs.collect_results.values.results
           | jq(&#39;all(.[]; .passed == true)&#39;)
           | first
         ) == true
       &#125;&#125;
   errorMessage: Every expected environment must return passed=true.</code></pre></div><p class="paragraph" style="text-align:left;">Because <span style="color:#188038;">record_result</span> runs inside <span style="color:#188038;">ForEach</span>, Kestra stores one output for each iteration, keyed by the current <span style="color:#188038;">taskrun.value</span>, such as <span style="color:#188038;">prod_us_east_1</span>. Each output contains the values emitted by <span style="color:#188038;">record_result</span> under <span style="color:#188038;">.values</span>. The new <span style="color:#188038;">collect_results</span> task extracts those values into a list and sorts it by <span style="color:#188038;">target</span>. The assertion then checks that the target inventory isn’t empty, that the number of results matches the number of configured environments, and that every result contains the Boolean value <span style="color:#188038;">passed: true</span>.</p><p class="paragraph" style="text-align:left;">If a role assumption or invocation fails, <span style="color:#188038;">run_checks</span> fails before we call the <span style="color:#188038;">collect_results</span> task. If a response doesn&#39;t match the expected format, <span style="color:#188038;">validate_response</span> fails and stops that iteration. If a response contains <span style="color:#188038;">passed: false</span>, the Kestra flow execution reaches the <span style="color:#188038;">require_all_to_pass</span> task, which in turn fails.</p><p class="paragraph" style="text-align:left;">When <span style="color:#188038;">require_all_to_pass</span> succeeds, it means every target environment&#39;s Readiness Lambda function returned <span style="color:#188038;">passed: true</span>, meaning all our automated checks pass. Now let&#39;s get a human reviewer in here for the final approval.</p><h2 class="heading" style="text-align:left;" id="asking-a-reviewer-to-approve-the-re">Asking a reviewer to approve the release</h2><p class="paragraph" style="text-align:left;">We have a readiness snapshot from the automated checks; it does not guarantee the environments are still healthy after the approval pause. A human reviewer still needs to authorize the release process, since we might be using human-controlled release windows, and someone needs to check that an incident isn&#39;t happening at the same time. Or we could always use the corporate reason: If something goes wrong, we need someone to blame. It&#39;s a joke, but if you work at a place where that&#39;s not a joke, I&#39;m sorry.</p><p class="paragraph" style="text-align:left;">We&#39;re going to introduce a new task called <span style="color:#188038;">approve_change</span>, right after <span style="color:#188038;">require_all_to_pass</span>:</p><div class="codeblock"><pre><code> - id: approve_change
   type: io.kestra.plugin.ee.flow.HumanTask
   description: &gt;-
     Review the preflight results for release &#123;&#123; inputs.release_id &#125;&#125;:
     &#123;&#123; outputs.collect_results.values.results | toJson &#125;&#125;
   assignment:
     groups:
       - Production Change Approvers
   onResume:
     - id: approved
       type: BOOL
       displayName: Approve this release?
       required: true

     - id: reason
       type: STRING
       displayName: Decision reason
       required: true
       validator: &#39;(?s).*\S.*&#39;

   pauseDuration: PT15M
   behavior: FAIL</code></pre></div><p class="paragraph" style="text-align:left;">This is a <span style="color:#188038;">HumanTask</span>, assigned to the group <span style="color:#188038;">Production Change Approvers</span>. It requires both an answer of type BOOL and a reason. This pauses the Kestra flow for up to 15 minutes (defined in <span style="color:#188038;">pauseDuration</span>) and waits for a human input. If nobody in that group responds within 15 minutes, the task fails with <span style="color:#188038;">behavior: FAIL</span>.</p><p class="paragraph" style="text-align:left;">Note that <a class="link" href="https://fandf.co/4xU5IXD?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow">HumanTask</a> and group assignment are Kestra Enterprise features, meaning you need to pay for them. Other interesting enterprise features are<a class="link" href="https://fandf.co/4xnlhac?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow"> RBAC</a> and<a class="link" href="https://fandf.co/4qqW4cB?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow"> Audit Logs</a>, but I won&#39;t turn this into a list of Kestra features, you can check the whole list of enterprise features at the <a class="link" href="https://fandf.co/4iqucTE?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow">Kestra Enterprise</a> docs page.</p><h2 class="heading" style="text-align:left;" id="sending-the-approved-result-to-the-">Sending the approved result to the change system</h2><p class="paragraph" style="text-align:left;">Once we have human approval, we want to hand off the process to a ticket system (something where you&#39;re presumably keeping track of releases). We&#39;re going to have Kestra send the responses of all environments, the reviewer&#39;s identity (so we can blame them if something goes wrong), approval reason, and the execution ID used by the Kestra flow.</p><div class="codeblock"><pre><code> - id: handoff
   type: io.kestra.plugin.core.flow.If
   condition: &quot;&#123;&#123; outputs.approve_change.onResume.approved == true &#125;&#125;&quot;
   then:
     - id: update_change_ticket
       type: io.kestra.plugin.core.http.Request
       uri: &quot;&#123;&#123; secret(&#39;CHANGE_API_URL&#39;) &#125;&#125;/changes/&#123;&#123; inputs.change_id &#125;&#125;&quot;
       method: PATCH
       contentType: application/json
       headers:
         Authorization: &quot;Bearer &#123;&#123; secret(&#39;CHANGE_API_TOKEN&#39;) &#125;&#125;&quot;
       body: &gt;-
         &#123;&#123;
           &#123;
             &quot;status&quot;: &quot;preflight-approved&quot;,
             &quot;changeId&quot;: inputs.change_id,
             &quot;releaseId&quot;: inputs.release_id,
             &quot;approvedBy&quot;: outputs.approve_change.resumed.by,
             &quot;approvedAt&quot;: outputs.approve_change.resumed.on,
             &quot;reason&quot;: outputs.approve_change.onResume.reason,
             &quot;executionId&quot;: execution.id,
             &quot;results&quot;: outputs.collect_results.values.results
           &#125; | toJson
         &#125;&#125;
   else:
     - id: reject_change
       type: io.kestra.plugin.core.execution.Fail
       errorMessage: &gt;-
         Release &#123;&#123; inputs.release_id &#125;&#125; was rejected by
         &#123;&#123; outputs.approve_change.resumed.by &#125;&#125;.</code></pre></div><p class="paragraph" style="text-align:left;">All the<a class="link" href="https://fandf.co/45EhwkO?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow"> Request task</a> does is send an HTTP request to an endpoint. In this case we&#39;re sending the complete result and approval metadata to our ticket system at <span style="color:#188038;">CHANGE_API_URL</span>. Note that URL and Bearer token used for auth come from Kestra secrets, they&#39;re not hardcoded into the Kestra flow or inputs. By default, the task fails on HTTP status codes of 400 or higher, so an unsuccessful ticket update also fails the Kestra execution.</p><h2 class="heading" style="text-align:left;" id="where-kestra-fits-alongside-aw-snat">Where Kestra fits alongside AWS-native orchestration</h2><p class="paragraph" style="text-align:left;">This was all super fun, but you might be wondering, is there a reason we&#39;re using Kestra here instead of an AWS native option? Let&#39;s look at the alternatives AWS gives us.</p><p class="paragraph" style="text-align:left;">If you look at that whole YAML, the first solution that comes to mind is probably <a class="link" href="https://docs.aws.amazon.com/step-functions/latest/dg/connect-lambda.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow">AWS Step Functions</a>. It&#39;s a state machine where you can define workflows, it can invoke Lambda and assume a task role in another account, and can split executions and aggregate results. One limitation is that<a class="link" href="https://docs.aws.amazon.com/step-functions/latest/dg/concepts-access-cross-acct-resources.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow"> AWS service integrations do not provide cross-Region resource access</a>. I still love Step Functions though.</p><p class="paragraph" style="text-align:left;">Another option is <a class="link" href="https://docs.aws.amazon.com/systems-manager/latest/userguide/running-automations-multiple-accounts-regions.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow">Systems Manager Automation</a>, which can run operational workflows across accounts and Regions with centralized targeting and rate controls. On paper it works well, but you&#39;ll find that its<a class="link" href="https://docs.aws.amazon.com/systems-manager/latest/userguide/automation-action-approve.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow"> aws:approve</a> action is not supported in multi-account and multi-Region automations.</p><h2 class="heading" style="text-align:left;" id="conclusion">Conclusion</h2><p class="paragraph" style="text-align:left;">We defined the list of target environments (which all have previously deployed Readiness Lambda functions), Kestra handles invoking those functions, rejecting missing or malformed responses, and aggregating responses into a release decision. Then it escalates to a human approver, who authorizes the release. Kestra then hands off the process to a ticket system that keeps track of everything. Kestra keeps the cross-Region invocations, assigned approval, and external ticket handoff in a single YAML workflow. It&#39;s useful to us because our process crosses AWS accounts and Regions, needs a human decision, and calls a system outside AWS.</p><p class="paragraph" style="text-align:left;"><i>This article is sponsored by Kestra</i>. I said it upfront, and I&#39;m saying it again. However, I only agreed to write and publish it because I genuinely think it&#39;s a good and useful solution. Simple problems should use simple solutions, so if you&#39;re dealing with a single environment in a single account and region, probably use Step Functions or just a single Lambda. Complex problems still deserve simple solutions whenever possible, and that&#39;s where I think Kestra is genuinely valuable.</p><p class="paragraph" style="text-align:left;">Remember that Kestra’s Open Source Edition is available under the<a class="link" href="https://fandf.co/4zyNlt2?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow"> Apache License 2.0</a>, with its source code in the<a class="link" href="https://fandf.co/3SrmSfY?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow"> Kestra GitHub repository</a>. Kestra also has two paid offerings: the self-hosted<a class="link" href="https://fandf.co/4zyNs7W?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow"> Enterprise Edition</a> and the fully managed service <a class="link" href="https://fandf.co/4hKS1Wf?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow">Kestra Cloud</a>. The assigned <span style="color:#188038;">HumanTask</span> approval used in this article requires Enterprise Edition or Cloud. With the Open Source Edition, you can run the automated portion and use the standard <span style="color:#188038;">Pause</span> task instead, but Kestra will not restrict approval to a specified user or RBAC group. You can find their<a class="link" href="https://fandf.co/4gtd5hF?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow"> documentation</a> and<a class="link" href="https://fandf.co/4bXwBRZ?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow"> Quickstart guide</a> on their website or their <a class="link" href="https://fandf.co/3SrmSfY?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=how-to-orchestrate-aws-lambda-preflight-checks-across-accounts-and-regions-with-kestra" target="_blank" rel="noopener noreferrer nofollow">GitHub repository</a>.</p><p class="paragraph" style="text-align:left;">Here&#39;s the complete Kestra runbook for you to try:</p><div class="codeblock"><pre><code>id: multi_account_lambda_preflight
namespace: company.platform

inputs:
  - id: change_id
    type: STRING
    required: true
    validator: &#39;^[A-Za-z0-9][A-Za-z0-9._-]&#123;0,126&#125;$&#39;

  - id: release_id
    type: STRING
    required: true

variables:
  targets:
    prod_us_east_1:
      region: us-east-1
      role_arn: arn:aws:iam::111122223333:role/KestraPreflightInvoker
      function_arn: arn:aws:lambda:us-east-1:111122223333:function:release-readiness:42

    prod_eu_west_1:
      region: eu-west-1
      role_arn: arn:aws:iam::444455556666:role/KestraPreflightInvoker
      function_arn: arn:aws:lambda:eu-west-1:444455556666:function:release-readiness:17

    prod_ap_southeast_2:
      region: ap-southeast-2
      role_arn: arn:aws:iam::777788889999:role/KestraPreflightInvoker
      function_arn: arn:aws:lambda:ap-southeast-2:777788889999:function:release-readiness:9

tasks:
  - id: run_checks
    type: io.kestra.plugin.core.flow.ForEach
    values: &quot;&#123;&#123; vars.targets | keys &#125;&#125;&quot;
    concurrencyLimit: 3
    tasks:
      - id: invoke
        type: io.kestra.plugin.aws.lambda.Invoke
        region: &quot;&#123;&#123; vars.targets[taskrun.value].region &#125;&#125;&quot;
        stsRoleArn: &quot;&#123;&#123; vars.targets[taskrun.value].role_arn &#125;&#125;&quot;
        stsRoleSessionName: &quot;kestra-&#123;&#123; execution.id &#125;&#125;&quot;
        functionArn: &quot;&#123;&#123; vars.targets[taskrun.value].function_arn &#125;&#125;&quot;
        functionPayload:
          changeId: &quot;&#123;&#123; inputs.change_id &#125;&#125;&quot;
          releaseId: &quot;&#123;&#123; inputs.release_id &#125;&#125;&quot;
          executionId: &quot;&#123;&#123; execution.id &#125;&#125;&quot;
          target: &quot;&#123;&#123; taskrun.value &#125;&#125;&quot;
        timeout: PT2M

      - id: validate_response
        type: io.kestra.plugin.core.execution.Assert
        conditions:
          - &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).schemaVersion == &#39;1&#39; &#125;&#125;&quot;
          - &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).changeId == inputs.change_id &#125;&#125;&quot;
          - &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).releaseId == inputs.release_id &#125;&#125;&quot;
          - &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).executionId == execution.id &#125;&#125;&quot;
          - &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).target == taskrun.value &#125;&#125;&quot;
          - &gt;-
            &#123;&#123;
              fromJson(read(outputs.invoke[taskrun.value].uri)).functionVersion ==
              (vars.targets[taskrun.value].function_arn | split(&#39;:&#39;) | last)
            &#125;&#125;
          - &gt;-
            &#123;&#123;
              (
                fromJson(read(outputs.invoke[taskrun.value].uri))
                | jq(&#39;((.requestId | type) == &quot;string&quot;) and
                      (.requestId | length &gt; 0) and
                      ((.passed | type) == &quot;boolean&quot;) and
                      ((.message | type) == &quot;string&quot;) and
                      (.message | length &gt; 0) and
                      ((.observedAt | type) == &quot;string&quot;) and
                      (.observedAt | length &gt; 0)&#39;)
                | first
              ) == true
            &#125;&#125;
        errorMessage: &quot;Invalid preflight response for &#123;&#123; taskrun.value &#125;&#125;&quot;

      - id: record_result
        type: io.kestra.plugin.core.output.OutputValues
        values:
          target: &quot;&#123;&#123; taskrun.value &#125;&#125;&quot;
          region: &quot;&#123;&#123; vars.targets[taskrun.value].region &#125;&#125;&quot;
          functionArn: &quot;&#123;&#123; vars.targets[taskrun.value].function_arn &#125;&#125;&quot;
          functionVersion: &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).functionVersion &#125;&#125;&quot;
          requestId: &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).requestId &#125;&#125;&quot;
          passed: &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).passed &#125;&#125;&quot;
          message: &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).message &#125;&#125;&quot;
          observedAt: &quot;&#123;&#123; fromJson(read(outputs.invoke[taskrun.value].uri)).observedAt &#125;&#125;&quot;

  - id: collect_results
    type: io.kestra.plugin.core.output.OutputValues
    values:
      results: &gt;-
        &#123;&#123; outputs.record_result | values | jq(&#39;map(.values) | sort_by(.target)&#39;) | first &#125;&#125;

  - id: require_all_to_pass
    type: io.kestra.plugin.core.execution.Assert
    conditions:
      - &quot;&#123;&#123; (vars.targets | keys | length) &gt; 0 &#125;&#125;&quot;
      - &gt;-
        &#123;&#123;
          (outputs.collect_results.values.results | length) ==
          (vars.targets | keys | length)
        &#125;&#125;
      - &gt;-
        &#123;&#123;
          (
            outputs.collect_results.values.results
            | jq(&#39;all(.[]; .passed == true)&#39;)
            | first
          ) == true
        &#125;&#125;
    errorMessage: Every expected environment must return passed=true.

  - id: approve_change
    type: io.kestra.plugin.ee.flow.HumanTask
    description: &gt;-
      Review the preflight results for release &#123;&#123; inputs.release_id &#125;&#125;:
      &#123;&#123; outputs.collect_results.values.results | toJson &#125;&#125;
    assignment:
      groups:
        - Production Change Approvers
    onResume:
      - id: approved
        type: BOOL
        displayName: Approve this release?
        required: true

      - id: reason
        type: STRING
        displayName: Decision reason
        required: true
        validator: &#39;(?s).*\S.*&#39;

    pauseDuration: PT15M
    behavior: FAIL

  - id: handoff
    type: io.kestra.plugin.core.flow.If
    condition: &quot;&#123;&#123; outputs.approve_change.onResume.approved == true &#125;&#125;&quot;
    then:
      - id: update_change_ticket
        type: io.kestra.plugin.core.http.Request
        uri: &quot;&#123;&#123; secret(&#39;CHANGE_API_URL&#39;) &#125;&#125;/changes/&#123;&#123; inputs.change_id &#125;&#125;&quot;
        method: PATCH
        contentType: application/json
        headers:
          Authorization: &quot;Bearer &#123;&#123; secret(&#39;CHANGE_API_TOKEN&#39;) &#125;&#125;&quot;
        body: &gt;-
          &#123;&#123;
            &#123;
              &quot;status&quot;: &quot;preflight-approved&quot;,
              &quot;changeId&quot;: inputs.change_id,
              &quot;releaseId&quot;: inputs.release_id,
              &quot;approvedBy&quot;: outputs.approve_change.resumed.by,
              &quot;approvedAt&quot;: outputs.approve_change.resumed.on,
              &quot;reason&quot;: outputs.approve_change.onResume.reason,
              &quot;executionId&quot;: execution.id,
              &quot;results&quot;: outputs.collect_results.values.results
            &#125; | toJson
          &#125;&#125;
    else:
      - id: reject_change
        type: io.kestra.plugin.core.execution.Fail
        errorMessage: &gt;-
          Release &#123;&#123; inputs.release_id &#125;&#125; was rejected by
          &#123;&#123; outputs.approve_change.resumed.by &#125;&#125;.</code></pre></div><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>AWS Lambda MicroVM: A New Compute Model Inside Lambda</title>
  <description>Learn how AWS Lambda MicroVMs provide isolated sessions with suspend and resume, plus lifecycle hooks, VPC networking, scaling, and a worked cost example.</description>
      <enclosure url="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/b5650251-a973-45ec-bf51-f16be80050c6/image.png" length="1772692" type="image/png"/>
  <link>https://newsletter.simpleaws.dev/p/aws-lambda-microvm</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/aws-lambda-microvm</guid>
  <pubDate>Fri, 31 Jul 2026 21:41:49 +0000</pubDate>
  <atom:published>2026-07-31T21:41:49Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Serverless]]></category>
    <category><![CDATA[Architecture]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">AWS Lambda gives you a way to invoke a piece of code with an events-based model, and to run it in an extremely small and quick to start VM based on <a class="link" href="https://github.com/firecracker-microvm/firecracker?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-lambda-microvm-a-new-compute-model-inside-lambda" target="_blank" rel="noopener noreferrer nofollow">Firecracker</a> (which AWS created for this, and open sourced). It&#39;s a fantastic value proposition, and has been one of the main carriers of the serverless torch since 2014. But what if you needed the whole Firecracker as a Service runtime, on its own, without the Lambda execution model on top of it?</p><p class="paragraph" style="text-align:left;">Let me introduce you to <a class="link" href="https://docs.aws.amazon.com/lambda/latest/dg/microvms-how-it-works.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-lambda-microvm-a-new-compute-model-inside-lambda" target="_blank" rel="noopener noreferrer nofollow">Lambda MicroVM</a>, AWS&#39;s new compute primitive, launched on June 22, 2026. It lets you launch a Firecracker MicroVM in less than 2 seconds (just like Lambda), and it lets you fully manage that MicroVM (unlike Lambda). You can drop a web server in it and send as many requests as you want, it&#39;ll behave just like a server. You can pause and resume it, and it maintains memory and disk state. You can set it to auto-pause when idle, and auto-resume when it receives a request, and it&#39;ll start back up in less than 500 milliseconds (not a marketing claim from AWS, I&#39;ve tested this thoroughly, my average was 412 ms).</p><p class="paragraph" style="text-align:left;">All of that sounds great, but tools without a purpose are just toys. So let me dive into where Lambda MicroVM is a true differentiator, and the tech details you need to know. Spoiler: Lambda MicroVM powers AgentCore Runtime&#39;s serverless compute model.</p><hr class="content_break"><h2 class="heading" style="text-align:left;" id="this-article-is-sponsored-by-palark"><span style="font-size:24px;">This article is sponsored by </span>Palark</h2><div class="image"><a class="image__link" href="https://palark.com/services/devops-insurance/?utm_source=simpleaws&utm_medium=email&utm_campaign=di0726" rel="noopener" target="_blank"><img alt="Palark DevOps Insurance advertisement: 24/7 on-call support from €500 per month, shown as firefighters tackling a server fire." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/748cbc3e-59e7-46dc-a058-a80c01ef4ded/palark-di-banner.png?t=1785532899"/></a></div><p class="paragraph" style="text-align:left;">Palark, a holistic DevOps and SRE agency from Germany, just launched an affordable support service that works like a fire brigade for your cloud or server infra.</p><p class="paragraph" style="text-align:left;">Similar to health or car insurance, DevOps insurance offers subscription plans with reasonable monthly fees and prompt assistance in case of infrastructure emergencies.</p><div class="button" style="text-align:center;"><a target="_blank" rel="noopener nofollow noreferrer" class="button__link" style="" href="https://palark.com/services/devops-insurance/?utm_source=simpleaws&utm_medium=email&utm_campaign=di0726"><span class="button__text" style=""> Learn more </span></a></div><p class="paragraph" style="text-align:left;"><i>Personal opinion: I&#39;ve reviewed this thoroughly, and it sounds like a fantastic idea for teams that can&#39;t afford on-call engineers due to their small size, and at the same time don&#39;t want to completely offload infrastructure management to an MSP that would provide 24/7 support as one more line item in a very large package. I&#39;ve spoken with the Palark team and I&#39;m confident they really know what they&#39;re doing.</i></p><hr class="content_break"><h2 class="heading" style="text-align:left;" id="dedicated-environments">Dedicated Environments</h2><p class="paragraph" style="text-align:left;">Lambda MicroVM lets you create a dedicated environment that belongs to a single tenant, session, or job. It&#39;s a perfect fit for work that needs continuity inside an isolation boundary, such as an interactive workspace that installs packages, an analysis session that keeps local processes and intermediate files, a scanner that runs third-party code, or an agent that makes several tool calls against the same execution environment.</p><p class="paragraph" style="text-align:left;">Lambda Functions are oriented toward event-driven, request-response execution. They provide isolation at the execution level, with each request or triggering event managing its own state. Some state is shared across invocations, such as database connections, but that&#39;s more an artifact of reusing resources and avoiding initialization delays, and less an intentional, conceptual shared boundary.</p><p class="paragraph" style="text-align:left;">Lambda MicroVMs provide you with an isolated state that&#39;s meant to be shared across related events and requests, while still being fast enough to spin up and tear down that you can afford to launch a new environment for each conceptually separate session, job or set of events. Moreover, it gives you the degree of network and process isolation needed to protect other environments or other tenants from untrusted or potentially unsafe executions.</p><p class="paragraph" style="text-align:left;">A great example of potentially unsafe executions is code ran by Generative AI. You want to provide a dedicated, tightly-controlled execution environment, with only the permissions it needs. Lambda MicroVM lets you do this, with startup times fast enough to not drag down an agent&#39;s response time. That&#39;s why AgentCore Runtime&#39;s serverless compute model runs on Lambda MicroVM.</p><h3 class="heading" style="text-align:left;" id="managing-dedicated-environments">Managing Dedicated Environments</h3><p class="paragraph" style="text-align:left;">One important point is that the application now needs a durable relationship between its own identity model and the AWS resource it runs on. It must record which MicroVM belongs to a session, route requests to the correct endpoint, issue access tokens, decide when to suspend or terminate the environment, and persist any state that must outlive it. Your product’s control plane owns that mapping, because every later request and lifecycle operation depends on reaching the correct environment.</p><p class="paragraph" style="text-align:left;">Capacity and delivery planning must also be done at the environment-level. Request volume alone does not describe the service footprint. Teams also need the number of running and suspended environments, baseline memory per environment, expected creation rate, idle policy, and maximum lifetime.</p><h2 class="heading" style="text-align:left;" id="lifecycle-of-a-lambda-micro-vm"><a class="link" href="https://docs.aws.amazon.com/lambda/latest/dg/microvms-launching.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-lambda-microvm-a-new-compute-model-inside-lambda" target="_blank" rel="noopener noreferrer nofollow">Lifecycle of a Lambda MicroVM</a></h2><p class="paragraph" style="text-align:left;">To run something in Lambda MicroVM, you upload a ZIP package with a Dockerfile and application artifacts to Amazon S3. Lambda starts a fresh MicroVM from a managed Amazon Linux 2023 base, runs the Dockerfile, starts the application, waits for readiness when that hook is configured, and captures a Firecracker snapshot. The snapshot includes the root filesystem, memory used by running processes, network connections, file descriptors, and other state present when the build completes. That snapshot is a Lambda MicroVM image, from which you launch MicroVMs. The image is versioned, and one image version can create multiple independent MicroVM resources.</p><p class="paragraph" style="text-align:left;">The image establishes the initial state shared by every MicroVM created from that version. A MicroVM launched from that image resumes at the captured point, and will have all the common dependencies, background processes, compiled artifacts, and loaded application state from when the snapshot was created. Tenant-specific values belong after restoration because every new environment begins with an identical copy of the snapshot.</p><p class="paragraph" style="text-align:left;">The runtime hooks separate common initialization from environment-specific work. The <code>/run</code> hook receives the MicroVM identifier and can receive a payload of per-environment data such as a tenant ID, session token, signed URL, or Secrets Manager path. The <code>/suspend</code> hook can be used to flush writes and close resources before checkpointing. The <code>/resume</code> hook can refresh credentials, re-establish connections, and validate restored state. The <code>/terminate</code> hook can perform final cleanup before Lambda releases the environment.</p><p class="paragraph" style="text-align:left;">Image versioning also becomes part of application delivery. Updating code, configuration, hooks, capabilities, or the managed base produces a new image version. Versions have independent build and activation states, so a platform can validate a build before allowing it to create environments. The runtime remains session-oriented, but the release unit is the image version from which those sessions start.</p><p class="paragraph" style="text-align:left;"><code>RunMicrovm</code> restores the image snapshot, assigns a unique ID, creates an endpoint, and moves the resource into its running state after the <code>/run</code> hook succeeds. Suspension checkpoints memory and disk, resume restores that checkpoint, and termination is final. The snapshot that reduces repeated initialization also copies initial state across environments and adds restore transitions to the application lifecycle. </p><div class="image"><img alt="Lambda MicroVM lifecycle diagram from Dockerfile and initialized snapshot to a dedicated environment and HTTPS endpoint, with running, suspended and terminated states." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/52661aa5-7ab9-487d-a2e3-4c930f58e189/image.png?t=1785530612"/><div class="image__source"><span class="image__source_text"><p>The lifecycle of a Lambda MicroVM</p></span></div></div><p class="paragraph" style="text-align:left;">The maximum duration parameter can be set from one second to 8 hours, and Lambda terminates the MicroVM when that limit is reached while the resource is running or suspended. If you need an environment that needs to survive beyond 8 hours, you have to persist its durable state elsewhere and continue in a new MicroVM. Lambda team, if you&#39;re reading this, please increase this limit to 30 days for re:Invent!</p><p class="paragraph" style="text-align:left;">Automatic suspension uses endpoint traffic as its activity signal. When no request reaches the endpoint for the configured idle period, Lambda can suspend the MicroVM. A background process may still be doing useful work while the endpoint remains quiet, so asynchronous jobs need to disable automatic suspension.</p><p class="paragraph" style="text-align:left;">Auto-resume adds a user-visible transition. Lambda holds the first inbound request while it restores the suspended state and runs the <code>/resume</code> hook. The delay depends on the checkpoint and the hook, and a failed resume returns <code>502 Bad Gateway</code>. Products with interactive latency targets need to test this first request explicitly and define retry behavior for resume failures.</p><p class="paragraph" style="text-align:left;">Each MicroVM also receives a unique service-managed HTTPS endpoint. The endpoint supports HTTP/1.1, HTTP/2, WebSockets, gRPC, and Server-Sent Events, which gives interactive products several ways to maintain direct communication with the assigned MicroVM. Every request requires an encrypted JSON Web Encryption token scoped to a specific MicroVM, allowed ports, and an expiration time. </p><p class="paragraph" style="text-align:left;">A coding environment can keep its toolchain and working directory alive while it works or waits. An analysis session can retain local results and a running process while it finishes. An agent can execute several steps in one isolated environment while its orchestration layer remains elsewhere, providing full containment and isolating any unexpected behavior. In each case, the session keeps its state, receives traffic directly, and stays separated from other sessions without you needing to manage the virtualization layer.</p><p class="paragraph" style="text-align:left;">One thing to keep in mind is that Lambda MicroVM doesn&#39;t autoscale horizontally. New capacity comes from additional <code>RunMicrovm</code> calls, and every MicroVM has its own endpoint. Lambda does not place several MicroVMs behind one shared endpoint, since MicroVMs are meant to be stateful. It does autoscale vertically though, as you&#39;ll see in the next section.</p><h2 class="heading" style="text-align:left;" id="lambda-micro-vm-pricing-explained"><a class="link" href="https://aws.amazon.com/lambda/pricing/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-lambda-microvm-a-new-compute-model-inside-lambda" target="_blank" rel="noopener noreferrer nofollow">Lambda MicroVM Pricing Explained</a></h2><p class="paragraph" style="text-align:left;">Compute uses a baseline-and-peak model. The image sets baseline memory, with vCPU allocated at 2 GB per vCPU. During higher demand, the environment can use up to four times its baseline, and billing covers the baseline while the MicroVM runs plus the active duration of memory and CPU consumed above it. As of July 31, 2026, ARM64 pricing in US East (N. Virginia) is $0.0000276944 per vCPU-second and $0.0000036667 per GB-second, equivalent to approximately $0.0997 per vCPU-hour and $0.0132 per GB-hour. Available configurations range from 0.5 GB and 0.25 vCPU at baseline to 8 GB and 4 vCPUs, with peak resources reaching 32 GB and 16 vCPUs. The service currently supports only ARM64 and is available in five AWS Regions: US East (N. Virginia), US East (Ohio), US West (Oregon), Europe (Ireland), and Asia Pacific (Tokyo).</p><p class="paragraph" style="text-align:left;">Suspending a MicroVM stops compute charges, but snapshot-related charges continue. In US East (N. Virginia), AWS charges $0.00155 per GB read when a MicroVM starts or resumes, $0.0038 per GB written when it suspends, and $0.08 per GB-month for MicroVM image and suspended-state storage. MicroVM image storage has a one-week minimum retention period. AWS also charges standard data-transfer rates, including applicable traffic between a MicroVM and an Amazon VPC. The cost model therefore depends on baseline size, time above baseline, active duration, image and checkpoint size, suspend frequency, retained duration, and network path.</p><p class="paragraph" style="text-align:left;">For example, consider a developer environment in US East (N. Virginia) with a 2 GB and 1 vCPU baseline. It runs for 2 hours per day on 20 working days, reaching the full 8 GB and 4 vCPU peak for 30 minutes each day. It also spends four hours suspended each day, with one launch and one suspend/resume cycle. Assuming a 2 GB image, a 2 GB suspend checkpoint, 720 hours per month, and no data transfer:</p><ul><li><p class="paragraph" style="text-align:left;">Baseline compute for 40 hours: <b>$5.04</b></p></li><li><p class="paragraph" style="text-align:left;">Additional 6 GB and 3 vCPUs used above baseline for 10 hours: <b>$3.78</b></p></li><li><p class="paragraph" style="text-align:left;">Snapshot reads and writes:<br><code>20 × 2 GB × ($0.00155 launch + $0.0038 suspend + $0.00155 resume)</code> = <b>$0.276</b></p></li><li><p class="paragraph" style="text-align:left;">Suspended-state storage:<br><code>2 GB × 80 / 720 × $0.08</code> = <b>$0.018</b></p></li><li><p class="paragraph" style="text-align:left;">Image storage for the month:<br><code>2 GB × $0.08</code> = <b>$0.16</b></p></li></ul><p class="paragraph" style="text-align:left;">The resulting monthly cost is approximately <b>$9.28</b>, excluding data transfer. The image-storage charge is shared by every MicroVM launched from that image, so its effective per-environment cost decreases as the image is reused</p><h2 class="heading" style="text-align:left;" id="networking-in-lambda-micro-vm"><a class="link" href="https://docs.aws.amazon.com/lambda/latest/dg/microvms-networking.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-lambda-microvm-a-new-compute-model-inside-lambda" target="_blank" rel="noopener noreferrer nofollow">Networking in Lambda MicroVM</a></h2><p class="paragraph" style="text-align:left;">A Lambda MicroVM has separate configurations for traffic coming in and traffic going out. Inbound requests use the unique service-managed HTTPS endpoint described above, while outbound connections use an egress network connector selected when you call <code>RunMicrovm</code>. The two paths are independent, so routing a MicroVM through your VPC does not move its endpoint into that VPC or change how clients reach it. Network connectors are attached when the environment starts and cannot be changed while it is running, so moving a session to a different network policy means launching a new MicroVM.</p><p class="paragraph" style="text-align:left;">By default, Lambda MicroVMs <b>can</b> reach the public internet. Lambda provides a managed <code>INTERNET_EGRESS</code> connector for this, so the environment can download packages, call a model provider, reach a SaaS API, or send telemetry without you configuring a VPC or NAT gateway. That&#39;s convenient for a coding environment or an agent, but it&#39;s also an important part of the security model. Isolating untrusted code from other tenants does not stop that code from sending data somewhere on the internet, so public egress should be an explicit decision rather than a default you accept without thinking about it.</p><p class="paragraph" style="text-align:left;">To reach private resources, you create a Lambda Network Connector with the subnets, security groups, and either IPv4 or DualStack configuration from one of your VPCs. Lambda provisions elastic network interfaces (ENIs) for the connector, and the resource moves through <code>PENDING</code> before it becomes <code>ACTIVE</code>. You then pass its ARN in the <code>egressNetworkConnectors</code> parameter when calling <code>RunMicrovm</code>. A connector is meant to be reused across many MicroVMs, not created for every session. Connector creation can take up to 10 minutes while Lambda provisions the ENIs, so it belongs in your platform infrastructure, not in the hot path that launches an environment for a user.</p><p class="paragraph" style="text-align:left;">Once the VPC connector is attached, outbound traffic follows the routing and security controls of those subnets. The MicroVM can connect to resources such as RDS, ElastiCache, internal APIs, and systems reachable through Direct Connect or a VPN, as long as the route tables, security groups, network ACLs, and the destination itself allow it. If the MicroVM also needs public internet access, the VPC must provide that path, typically through a NAT gateway for IPv4 or an egress-only internet gateway for IPv6. If the subnets have no internet route, AWS SDK calls to public service endpoints also need the corresponding VPC endpoints, such as an interface endpoint through AWS PrivateLink or a gateway endpoint for S3 and DynamoDB.</p><p class="paragraph" style="text-align:left;">One thing to keep in mind is that the VPC connector only controls egress. The MicroVM still receives inbound requests through its Lambda-managed HTTPS endpoint, using the JWE token and allowed-port rules described earlier. It does not become directly addressable through the private IP space of your VPC. If an environment does not need inbound traffic, Lambda also provides a <code>NO_INGRESS</code> connector, which is a better fit for a worker that communicates only through outbound requests or external state.</p><p class="paragraph" style="text-align:left;">The inbound endpoint also has its own bandwidth limit. Request and response bandwidth scales with the baseline size, from 1 MB per second for a 0.5 GB MicroVM to 16 MB per second for an 8 GB MicroVM. That limit covers traffic passing through the service-managed endpoint, which means a small environment that streams large responses, transfers artifacts, or maintains several active connections can become network-bound before it becomes CPU-bound.</p><p class="paragraph" style="text-align:left;">Networking and IAM solve different problems. The execution role passed to RunMicrovm controls which AWS API operations the code is authorized to perform, while the network connector controls which destinations it can reach. Routing traffic into a VPC does not grant permission to read a secret or access an S3 object, and granting an IAM permission does not create a route to a private database. A production environment needs both layers to agree: the packet needs a valid network path, and the request still needs authorization.</p><p class="paragraph" style="text-align:left;">Suspension adds one more wrinkle. A checkpoint can preserve the in-memory object that represents a database client, HTTP connection, or WebSocket, but the remote side may close that connection while the MicroVM is suspended. The <code>/suspend</code> hook should flush writes and close connections that should not cross the checkpoint, while the <code>/resume</code> hook should recreate them, refresh credentials, and validate restored state.</p><div class="image"><img alt="Example Lambda MicroVM architecture with a session control plane, DynamoDB endpoint mapping, isolated MicroVMs, HTTPS access, and internet or VPC egress." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/b5650251-a973-45ec-bf51-f16be80050c6/image.png?t=1785534002"/></div><h2 class="heading" style="text-align:left;" id="conclusion">Conclusion</h2><p class="paragraph" style="text-align:left;">Lambda MicroVM lets you maintain an assigned environment for a bounded session or job, with memory, disk, processes, endpoint access, suspension, and resumption built into the service. It&#39;s essentially Firecracker MicroVM as a Service, or Lambda Functions without the Functions part.</p><p class="paragraph" style="text-align:left;">Apps that use Lambda MicroVM should be designed around which state belongs in the image, which state must be unique per environment, how the control plane maps users to endpoints, what the workload can reach, how resume affects latency, how long the environment may live, and when its cost should end.</p><p class="paragraph" style="text-align:left;">It&#39;s a game changer for running code you don&#39;t trust, such as when building or using agents. But it&#39;s also a really exciting new compute primitive, with a lot of potential!</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>Layered Authorization for AI Agents using Amazon Bedrock AgentCore</title>
  <description>Explore layered AI agent authorization with AgentCore Runtime, Gateway, Cedar policies and Verified Permissions, with illustrative policy examples.</description>
      <enclosure url="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/009263d6-9724-479c-aaf1-dd188f918b9d/agentcore_ops_architecture_v4.png" length="433219" type="image/png"/>
  <link>https://newsletter.simpleaws.dev/p/layered-authorization-ai-agents-amazon-bedrock-agentcore</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/layered-authorization-ai-agents-amazon-bedrock-agentcore</guid>
  <pubDate>Fri, 06 Mar 2026 18:55:55 +0000</pubDate>
  <atom:published>2026-03-06T18:55:55Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Artificial Intelligence]]></category>
    <category><![CDATA[Security]]></category>
    <category><![CDATA[Architecture]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">Say you&#39;re building an AI agent capable of handling this request:</p><div class="codeblock"><pre><code>Investigate elevated 5XXs for tenant Acme in prod. Check metrics, search logs, correlate with recent deployments, summarize the likely cause, and if it is safe restart the affected instance.</code></pre></div><p class="paragraph" style="text-align:left;">I bet you&#39;d start with the prompt, right? Adding instructions like <code>Make sure you find the actual root cause</code>, <code>Read the logs carefully</code>, <code>Verify your findings before taking destructive actions</code>. Or even better, <code>Do not make any other changes to AWS resources other than restarting affected instances</code>.</p><p class="paragraph" style="text-align:left;">You&#39;re thinking about the right problem: Let&#39;s prevent this agent from doing something bad. We&#39;re not dealing with a chatbot, which might just say something incorrect. The failure mode for this agent is that it might generate a perfectly valid tool call with the wrong tenant, the wrong environment, or the wrong parameter, and everything still looks syntactically correct on the way down. Looks right, kabooms the wrong thing.</p><p class="paragraph" style="text-align:left;"><b>A prompt is not a permission model. </b>It is a hopeful string, which your LLM will strictly obey until it doesn&#39;t.</p><p class="paragraph" style="text-align:left;">In both a chatbot that outputs something incorrect and an operations agent that kills the wrong server, we&#39;re looking at the same root cause: LLMs hallucinate. The difference between an incorrect answer and 💥 is in what the agent can do. The key point here is that once an agent can run tools, it effectively acts as a principal, and its execution backplane has to be secured the way you would secure an internal platform API. In this article I&#39;ll walk you through a layered design for that. The illustrative authorization code still needs an end-to-end review of trusted identity propagation, tenant isolation and policy behavior before production use.</p><hr class="content_break"><h2 class="heading" style="text-align:left;" id="this-article-is-sponsored-by-depot">This article is sponsored by Depot</h2><div class="image"><img alt="Depot sponsorship graphic: How simulations reduce guesswork in infrastructure decisions." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/c094185e-dbc1-4884-b350-912daa0b22af/how-simulations-reduce-guesswork.jpeg?t=1772823850"/></div><p class="paragraph" style="text-align:left;">At Depot we use AWS for our CI builds. To provide the fastest experience we create a standby pool of machines that are warmed and ready to take jobs.</p><p class="paragraph" style="text-align:left;"><b>The problem:</b> How do you right-size standby pools when too few means latency spikes and too many means paying for idle instances?</p><p class="paragraph" style="text-align:left;">Instead of guessing, we built a simulator using real customer data. We fed real job data in and compared results against our real-world latency and cost. We used hyperparameter optimization to test several thousand simulated parameter combinations in ~10 minutes.</p><p class="paragraph" style="text-align:left;"><b>Results:</b></p><ul><li><p class="paragraph" style="text-align:left;"><b>Faster Builds:</b> We decreased p99 latency by two seconds through simulation-driven scaling.</p></li><li><p class="paragraph" style="text-align:left;"><b>Lower Costs:</b> Standby pool expenses dropped by 2%, proving that&quot;faster&quot; doesn&#39;t have to mean &quot;more expensive.”</p></li></ul><p class="paragraph" style="text-align:left;">This is the kind of engineering that goes into every part of Depot. We simulate, measure, and optimize so your builds are faster and cheaper without you having to think about it.</p><p class="paragraph" style="text-align:left;"><a class="link" href="https://fandf.co/4b8Sxbx?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Read more about our simulation here</a>. And If you want a CI platform built by a team that sweats the details, <a class="link" href="https://fandf.co/40QFSFe?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">give Depot a try</a>.</p><hr class="content_break"><h2 class="heading" style="text-align:left;" id="autonomy-is-a-ladder-not-a-switch">Autonomy is a Ladder, Not a Switch</h2><p class="paragraph" style="text-align:left;">One of the easiest ways to get agent design wrong is to talk about autonomy as if there are only two modes: <i>human-in-the-loop</i> or <i>fully autonomous</i>. That framing is too coarse to be useful. Here&#39;s a better model: a five-level ladder with explicit promotion criteria between levels, where every step up the ladder increases the blast radius, and it also changes what you need from <a class="link" href="https://newsletter.simpleaws.dev/p/the-essential-guide-to-llm-evaluations?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">evals</a>, observability, and permissions. If you cannot prove correctness, traceability, and containment at one level, promoting the system to the next one is an operational liability.</p><h3 class="heading" style="text-align:left;" id="level-0-suggest">Level 0: Suggest</h3><p id="at-level-0-the-agent-does-not-execu" class="paragraph" style="text-align:left;">At Level 0, the agent does not execute anything. It proposes actions, plans, or outputs, but it does not call tools and it does not mutate state. An example would be a chatbot that just outputs migration plans, remediation suggestions, or cost-saving recommendations.</p><p id="the-main-questions-here-are-whether" class="paragraph" style="text-align:left;">The main questions here are whether the output is any good, whether it follows the expected format, whether it hallucinates, and whether retrieval is correct when grounding or RAG is involved. Observability is still important even at this level: prompt version, model version, retrieval traces, output logging with redaction, latency, cost per request, and user feedback. Permissions should remain effectively read-only, apart from things like logging and protective controls such as PII redaction or secrets filtering.</p><h3 class="heading" style="text-align:left;" id="level-1-execute-readonly-actions">Level 1: Execute read-only actions</h3><p class="paragraph" style="text-align:left;">This is where most production operations agents should start. At Level 1, the agent can call tools, but only tools that are read-only and non-mutating. Actions like querying logs, fetching metrics, reading tickets, inspecting configuration, summarizing incident timelines, analyzing invoices, or enumerating resources.</p><p class="paragraph" style="text-align:left;">On paper this sounds exactly like RAG, but in practice the difference is that the agent is now selecting tools and constructing parameters, and that changes your evals. You now care about whether the agent chose the right tool, whether it passed the right filters and scopes, whether summaries remain faithful to source material, and whether the system avoids unintended state changes or accidental use of mutation-capable tools. Observability also gets stricter: you want traces of tool calls, correlation IDs tying model calls to tool calls and outputs, and latency and cost per <i>step</i>, not just per request. Permissions should still be tightly scoped read-only credentials, with least privilege, tenant boundaries, environment separation, and time-bounded access for longer-running work.</p><h3 class="heading" style="text-align:left;" id="level-2-execute-reversible-writes">Level 2: Execute reversible writes</h3><p class="paragraph" style="text-align:left;">This is where agents start making changes, but only changes with naturally limited downstream impact or with explicit reversibility. Things like creating tickets, posting Slack updates, tagging resources, updating dashboards, opening PRs but not merging them, writing to append-only logs, or initiating workflows with explicit rollback.</p><p class="paragraph" style="text-align:left;">This is a very different category from read-only operations, even if the business impact still looks modest. At this level, it is no longer enough to know that the agent produced a sensible plan. You need to know that it took the correct action for the scenario, that it behaves idempotently, that it does not duplicate actions, that it respects its boundaries, and that it behaves predictably when tools or actions fail. Observability needs to become mutation-aware: audit logs of intended changes, rationale for the change, diffs where applicable, and visibility into retries. Permissions should be limited write access, scoped to reversible endpoints, with request tokens or similar controls enforced by the tool layer. And it&#39;s where we start caring about explainability: By looking at the observability data we collect, can we explain why the agent took the action that it took?</p><h3 class="heading" style="text-align:left;" id="level-3-execute-bounded-writes">Level 3: Execute bounded writes</h3><p class="paragraph" style="text-align:left;">At this level the write actions can have a more meaningful (or severe I guess) impact, and the controls have to become more explicit. The agent can now perform higher-impact actions, but only inside hard constraints: scoped permissions, quotas, explicit allowlists, deny-by-default rules, and human approvals for specific action classes. This includes applying configuration changes in bounded domains, scaling resources within limits, modifying policies within constraints, merging PRs with approvals, or initiating controlled migrations.</p><p class="paragraph" style="text-align:left;">This is also the level where just giving an agent a role is no longer enough. A role is great for identity, but that alone is too coarse for permissions. The authorization question is whether the agent can perform <i>this</i> action, on <i>this</i> resource, in <i>this</i> environment, under <i>these</i> constraints. Evals need to cover policy compliance, quota limits, approval requirements, environment boundaries, and near-miss scenarios designed to tempt the agent into unsafe shortcuts, such as prompt injection, ambiguous intent, or partial context. Observability has to capture the full action path from intent to plan to approval to execution to outcome, with live signals for anomalies like spend spikes, permission denials, or repeated failures. Permissions at this level should be narrow, deny-by-default, and enforced with explicit escalation paths and platform-level quota controls.</p><h3 class="heading" style="text-align:left;" id="level-4-unsupervised-execution-with">Level 4: Unsupervised execution with continuous monitoring and rollback guarantees</h3><p class="paragraph" style="text-align:left;">Level 4 is a true “autonomous agent”. Note how far beyond we are from a clever prompt and a few tools. We are talking about a controlled operating environment. At this level, the agent executes end-to-end workflows without human approval in most cases, but only under continuous monitoring, automated containment, and rapid rollback or compensating-action guarantees. It&#39;s a closed-loop execution for well-defined workflows such as auto-remediation, routine cost optimization, automated incident response under strict playbooks, and continuous governance tasks.</p><p class="paragraph" style="text-align:left;">The eval bar needs to rise accordingly: we need high-confidence coverage of critical paths, ongoing monitoring, failure injection, adversarial inputs, and economic guardrails. Observability needs complete replayability of decisions, tool calls, retrieved sources, and state transitions, plus anomaly detection and workflow SLOs such as success rate, time-to-completion, and rollback time. Permissions at this level should have containment as part of the design: session isolation, sandboxing, automated circuit breakers that can revoke credentials and halt execution, and explicit rollback or compensating actions for every mutation-capable tool.</p><h3 class="heading" style="text-align:left;" id="using-the-autonomy-ladder">Using the Autonomy Ladder</h3><p class="paragraph" style="text-align:left;">The higher you go in this ladder, the less acceptable it becomes to manage permissions with just prompts. Level 0 can tolerate relatively loose boundaries because nothing executes. Level 1 already needs real tool selection controls and parameter discipline. By Levels 2 through 4, permissions have to move from “the agent has a role” to “each tool call is authorized”, with strong authentication, Role-Based Access Control (RBAC) for coarse access, Fine-Grained Access Control (FGAC) for tool and parameter constraints, dual-identity delegation, and short-lived credentials. In other words, the more autonomy you want, the more authorization has to become infrastructure instead of text in a prompt.</p><h2 class="heading" style="text-align:left;" id="our-example-agent-explained">Our Example Agent Explained</h2><p class="paragraph" style="text-align:left;">As I mentioned, we&#39;re going to build an agent that can answer to this query:</p><div class="codeblock"><pre><code>Investigate elevated 5XXs for tenant Acme in prod. Check metrics, search logs, correlate with recent deployments, summarize the likely cause, and if it is safe restart the affected instance.</code></pre></div><p class="paragraph" style="text-align:left;">The first part, checking metrics and searching logs, is level 1 of autonomy: read only actions. Once we add the ability to restart instances, we&#39;re turning it to level 4: unsupervised execution.</p><p class="paragraph" style="text-align:left;">Our agent&#39;s tools look roughly like this:</p><ul><li><p class="paragraph" style="text-align:left;"><code>CloudOps___read_metrics</code></p></li><li><p class="paragraph" style="text-align:left;"><code>CloudOps___search_logs</code></p></li><li><p class="paragraph" style="text-align:left;"><code>CloudOps___list_recent_deployments</code></p></li><li><p class="paragraph" style="text-align:left;"><code>CloudOps___create_incident_ticket</code></p></li><li><p class="paragraph" style="text-align:left;"><code>CloudOps___restart_instance</code></p></li></ul><p class="paragraph" style="text-align:left;">Our solution is going to involve the following AWS services:</p><ul><li><p class="paragraph" style="text-align:left;"><a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Amazon Bedrock AgentCore Runtime</a> to run the agent. It&#39;s a serverless runtime for AI agents.</p></li><li><p class="paragraph" style="text-align:left;"><a class="link" href="https://github.com/strands-agents/sdk-python?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Strands SDK for Python</a> for code-first agent orchestration. It&#39;s an agentic framework like LangChain 🦜⛓️ and others, it lets you write plain code that it turns into LLM calls.</p></li><li><p class="paragraph" style="text-align:left;"><a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Amazon Bedrock AgentCore Gateway</a> to manage the tools. It&#39;s a registry of tools and MCP servers, which solves management and discovery.</p></li><li><p class="paragraph" style="text-align:left;"><a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Amazon Bedrock AgentCore Policy</a> to define and enforce permissions. It&#39;s a service that lets you create policies, attach them to AgentCore Gateway, and it enforces them. Uses <a class="link" href="https://www.cedarpolicy.com/en?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Cedar</a>.</p></li><li><p class="paragraph" style="text-align:left;"><a class="link" href="https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/what-is-avp.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Amazon Verified Permissions</a> inside the tool implementation for business/resource authorization. It lets you write and manage <a class="link" href="https://www.cedarpolicy.com/en?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Cedar</a> policies, and it enforces them.</p></li></ul><p class="paragraph" style="text-align:left;">I&#39;m not going to explain those services from scratch, but I will do my best to give you context on what we&#39;re doing with these services. If you want to dive deeper, I recommend <a class="link" href="https://dev.to/aws/ai-agents-from-prototype-to-production-a-free-learning-path-with-strands-agents-481l?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">this list of resources</a> collected by my friend and <b>AWS Developer Advocate</b> <a class="link" href="https://elifuentes.tech/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Elizabeth Fuentes Leone</a>.</p><div class="image"><img alt="Layered authorization architecture: Cognito JWTs invoke AgentCore Runtime, Gateway and Policy authorize tool calls, and Verified Permissions checks backend resource access." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/009263d6-9724-479c-aaf1-dd188f918b9d/agentcore_ops_architecture_v4.png?t=1772823254"/><div class="image__source"><span class="image__source_text"><p>AgentCore operations agent with layered authorization</p></span></div></div><p class="paragraph" style="text-align:left;">This is what should happen when the user asks the agent to investigate the Acme outage:</p><ol start="1"><li><p class="paragraph" style="text-align:left;">The user authenticates with <a class="link" href="https://newsletter.simpleaws.dev/p/securing-microservices-aws-cognito?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Cognito</a> or another OIDC provider and gets a JWT.</p></li><li><p class="paragraph" style="text-align:left;">The client invokes the agent running in <b>AgentCore Runtime</b> with that bearer token.</p></li><li><p class="paragraph" style="text-align:left;">Runtime validates the JWT using the configured discovery URL and client/audience constraints.</p></li><li><p class="paragraph" style="text-align:left;">Runtime starts the Strands-based operations agent.</p></li><li><p class="paragraph" style="text-align:left;">Runtime passes the validated <code>Authorization</code> header into the agent only because we explicitly allowlist it.</p></li><li><p class="paragraph" style="text-align:left;">The Strands agent talks to <b>AgentCore Gateway</b> over MCP.</p></li><li><p class="paragraph" style="text-align:left;">Gateway validates inbound auth again for the tool boundary.</p></li><li><p class="paragraph" style="text-align:left;"><b>AgentCore Policy</b> evaluates the actual tool call: principal tags from JWT claims plus <code>context.input</code> from tool arguments.</p></li><li><p class="paragraph" style="text-align:left;">If Gateway allows the tool call, the target API or Lambda behind the tool performs its own <b>Verified Permissions</b> check against real business entities like tenant, team ownership, and environment.</p></li><li><p class="paragraph" style="text-align:left;">The downstream tool executes only if both boundaries agree.</p></li><li><p class="paragraph" style="text-align:left;">Runtime, Gateway, Policy, and the tool layer emit enough telemetry that you can reconstruct the path later.</p></li></ol><p class="paragraph" style="text-align:left;">The important thing to notice is that there are <b>three different permission decisions</b> here:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Who may invoke the runtime or gateway at all</b></p></li><li><p class="paragraph" style="text-align:left;"><b>Whether the agent is allowed to invoke a certain tool</b></p></li><li><p class="paragraph" style="text-align:left;"><b>Whether the action executed by the tool is authorized on the resource</b></p></li></ul><p class="paragraph" style="text-align:left;">Let&#39;s explore and implement each of those separately.</p><h2 class="heading" style="text-align:left;" id="who-can-invoke-the-agent">Who Can Invoke the Agent</h2><p class="paragraph" style="text-align:left;">AgentCore Runtime supports <b>IAM SigV4</b> or <b>JWT bearer tokens</b> as inbound auth modes. A given runtime version uses one or the other, not both. Gateway works the same: you pick an authorizer type when you create it, and if you use <code>CUSTOM_JWT</code>, you configure a discovery URL plus claim restrictions such as <code>allowedClients</code>, <code>allowedAudience</code>, <code>allowedScopes</code>, and optional custom claim validations. For heterogeneous clients, JWT is usually the cleaner choice.</p><p class="paragraph" style="text-align:left;">AgentCore also supports resource-based policies on <b>Runtime</b>, <b>Runtime endpoints</b>, and <b>Gateway</b> resources. These policies control who can invoke and manage those resources, and they are evaluated together with identity-based policies. If you are invoking a Runtime endpoint, AWS evaluates both the Runtime resource and the endpoint resource. If either resource denies or lacks the required allow, the request fails.</p><p class="paragraph" style="text-align:left;">Important note: If your Runtime or Gateway is configured for OAuth/JWT auth, the resource-based policy must use a wildcard principal. You should not list end users in the <code>Principal</code> field the way you would with SigV4. AWS validates the OAuth token before policy evaluation, and then the resource policy can further restrict access with condition keys like <code>aws:SourceVpc</code> or <code>aws:SourceVpce</code>. Also, the <code>Resource</code> field must contain the exact ARN of the attached resource, <code>*</code> is not valid.</p><p class="paragraph" style="text-align:left;">Here is a Runtime resource policy for an OAuth-authenticated Runtime restricted to a specific VPC:</p><div class="codeblock"><pre><code>&#123;
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    &#123;
      &quot;Sid&quot;: &quot;AllowOAuthFromVPC&quot;,
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: &quot;*&quot;,
      &quot;Action&quot;: &quot;bedrock-agentcore:InvokeAgentRuntime&quot;,
      &quot;Resource&quot;: &quot;arn:aws:bedrock-agentcore:us-west-2:111122223333:runtime/AGENTID&quot;,
      &quot;Condition&quot;: &#123;
        &quot;StringEquals&quot;: &#123;
          &quot;aws:SourceVpc&quot;: &quot;vpc-1a2b3c4d&quot;
        &#125;
      &#125;
    &#125;
  ]
&#125;</code></pre></div><p class="paragraph" style="text-align:left;">The same pattern applies to Gateway resource policies, except the action should be <code>bedrock-agentcore:InvokeGateway</code> and the resource should be the Gateway ARN. If you are using Runtime endpoints, remember that both the Runtime and the endpoint resource policies are in play.</p><p class="paragraph" style="text-align:left;">Here is a minimal client-side invoke path:</p><div class="codeblock"><pre><code>import json
import os
import urllib.parse
import uuid

import requests

REGION = &quot;us-east-1&quot;
RUNTIME_ARN = os.environ[&quot;AGENT_RUNTIME_ARN&quot;]
TOKEN = os.environ[&quot;TOKEN&quot;]

escaped_runtime_arn = urllib.parse.quote(RUNTIME_ARN, safe=&quot;&quot;)
url = (
    f&quot;https://bedrock-agentcore.&#123;REGION&#125;.amazonaws.com/&quot;
    f&quot;runtimes/&#123;escaped_runtime_arn&#125;/invocations?qualifier=DEFAULT&quot;
)

headers = &#123;
    &quot;Authorization&quot;: f&quot;Bearer &#123;TOKEN&#125;&quot;,
    &quot;Content-Type&quot;: &quot;application/json&quot;,
    &quot;X-Amzn-Bedrock-AgentCore-Runtime-Session-Id&quot;: str(uuid.uuid4()),
&#125;

payload = &#123;
    &quot;prompt&quot;: (
        &quot;Investigate elevated 5XXs for tenant acme in prod. &quot;
        &quot;Check metrics, search logs, correlate with recent deployments, &quot;
        &quot;summarize the likely cause, and if it is safe restart the affected instance.&quot;
    )
&#125;

response = requests.post(url, headers=headers, data=json.dumps(payload))
print(response.status_code)
print(response.text)</code></pre></div><p class="paragraph" style="text-align:left;">Notice how I&#39;m including the bearer token? Without that, we wouldn&#39;t be able to call the agent. Well, we won&#39;t be, after we do the next step.</p><h2 class="heading" style="text-align:left;" id="strands-agent-in-agent-core-runtime">Strands Agent in AgentCore Runtime</h2><p class="paragraph" style="text-align:left;">The first Runtime configuration step is to enable JWT auth and explicitly allowlist the <code>Authorization</code> header so Runtime can pass it into agent code. Runtime can read allowlisted request headers via <code>context.request_headers</code>. Since Runtime already validated the token, your code does not need to revalidate signature just to pass the token onward for policy context.</p><p class="paragraph" style="text-align:left;">Here is the Runtime configuration you should use:</p><div class="codeblock"><pre><code>agentcore configure --entrypoint src/main.py \
  --name ops-agent \
  --execution-role arn:aws:iam::123456789012:role/OpsAgentRuntimeRole \
  --requirements-file requirements.txt \
  --authorizer-config &quot;&#123;\&quot;customJWTAuthorizer\&quot;:&#123;\&quot;discoveryUrl\&quot;:\&quot;$DISCOVERY_URL\&quot;,\&quot;allowedClients\&quot;:[\&quot;$CLIENT_ID\&quot;]&#125;&#125;&quot; \
  --request-header-allowlist &quot;Authorization&quot;

agentcore launch</code></pre></div><p class="paragraph" style="text-align:left;">And here is the code for the Strands agent the above command creates (this would be <code>src/main.py</code>):</p><div class="codeblock"><pre><code>import os

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from mcp.client.streamable_http import streamablehttp_client
from strands import Agent
from strands.models import BedrockModel
from strands.tools.mcp.mcp_client import MCPClient

app = BedrockAgentCoreApp()


@app.entrypoint
def agent_invocation(payload, context):
    prompt = payload[&quot;prompt&quot;]
    auth_header = context.request_headers.get(&quot;Authorization&quot;)

    if not auth_header:
        return &#123;&quot;error&quot;: &quot;Missing Authorization header&quot;&#125;

    gateway_url = os.environ[&quot;AGENTCORE_GATEWAY_URL&quot;]

    mcp_client = MCPClient(
        lambda: streamablehttp_client(
            gateway_url,
            headers=&#123;&quot;Authorization&quot;: auth_header&#125;,
        )
    )

    with mcp_client:
        tools = mcp_client.list_tools_sync()

        agent = Agent(
            model=BedrockModel(
                model_id=os.environ[&quot;BEDROCK_INFERENCE_PROFILE_ID&quot;],
                temperature=0.0,
                streaming=True,
            ),
            tools=tools,
            system_prompt=(
                &quot;You are a production operations agent. &quot;
                &quot;Start with read-only diagnostics. &quot;
                &quot;Never claim a tool succeeded unless you received a successful tool result. &quot;
                &quot;If a tool call is denied, explain that the platform blocked it.&quot;
            ),
        )

        response = agent(prompt)
        return &#123;&quot;result&quot;: response.message&#125;


if __name__ == &quot;__main__&quot;:
    app.run()</code></pre></div><p class="paragraph" style="text-align:left;">Note that the agent only gets tools through MCP, and MCP only comes from Gateway. If our agent could bypass Gateway and call SDK clients directly, our Gateway policies would be just theater.</p><h3 class="heading" style="text-align:left;" id="give-the-agent-its-own-identity">Give the Agent its Own Identity</h3><p class="paragraph" style="text-align:left;">When you&#39;re executing actions, it&#39;s ok to use your user. However, with an agent like this it&#39;s not you who&#39;s deciding and executing the actions. Your action is to call the agent. The agent&#39;s actions are its own, so it needs its own identity. This is important for both permissions management and traceability: We need to define what the agent is allowed to do (which may not be the same set of actions you&#39;re allowed to do), and we need to know that it was the agent who executed them.</p><p class="paragraph" style="text-align:left;">We&#39;ll forward the user JWT from Runtime to AgentCore Gateway because Gateway Policy uses OAuth claims as tags on <code>AgentCore::OAuthUser</code>. But the actual downstream execution path won&#39;t use the user&#39;s User. Gateway uses its service role or configured outbound auth to reach targets. AgentCore Identity also supports both user-delegated and machine-to-machine auth patterns for outbound access, and Runtime automatically creates a workload identity. For first-party AgentCore services, Runtime can exchange the inbound JWT for a Workload Access Token and deliver it into your agent execution.</p><p class="paragraph" style="text-align:left;">And things look like this:</p><ul><li><p class="paragraph" style="text-align:left;"><b>User JWT</b>: who is asking, what tenant/role/scope context applies</p></li><li><p class="paragraph" style="text-align:left;"><b>Runtime / Gateway / workload identities</b>: what infrastructure identity actually executes</p></li><li><p class="paragraph" style="text-align:left;"><b>Policy engines</b>: where the decision is made</p></li></ul><h2 class="heading" style="text-align:left;" id="agent-core-gateway-as-the-tool-surf">AgentCore Gateway as the Tool Surface</h2><p class="paragraph" style="text-align:left;">Our agent should have just 3 or 4 tools. However, once you have dozens or hundreds of tools, the failure mode of agents shifts away from “the model is weak” and toward drift: auth changes, schema changes, rate limits, ownership confusion, and tool access becoming unmanageable.</p><p class="paragraph" style="text-align:left;">The solution is a platform layer with tool governance, version control, owners, SLAs, contract tests, and FGAC at the gateway or policy layer. AgentCore Gateway is built to do exactly that. It gives you one MCP endpoint for tool discovery and invocation, explicit inbound and outbound auth, and a unified place to attach Policy. Gateway supports IAM-based outbound auth using the gateway service role (ideal for AWS targets), and OAuth or API keys (ideal for external targets). You can also use “no auth”, but please don&#39;t.</p><h3 class="heading" style="text-align:left;" id="policy-engine-and-agent-core-gatewa">Policy Engine and AgentCore Gateway</h3><p class="paragraph" style="text-align:left;">First you create a Policy Engine:</p><div class="codeblock"><pre><code>import boto3

control = boto3.client(&quot;bedrock-agentcore-control&quot;)

policy_engine = control.create_policy_engine(
    name=&quot;ops-policy-engine&quot;,
    description=&quot;FGAC for operations-agent tool calls&quot;
)

print(policy_engine[&quot;policyEngineId&quot;])
print(policy_engine[&quot;policyEngineArn&quot;])</code></pre></div><p class="paragraph" style="text-align:left;">Then you create the Gateway with JWT inbound auth and attach the Policy Engine in <code>ENFORCE</code> mode:</p><div class="codeblock"><pre><code>import boto3

control = boto3.client(&quot;bedrock-agentcore-control&quot;)

gateway = control.create_gateway(
    name=&quot;ops-gateway&quot;,
    roleArn=&quot;arn:aws:iam::123456789012:role/OpsGatewayExecutionRole&quot;,
    protocolType=&quot;MCP&quot;,
    authorizerType=&quot;CUSTOM_JWT&quot;,
    authorizerConfiguration=&#123;
        &quot;customJWTAuthorizer&quot;: &#123;
            &quot;discoveryUrl&quot;: &quot;https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Example/.well-known/openid-configuration&quot;,
            &quot;allowedClients&quot;: [&quot;exampleclientid&quot;],
        &#125;
    &#125;,
    policyEngineConfiguration=&#123;
        &quot;arn&quot;: policy_engine[&quot;policyEngineArn&quot;],
        &quot;mode&quot;: &quot;ENFORCE&quot;,
    &#125;,
    exceptionLevel=&quot;DEBUG&quot;,
)

print(gateway[&quot;gatewayUrl&quot;])</code></pre></div><h3 class="heading" style="text-align:left;" id="agent-core-gateway-execution-role">AgentCore Gateway Execution Role</h3><p class="paragraph" style="text-align:left;">If you use a custom Gateway execution role, that role needs the permissions required for AgentCore Policy to work:</p><ul><li><p class="paragraph" style="text-align:left;"><code>bedrock-agentcore:AuthorizeAction</code></p></li><li><p class="paragraph" style="text-align:left;"><code>bedrock-agentcore:PartiallyAuthorizeActions</code></p></li><li><p class="paragraph" style="text-align:left;"><code>bedrock-agentcore:GetPolicyEngine</code></p></li></ul><p class="paragraph" style="text-align:left;">The trust policy for the execution role should look like this:</p><div class="codeblock"><pre><code>&#123;
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    &#123;
      &quot;Sid&quot;: &quot;AllowBedrockAgentCoreAssumeRole&quot;,
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Principal&quot;: &#123;
        &quot;Service&quot;: &quot;bedrock-agentcore.amazonaws.com&quot;
      &#125;,
      &quot;Action&quot;: &quot;sts:AssumeRole&quot;,
      &quot;Condition&quot;: &#123;
        &quot;StringEquals&quot;: &#123;
          &quot;aws:SourceAccount&quot;: &quot;123456789012&quot;
        &#125;,
        &quot;ArnLike&quot;: &#123;
          &quot;aws:SourceArn&quot;: &quot;arn:aws:bedrock-agentcore:us-east-1:123456789012:*&quot;
        &#125;
      &#125;
    &#125;
  ]
&#125;</code></pre></div><p class="paragraph" style="text-align:left;">And the permission policy excerpt for Policy integration looks like this:</p><div class="codeblock"><pre><code>&#123;
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    &#123;
      &quot;Sid&quot;: &quot;PolicyEngineConfiguration&quot;,
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: [
        &quot;bedrock-agentcore:GetPolicyEngine&quot;
      ],
      &quot;Resource&quot;: [
        &quot;arn:aws:bedrock-agentcore:us-east-1:123456789012:policy-engine/&lt;policy-engine-id&gt;&quot;
      ]
    &#125;,
    &#123;
      &quot;Sid&quot;: &quot;PolicyEngineAuthorization&quot;,
      &quot;Effect&quot;: &quot;Allow&quot;,
      &quot;Action&quot;: [
        &quot;bedrock-agentcore:AuthorizeAction&quot;,
        &quot;bedrock-agentcore:PartiallyAuthorizeActions&quot;
      ],
      &quot;Resource&quot;: [
        &quot;arn:aws:bedrock-agentcore:us-east-1:123456789012:policy-engine/&lt;policy-engine-id&gt;&quot;,
        &quot;arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/&lt;gateway-id&gt;&quot;
      ]
    &#125;
  ]
&#125;</code></pre></div><p class="paragraph" style="text-align:left;">You still need the target-specific permissions too, because Gateway also has to invoke your downstream Lambda, API Gateway stage, OpenAPI target, Smithy target, or MCP server. Those vary by integration type, so I&#39;ll leave them out.</p><p class="paragraph" style="text-align:left;">By the way, if you create the Gateway with the <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-get-started-toolkit.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">AgentCore starter toolkit</a>, it may auto-create an execution role with wide <code>bedrock-agentcore:*</code> permissions. That&#39;s great for demos, but in production please tighten it back down to least privilege.</p><h3 class="heading" style="text-align:left;" id="tools-in-agent-core-gateway">Tools in AgentCore Gateway</h3><p class="paragraph" style="text-align:left;">Gateway tool names use the format <code>$&#123;target_name&#125;___$&#123;tool_name&#125;</code>. For example, our tools look like <code>CloudOps___restart_instance</code>. The target prefix is the string that becomes the <code>AgentCore::Action</code> in Policy. If you rename a tool, you are changing your authorization surface. Which is intentional.</p><h2 class="heading" style="text-align:left;" id="permissions-at-the-tool-level">Permissions at the Tool Level</h2><p class="paragraph" style="text-align:left;"><a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">AgentCore Policy</a> (generally available since March 3, 2026) sits in front of AgentCore Gateway tool execution and evaluates <a class="link" href="https://www.cedarpolicy.com/en?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Cedar</a> policies against the actual incoming request. The Gateway builds a Cedar authorization request from two things:</p><ul><li><p class="paragraph" style="text-align:left;">the <b>JWT token</b>, which becomes the <code>AgentCore::OAuthUser</code> principal plus tags for claims</p></li><li><p class="paragraph" style="text-align:left;">the <b>MCP tool call</b>, whose arguments become <code>context.input</code></p></li></ul><p class="paragraph" style="text-align:left;">The resulting Cedar request has the following elements:</p><ul><li><p class="paragraph" style="text-align:left;">principal: <code>AgentCore::OAuthUser::&quot;sub&quot;</code></p></li><li><p class="paragraph" style="text-align:left;">action: <code>AgentCore::Action::&quot;Target___tool&quot;</code></p></li><li><p class="paragraph" style="text-align:left;">resource: <code>AgentCore::Gateway::&quot;gateway-arn&quot;</code></p></li><li><p class="paragraph" style="text-align:left;">context: <code>&#123; input: ...tool arguments... &#125;</code></p></li></ul><h3 class="heading" style="text-align:left;" id="a-policy-for-the-operations-agent">A Policy for the Operations Agent</h3><p class="paragraph" style="text-align:left;">We want the following permissions:</p><ul><li><p class="paragraph" style="text-align:left;">readers with <code>ops:read</code> scope to use read-only tools</p></li><li><p class="paragraph" style="text-align:left;">only SREs to even attempt restart</p></li><li><p class="paragraph" style="text-align:left;">tenant match between caller claim and tool arguments</p></li><li><p class="paragraph" style="text-align:left;">restart only in <code>staging</code></p></li><li><p class="paragraph" style="text-align:left;">explicit deny for non-staging restart calls</p></li></ul><div class="codeblock"><pre><code>permit(
  principal is AgentCore::OAuthUser,
  action in [
    AgentCore::Action::&quot;CloudOps___read_metrics&quot;,
    AgentCore::Action::&quot;CloudOps___search_logs&quot;,
    AgentCore::Action::&quot;CloudOps___list_recent_deployments&quot;,
    AgentCore::Action::&quot;CloudOps___create_incident_ticket&quot;
  ],
  resource == AgentCore::Gateway::&quot;arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/ops-gateway&quot;
)
when &#123;
  principal.hasTag(&quot;scope&quot;) &amp;&amp;
  principal.getTag(&quot;scope&quot;) like &quot;*ops:read*&quot;
&#125;;

permit(
  principal is AgentCore::OAuthUser,
  action == AgentCore::Action::&quot;CloudOps___restart_instance&quot;,
  resource == AgentCore::Gateway::&quot;arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/ops-gateway&quot;
)
when &#123;
  principal.hasTag(&quot;role&quot;) &amp;&amp;
  principal.getTag(&quot;role&quot;) == &quot;sre&quot; &amp;&amp;
  principal.hasTag(&quot;tenant_id&quot;) &amp;&amp;
  principal.getTag(&quot;tenant_id&quot;) == context.input.tenantId &amp;&amp;
  context.input.environment == &quot;staging&quot;
&#125;;

forbid(
  principal is AgentCore::OAuthUser,
  action == AgentCore::Action::&quot;CloudOps___restart_instance&quot;,
  resource == AgentCore::Gateway::&quot;arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/ops-gateway&quot;
)
unless &#123;
  context.input.environment == &quot;staging&quot;
&#125;;</code></pre></div><p class="paragraph" style="text-align:left;">And here is how you add it to the Policy Engine:</p><div class="codeblock"><pre><code>import boto3

control = boto3.client(&quot;bedrock-agentcore-control&quot;)

policy_statement = r&#39;&#39;&#39;
permit(
  principal is AgentCore::OAuthUser,
  action in [
    AgentCore::Action::&quot;CloudOps___read_metrics&quot;,
    AgentCore::Action::&quot;CloudOps___search_logs&quot;,
    AgentCore::Action::&quot;CloudOps___list_recent_deployments&quot;,
    AgentCore::Action::&quot;CloudOps___create_incident_ticket&quot;
  ],
  resource == AgentCore::Gateway::&quot;arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/ops-gateway&quot;
)
when &#123;
  principal.hasTag(&quot;scope&quot;) &amp;&amp;
  principal.getTag(&quot;scope&quot;) like &quot;*ops:read*&quot;
&#125;;

permit(
  principal is AgentCore::OAuthUser,
  action == AgentCore::Action::&quot;CloudOps___restart_instance&quot;,
  resource == AgentCore::Gateway::&quot;arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/ops-gateway&quot;
)
when &#123;
  principal.hasTag(&quot;role&quot;) &amp;&amp;
  principal.getTag(&quot;role&quot;) == &quot;sre&quot; &amp;&amp;
  principal.hasTag(&quot;tenant_id&quot;) &amp;&amp;
  principal.getTag(&quot;tenant_id&quot;) == context.input.tenantId &amp;&amp;
  context.input.environment == &quot;staging&quot;
&#125;;

forbid(
  principal is AgentCore::OAuthUser,
  action == AgentCore::Action::&quot;CloudOps___restart_instance&quot;,
  resource == AgentCore::Gateway::&quot;arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/ops-gateway&quot;
)
unless &#123;
  context.input.environment == &quot;staging&quot;
&#125;;
&#39;&#39;&#39;

response = control.create_policy(
    policyEngineId=&quot;pe-1234567890abcdef&quot;,
    name=&quot;ops-tool-fgac&quot;,
    validationMode=&quot;FAIL_ON_ANY_FINDINGS&quot;,
    description=&quot;Tool-level and parameter-level authorization for the operations agent&quot;,
    definition=&#123;
        &quot;cedar&quot;: &#123;
            &quot;statement&quot;: policy_statement
        &#125;
    &#125;
)

print(response[&quot;policyId&quot;])</code></pre></div><p class="paragraph" style="text-align:left;">It&#39;s important to note that <code>tools/list</code> is treated as a meta action. When Gateway lists tools, it does not have the full input parameters for a specific invocation yet. So a tool may appear in the list if there exists any circumstance under which the user could call it. However, a tool showing up in <code>tools/list</code> does not guarantee that a later <code>tools/call</code> will be allowed. The real authorization decision happens on the invocation with full <code>context.input</code>.</p><h2 class="heading" style="text-align:left;" id="verified-permissions-for-agent-core">Verified Permissions for AgentCore Gateway Tools</h2><p class="paragraph" style="text-align:left;">Gateway Policy is excellent at answering questions like:</p><ul><li><p class="paragraph" style="text-align:left;">does this caller have the <code>ops:read</code> scope</p></li><li><p class="paragraph" style="text-align:left;">is this caller an <code>sre</code></p></li><li><p class="paragraph" style="text-align:left;">does the tenant claim match the requested tenant</p></li><li><p class="paragraph" style="text-align:left;">is <code>environment == &quot;staging&quot;</code></p></li></ul><p class="paragraph" style="text-align:left;">That is already a massive improvement over just giving the agent a role. It is Fine-Grained Access Control (FGAC) at the tool and parameter level. But Gateway Policy cannot answer questions that depend on application entities outside the AgentCore schema. It cannot reference your own <code>Service</code>, <code>Team</code>, <code>Tenant</code>, or <code>EnvironmentPolicy</code> entity types.</p><p class="paragraph" style="text-align:left;">That means Gateway Policy cannot natively answer things like:</p><ul><li><p class="paragraph" style="text-align:left;">Does the caller’s team actually own <code>payments-api</code>?</p></li><li><p class="paragraph" style="text-align:left;">Is <code>payments-api</code> even associated with tenant Acme?</p></li><li><p class="paragraph" style="text-align:left;">Is this service currently under an additional freeze or maintenance rule?</p></li><li><p class="paragraph" style="text-align:left;">Does this caller belong to the allowed break-glass group for this exact service?</p></li></ul><p class="paragraph" style="text-align:left;">Those are not tool-boundary questions anymore. They are business-resource questions, and we&#39;ll use <a class="link" href="https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/what-is-avp.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Amazon Verified Permissions</a> to answer them.</p><h3 class="heading" style="text-align:left;" id="model-the-application-authorization">Model the application authorization explicitly</h3><p class="paragraph" style="text-align:left;">For the internal ops API behind <code>CloudOps___restart_instance</code>, I would model at least:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Namespace</b>: <code>Ops</code></p></li><li><p class="paragraph" style="text-align:left;"><b>Principal types</b>: <code>Ops::User</code>, <code>Ops::Role</code></p></li><li><p class="paragraph" style="text-align:left;"><b>Resource type</b>: <code>Ops::Service</code></p></li><li><p class="paragraph" style="text-align:left;"><b>Actions</b>: <code>Ops::Action::readMetrics</code>, <code>Ops::Action::searchLogs</code>, <code>Ops::Action::listRecentDeployments</code>, <code>Ops::Action::createIncidentTicket</code>, <code>Ops::Action::restartInstance</code></p></li><li><p class="paragraph" style="text-align:left;"><b>Service attributes</b>: <code>ownerTeam</code>, <code>tenantId</code></p></li><li><p class="paragraph" style="text-align:left;"><b>Context keys</b>: <code>environment</code>, maybe <code>maintenanceWindow</code>, maybe <code>requestSource</code></p></li></ul><h3 class="heading" style="text-align:left;" id="a-business-policy-in-cedar">A business policy in Cedar</h3><p class="paragraph" style="text-align:left;">Here is the kind of policy I actually want for the restart path:</p><div class="codeblock"><pre><code>permit(
  principal in Ops::Role::&quot;sre&quot;,
  action == Ops::Action::&quot;restartInstance&quot;,
  resource
)
when &#123;
  principal.team == resource.ownerTeam &amp;&amp;
  principal.tenantId == resource.tenantId &amp;&amp;
  context.environment == &quot;staging&quot;
&#125;;</code></pre></div><p class="paragraph" style="text-align:left;">We&#39;re considering more than just the role and Role-Based Access Control (RBAC). We&#39;re considering if the caller is in the <code>sre</code> role, if the caller&#39;s team owns the service, if the caller’s tenant matches the service’s tenant, and if the environment is staging.</p><h3 class="heading" style="text-align:left;" id="verified-permissions-in-ap-is">Verified Permissions in APIs</h3><p class="paragraph" style="text-align:left;">Now that we have our Verified Permissions policy, we need to add it to whatever internal API we have behind the tool:</p><div class="codeblock"><pre><code>import boto3

vp = boto3.client(&quot;verifiedpermissions&quot;)


def authorize_restart(
    *,
    policy_store_id: str,
    user_sub: str,
    user_team: str,
    user_tenant_id: str,
    service_id: str,
    service_owner_team: str,
    service_tenant_id: str,
    environment: str,
) -&gt; bool:
    response = vp.is_authorized(
        policyStoreId=policy_store_id,
        principal=&#123;
            &quot;entityType&quot;: &quot;Ops::User&quot;,
            &quot;entityId&quot;: user_sub,
        &#125;,
        action=&#123;
            &quot;actionType&quot;: &quot;Ops::Action&quot;,
            &quot;actionId&quot;: &quot;restartInstance&quot;,
        &#125;,
        resource=&#123;
            &quot;entityType&quot;: &quot;Ops::Service&quot;,
            &quot;entityId&quot;: service_id,
        &#125;,
        context=&#123;
            &quot;contextMap&quot;: &#123;
                &quot;environment&quot;: &#123;&quot;string&quot;: environment&#125;,
            &#125;
        &#125;,
        entities=&#123;
            &quot;entityList&quot;: [
                &#123;
                    &quot;identifier&quot;: &#123;
                        &quot;entityType&quot;: &quot;Ops::User&quot;,
                        &quot;entityId&quot;: user_sub,
                    &#125;,
                    &quot;attributes&quot;: &#123;
                        &quot;team&quot;: &#123;&quot;string&quot;: user_team&#125;,
                        &quot;tenantId&quot;: &#123;&quot;string&quot;: user_tenant_id&#125;,
                    &#125;,
                    &quot;parents&quot;: [
                        &#123;
                            &quot;entityType&quot;: &quot;Ops::Role&quot;,
                            &quot;entityId&quot;: &quot;sre&quot;,
                        &#125;
                    ],
                &#125;,
                &#123;
                    &quot;identifier&quot;: &#123;
                        &quot;entityType&quot;: &quot;Ops::Service&quot;,
                        &quot;entityId&quot;: service_id,
                    &#125;,
                    &quot;attributes&quot;: &#123;
                        &quot;ownerTeam&quot;: &#123;&quot;string&quot;: service_owner_team&#125;,
                        &quot;tenantId&quot;: &#123;&quot;string&quot;: service_tenant_id&#125;,
                    &#125;,
                &#125;,
            ]
        &#125;,
    )

    return response[&quot;decision&quot;] == &quot;ALLOW&quot;</code></pre></div><p class="paragraph" style="text-align:left;">And inside the tool implementation:</p><div class="codeblock"><pre><code>def restart_instance_tool(
    *,
    user_sub: str,
    user_team: str,
    user_tenant_id: str,
    service_id: str,
    environment: str,
) -&gt; dict:
    # Resolve business data from your control plane or service catalog.
    service = lookup_service(service_id)

    allowed = authorize_restart(
        policy_store_id=&quot;ps-1234567890abcdef&quot;,
        user_sub=user_sub,
        user_team=user_team,
        user_tenant_id=user_tenant_id,
        service_id=service_id,
        service_owner_team=service[&quot;ownerTeam&quot;],
        service_tenant_id=service[&quot;tenantId&quot;],
        environment=environment,
    )

    if not allowed:
        raise PermissionError(
            f&quot;Restart denied for service=&#123;service_id&#125;, env=&#123;environment&#125;&quot;
        )

    # Only now do the restart.
    return do_restart(service_id=service_id, environment=environment)</code></pre></div><p class="paragraph" style="text-align:left;">If Gateway Policy allowed the call because it was <code>staging</code> and the JWT tenant matched the request tenant, but the service is actually owned by another team, Verified Permissions still denies it.</p><h2 class="heading" style="text-align:left;" id="what-this-architecture-prevents">What This Architecture Prevents</h2><p class="paragraph" style="text-align:left;">Let’s go back to the original request. The user asks the operations agent to investigate Acme 5XXs in production and “restart the affected instance if it is safe”. Let&#39;s consider a few scenarios for our agent.</p><h3 class="heading" style="text-align:left;" id="scenario-1-readonly-diagnostics">Scenario 1: Read-only diagnostics</h3><p class="paragraph" style="text-align:left;">The agent calls:</p><ul><li><p class="paragraph" style="text-align:left;"><code>CloudOps___read_metrics(tenantId=&quot;acme&quot;, environment=&quot;prod&quot;)</code></p></li><li><p class="paragraph" style="text-align:left;"><code>CloudOps___search_logs(tenantId=&quot;acme&quot;, environment=&quot;prod&quot;)</code></p></li><li><p class="paragraph" style="text-align:left;"><code>CloudOps___list_recent_deployments(serviceId=&quot;payments-api&quot;, environment=&quot;prod&quot;)</code></p></li></ul><p class="paragraph" style="text-align:left;">Gateway Policy allows these because the caller has <code>ops:read</code>. No mutation happened. This was kind of the easy path though, level 1 </p><h3 class="heading" style="text-align:left;" id="scenario-2-the-agent-proposes-a-res">Scenario 2: The agent proposes a restart in production</h3><p class="paragraph" style="text-align:left;">Say the agent detects a problem, and it decides the fastest path to mitigate it is to send this action</p><div class="codeblock"><pre><code>&#123;
  &quot;serviceId&quot;: &quot;payments-api&quot;,
  &quot;tenantId&quot;: &quot;acme&quot;,
  &quot;environment&quot;: &quot;prod&quot;
&#125;</code></pre></div><p class="paragraph" style="text-align:left;">to the tool <code>CloudOps___restart_instance</code>. Watch closely, it says <code>&quot;environment&quot;: &quot;prod&quot;</code>.</p><p class="paragraph" style="text-align:left;">Gateway Policy denies it because the environment is not <code>staging</code>, and the tool call does not execute. Exactly as we intended, done automatically and reliably, with no human intervention, and not relying on a prompt like “Plz don&#39;t break prod bro, we&#39;ll both get fired”.</p><h3 class="heading" style="text-align:left;" id="scenario-3-the-agent-proposes-a-res">Scenario 3: The agent proposes a restart in staging</h3><p class="paragraph" style="text-align:left;">Now let&#39;s say we have the same request as above, but for staging:</p><div class="codeblock"><pre><code>&#123;
  &quot;serviceId&quot;: &quot;payments-api&quot;,
  &quot;tenantId&quot;: &quot;acme&quot;,
  &quot;environment&quot;: &quot;staging&quot;
&#125;</code></pre></div><p class="paragraph" style="text-align:left;">Gateway Policy may allow this, because the request is structurally within the allowed boundary. But the internal ops API still calls Amazon Verified Permissions. If the caller’s team does not own <code>payments-api</code>, or if <code>payments-api</code> is not actually an Acme service, AVP denies the action.</p><p class="paragraph" style="text-align:left;">It&#39;s not enough that the model can call the tool with the right parameters. We also check outside the tool itself, for characteristics of the target resource like which team owns it or what tenant does it belong to.</p><h2 class="heading" style="text-align:left;" id="wrapping-up">Wrapping Up</h2><p class="paragraph" style="text-align:left;">The model can decide which tool might help. But it&#39;s the underlying platform and security configuration that decides whether that tool call is allowed, with those parameters, for those resources, from that user.</p><p class="paragraph" style="text-align:left;">Use <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">AgentCore Runtime</a> as the runtime platform. Use resource-based policies there, and if you use endpoints, remember the Runtime and endpoint policies are both part of authorization. Use <a class="link" href="https://github.com/elizabethfuentes12/strands-agents-tools?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Strands</a>, but don&#39;t give the agent direct access to resources through the AWS SDK. Use <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/gateway.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">AgentCore Gateway</a> as the only tool surface, with explicit inbound and outbound auth. Use <a class="link" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/policy.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">AgentCore Policy</a> to evaluate the actual tool call against JWT-derived principal tags and real input parameters before execution. Then use <a class="link" href="https://docs.aws.amazon.com/verifiedpermissions/latest/userguide/what-is-avp.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=layered-authorization-for-ai-agents-using-amazon-bedrock-agentcore" target="_blank" rel="noopener noreferrer nofollow">Amazon Verified Permissions</a> inside the tool implementation when the decision depends on actual business resources and state.</p><p class="paragraph" style="text-align:left;">And use evals and observability. But I&#39;ll leave that for another article.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>AWS Lambda Durable Functions</title>
  <description>Learn Lambda durable execution, checkpoint replay, steps, waits, callbacks, idempotency, and how code-first workflows compare with Step Functions.</description>
  <link>https://newsletter.simpleaws.dev/p/aws-lambda-durable-functions</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/aws-lambda-durable-functions</guid>
  <pubDate>Sat, 21 Feb 2026 05:25:45 +0000</pubDate>
  <atom:published>2026-02-21T05:25:45Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Serverless]]></category>
    <category><![CDATA[Architecture]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">You usually use Step Functions when a workflow&#39;s complexity outgrows a single Lambda invocation, in terms of duration or of complexity. <a class="link" href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-lambda-durable-functions" target="_blank" rel="noopener noreferrer nofollow">Lambda durable functions</a> are another path, <a class="link" href="https://aws.amazon.com/about-aws/whats-new/2025/12/lambda-durable-multi-step-applications-ai-workflows/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-lambda-durable-functions" target="_blank" rel="noopener noreferrer nofollow">announced during re:Invent 2025</a>. You write a Lambda handler, and you use a durable execution SDK that checkpoints after each durable operation and replays your code across multiple invocations so the execution can span a much longer duration: from a few minutes to up to one year.</p><p class="paragraph" style="text-align:left;">Essentially it&#39;s a Lambda programming model + SDK (including JavaScript/TypeScript and Python) that provides a <code>DurableContext</code> with durable operations (steps, waits, callbacks, parallel/map, child contexts). Each durable execution persists a checkpoint log and resumes by replaying from the top and skipping completed operations.</p><p class="paragraph" style="text-align:left;">You enable durable execution when creating the Lambda function (you can’t retrofit an existing one), by setting <code>DurableConfig</code> (timeout + retention) via console/CLI/API/IaC; then you deploy code that uses the SDK wrapper/decorator. Then you invoke the function (typically asynchronously) using a qualified version/alias, monitor progress via CloudWatch metrics and EventBridge status-change events, inspect/stop executions via durable execution management commands, and complete callbacks via dedicated Lambda APIs.</p><h2 class="heading" style="text-align:left;" id="lambda-durable-execution-lifecycle-">Lambda Durable Execution Lifecycle and Replay</h2><p class="paragraph" style="text-align:left;">A durable execution is a <i>logical run</i> of your workflow that may span many Lambda invocations. The SDK checkpoints after each durable operation, and when the function resumes it replays your handler from the beginning while substituting stored results for already-completed operations. Basically, you write your orchestration code so that anything non-deterministic or side-effecting happens inside durable operations (especially <code>step</code>).</p><h3 class="heading" style="text-align:left;" id="the-four-phases-of-lambda-durable-f">The Four Phases of Lambda Durable Functions</h3><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Start:</b> A durable execution begins when you invoke the function with durable execution enabled.</p></li><li><p class="paragraph" style="text-align:left;"><b>Checkpoint:</b> Each durable operation (e.g., <code>step</code>, <code>wait</code>, callback) causes a checkpoint record to be persisted before the execution moves on.</p></li><li><p class="paragraph" style="text-align:left;"><b>Suspend and resume:</b> Waits/callbacks intentionally <i>end the current Lambda invocation</i> and schedule/trigger a later resume. Lambda invokes the function again later and the SDK replays to the suspension point.</p></li><li><p class="paragraph" style="text-align:left;"><b>Complete:</b> When your handler returns, the execution completes. Monitoring surfaces succeed/failed/timed out outcomes.</p></li></ol><h3 class="heading" style="text-align:left;" id="replay-is-not-a-resume-from-line-n">Replay is not a “resume from line N”</h3><p class="paragraph" style="text-align:left;">Replay means your code runs again from the top, but durable operations short-circuit by returning stored results. Determinism matters a lot in those operations: if you read <code>Date.now()</code>, random IDs, or environment values that change between runs <i>outside</i> a durable operation, you can diverge during replay and hit nondeterminism errors.</p><p class="paragraph" style="text-align:left;">Here&#39;s a code example using TypeScript:</p><div class="codeblock"><pre><code>import &#123; withDurableExecution, DurableContext &#125; from &quot;@aws/durable-execution-sdk-js&quot;;

export const handler = withDurableExecution(async (event: any, ctx: DurableContext) =&gt; &#123;
  // Pure orchestration logic here (replay-safe).
  const input = event.input;

  const validated = await ctx.step(&quot;validate&quot;, async () =&gt; &#123;
    // Do side effects and nondeterministic reads inside steps.
    return &#123; ok: true, input &#125;;
  &#125;);

  if (!validated.ok) return &#123; status: &quot;rejected&quot; &#125;;

  await ctx.wait(&quot;cooldown&quot;, &#123; seconds: 30 &#125;);

  return &#123; status: &quot;approved&quot; &#125;;
&#125;);
</code></pre></div><p class="paragraph" style="text-align:left;">Now your function is a deterministic program that can be re-run, and durable ops are the boundaries where results are persisted and re-used.</p><hr class="content_break"><h2 class="heading" style="text-align:left;" id="anyshift-the-ai-engineer-that-shows"><b>Anyshift, the AI engineer that shows you exactly what broke and why</b></h2><p class="paragraph" style="text-align:left;">Production incidents aren&#39;t hard because systems are complex. They&#39;re hard because nobody has the full picture when things break, and your team ends up burning 45 minutes just rebuilding context that should already exist somewhere.</p><p class="paragraph" style="text-align:left;">You could ask AI, but generic copilots don&#39;t know your stack. You&#39;ll spend more time copypasting logs and filtering through guesses than doing actual root cause analysis.</p><p class="paragraph" style="text-align:left;">Unlike other AI SREs, <b>Anyshift</b> automatically maps your cloud, K8s, code, and observability data into a live dependency graph. When something breaks, it already knows how everything connects.</p><ul><li><p class="paragraph" style="text-align:left;"><b>Root cause in minutes:</b> Solve incidents with full context.</p></li><li><p class="paragraph" style="text-align:left;"><b>Noise reduction:</b> P3/P4 alerts are auto-triaged before they hit your phone.</p></li><li><p class="paragraph" style="text-align:left;"><b>Proactive safety:</b> Risks are caught before they become incidents.</p></li></ul><p class="paragraph" style="text-align:left;">With security and auditability built into the foundation (SOC 2 Type II certified), it’s a 5-minute setup with no lock-in.</p><p class="paragraph" style="text-align:left;"><b>Get started at </b><b><a class="link" href="https://www.anyshift.io/vp/visibility/one-graph?utm_source=guille-ojeda&utm_medium=newsletter&utm_campaign=creator-q1-2026&utm_content=show_you_what_broke_and_why" target="_blank" rel="noopener noreferrer nofollow">Anyshift.io</a></b></p><hr class="content_break"><h2 class="heading" style="text-align:left;" id="lambda-durable-operations-steps-wai">Lambda Durable Operations: steps, waits, callbacks</h2><p class="paragraph" style="text-align:left;">Durable functions give you three core primitives: <b>steps</b> (checkpointed work with retries), <b>waits</b> (pause without compute billing), and <b>callbacks</b> (pause until an external system responds). You access them via <code>DurableContext</code> methods. Design-wise, you should put all side effects in steps, and use waits/callbacks to “sleep” without tying up a Lambda invocation.</p><h3 class="heading" style="text-align:left;" id="steps">Steps</h3><p class="paragraph" style="text-align:left;">A <code>step</code> runs code and records its result to the checkpoint log. On replay, completed steps return the stored result instead of re-running. Steps can be retried, which implies they can run <b>more than once</b> unless you configure stricter semantics.</p><h3 class="heading" style="text-align:left;" id="waits">Waits</h3><p class="paragraph" style="text-align:left;">A wait checkpoints, ends the current invocation, and schedules resumption later. You use waits for backoff, human approval timeouts, polling intervals, etc. The key here is that you stop paying for Lambda compute while waiting.</p><h3 class="heading" style="text-align:left;" id="callbacks">Callbacks</h3><p class="paragraph" style="text-align:left;">Callbacks give you a <b>callback ID</b> you can hand to an external system. Your durable execution suspends until that system calls the Lambda callback completion APIs.</p><p class="paragraph" style="text-align:left;">The callback ID is the primary identifier in the callback completion APIs (URI path parameter). Treat it as sensitive data and scope who can use it.</p><h3 class="heading" style="text-align:left;" id="rules-of-thumb">Rules of Thumb</h3><ul><li><p class="paragraph" style="text-align:left;">If the code does I/O or side effects, put it in <code>step</code> and make it idempotent.</p></li><li><p class="paragraph" style="text-align:left;">If the code needs to wait for some period of time, use <code>wait</code> and don’t poll inside a single Lambda invocation.</p></li><li><p class="paragraph" style="text-align:left;">If the code needs to wait for some external call, use callback + completion APIs.</p></li></ul><h2 class="heading" style="text-align:left;" id="invocation-modes-and-idempotency-ke">Invocation modes and idempotency keys</h2><p class="paragraph" style="text-align:left;">Durable functions are designed to be invoked asynchronously, so the caller doesn’t block while the workflow may run for minutes, hours, or days. To prevent duplicate starts, use an execution name as an idempotency key. Within the execution retention period, invoking again with the same name and payload returns information about the existing execution instead of starting another one. Reusing the name with a different payload returns DurableExecutionAlreadyStartedException. </p><p class="paragraph" style="text-align:left;">You can supply an execution name when invoking a Lambda Durable function. If an execution with that name is running or completed, an identical payload returns the existing execution information; a different payload produces a DurableExecutionAlreadyStartedException error.</p><h3 class="heading" style="text-align:left;" id="step-idempotency-vs-start-idempoten">Step Idempotency vs Start Idempotency</h3><p class="paragraph" style="text-align:left;">Start idempotency prevents duplicate workflow starts. You set it via the execution name. Step idempotency prevents duplicate side effects inside the workflow, and you set it by using step execution mode plus external idempotency keys.</p><p class="paragraph" style="text-align:left;">If duplicates come from the event source or caller retries, enforce execution name. That way retries using the same name and payload reuse the existing execution instead of starting a duplicate.</p><p class="paragraph" style="text-align:left;">If duplicates come from retries or timeouts within a step, use <code>AT_MOST_ONCE_PER_RETRY</code> and external idempotency tokens. External operations still need idempotency handling; this mode alone does not guarantee exactly-once side effects.</p><p class="paragraph" style="text-align:left;">If you can’t define a stable idempotency key, Lambda Durable functions will just end up being very complex, and you should consider Step Functions with explicit task tokens, or a <a class="link" href="https://newsletter.simpleaws.dev/p/distributed-transactions-event-driven-architectures?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-lambda-durable-functions" target="_blank" rel="noopener noreferrer nofollow">saga pattern for event-driven architectures</a>.</p><h2 class="heading" style="text-align:left;" id="lambda-durable-functions-vs-step-fu">Lambda Durable Functions vs Step Functions</h2><p class="paragraph" style="text-align:left;">AWS Lambda durable functions are a Lambda execution mode plus a Durable Execution SDK that adds durable primitives (steps, waits, callbacks, etc.) to a single Lambda handler. Your code is replayed from the start on resume, and completed durable operations are skipped using stored checkpoints. </p><p class="paragraph" style="text-align:left;">AWS Step Functions is a separate orchestration service where you define a state machine (ASL JSON) and run executions. You wire tasks to AWS services (including Lambda) and get built-in workflow history + visualization.</p><h3 class="heading" style="text-align:left;" id="execution-model-and-guarantees">Execution model and guarantees</h3><p class="paragraph" style="text-align:left;">In Step Functions, your source of truth is the state machine graph (ASL). Each state transition is managed by Step Functions, which records progress and drives task scheduling. Pricing and limits are centered around state transitions / executions.</p><p class="paragraph" style="text-align:left;">Standard workflows guarantee exactly-once workflow execution, and can run up to one year. Asynchronous Express workflows use at-least-once execution; Synchronous Express workflows use at-most-once execution. Both can run for up to five minutes.</p><p class="paragraph" style="text-align:left;">For Lambda Durable Functions, your source of truth is the Lambda handler code, but durable operations create checkpoints. On <code>resume</code>, the SDK replays the handler from the beginning and returns stored results for previously completed durable operations. This creates an important constraint: your code must be deterministic across replays (especially any logic outside durable operations). The SDK explicitly warns about replay behavior and determinism.</p><h3 class="heading" style="text-align:left;" id="waits-and-humanintheloop">Waits and human-in-the-loop</h3><p class="paragraph" style="text-align:left;">Step Functions uses patterns like Wait states or callback (“task token”) patterns. In the callback pattern, Step Functions hands you a <code>taskToken</code>, and an external actor later resumes the workflow by calling APIs like <code>SendTaskSuccess</code> (or the failure variant).</p><p class="paragraph" style="text-align:left;">Lambda durable functions, on the other hand, uses durable operations like <code>wait()</code> and callback primitives like <code>createCallback()</code> / <code>waitForCallback()</code>. When waiting, the invocation terminates and later resumes without on-demand compute billed during the wait. External systems resume via Lambda APIs <code>SendDurableExecutionCallbackSuccess</code> / <code>SendDurableExecutionCallbackFailure</code>.</p><h3 class="heading" style="text-align:left;" id="integrations">Integrations</h3><p class="paragraph" style="text-align:left;">Step Functions has native integrations that let you orchestrate many AWS services without creating Lambda functions just to call a service. You also get visual workflow debugging/history (though in my opinion the UI is pretty ugly).</p><p class="paragraph" style="text-align:left;">With Lambda Durable Functions you can integrate with anything via AWS SDK calls inside steps (or use <code>context.invoke()</code> to call other Lambda functions as durable operations), which is more natural if you&#39;re more used to code. However, you’re building the control flow in code and you won’t get Step Functions’ state-machine-level service integration patterns that would let you just call a service without writing the code for that call.</p><h3 class="heading" style="text-align:left;" id="cost-model">Cost model</h3><p class="paragraph" style="text-align:left;">In Step Functions Standard workflows you pay per state transition (retries count too). The free tier includes 4,000 transitions/month (does not expire), and in us-east-1 you pay $0.000025 per transition. Step Functions Express workflows make you pay per request + duration (rounded to 100 ms) + memory used (billed in 64 MB chunks). In us-east-1 you&#39;ll pay $1.00 per million requests and $0.00001667 per GB-second (plus tiering at higher GB-hours).</p><p class="paragraph" style="text-align:left;">With Lambda Durable Functions you still pay normal Lambda requests + duration (including sub-invocations due to resume/replay), <i>plus</i> durable charges:</p><ul><li><p class="paragraph" style="text-align:left;">Durable operations: $8.00 per million operations.</p></li><li><p class="paragraph" style="text-align:left;">Data written by durable operations (GB): $0.25 per GB.</p></li><li><p class="paragraph" style="text-align:left;">Data retained (GB-month, prorated): $0.15 per GB-month.</p></li></ul><h3 class="heading" style="text-align:left;" id="decision-guidance">Decision guidance</h3><p class="paragraph" style="text-align:left;">Use <b>Step Functions</b> when you need:</p><ul><li><p class="paragraph" style="text-align:left;">A workflow as a first-class managed artifact (ASL + visualization + execution history).</p></li><li><p class="paragraph" style="text-align:left;">Broad AWS-service orchestration with less “glue code” (especially across many services).</p></li><li><p class="paragraph" style="text-align:left;">Standard’s semantics and tooling for long-running orchestration (up to 1 year) with exactly-once workflow execution.</p></li></ul><p class="paragraph" style="text-align:left;">Use <b>Lambda durable functions</b> when you need:</p><ul><li><p class="paragraph" style="text-align:left;">A code-first workflow where the orchestration logic lives in the Lambda handler and you want to use durable primitives (<code>step</code>, <code>wait</code>, <code>waitForCallback</code>, <code>parallel</code>, etc.) with checkpoint+replay.</p></li><li><p class="paragraph" style="text-align:left;">To wait without paying for idle compute, and prefer to stay inside the Lambda programming model, and you’re willing to design for determinism/replay.</p></li><li><p class="paragraph" style="text-align:left;">Therapy after dealing with Amazon States Language (ASL).</p></li></ul><h2 class="heading" style="text-align:left;" id="building-a-human-approval-workflow-">Building a Human Approval Workflow in Step Functions and Lambda Durable Functions</h2><p class="paragraph" style="text-align:left;">Let&#39;s see an example. Say our app needs to process a loan application with the following process:</p><ol start="1"><li><p class="paragraph" style="text-align:left;">Risk scoring (fast, deterministic-ish compute + external call)</p></li><li><p class="paragraph" style="text-align:left;">If risk is low, approve automatically</p></li><li><p class="paragraph" style="text-align:left;">Else, request human review and pause until the reviewer approves/rejects</p></li><li><p class="paragraph" style="text-align:left;">Finalize decision (write to DB / emit event)</p></li></ol><p class="paragraph" style="text-align:left;">These illustrative examples focus on the “pause + resume via external callback” pattern because it shows the biggest mechanical difference between Step Functions and Lambda Durable Functions. The approval, rejection and timeout paths still need an integrated code review and testing.</p><h3 class="heading" style="text-align:left;" id="step-functions-implementation-stand">Step Functions implementation (Standard workflow + task token callback)</h3><h4 class="heading" style="text-align:left;" id="1-state-machine-definition-asl-json">1) State machine definition (ASL JSON)</h4><div class="codeblock"><pre><code>&#123;
  &quot;Comment&quot;: &quot;Loan approval with human-in-the-loop review (Standard)&quot;,
  &quot;StartAt&quot;: &quot;RiskScore&quot;,
  &quot;States&quot;: &#123;
    &quot;RiskScore&quot;: &#123;
      &quot;Type&quot;: &quot;Task&quot;,
      &quot;Resource&quot;: &quot;arn:aws:states:::lambda:invoke&quot;,
      &quot;Parameters&quot;: &#123;
        &quot;FunctionName&quot;: &quot;$&#123;RiskScoreLambdaArn&#125;&quot;,
        &quot;Payload.$&quot;: &quot;$&quot;
      &#125;,
      &quot;OutputPath&quot;: &quot;$.Payload&quot;,
      &quot;Next&quot;: &quot;RiskDecision&quot;
    &#125;,
    &quot;RiskDecision&quot;: &#123;
      &quot;Type&quot;: &quot;Choice&quot;,
      &quot;Choices&quot;: [
        &#123;
          &quot;Variable&quot;: &quot;$.riskScore&quot;,
          &quot;NumericLessThanEquals&quot;: 30,
          &quot;Next&quot;: &quot;AutoApprove&quot;
        &#125;
      ],
      &quot;Default&quot;: &quot;RequestHumanApproval&quot;
    &#125;,
    &quot;AutoApprove&quot;: &#123;
      &quot;Type&quot;: &quot;Pass&quot;,
      &quot;Parameters&quot;: &#123;
        &quot;decision&quot;: &quot;APPROVE&quot;,
        &quot;reason&quot;: &quot;Auto-approved by risk threshold&quot;,
        &quot;application.$&quot;: &quot;$&quot;
      &#125;,
      &quot;Next&quot;: &quot;FinalizeDecision&quot;
    &#125;,
    &quot;RequestHumanApproval&quot;: &#123;
      &quot;Type&quot;: &quot;Task&quot;,
      &quot;Resource&quot;: &quot;arn:aws:states:::lambda:invoke.waitForTaskToken&quot;,
      &quot;TimeoutSeconds&quot;: 86400,
      &quot;Parameters&quot;: &#123;
        &quot;FunctionName&quot;: &quot;$&#123;RequestApprovalLambdaArn&#125;&quot;,
        &quot;Payload&quot;: &#123;
          &quot;taskToken.$&quot;: &quot;$$.Task.Token&quot;,
          &quot;application.$&quot;: &quot;$&quot;
        &#125;
      &#125;,
      &quot;OutputPath&quot;: &quot;$.Payload&quot;,
      &quot;Next&quot;: &quot;FinalizeDecision&quot;
    &#125;,
    &quot;FinalizeDecision&quot;: &#123;
      &quot;Type&quot;: &quot;Task&quot;,
      &quot;Resource&quot;: &quot;arn:aws:states:::lambda:invoke&quot;,
      &quot;Parameters&quot;: &#123;
        &quot;FunctionName&quot;: &quot;$&#123;FinalizeLambdaArn&#125;&quot;,
        &quot;Payload.$&quot;: &quot;$&quot;
      &#125;,
      &quot;OutputPath&quot;: &quot;$.Payload&quot;,
      &quot;End&quot;: true
    &#125;
  &#125;
&#125;</code></pre></div><h4 class="heading" style="text-align:left;" id="2-risk-score-lambda">2) <code>riskScore</code> Lambda</h4><div class="codeblock"><pre><code>// riskScore.js
export const handler = async (event) =&gt; &#123;
  // event: &#123; applicationId, applicant, amount, ... &#125;
  // Keep this deterministic-ish; any nondeterminism should be inside the Lambda call itself (Step Functions won&#39;t replay your code).
  const riskScore = Math.min(100, Math.max(0, (event.amount ?? 0) / 1000)); // placeholder logic

  return &#123;
    ...event,
    riskScore,
    scoredAt: new Date().toISOString()
  &#125;;
&#125;;</code></pre></div><h4 class="heading" style="text-align:left;" id="3-request-approval-lambda-sends-tok">3) <code>requestApproval</code> Lambda (sends token to reviewer channel)</h4><p class="paragraph" style="text-align:left;">This Lambda receives <code>taskToken</code> and must deliver it to your approver UI/system. Step Functions will wait until something calls <code>SendTaskSuccess</code> or <code>SendTaskFailure</code> for that token. </p><div class="codeblock"><pre><code>// requestApproval.js
// This function is invoked by Step Functions with a taskToken.
// It should NOT call SendTaskSuccess itself unless you&#39;re auto-approving.
// It should send the token to your approval system (email, Slack, ticket, etc.)

export const handler = async (event) =&gt; &#123;
  const &#123; taskToken, application &#125; = event;

  // In production: publish to SNS/SQS/EventBridge, create a ticket, notify Slack, etc.
  console.log(&quot;Approval requested&quot;, &#123;
    applicationId: application.applicationId,
    taskTokenPreview: taskToken.slice(0, 16) + &quot;...&quot;
  &#125;);

  // Step Functions &quot;waitForTaskToken&quot; pattern keeps the state open until callback.
  // Return quickly (this Lambda call is just the submission step).
  return &#123;
    status: &quot;PENDING_REVIEW&quot;,
    applicationId: application.applicationId
  &#125;;
&#125;;</code></pre></div><h4 class="heading" style="text-align:left;" id="4-approver-callback-lambda-calls-se">4) “Approver callback” Lambda (calls <code>SendTaskSuccess</code> / <code>SendTaskFailure</code>)</h4><p class="paragraph" style="text-align:left;">Mechanics:</p><ul><li><p class="paragraph" style="text-align:left;">It receives <code>&#123; taskToken, decision, notes &#125;</code> (e.g., from API Gateway).</p></li><li><p class="paragraph" style="text-align:left;">On approve, call <code>SendTaskSuccess</code> with <code>output</code> JSON.</p></li><li><p class="paragraph" style="text-align:left;">On reject, call <code>SendTaskFailure</code>.</p></li></ul><div class="codeblock"><pre><code>// approverCallback.js
import &#123; SFNClient, SendTaskSuccessCommand, SendTaskFailureCommand &#125; from &quot;@aws-sdk/client-sfn&quot;;

const sfn = new SFNClient(&#123;&#125;);

export const handler = async (event) =&gt; &#123;
  const &#123; taskToken, decision, notes &#125; = event;

  if (!taskToken) throw new Error(&quot;Missing taskToken&quot;);

  if (decision === &quot;APPROVE&quot;) &#123;
    const output = JSON.stringify(&#123; decision: &quot;APPROVE&quot;, notes: notes ?? null &#125;);
    await sfn.send(new SendTaskSuccessCommand(&#123; taskToken, output &#125;));
    return &#123; ok: true &#125;;
  &#125;

  // Reject path: you can send structured error info.
  await sfn.send(
    new SendTaskFailureCommand(&#123;
      taskToken,
      error: &quot;RejectedByReviewer&quot;,
      cause: notes ?? &quot;No notes&quot;
    &#125;)
  );
  return &#123; ok: true &#125;;
&#125;;</code></pre></div><h3 class="heading" style="text-align:left;" id="lambda-durable-functions-implementa">Lambda durable functions implementation (single handler + SDK callback)</h3><h4 class="heading" style="text-align:left;" id="0-what-you-need-in-code">0) What you need in code</h4><p class="paragraph" style="text-align:left;">You wrap your handler with the durable wrapper and use <code>DurableContext</code> operations. </p><div class="codeblock"><pre><code>npm install @aws/durable-execution-sdk-js</code></pre></div><h4 class="heading" style="text-align:left;" id="1-durable-workflow-handler-steps-wa">1) Durable workflow handler (steps + waitForCallback)</h4><p class="paragraph" style="text-align:left;">This version:</p><ul><li><p class="paragraph" style="text-align:left;">Calls <code>context.step()</code> for risk scoring and finalization</p></li><li><p class="paragraph" style="text-align:left;">Uses <code>context.waitForCallback()</code> to create a callback ID, run the submitter function, and then block until callback completes (or times out) </p></li></ul><div class="codeblock"><pre><code>// durableLoanWorkflow.js
import &#123; withDurableExecution &#125; from &quot;@aws/durable-execution-sdk-js&quot;;

/**
 * event: &#123; applicationId, amount, applicant, ... &#125;
 */
export const handler = withDurableExecution(async (event, context) =&gt; &#123;
  // Step 1: risk score (checkpointed)
  const scored = await context.step(&quot;risk-score&quot;, async () =&gt; &#123;
    const riskScore = Math.min(100, Math.max(0, (event.amount ?? 0) / 1000)); // placeholder
    return &#123; ...event, riskScore, scoredAt: new Date().toISOString() &#125;;
  &#125;);

  let decision;
  if (scored.riskScore &lt;= 30) &#123;
    decision = &#123; decision: &quot;APPROVE&quot;, notes: &quot;Auto-approved by threshold&quot; &#125;;
  &#125; else &#123;
    // Step 2: human approval via callback
    // waitForCallback creates callbackId, runs submitter (send request), then waits.
    decision = await context.waitForCallback(
      &quot;human-approval&quot;,
      async (callbackId) =&gt; &#123;
        // In production: publish callbackId + context to SNS/SQS/EventBridge, create ticket, etc.
        console.log(&quot;Approval requested&quot;, &#123;
          applicationId: scored.applicationId,
          callbackIdPreview: callbackId.slice(0, 16) + &quot;...&quot;
        &#125;);
      &#125;,
      &#123; timeout: &#123; hours: 24 &#125; &#125;
    );
  &#125;

  // Step 3: finalize (checkpointed)
  const finalized = await context.step(&quot;finalize-decision&quot;, async () =&gt; &#123;
    // Write to DynamoDB, emit EventBridge event, etc.
    return &#123;
      applicationId: scored.applicationId,
      riskScore: scored.riskScore,
      ...decision,
      finalizedAt: new Date().toISOString()
    &#125;;
  &#125;);

  return finalized;
&#125;);</code></pre></div><p class="paragraph" style="text-align:left;">Note that this is a single Lambda handler that will be <b>replayed</b> on resume, skipping completed durable operations using checkpoint data. The callback token (“callbackId”) is completed via Lambda callback APIs (<code>SendDurableExecutionCallbackSuccess</code> / <code>Failure</code>). </p><h4 class="heading" style="text-align:left;" id="2-approver-callback-function-calls-">2) Approver callback function (calls Lambda callback APIs)</h4><p class="paragraph" style="text-align:left;">Durable callback completion APIs:</p><ul><li><p class="paragraph" style="text-align:left;">Success: <code>SendDurableExecutionCallbackSuccess</code> takes a <code>CallbackId</code> (URI) and a binary <code>Result</code> payload.</p></li><li><p class="paragraph" style="text-align:left;">Failure: <code>SendDurableExecutionCallbackFailure</code> takes a <code>CallbackId</code> and error fields like <code>ErrorType</code>, <code>ErrorMessage</code>, etc. </p></li></ul><div class="codeblock"><pre><code>// durableApproverCallback.js
import &#123;
  LambdaClient,
  SendDurableExecutionCallbackSuccessCommand,
  SendDurableExecutionCallbackFailureCommand
&#125; from &quot;@aws-sdk/client-lambda&quot;;

const lambda = new LambdaClient(&#123;&#125;);

export const handler = async (event) =&gt; &#123;
  const &#123; callbackId, decision, notes &#125; = event;
  if (!callbackId) throw new Error(&quot;Missing callbackId&quot;);

  if (decision === &quot;APPROVE&quot;) &#123;
    const resultObj = &#123; decision: &quot;APPROVE&quot;, notes: notes ?? null &#125;;
    // API expects binary payload; SDK accepts Uint8Array/Buffer for &quot;Result&quot;
    const resultBytes = Buffer.from(JSON.stringify(resultObj), &quot;utf-8&quot;);

    await lambda.send(
      new SendDurableExecutionCallbackSuccessCommand(&#123;
        CallbackId: callbackId,
        Result: resultBytes
      &#125;)
    );

    return &#123; ok: true &#125;;
  &#125;

  await lambda.send(
    new SendDurableExecutionCallbackFailureCommand(&#123;
      CallbackId: callbackId,
      ErrorType: &quot;RejectedByReviewer&quot;,
      ErrorMessage: notes ?? &quot;No notes&quot;
      // ErrorData / StackTrace optional
    &#125;)
  );

  return &#123; ok: true &#125;;
&#125;;</code></pre></div><h2 class="heading" style="text-align:left;" id="pricing-for-lambda-durable-function">Pricing for Lambda Durable Functions</h2><p class="paragraph" style="text-align:left;">These values are current as of 2026-02-21, and are for us-east-1. Always check <a class="link" href="https://aws.amazon.com/lambda/pricing/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-lambda-durable-functions" target="_blank" rel="noopener noreferrer nofollow">the pricing page</a>.</p><p class="paragraph" style="text-align:left;">Lambda durable functions have three pricing components:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Lambda compute + requests</b> (same as normal Lambda): You pay for requests and duration, at for example $0.0000133334 per GB-second for ARM, plus $0.20 per million requests. The free tier includes 1M requests/month and 400,000 GB-seconds/month. </p></li><li><p class="paragraph" style="text-align:left;"><b>Durable operations</b>: Each durable operation (execution start, steps, waits, etc.) is metered; the SDK doc shows how operations count by operation type (e.g., <code>Execution Started</code> is 1 op; <code>Step</code> is 1 + retries; <code>WaitForCallback</code> is 3 + retries). You pay $8.00 per million operations.</p></li><li><p class="paragraph" style="text-align:left;"><b>Data written (GB) and data retained (GB-month)</b>: Durable operations persist checkpoints; you pay for the data written and for retained storage over time (prorated GB-month). You pay $0.25 per GB written and $0.15 per GB-month of data retained.</p></li></ol><h2 class="heading" style="text-align:left;" id="conclusion">Conclusion</h2><p class="paragraph" style="text-align:left;">Durable functions are a strong fit when you need a long-running workflow but want to stay in Lambda’s programming model.</p><h3 class="heading" style="text-align:left;" id="when-to-use-durable-functions">When to use durable functions</h3><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="25%"><p class="paragraph" style="text-align:left;">Situation</p></th><th class="bh__table_header" width="25%"><p class="paragraph" style="text-align:left;">Use Lambda durable functions?</p></th><th class="bh__table_header" width="25%"><p class="paragraph" style="text-align:left;">Prefer instead</p></th><th class="bh__table_header" width="25%"><p class="paragraph" style="text-align:left;">Why</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">One Lambda needs to pause for hours/days (waiting on a human/external system)</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Yes</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">—</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Callbacks/waits checkpoint and resume across invocations.</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Workflow spans many AWS services and you want orchestration outside code</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Maybe</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Step Functions</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Service-native orchestration/control plane, integrations.</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Event source mapping (SQS/Streams) with &gt;15 minute processing time</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Not with direct invocation</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Intermediary + durable</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Event source mapping target must stay within 15 minutes; durable functions time out at 15 minutes.</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">You can’t keep code deterministic across replay (frequent hotfixes, nondeterministic reads)</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Probably not</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Step Functions or saga</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Replay requires determinism. Use versions/aliases.</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Workflow risks hitting 3,000 ops / 100 MB written per execution</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">No</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Re-architect steps or use Step Functions</p></td><td class="bh__table_cell" width="25%"><p class="paragraph" style="text-align:left;">Those are hard constraints per execution.</p></td></tr></table></div><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>The Essential Guide To LLM Evaluations</title>
  <description>Build useful LLM evaluations with programmatic metrics, human review, calibrated LLM judges, reference datasets, and quality gates in CI.</description>
  <link>https://newsletter.simpleaws.dev/p/the-essential-guide-to-llm-evaluations</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/the-essential-guide-to-llm-evaluations</guid>
  <pubDate>Sat, 24 Jan 2026 02:02:18 +0000</pubDate>
  <atom:published>2026-01-24T02:02:18Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Artificial Intelligence]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">Shipping an LLM-powered feature to production without evals is like deploying software without tests: <b>it works, kind of, hopefully, but not really, and you don&#39;t even know how it can fail</b>. This whole article assumes you wouldn&#39;t deploy untested code, and it&#39;s going to teach you how to apply the same rigor of tests to Generative AI applications.</p><p class="paragraph" style="text-align:left;">Traditional code is deterministic: for the same input you get the same output, every time. LLMs aren’t. They sample from a probability distribution over an effectively infinite response space, and don&#39;t always produce the same output.</p><p class="paragraph" style="text-align:left;">For deterministic tasks that&#39;s an implementation challenge, but not really a testing challenge. So long as you know what the expected output should be, you can just test for <code>actual_output == expected_output</code> and easily know whether the LLM says 2+2 equals 4 or 5. The challenge is when you <i>want</i> that variation in the output. When you can&#39;t define the expected output, because it&#39;s not a single thing, but rather a potentially infinite subset of the larger potentially infinite possible outputs. For example, if you ask AI to write this article. What value would you assign to <code>expected_output</code> in that case?</p><p class="paragraph" style="text-align:left;">Btw, no, AI didn&#39;t write this article. I did ask, many times. It&#39;s still not there yet, not even close. But that&#39;s a separate discussion.</p><p class="paragraph" style="text-align:left;">So, how do we test that the article the LLM writes is “good”, if we don&#39;t have a single value or a finite set of values to compare to? We don&#39;t test it, we <i>evaluate</i> it. With evaluations (evals, once you get familiar with them).</p><hr class="content_break"><p class="paragraph" style="text-align:left;"><b>Original January 2026 sponsorship (the February 3, 2026 launch event has passed):</b></p><div class="image"><a class="image__link" href="https://events.cyberhaven.com/winter-2026-launch?utm_campaign=wbn-2026-02-03-product-launch-winter-2026&utm_source=passionfroot&utm_medium=paid-influencers&utm_content=guilleojeda" rel="noopener" target="_blank"><img alt="Cyberhaven sponsorship graphic: DSPM and DLP Better Together, promoting the February 3 platform launch." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/3e564b75-e1b2-4bcf-bc49-6131c1a501d8/250x250_Display_DSPM.png?t=1769219721"/></a></div><p class="paragraph" style="text-align:left;">Every week, AI makes your team faster—and your data more exposed. Files move to new tools, models train on sensitive inputs, and traditional DLP is blind to the context that matters most.</p><p class="paragraph" style="text-align:left;">On <b>February 3 at 11:00 AM PT</b>, we’re pulling back the curtain on <b>Cyberhaven’s unified DSPM & DLP platform</b>—and showing how a single, AI-native platform can finally keep up with how data actually moves.</p><p class="paragraph" style="text-align:left;"><b>Join the live launch to see:</b></p><ul><li><p class="paragraph" style="text-align:left;"><b>The first public demo</b> of our unified AI & data security platform, built for the realities of <b>2026 and beyond</b>—SaaS sprawl, shadow AI tools, and data that never sits still.</p></li><li><p class="paragraph" style="text-align:left;"><b>How security teams gain x-ray vision into data usage</b>, so they can spot the risky handful of actions hidden in millions of “normal” events—and stop them in real time, not after the damage is done.</p></li><li><p class="paragraph" style="text-align:left;"><b>Candid stories from security leaders</b> on where legacy DLP and standalone DSPM break down, and how they’re rethinking data protection around context instead of static rules.</p></li><li><p class="paragraph" style="text-align:left;"><b>A look ahead at what’s next</b> for DLP, insider risk, AI security, and DSPM from Cyberhaven’s product and leadership teams—plus where we’re investing next.</p></li></ul><div class="button" style="text-align:center;"><a target="_blank" rel="noopener nofollow noreferrer" class="button__link" style="" href="https://events.cyberhaven.com/winter-2026-launch?utm_campaign=wbn-2026-02-03-product-launch-winter-2026&utm_source=passionfroot&utm_medium=paid-influencers&utm_content=guilleojeda"><span class="button__text" style=""> Register now </span></a></div><p class="paragraph" style="text-align:left;">Don’t wait for the next AI-related incident to show you the gaps in your data security stack. <b>Reserve your spot</b> and be among the first to see how a unified DSPM + DLP platform can change the way your organization protects its most critical data.</p><hr class="content_break"><h2 class="heading" style="text-align:left;" id="how-to-measure-quality-in-generativ">How to Measure Quality in Generative AI Applications</h2><p class="paragraph" style="text-align:left;">If you ask me to tell you if something is “good”, the first thing I&#39;ll do is ask you what do you mean by “good”. We can&#39;t define a golden answer (that&#39;s what you&#39;d call that <code>expected_output</code>), but we should be able to identify a set of measurable characteristics that we can use to determine if any given answer is “good” or not, in a repeatable way.</p><p class="paragraph" style="text-align:left;">Some of these characteristics may be factual accuracy, harmlessness, contextual relevance, or even overall tone. And for each characteristic we need to define how we&#39;re going to measure it.</p><p class="paragraph" style="text-align:left;">Depending on the metric you&#39;re evaluating, you&#39;ll have to pick one of these two:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Reference-based evaluation </b>compares LLM output against predefined ground truth or golden answers. It&#39;s great when you can come up with several examples of a good answer, though creating this golden dataset is not as trivial as it sounds, you need to carefully choose examples that represent all the ways in which an answer can be “good” in this specific characteristic.</p></li><li><p class="paragraph" style="text-align:left;"><b>Reference-free evaluation</b> evaluates intrinsic qualities without comparing the output to a specific reference. It&#39;s a good fit for open-ended questions where the quality is judged on criteria like coherence, relevance, or adherence to guidelines.</p></li></ul><p class="paragraph" style="text-align:left;">And you can pick how you evaluate it (most semantic metrics don&#39;t support the first option):</p><ul><li><p class="paragraph" style="text-align:left;"><b>Automated evaluation</b> uses algorithmic metrics like the presence of certain words or repetition of patterns. These are much faster and cheaper to calculate than more nuanced metrics that rely on semantics, but they&#39;re also much less reliable. They&#39;re usually a good complement.</p></li><li><p class="paragraph" style="text-align:left;"><b>Human evaluation </b>consists of having a human judge the outputs based on certain guidelines. This is a very tried and true method, literally what we&#39;ve been doing in education, interviews, etc for centuries. It&#39;s also slow and expensive, so we generally try to avoid it, but it&#39;s useful to know that it&#39;s there. Tools like <a class="link" href="https://lmarena.ai/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=the-essential-guide-to-llm-evaluations" target="_blank" rel="noopener noreferrer nofollow">https://lmarena.ai/</a> use it to generate benchmarks on models.</p></li><li><p class="paragraph" style="text-align:left;"><b>Model-based evaluation</b> is like human evaluation, but using an LLM as a judge (hence why we often call it LLM-as-a-Judge). The Judge LLM can understand nuance at a similar level to a human, and we can encode the evaluation guidelines into the prompt. This can be somewhat expensive, to the tune of a few cents or even a few dollars for every run (which is a lot if you compare it to running unit tests), but it&#39;s much cheaper than human evaluation.</p></li></ul><p class="paragraph" style="text-align:left;">Let&#39;s dive a bit deeper into those.</p><h2 class="heading" style="text-align:left;" id="automated-evaluation-and-programmat">Automated Evaluation and Programmatic Metrics</h2><p class="paragraph" style="text-align:left;">Programmatic metrics are scalable, repeatable, very cheap to run, and they catch regressions fast. You&#39;ll thank me when you accidentally break formatting in a way that causes everything to crash, and detect it via programmatic metrics. They tend to work reasonably well for Reference-based evaluation.</p><h3 class="heading" style="text-align:left;" id="foundational-lexical-metrics">Foundational lexical metrics</h3><p class="paragraph" style="text-align:left;">These metrics check for lexical overlap, quantifying the similarity between model-generated text and the references you provide by counting shared word sequences. Basically, they look at the text without really understanding it, at all. They&#39;re older than LLMs, but they&#39;re still reasonably useful and very cheap.</p><p class="paragraph" style="text-align:left;">The most common are BLEU (Bilingual Evaluation Understudy), ROUGE (Recall-Oriented Understudy for Gisting Evaluation) and METEOR (Metric for Evaluation of Translation with Explicit Ordering). There are a few more, but honestly they&#39;re boring to explain.</p><h3 class="heading" style="text-align:left;" id="embeddingbased-metrics">Embedding-based metrics</h3><p class="paragraph" style="text-align:left;">Lexical metrics fail because different words can mean the same thing. A pretty cool idea to beat that is embedding-based methods, which try to score meaning similarity instead of string similarity.</p><p class="paragraph" style="text-align:left;"><b>BERTScore</b><span style="color:#000000;font-family:Roboto, ui-sans-serif, system-ui, sans-serif;font-size:17px;"> generates contextual embeddings for each token in candidate and reference texts. It computes cosine similarity between each candidate token and the most similar reference token, and then aggregates these scores to calculate embedding-based precision (how well candidate tokens are supported by reference), recall (how well reference tokens are captured by candidate), and F1 score (the harmonic mean of precision and recall).</span></p><p class="paragraph" style="text-align:left;"><span style="color:#000000;font-family:Roboto, ui-sans-serif, system-ui, sans-serif;font-size:17px;">The advantage of this is that it recognizes paraphrases and synonyms as high-quality matches, instead of looking for literal words. The main limitation is that if there&#39;s a one-word difference you&#39;ll still get a high score, even if that word is “no” and it changes the meaning of a sentence as a whole.</span></p><h2 class="heading" style="text-align:left;" id="ll-masa-judge-evaluation">LLM-as-a-Judge Evaluation</h2><p class="paragraph" style="text-align:left;">Ideally you&#39;d have reference outputs for everything. However, in some use cases such as very open-ended tasks like creative writing, or when the input space is very large like in multi-turn conversations, a single golden answer can&#39;t possibly cover everything that a “good” answer should have. In those situations human evaluation would be the best, if it wasn&#39;t so slow and expensive. So we tend to prefer LLM-as-a Judge evaluations.</p><p class="paragraph" style="text-align:left;">The idea is to leverage the contextual understanding capabilities of modern LLMs to perform nuanced qualitative assessments. This LLM will evaluate outputs where “good” is subjective and depends on many factors such as helpfulness, coherence, creativity, or adherence to a certain tone.</p><p class="paragraph" style="text-align:left;">However, you can&#39;t just send a prompt saying “Tell me if this answer is good”. You need your Judge prompt to include:</p><ul><li><p class="paragraph" style="text-align:left;">The original prompt that produced the output you&#39;re evaluating</p></li><li><p class="paragraph" style="text-align:left;">The output you&#39;re evaluating</p></li><li><p class="paragraph" style="text-align:left;">The criteria that output should be evaluated on, including the scale (I recommend binary YES-NO, 1 to 3, or 1 to 5)</p></li><li><p class="paragraph" style="text-align:left;">Examples on what each number on the scale means. This is, an output that would be scored 1, an output that would be scored 2, and an output that would be scored 3. You shouldn&#39;t use more than 3 to 5 examples, even if you&#39;re evaluating multiple criteria</p></li></ul><p class="paragraph" style="text-align:left;">The output of the Judge should include for each criterion:</p><ul><li><p class="paragraph" style="text-align:left;">The name of the criterion, e.g. clarity, completeness, etc</p></li><li><p class="paragraph" style="text-align:left;">The score</p></li><li><p class="paragraph" style="text-align:left;">The reasoning</p></li></ul><p class="paragraph" style="text-align:left;">That output should be in JSON, so you can parse it programmatically and calculate metrics like number of evals failed, and use that to fail CI pipelines and create reports just like you do for tests.</p><p class="paragraph" style="text-align:left;">Here&#39;s an example of a Judge prompt:</p><div class="codeblock"><pre><code>Evaluate whether a customer service response provides actionable guidance.

CRITERION

Actionability: Does the response provide clear, specific steps the customer can immediately take?

- YES: Includes concrete actions with sufficient detail to execute
- NO: Remains vague, theoretical, or lacks practical guidance

EVALUATION PROCESS

Think step-by-step:
1. Identify what action the customer needs to take
2. Check if the response provides specific, executable steps
3. Note any vague language (&quot;soon&quot;, &quot;should&quot;, &quot;might&quot;) that reduces actionability
4. Render your verdict with supporting evidence

OUTPUT FORMAT (valid JSON; score must be &quot;YES&quot; or &quot;NO&quot;)

&#123;
  &quot;score&quot;: &quot;YES&quot;,
  &quot;reasoning&quot;: &quot;Brief explanation with specific evidence&quot;
&#125;

CALIBRATION EXAMPLES

Example 1 - YES:
- Query: &quot;How do I reset my password?&quot;
- Response: &quot;Click &#39;Forgot Password&#39; on the login page, enter your email (john@example.com), then check your inbox for a reset link valid for 2 hours.&quot;
- Evaluation: `&#123;&quot;score&quot;: &quot;YES&quot;, &quot;reasoning&quot;: &quot;Provides 3 specific steps with concrete details (button name, which email, time limit)&quot;&#125;`

Example 2 - NO:
- Query: &quot;How do I reset my password?&quot;  
- Response: &quot;You can reset your password through our account recovery process. Let me know if you need help!&quot;
- Evaluation: `&#123;&quot;score&quot;: &quot;NO&quot;, &quot;reasoning&quot;: &quot;Says what is possible but provides no steps on how to actually do it&quot;&#125;`

---

NOW EVALUATE

Customer Query: &#123;CUSTOMER_MESSAGE&#125;

Response: &#123;RESPONSE&#125;

Your Evaluation:</code></pre></div><p class="paragraph" style="text-align:left;">You&#39;re going to use that Judge prompt to evaluate Actionability. Once you have it, you&#39;ll need to collect several relevant CUSTOMER_MESSAGE and RESPONSE pairs, and those are going to be your test cases of sorts. But before you&#39;re ready to run these, you need to make sure the Judge agrees with you on your definition of “good”.</p><h3 class="heading" style="text-align:left;" id="calibrating-the-judge">Calibrating the Judge</h3><p class="paragraph" style="text-align:left;">LLM-as-a-Judge evaluation mainly consists of three steps (which you repeat for each evaluation criterion):</p><ol start="1"><li><p class="paragraph" style="text-align:left;">Figuring out what a “good” answer looks like, to the point where you can look at several answers and reliably answer whether they&#39;re good or not</p></li><li><p class="paragraph" style="text-align:left;">Collecting sufficient queries and responses to cover many use cases relevant to your application</p></li><li><p class="paragraph" style="text-align:left;">Creating a Judge prompt that gives to those responses the same score that you would give them (this is calibrating the Judge)</p></li></ol><p class="paragraph" style="text-align:left;">That&#39;s why you want that “reasoning” field in the response. You need to make sure the Judge&#39;s score agrees with your score. You&#39;ll find that the main limitation to this isn&#39;t really in how you write the Judge prompt, but rather in your own understanding of why you would pick a certain score, and overall what a “good” answer looks like.</p><h2 class="heading" style="text-align:left;" id="llm-evaluation-frameworks-and-tools">LLM Evaluation Frameworks and Tools</h2><p class="paragraph" style="text-align:left;"><a class="link" href="https://aws.amazon.com/bedrock/evaluations/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=the-essential-guide-to-llm-evaluations" target="_blank" rel="noopener noreferrer nofollow" style="color: rgb(100 61 212/var(--tw-text-opacity))">Amazon Bedrock Evaluations</a> is AWS&#39;s option for evals. You can use Amazon Bedrock directly to generate the outputs and evaluate them, or you can <a class="link" href="https://aws.amazon.com/blogs/machine-learning/use-custom-metrics-to-evaluate-your-generative-ai-application-with-amazon-bedrock/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=the-essential-guide-to-llm-evaluations" target="_blank" rel="noopener noreferrer nofollow" style="color: rgb(100 61 212/var(--tw-text-opacity))">Bring Your Own Inference</a>, importing a JSONL file with your prompts and outputs, and letting Amazon Bedrock run automated metrics and LLM-as-a-Judge evaluations.</p><p class="paragraph" style="text-align:left;">Another good option is <a class="link" href="https://github.com/langchain-ai/openevals?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=the-essential-guide-to-llm-evaluations" target="_blank" rel="noopener noreferrer nofollow">LangChain&#39;s OpenEvals</a>, especially if you&#39;re using LangChain already. Though if you&#39;re using Python I prefer <a class="link" href="https://github.com/confident-ai/deepeval?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=the-essential-guide-to-llm-evaluations" target="_blank" rel="noopener noreferrer nofollow">DeepEval</a>, which lets you treat evals like unit tests (they&#39;re more akin to functional tests in my opinion, but that&#39;s besides the point, they&#39;re definitely tests). And if you like TypeScript, I&#39;ve used <a class="link" href="https://github.com/promptfoo/promptfoo?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=the-essential-guide-to-llm-evaluations" target="_blank" rel="noopener noreferrer nofollow">Promptfoo</a> with a couple of customers and it works very well. There are more tools around though. Treat them like you&#39;d treat a unit tests framework: try a few if you want, pick one, and mainly focus on the tests themselves (or the evals themselves in this case) instead of on the framework.</p><h2 class="heading" style="text-align:left;" id="evals-in-the-development-lifecycle">Evals in the Development Lifecycle</h2><p class="paragraph" style="text-align:left;">Evals are tests for LLMs. That&#39;s the role they play in Generative AI applications. And like tests, the real value isn&#39;t in using them once to test one single version, but in running them every single time you make a change, giving you confidence that you&#39;re not inadvertently breaking something.</p><p class="paragraph" style="text-align:left;">The difference between evals and tests (besides evals being a lot harder to write) is that you shouldn&#39;t aim for a perfect score with evals. If you get a perfect score, it&#39;s likely that your evals are not testing some edge cases.</p><p class="paragraph" style="text-align:left;">Instead of aiming for a perfect score, what you need to do is define a threshold of acceptable quality. You can set for each metric a minimum value that all evals must meet, e.g. a minimum score of 4 in a 1-5 scale, plus a threshold for the average score across all evals in that metric, such as 4.5/5. This gives you a reliable quality gate that lets you catch regressions.</p><p class="paragraph" style="text-align:left;">Moreover, it gives you room to improve upon your prompts. You can calculate your evals scores for the current version of a prompt, make some changes, and run the evals suite again. Comparing the score will tell you if the change produces better, worse, or equivalent results. You can use this to improve your evals scores, to deal with specific edge cases without regressing on other use cases, and even to try to reduce your prompt without losing quality.</p><p class="paragraph" style="text-align:left;">You should also use evals to evaluate models. Everyone knows Claude Opus 4.5 is “better” than Claude Sonnet 4.5, though it&#39;s also more expensive. With a good evals suite you can get a score for both models and determine precisely how much better Opus is for your specific application, and make an informed decision based on price and actual performance for you, not on a generic and probably gamed benchmark. Again, use this to improve the scores, or to find the cheapest model that gives you acceptable scores.</p><p class="paragraph" style="text-align:left;">Of course, don&#39;t (or don&#39;t just) do this in your computer, as an experiment. To be effective at catching regressions, evals should be a quality gate in your CI pipeline, just like normal tests are. Pick a threshold, and commit to never going below that level of quality. If a change pulls you down, it&#39;s a regression and it should be considered equivalent to introducing a bug.</p><h2 class="heading" style="text-align:left;" id="conclusion">Conclusion</h2><p class="paragraph" style="text-align:left;">The LLM is part of your Generative AI application, and <b>evals are the only tests you can write for it</b>. Moreover, as you write your evals you&#39;ll find that defining what “good” means helps you improve your application significantly, at least in my experience.</p><p class="paragraph" style="text-align:left;">Treat evals like tests, use them to catch regressions, and commit to a certain level of quality, failing your CI if you go below it. Vibes are fine to get started, but if you want to build serious software, make quality measurable in a repeatable way. And evaluate it. With evals.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>AWS Frontier Agents in Practice: Kiro, DevOps Agent, and Security Agent</title>
  <description>A December 2025 look at Kiro, AWS DevOps Agent and Security Agent, with IAM boundaries, MCP tools, audit trails and adoption controls.</description>
      <enclosure url="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/36c30e4c-7de8-4fb5-84a3-5cb9b5dd9b71/Gemini_Generated_Image_m3ln6mm3ln6mm3ln.png" length="2649878" type="image/png"/>
  <link>https://newsletter.simpleaws.dev/p/aws-frontier-agents-kiro-devops-agent-security-agent</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/aws-frontier-agents-kiro-devops-agent-security-agent</guid>
  <pubDate>Fri, 19 Dec 2025 18:00:05 +0000</pubDate>
  <atom:published>2025-12-19T18:00:05Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Artificial Intelligence]]></category>
    <category><![CDATA[Security]]></category>
    <category><![CDATA[Operations]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;"><a class="link" href="https://aws.amazon.com/about-aws/whats-new/2025/12/introducing-frontier-agents-preview/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow">Frontier Agents</a> are the first AWS-native developer tools that don’t just <i>recommend</i> actions, they can <i>carry them out</i> continuously, at <a class="link" href="https://aws.amazon.com/blogs/aws/aws-reinvent-2025-ai-agents-roundup/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow">“real operational speed”</a>. This can feel like magic if you&#39;ve never built an agent before, but if you look under the hood you&#39;ll see it’s “just” identity, tooling, and auditability patterns. Even so, the outcomes you can get from these agents are pretty impressive.</p><p class="paragraph" style="text-align:left;">I know, these are not the first software agents ever. I mean, we&#39;re all using Claude Code at this point, right? Still, I wanted to focus on how agents in general (again, not just AWS&#39;s new agents) change how we think and build. The question is no longer “Is this code change correct?”, and instead it becomes “What is the agent allowed to touch, how do we constrain it, and how do we prove what happened afterward?”</p><p class="paragraph" style="text-align:left;">This article breaks down AWS’s Frontier Agents (Kiro, AWS DevOps Agent, and AWS Security Agent) as operational primitives: where they run, how they act, what new threat surfaces they create, and which guardrails actually hold up under incident pressure.</p><p class="paragraph" style="text-align:left;">And it&#39;s my first article of many about re:Invent 2025, because we had a lot of great announcements! This article reflects the December 2025 launch context; product interfaces and capabilities have since evolved.</p><h2 class="heading" style="text-align:left;" id="why-frontier-agents-matter-and-why-">Why “Frontier Agents” matter, and why you should be skeptical</h2><p class="paragraph" style="text-align:left;">AWS introduced “<a class="link" href="https://aws.amazon.com/about-aws/whats-new/2025/12/introducing-frontier-agents-preview/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow">Frontier Agents</a>” as a category at re:Invent 2025, and launched Kiro Autonomous Agent, AWS DevOps Agent, and AWS Security Agent as early examples. They&#39;re positioning them not as “AI in the console” like we saw at re:Invent 2024 with Q, AI in CloudWatch Logs queries, etc, but instead as “delegated execution”:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Agents turn intent into action</b>, like writing code, changing infrastructure, running operational workflows, rather than producing a human-readable checklist.</p></li><li><p class="paragraph" style="text-align:left;"><b>Agents are stateful over time</b>, especially in always-on operational roles, which changes both reliability expectations and failure blast radius.</p></li><li><p class="paragraph" style="text-align:left;"><b>Agents widen the boundary of “software supply chain”</b> to include prompts, tool I/O, agent journals/transcripts, and any protocol used to connect tools. </p></li></ul><p class="paragraph" style="text-align:left;">The main problem with these agents is not just that AI hallucinates, that&#39;s a well known issue. The problem is that they don&#39;t just redefine the good things, but also:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>The failure modes are qualitatively different.</b> A wrong suggestion wastes time, a wrong action changes state, possibly in a destructive way.</p></li><li><p class="paragraph" style="text-align:left;"><b>The audit and accountability model is often under-specified at launch</b>, and operational controls and evidence trails are typically the last pieces that get to a mature state.</p></li><li><p class="paragraph" style="text-align:left;"><b>Most orgs are not staffed to review agent actions at the speed agents can operate</b>, which usually leads to people not dedicating enough time to reviews. Your guardrails need to be preventative (policy), not purely detective (review after the fact).</p></li></ol><p class="paragraph" style="text-align:left;">My advice: adopt Frontier Agents when you can constrain them like production automation, and wait when you’d be forced to treat them like a superuser with a chat window.</p><h2 class="heading" style="text-align:left;" id="understanding-aws-frontier-agents">Understanding AWS Frontier Agents</h2><p class="paragraph" style="text-align:left;">The fastest way to reason about agent risk is to classify each agent by three properties:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Execution venue:</b> IDE sandbox, SaaS control plane, your AWS accounts, your CI runners, etc.</p></li><li><p class="paragraph" style="text-align:left;"><b>Tool plane:</b> what APIs/commands it can invoke (native tools vs an extensibility protocol like MCP).</p></li><li><p class="paragraph" style="text-align:left;"><b>Accountability surface:</b> what logs, diffs, and traces exist when something goes wrong.</p></li></ol><p class="paragraph" style="text-align:left;">Let&#39;s take a look at each of AWS&#39;s Frontier Agents considering these properties.</p><h3 class="heading" style="text-align:left;" id="kiro-autonomous-agent">Kiro Autonomous Agent</h3><p class="paragraph" style="text-align:left;">Kiro is two things with the same brand name: <a class="link" href="https://kiro.dev/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow"><b>Kiro IDE</b></a> is the VS Code-based desktop client you install and use day-to-day, and <a class="link" href="https://kiro.dev/docs/autonomous-agent/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow"><b>Kiro Autonomous Agent</b></a> is the remote agent runtime that does the long-running work. You access the autonomous agent through the IDE’s chat/task UI (and, in some workflows, via repo events like an issue comment/label that triggers work). The IDE is where you give a goal, inspect diffs, approve/stop actions, and see progress. The autonomous agent is where planning/execution happens, in an isolated sandbox that can clone repos, run builds/tests, and produce branches and PRs.</p><p class="paragraph" style="text-align:left;">Operationally, you hand Kiro a “task-shaped” problem such as “implement feature X”, “upgrade dependency Y across these services”, or “fix these failing tests”, and the autonomous agent decomposes it into subtasks, pulls the relevant repo context, executes commands and tests in its sandbox, and returns its work as normal engineering artifacts (commits, PRs, explanations, test outputs). Then you review what it produced the same way you’d review a teammate’s PR.</p><p class="paragraph" style="text-align:left;">Kiro Autonomous Agent explicitly frames autonomy as happening in an <b>isolated agent sandbox</b> (per task) and discusses sandbox controls like limiting internet access by domain allowlists. That’s good news, but you still need to treat “sandbox” as a security boundary you verify, not a talisman you trust blindly.</p><p class="paragraph" style="text-align:left;">Kiro Autonomous Agent IMO seems to fit best in accelerating code changes with a PR-first workflow where you already have review gates, tests, and policy checks (because those become your safety net).</p><p class="paragraph" style="text-align:left;">As a side note, I personally think the Kiro IDE is very good in general, especially because it supports spec-driven development, where you first design the solution based on specs, and only then let AI build it. I believe this is how everyone should be using AI coding assistants, and I love that Kiro is designed from the ground up to support this workflow.</p><h3 class="heading" style="text-align:left;" id="aws-dev-ops-agent">AWS DevOps Agent</h3><p class="paragraph" style="text-align:left;">The <a class="link" href="https://docs.aws.amazon.com/devopsagent/latest/userguide/about-aws-devops-agent.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow">AWS DevOps Agent</a> is a managed service, configured from the AWS Management Console around a construct called an <b>Agent Space</b>, which is something like the slice of your estate and toolchain that the agent is allowed to reason over. In that Agent Space you wire up the data sources and collaboration surfaces needed for incident response: CloudWatch metrics and logs, other AWS resource context via an IAM role you define, optional third-party observability tools like Datadog, Dynatrace, New Relic, Splunk, etc, and operations tools like PagerDuty, ServiceNow, Slack, Teams (hopefully not Teams!! I hate it), plus CI/CD and source control signals so it can correlate issues with deploys.</p><p class="paragraph" style="text-align:left;">You then access the DevOps Agent in two places: the AWS console for setup and governance, and a DevOps Agent “operator” web application for day-to-day incident interactions, where on-call engineers watch investigations, ask questions, and review the agent’s evidence trail.</p><p class="paragraph" style="text-align:left;">The idea is that the DevOps Agent gets wired into the same event triggers and comms paths your on-call process already uses (alarms/incidents in, investigation updates out). An alert can trigger an investigation automatically, or you can start one manually in the operator UI, and the agent then pulls metrics, logs, traces and deploy context, builds a working hypothesis, and posts status and findings back into your incident workflow. For example, it can post Slack updates, ticket notes, a timeline, and a supporting “journal” of what it looked at. The key detail is that you don’t run a CLI to “ask it about production” in the abstract. You give it an incident anchor (alarm, incident, timestamp, scope), it does the correlation work across the integrated systems, and you consume the output in its operator web UI and in your comms or ticketing tools.</p><p class="paragraph" style="text-align:left;">A concrete example of how DevOps Agent goes beyond a simple chatbot: It can associate cloud resources with deployments and track deployment details for artifacts like CloudFormation templates, AWS CDK apps, Amazon ECR images, and Terraform configurations. There are also integration points for tracking deployment artifacts in GitHub and GitLab.</p><p class="paragraph" style="text-align:left;">DevOps Agent then becomes a consumer of your operational state (deployment metadata, system state), and potentially a driver of operational actions. Meaning it&#39;ll be constantly reviewing your infrastructure, and potentially (well, ideally!) making changes to it (which ideally are always correct).</p><p class="paragraph" style="text-align:left;">My advice: Treat it like production automation with a continuous control loop.</p><p class="paragraph" style="text-align:left;">Note that an AI agent controlling your AWS infrastructure is not something completely new. You&#39;ve been able to do this since AWS introduced its <a class="link" href="https://docs.aws.amazon.com/aws-mcp/latest/userguide/understanding-mcp-server-tools.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow">MCP server</a> with the capability of sending AWS API commands. In fact, my boss uses this to periodically review the company&#39;s AWS spend.</p><p class="paragraph" style="text-align:left;">You can do this from the web chats like ChatGPT or Claude.ai, or from your terminal using Claude Code, Codex CLI, etc. And if you can do it from your terminal, you can write a script around it and put it in a loop. What&#39;s actually new from the AWS DevOps Agent is that it&#39;s explicitly designed to be long running, and to not rely on luck and the length of its context window to remember what your cloudy things are and what it&#39;s supposed to do with them.</p><h3 class="heading" style="text-align:left;" id="aws-security-agent">AWS Security Agent</h3><p class="paragraph" style="text-align:left;"><a class="link" href="https://docs.aws.amazon.com/securityagent/latest/userguide/what-is-aws-security-agent.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow">AWS Security Agent</a> is also a managed service you enable and configure from the AWS Management Console, and you use through a dedicated Security Agent web application UI (plus your existing developer workflow tools). Conceptually, you create one or more <b>Agent Spaces</b> that represent an application or security scope, then you attach the agent to the inputs it needs: your security requirements (AWS-managed plus your custom rules), your design artifacts (uploaded docs or referenced materials, depending on what you provide), and your code repositories (commonly via a GitHub integration for PR-aware reviews).</p><p class="paragraph" style="text-align:left;">Once set up, Security Agents operates in three primary modes: <b>design reviews</b> let you start a review from a web UI and the agent produces a structured set of findings mapped to requirements, <b>code reviews</b> work as comments on pull requests with issues and remediations based on diffs and context, and <b>penetration tests</b> run an attacker-style workflow against a verified target scope and produce validated findings and a report.</p><p class="paragraph" style="text-align:left;">AWS positions AWS Security Agent as a Frontier Agent oriented around building and operating applications “secure from the start,” including across AWS and broader environments. It&#39;s supposed to influence design, implementation, and remediation decisions, making your applications secure-by-design (because it&#39;s a security “person” participating in the design phase).</p><p class="paragraph" style="text-align:left;">Honestly, it sounds pretty great, and if you&#39;re a developer who only really does security as a best practice and knows enough to know that you know very little (that&#39;d be me, by the way). However, security-focused agents introduce a subtle risk: they can create <i>false confidence</i> when outputs look authoritative. And I&#39;m not talking about confident hallucinations which can be proven false, though there&#39;s that as well. I&#39;m talking about the agent missing things, just like an automated security scan or a human security person might miss things.</p><p class="paragraph" style="text-align:left;">Truth is, the only secure software is the one that doesn&#39;t exist.</p><p class="paragraph" style="text-align:left;">I believe your adoption criteria should include: “Can we validate its findings and actions with independent controls?” For example policy checks, scanners, reproducible queries, and human review where it matters. And overall, treat it as yet another tool that will make your applications more-secure-but-never-100%-secure-by-design.</p><h2 class="heading" style="text-align:left;" id="what-agentic-changes-in-your-aws-th">What “agentic” changes in your AWS threat model</h2><p class="paragraph" style="text-align:left;">Most teams already model risk around humans and CI/CD. Agents add new principals, new data, and new execution paths simultaneously. And that means new ways in which things can fail.</p><h3 class="heading" style="text-align:left;" id="agents-as-principals">Agents as Principals</h3><p class="paragraph" style="text-align:left;">If an agent can run tools, it effectively acts as a principal. Even when a tool call is mediated (PR-based flows, approval prompts), the reality is you are delegating action authority.</p><p class="paragraph" style="text-align:left;">Key identity questions you need answered before letting this anywhere near production:</p><ul><li><p class="paragraph" style="text-align:left;">What IAM roles are used (or assumed) for actions, and how tightly can they be scoped?</p></li><li><p class="paragraph" style="text-align:left;">Are credentials time-bounded and context-bounded (environment, repo, change ticket, incident ID)?</p></li><li><p class="paragraph" style="text-align:left;">Is there a clean break-glass workflow, and can we prove it was used appropriately?</p></li></ul><p class="paragraph" style="text-align:left;">If you cannot express an agent’s permissions as “least privilege + explicit escalation,” you are not adopting an agent, you are adopting a superuser. I don&#39;t think I need to tell you folks this, but I&#39;ll say it anyways: superusers are super risky.</p><h3 class="heading" style="text-align:left;" id="new-data-surfaces">New Data Surfaces</h3><p class="paragraph" style="text-align:left;">Agents don’t just “see code.” They see prompts, intermediate plans, tool outputs, logs, and sometimes incident artifacts. <a class="link" href="https://kiro.dev/docs/autonomous-agent/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow">Kiro’s documentation</a> explicitly discusses data protection and privacy considerations (including where data is stored and how it’s protected), which should be your starting point for a formal data classification review.</p><p class="paragraph" style="text-align:left;">Examples of sensitive agent-adjacent data you should treat as controlled:</p><ul><li><p class="paragraph" style="text-align:left;">Incident timelines and hypotheses (often more sensitive than the raw alerts).</p></li><li><p class="paragraph" style="text-align:left;">Stack traces and logs that embed credentials or customer data.</p></li><li><p class="paragraph" style="text-align:left;">Tool outputs that include resource ARNs, account topology, or network details.</p></li></ul><p class="paragraph" style="text-align:left;">You need to decide whether agent transcripts are retained, where they’re stored, who can access them, and how long they live. Basically, now your chat history becomes an audit record.</p><h3 class="heading" style="text-align:left;" id="new-execution-surfaces">New Execution Surfaces</h3><p class="paragraph" style="text-align:left;">Agents are most dangerous when connected to mutation-capable tools: IaC apply, IAM writes, KMS key policy changes, SG/NACL edits, data deletion, or anything that can modify anyone&#39;s access. If you integrate agents through a tool protocol like MCP, you’ve created an execution backplane that must be secured like an internal platform API.</p><p class="paragraph" style="text-align:left;">The key takeaway is that the most critical risks introduced by agents are not really “bad reasoning” or hallucinations, no matter how much people on LinkedIn complain about that. The new critical risks are about <b>good reasoning + too much authority</b>.</p><h2 class="heading" style="text-align:left;" id="guardrails-that-actually-work">Guardrails That Actually Work</h2><p class="paragraph" style="text-align:left;">Guardrails should be designed assuming agents will occasionally be wrong, overly confident, or operating with incomplete context. Let&#39;s look at a few ways to safeguard yourself and limit what Frontier Agents can do.</p><h3 class="heading" style="text-align:left;" id="identity-boundaries-permission-boun">Identity Boundaries: Permission boundaries, SCPs, and “break-glass” workflows</h3><p class="paragraph" style="text-align:left;">Start with a hard rule: <i>agent roles should not be able to grant themselves more power</i>. That means explicit denies around IAM writes, Organizations policy edits, and access-key style credential creation.</p><p class="paragraph" style="text-align:left;">A practical pattern is an <b>IAM permission boundary</b> applied to any role the agent can assume (or any role used by automation it triggers). The following are example deny statements for a permissions boundary, not a complete boundary policy. A usable boundary must also explicitly allow the intended actions; a deny-only boundary does not permit identity-based actions.</p><div class="codeblock"><pre><code>&#123;
  &quot;Version&quot;: &quot;2012-10-17&quot;,
  &quot;Statement&quot;: [
    &#123;
      &quot;Sid&quot;: &quot;DenyPrivilegeEscalationPaths&quot;,
      &quot;Effect&quot;: &quot;Deny&quot;,
      &quot;Action&quot;: [
        &quot;iam:*&quot;,
        &quot;organizations:*&quot;,
        &quot;account:*&quot;
      ],
      &quot;Resource&quot;: &quot;*&quot;
    &#125;,
    &#123;
      &quot;Sid&quot;: &quot;DenyKMSAndLoggingDestruction&quot;,
      &quot;Effect&quot;: &quot;Deny&quot;,
      &quot;Action&quot;: [
        &quot;kms:ScheduleKeyDeletion&quot;,
        &quot;kms:DisableKey&quot;,
        &quot;kms:PutKeyPolicy&quot;,
        &quot;cloudtrail:StopLogging&quot;,
        &quot;cloudtrail:DeleteTrail&quot;,
        &quot;logs:DeleteLogGroup&quot;,
        &quot;logs:DeleteLogStream&quot;
      ],
      &quot;Resource&quot;: &quot;*&quot;
    &#125;,
    &#123;
      &quot;Sid&quot;: &quot;DenyOutsideApprovedRegions&quot;,
      &quot;Effect&quot;: &quot;Deny&quot;,
      &quot;Action&quot;: &quot;*&quot;,
      &quot;Resource&quot;: &quot;*&quot;,
      &quot;Condition&quot;: &#123;
        &quot;StringNotEquals&quot;: &#123;
          &quot;aws:RequestedRegion&quot;: [
            &quot;us-east-1&quot;,
            &quot;us-west-2&quot;
          ]
        &#125;
      &#125;
    &#125;,
    &#123;
      &quot;Sid&quot;: &quot;AllowOnlyTaggedResourcesForMutation&quot;,
      &quot;Effect&quot;: &quot;Deny&quot;,
      &quot;Action&quot;: [
        &quot;ec2:AuthorizeSecurityGroupIngress&quot;,
        &quot;ec2:AuthorizeSecurityGroupEgress&quot;,
        &quot;ec2:RevokeSecurityGroupIngress&quot;,
        &quot;ec2:RevokeSecurityGroupEgress&quot;,
        &quot;ssm:SendCommand&quot;,
        &quot;eks:UpdateClusterConfig&quot;,
        &quot;rds:ModifyDBInstance&quot;
      ],
      &quot;Resource&quot;: &quot;*&quot;,
      &quot;Condition&quot;: &#123;
        &quot;StringNotEquals&quot;: &#123;
          &quot;aws:ResourceTag/AgentManaged&quot;: &quot;true&quot;
        &#125;
      &#125;
    &#125;
  ]
&#125;
</code></pre></div><p class="paragraph" style="text-align:left;">The idea is to incorporate these denies into a complete, reviewed permissions boundary for <i>any</i> role an agent can use. You can also use <a class="link" href="https://newsletter.simpleaws.dev/p/aws-organizations-multiple-aws-accounts?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow">Service Control Policies (SCPs)</a> to limit what actions can be taken in an account.</p><p class="paragraph" style="text-align:left;">If you require an escalation, it should be done via a ticket or a manual approval step rather than letting the agent self-escalate. Plus, you can create a separate “break-glass” role that is human-only, MFA-required, and heavily alarmed.</p><h3 class="heading" style="text-align:left;" id="network-boundaries-sandbox-egress-c">Network Boundaries: Sandbox egress control + VPC endpoints</h3><p class="paragraph" style="text-align:left;">Network control is underrated for agents. If an agent can call arbitrary internet endpoints, it can exfiltrate data or pull untrusted code and execute it.</p><p class="paragraph" style="text-align:left;"><a class="link" href="https://kiro.dev/docs/autonomous-agent/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow">Kiro’s agent sandbox documentation</a> explicitly discusses controlling internet access by restricting which domains the agent can reach. That’s exactly the right kind of control you need, because it reduces the risk without relying on the model to behave.</p><p class="paragraph" style="text-align:left;">For tools on your AWS account, the analogous pattern is:</p><ul><li><p class="paragraph" style="text-align:left;">Prefer private connectivity to AWS APIs using VPC endpoints.</p></li><li><p class="paragraph" style="text-align:left;">Force outbound through egress controls where you can log and restrict destinations.</p></li><li><p class="paragraph" style="text-align:left;">Treat “tool servers” (including MCP servers) as Tier 0 assets and isolate them accordingly.</p></li></ul><p class="paragraph" style="text-align:left;">Note: <a class="link" href="https://www.oneidentity.com/learn/what-is-tier-zero.aspx?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow">Tier 0 assets</a> are the things that, if breached, can lead to catastrophic consequences such as total system takeover.</p><h3 class="heading" style="text-align:left;" id="change-boundaries-p-rbased-workflow">Change Boundaries: PR-based workflows and policy-as-code gates</h3><p class="paragraph" style="text-align:left;">The most reliable human-in-the-loop mechanism is still the one your org already understands: pull requests, code owners, and automated checks.</p><p class="paragraph" style="text-align:left;">Kiro’s GitHub flow is naturally PR-oriented (branches, commits, PRs). That means you can apply the same guardrails you apply to humans: required reviews, required checks, and policy-as-code.</p><p class="paragraph" style="text-align:left;">This doesn’t “solve agents” or anything like that, but it does solve unsafe mutations (unguarded <code>terraform apply</code>, etc). Agents make unsafe mutations easier to trigger (both because of hallucinations and corruption), so these guardrails become even more important.</p><h3 class="heading" style="text-align:left;" id="observability-boundaries-treat-agen">Observability Boundaries: Treat agents like production services</h3><p class="paragraph" style="text-align:left;">Your detection posture should assume:</p><ul><li><p class="paragraph" style="text-align:left;">Agents will run many small actions.</p></li><li><p class="paragraph" style="text-align:left;">Some actions will fail due to permissions, throttling, missing context, or partial state.</p></li><li><p class="paragraph" style="text-align:left;">You will need to reconstruct “who did what and why” under time pressure.</p></li></ul><p class="paragraph" style="text-align:left;">If you already use CloudTrail Lake, you can query API activity by role/session to build a defensible audit trail. If your agent platform provides an internal journal/transcript, treat that as an auditable artifact and retain it under the same governance as CI logs and change tickets.</p><p class="paragraph" style="text-align:left;">Basically treat an agent&#39;s actions as a principal&#39;s actions: Least privilege so out of scope things can&#39;t break, reviews when something may break, and an audit trail <span style="text-decoration:line-through;">in case</span> for when something does break.</p><h2 class="heading" style="text-align:left;" id="mcp-as-the-tool-plane">MCP as the Tool Plane</h2><p class="paragraph" style="text-align:left;">Tool protocols are where agents become real systems. <a class="link" href="https://modelcontextprotocol.io/specification/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow">MCP (Model Context Protocol)</a> is one such protocol: a standard way for a model/agent to connect to external tools and data sources through a client-server architecture.</p><p class="paragraph" style="text-align:left;">MCP is going to act as the infrastructure to grant agents access to both AWS resources and the broader ecosystem, and that means you should evaluate it like any other integration surface.</p><h3 class="heading" style="text-align:left;" id="how-mcp-works">How MCP Works</h3><p class="paragraph" style="text-align:left;">MCP defines how agents and LLMs discover tools, invoke them, and receive structured responses (rather than scraping text). That means if we give an agent access to a powerful tool via MCP, the agent gains power. Which is of course very desirable, we definitely want the agent to have the power to take the actions it needs to take to achieve the outcomes we expect of it. But it&#39;s also risky.</p><p class="paragraph" style="text-align:left;">Here&#39;s a simplified mental model for MCP, leaving out transport details:</p><div class="codeblock"><pre><code>+--------------------+       MCP        +-------------------+
| Agent / LLM Client | &lt;--------------&gt; | MCP Server        |
| (Kiro / custom)    |                  | (tool adapter)    |
+--------------------+                  +-------------------+
         |                                        |
         | &quot;call tool: deploy_service&quot;            | executes
         v                                        v
  tool request/response               AWS APIs / CI / DB / etc
</code></pre></div><h3 class="heading" style="text-align:left;" id="tool-design-patterns-idempotency-dr">Tool Design Patterns: Idempotency, dry-runs, and reversible changes</h3><p class="paragraph" style="text-align:left;">When you expose tools to agents, design them the way you design safe automation:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Idempotency:</b> repeated calls should not create repeated side effects.</p></li><li><p class="paragraph" style="text-align:left;"><b>Dry-run first:</b> expose a “plan” operation that returns what would change, then require a separate “apply”.</p></li><li><p class="paragraph" style="text-align:left;"><b>Reversible changes:</b> build “undo” primitives where possible, or at least generate a rollback plan.</p></li><li><p class="paragraph" style="text-align:left;"><b>Narrow contracts:</b> prefer “rotate credentials for service X” over “run arbitrary CLI”.</p></li></ul><h3 class="heading" style="text-align:left;" id="securing-mcp-servers">Securing MCP Servers</h3><p class="paragraph" style="text-align:left;">MCP servers become part of your trusted computing base. The minimum control set should include:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Strong authentication:</b> who is the client?</p></li><li><p class="paragraph" style="text-align:left;"><b>Authorization:</b> which tool calls are allowed for this client/context?</p></li><li><p class="paragraph" style="text-align:left;"><b>Input/output validation:</b> reject weird or oversized payloads, and enforce schemas</p></li><li><p class="paragraph" style="text-align:left;"><b>Rate limiting and timeouts:</b> so the agent can’t accidentally DoS your own control plane</p></li><li><p class="paragraph" style="text-align:left;"><b>Audit logging:</b> requests, responses, correlation IDs, caller identity</p></li></ul><p class="paragraph" style="text-align:left;"><a class="link" href="https://github.com/awslabs/mcp?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-frontier-agents-in-practice-kiro-devops-agent-and-security-agent" target="_blank" rel="noopener noreferrer nofollow">MCP’s ecosystem</a> includes reference implementations and server examples (including open-source MCP servers) that are useful for learning, but you still need to harden them before production use.</p><h2 class="heading" style="text-align:left;" id="conclusion">Conclusion</h2><p class="paragraph" style="text-align:left;">I believe Frontier Agents sound amazing, so long as you can realistically put in place the necessary controls for them. Don&#39;t just grant them broad permissions and hope nothing goes wrong, because, as I&#39;ve said in the past, <b>hope is not a strategy</b>. If your org is mature enough that you either already have or can put in place the necessary guardrails, Frontier Agents have a very decent chance of helping you reduce toil and making your humans even more amazing. Otherwise, I&#39;d focus on org maturity first.</p><p class="paragraph" style="text-align:left;">Good fits:</p><ul><li><p class="paragraph" style="text-align:left;"><b>PR-first engineering cultures</b> where code review and checks already happen for every change.</p></li><li><p class="paragraph" style="text-align:left;"><b>Well-instrumented services</b> where diagnosis can be automated because signals are reliable.</p></li><li><p class="paragraph" style="text-align:left;"><b>Typed, repeatable runbooks</b> where the tool plane can be made narrow and safe.</p></li></ul><p class="paragraph" style="text-align:left;">Bad fits:</p><ul><li><p class="paragraph" style="text-align:left;"><b>High blast radius environments with weak change management:</b> agents will amplify the weakness, and sooner rather than later, everything will blow up 🧨.</p></li><li><p class="paragraph" style="text-align:left;"><b>Orgs without clear ownership boundaries:</b> agents will route around ambiguity in surprising (and very damaging) ways.</p></li><li><p class="paragraph" style="text-align:left;"><b>Tooling that relies on manual, tacit knowledge:</b> agents will appear helpful while missing the real constraints, and you&#39;ll be left figuring out why there&#39;s so much output with so little outcomes.</p></li></ul><p class="paragraph" style="text-align:left;">Of course, you&#39;re free to try these yourself. I fully encourage you to! Just be very mindful of how you define success. Measure outcomes, not output or vibes. Some example metrics you might want to keep an eye on:</p><ul><li><p class="paragraph" style="text-align:left;">Time-to-diagnosis / time-to-mitigation deltas for incidents the agent touches</p></li><li><p class="paragraph" style="text-align:left;">Change failure rate and rollback frequency for agent-assisted changes vs baseline</p></li><li><p class="paragraph" style="text-align:left;">Escalation rate: how often the agent needed human approval to proceed</p></li><li><p class="paragraph" style="text-align:left;">Audit completeness: can you reconstruct a full timeline from logs + diffs without asking people?</p></li></ul><p class="paragraph" style="text-align:left;">If the agent makes you faster but makes your audit story worse or ends up overcomplicating your system and/or its operation, you’re borrowing time at a high interest rate.</p><p class="paragraph" style="text-align:left;">My suggestion is that you start with <b>read-only diagnostics</b> and PR generation (Kiro-style workflows), so you can see how your existing guardrails are enforced. Then you can introduce <b>narrow, reversible tools</b> via MCP, but only after schemas, auth, and logging are in place. From there you can expand to selective “execute” actions where you can prove whether permissions are least-privilege, approvals are enforced, rollback is viable, and audit trails are complete.</p><p class="paragraph" style="text-align:left;">And as always, turn your curiosity into ownership.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>DynamoDB Database Design in 2026</title>
  <description>Design DynamoDB tables around access patterns using item collections, sparse GSIs, multi-attribute keys, and single-table or multi-table layouts.</description>
  <link>https://newsletter.simpleaws.dev/p/dynamodb-database-design-in-2026</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/dynamodb-database-design-in-2026</guid>
  <pubDate>Sat, 29 Nov 2025 22:12:03 +0000</pubDate>
  <atom:published>2025-11-29T22:12:03Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Architecture]]></category>
    <category><![CDATA[Databases]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">DynamoDB is not a relational database, and you can&#39;t design for it like one. Hopefully this isn&#39;t news to you, it must be the 12th time I say it. But I also understand this isn&#39;t super useful: “Don&#39;t design like a relational DB” doesn&#39;t really tell you what to do.</p><p class="paragraph" style="text-align:left;">In this guide, we&#39;re going deep into DynamoDB data modeling. We&#39;ll cover how to think about your data, how to structure it for the access patterns your application actually needs, and how recent features (like multi-attribute composite keys in GSIs and the updated guidance on single-table vs. multi-table design) can make your life easier. This isn&#39;t a &quot;Getting Started with DynamoDB&quot; tutorial, I&#39;ll be assuming you already know the basics. We&#39;re here to learn how to design your data properly.</p><h2 class="heading" style="text-align:left;" id="why-database-design-matters-more-in">Why Database Design Matters More in DynamoDB</h2><p class="paragraph" style="text-align:left;">With Postgres or MySQL, you can design your schema, normalize it, and then figure out your queries later. Sure, you might need to add an index here and there, but the database will figure out how to execute your query. It might not be fast, but it&#39;ll work, and you can almost always optimize later.</p><p class="paragraph" style="text-align:left;">DynamoDB doesn&#39;t work that way. There&#39;s no query optimizer that figures out how to satisfy your request. Either your data is structured to answer a specific question efficiently, or it&#39;s not. And if it&#39;s not, you&#39;re paying for a full table scan, which is the difference between milliseconds and minutes (and between reasonable costs and a terrifying AWS bill).</p><p class="paragraph" style="text-align:left;">So here&#39;s the fundamental difference: in relational databases, you design your schema first and figure out queries later. In DynamoDB, you design your schema <i>around</i> the queries you need to run. Access patterns come first, schema comes second.</p><h2 class="heading" style="text-align:left;" id="understanding-the-basics-of-dynamo-">Understanding the Basics of DynamoDB</h2><p class="paragraph" style="text-align:left;">Before we get into the design process, let&#39;s make sure we&#39;re on the same page about how DynamoDB stores and retrieves data. (If you already know this, feel free to skip ahead.)</p><h3 class="heading" style="text-align:left;" id="partition-keys-and-sort-keys">Partition Keys and Sort Keys</h3><p class="paragraph" style="text-align:left;">Every item in DynamoDB needs a primary key. This can be either:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Simple primary key</b>: Just a partition key (PK). Each item must have a unique partition key value.</p></li><li><p class="paragraph" style="text-align:left;"><b>Composite primary key</b>: A partition key (PK) plus a sort key (SK). The combination must be unique.</p></li></ul><p class="paragraph" style="text-align:left;">The partition key determines which physical partition your data lives on. DynamoDB hashes the partition key and uses that hash to route your request to the right server. This is how DynamoDB scales horizontally: it adds more partitions as your data grows. (You don&#39;t see this happening, which is kind of the whole point of a managed service.)</p><p class="paragraph" style="text-align:left;">The sort key, when present, determines the order of items within a partition. Items with the same partition key are stored together (this is called an <i>item collection</i>), sorted by the sort key. This co-location is incredibly useful because it lets you retrieve multiple related items with a single query.</p><h3 class="heading" style="text-align:left;" id="global-secondary-indexes-gs-is-and-">Global Secondary Indexes (GSIs) and Local Secondary Indexes (LSIs)</h3><p class="paragraph" style="text-align:left;">Sometimes you need to query your data in ways that your primary key doesn&#39;t support. That&#39;s where indexes come in.</p><p class="paragraph" style="text-align:left;"><b>Local Secondary Indexes</b> use the same partition key as your table but a different sort key. They must be created when you create the table, you can&#39;t add them later, and they share the partition&#39;s throughput capacity. LSIs are useful when you need an alternative sort order within the same partition.</p><p class="paragraph" style="text-align:left;"><b>Global Secondary Indexes</b> can have a completely different partition key and sort key from the base table, and you can create or delete them at any time. GSIs have their own throughput capacity, separate from the base table. They&#39;re more flexible but also more expensive: you&#39;re essentially duplicating your data into a new structure (and you&#39;re paying for the write operations to do that).</p><p class="paragraph" style="text-align:left;">Here&#39;s a recent development worth knowing: as of November 2025, DynamoDB supports <b>multi-attribute composite keys</b> in GSIs. Instead of concatenating values into synthetic keys like <code>TOURNAMENT#WINTER2024#REGION#NA-EAST</code>, you can now use up to four attributes each for the partition key and sort key. This makes your data model cleaner and eliminates the need to backfill synthetic keys when adding new indexes. More on this later.</p><h2 class="heading" style="text-align:left;" id="the-access-patterns-approach">The Access Patterns Approach</h2><p class="paragraph" style="text-align:left;">Here&#39;s how you should approach DynamoDB design:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Identify your entities.</b> What are the &quot;things&quot; your application deals with? Customers, orders, products, invoices, etc.</p></li><li><p class="paragraph" style="text-align:left;"><b>Map the relationships.</b> How do these entities relate to each other? One-to-many? Many-to-many?</p></li><li><p class="paragraph" style="text-align:left;"><b>List your access patterns.</b> This is the critical step. What questions does your application need to answer? Be specific. Not &quot;get orders&quot; but &quot;get all orders for a customer in the last 30 days.&quot;</p></li><li><p class="paragraph" style="text-align:left;"><b>Design your keys to satisfy those patterns.</b> This is where the actual schema design happens.</p></li><li><p class="paragraph" style="text-align:left;"><b>Add indexes for patterns your primary key can&#39;t handle.</b> But only if you actually need them.</p></li></ol><p class="paragraph" style="text-align:left;">If you want to visualize your design before writing code, <a class="link" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.settingup.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=dynamodb-database-design-in-2026" target="_blank" rel="noopener noreferrer nofollow">NoSQL Workbench</a> (free tool created by AWS) lets you model your table and test queries against sample data. I find it invaluable for validating designs before committing to them.</p><p class="paragraph" style="text-align:left;">Let&#39;s work through a real example. We&#39;re building an e-commerce application. Here are our entities:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Customers</b>: People who buy stuff</p></li><li><p class="paragraph" style="text-align:left;"><b>Products</b>: Stuff people buy</p></li><li><p class="paragraph" style="text-align:left;"><b>Orders</b>: When a customer buys stuff</p></li><li><p class="paragraph" style="text-align:left;"><b>Order Items</b>: The individual products within an order</p></li><li><p class="paragraph" style="text-align:left;"><b>Invoices</b>: Payment records for orders</p></li></ul><p class="paragraph" style="text-align:left;">And here are our access patterns:</p><ol start="1"><li><p class="paragraph" style="text-align:left;">Get customer by customerId</p></li><li><p class="paragraph" style="text-align:left;">Get product by productId</p></li><li><p class="paragraph" style="text-align:left;">Get order by orderId</p></li><li><p class="paragraph" style="text-align:left;">Get all products in an order</p></li><li><p class="paragraph" style="text-align:left;">Get the invoice for an order</p></li><li><p class="paragraph" style="text-align:left;">Get all orders containing a specific product (for a date range)</p></li><li><p class="paragraph" style="text-align:left;">Get an invoice by invoiceId</p></li><li><p class="paragraph" style="text-align:left;">Get all invoices for a customer (for a date range)</p></li><li><p class="paragraph" style="text-align:left;">Get all products ordered by a customer (for a date range)</p></li></ol><p class="paragraph" style="text-align:left;">Now we design our schema to satisfy these patterns.</p><h2 class="heading" style="text-align:left;" id="single-table-vs-multi-table-design">Single-Table vs. Multi-Table Design</h2><p class="paragraph" style="text-align:left;">Before we dive into the actual design, we need to talk about a philosophical question in DynamoDB: should you put everything in one table, or use multiple tables?</p><p class="paragraph" style="text-align:left;"><b>Single-table design</b> stores multiple entity types in the same table. You use generic attribute names like <code>PK</code> and <code>SK</code> for your keys (yes, you literally name your PK <code>PK</code>, and your SK <code>SK</code>), and an <code>EntityType</code> attribute to distinguish between customers, orders, products, etc. This approach was heavily promoted by AWS for years, because it lets you fetch related entities with a single query.</p><p class="paragraph" style="text-align:left;"><b>Multi-table design</b> is more like traditional database design: one table per entity type. It&#39;s simpler to understand and implement, especially if you&#39;re coming from a relational background.</p><p class="paragraph" style="text-align:left;">My recommendation is a hybrid: Single table per domain. Single-table design works well when you frequently need to query multiple entity types together, and multi-table design is fine when your access patterns don&#39;t require cross-entity queries. So my recommendation is to group in a single table entities that need to be queried together, and end up with a handful of tables, each of which can resolve the queries of a single domain (meaning a group of entities that exist together).</p><p class="paragraph" style="text-align:left;">For our e-commerce example, we&#39;re going to use single-table design because several of our access patterns need to fetch related entities together (like getting an order with all its items), and because it&#39;s simpler. In a larger application, we&#39;d end up with multiple “single” tables.</p><h2 class="heading" style="text-align:left;" id="designing-the-e-commerce-schema">Designing the E-Commerce Schema</h2><p class="paragraph" style="text-align:left;">Let&#39;s implement our access patterns one by one. I&#39;ll show you how the table evolves as we add support for each pattern.</p><h3 class="heading" style="text-align:left;" id="access-patterns-1-2-get-customer-or">Access Patterns 1 & 2: Get Customer or Product by ID</h3><p class="paragraph" style="text-align:left;">These are the simplest patterns. We store customers with <code>PK=c#&lt;customerId&gt;</code> and <code>SK=c#&lt;customerId&gt;</code>. Products get <code>PK=p#&lt;productId&gt;</code> and <code>SK=p#&lt;productId&gt;</code>.</p><p class="paragraph" style="text-align:left;">Why the prefixes? Because we&#39;re storing multiple entity types in the same table, and we need the keys to be unambiguous. Also, this makes it easy to identify the entity type at a glance.</p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">PK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">SK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">EntityType</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">Name</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">Email</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">c#12345</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">c#12345</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">customer</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">Alice</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">alice@example.com</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">c#67890</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">c#67890</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">customer</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">Bob</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">bob@example.com</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">p#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">p#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">product</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">Widget</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;"></p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">p#002</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">p#002</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">product</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">Gadget</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;"></p></td></tr></table></div><p class="paragraph" style="text-align:left;">To get a customer: <code>Query where PK = &quot;c#12345&quot; AND SK = &quot;c#12345&quot;</code></p><h3 class="heading" style="text-align:left;" id="access-patterns-3-4-get-order-and-i">Access Patterns 3 & 4: Get Order and Its Products</h3><p class="paragraph" style="text-align:left;">Here&#39;s where single-table design starts to shine. An order and its items share the same partition key (<code>o#&lt;orderId&gt;</code>), but they have different sort keys. The order itself has <code>SK=o#&lt;orderId&gt;</code>, while each order item has <code>SK=p#&lt;productId&gt;</code>.</p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="16%"><p class="paragraph" style="text-align:left;">PK</p></th><th class="bh__table_header" width="16%"><p class="paragraph" style="text-align:left;">SK</p></th><th class="bh__table_header" width="16%"><p class="paragraph" style="text-align:left;">EntityType</p></th><th class="bh__table_header" width="16%"><p class="paragraph" style="text-align:left;">CustomerId</p></th><th class="bh__table_header" width="16%"><p class="paragraph" style="text-align:left;">Quantity</p></th><th class="bh__table_header" width="16%"><p class="paragraph" style="text-align:left;">Price</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">order</p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">c#12345</p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;"></p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">p#001</p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">orderItem</p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">2</p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">29.99</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">p#002</p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">orderItem</p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">1</p></td><td class="bh__table_cell" width="16%"><p class="paragraph" style="text-align:left;">49.99</p></td></tr></table></div><p class="paragraph" style="text-align:left;">To get everything about an order (the order itself plus all items): <code>Query where PK = &quot;o#001&quot;</code></p><p class="paragraph" style="text-align:left;">To get just the products in an order: <code>Query where PK = &quot;o#001&quot; AND SK begins_with &quot;p#&quot;</code></p><p class="paragraph" style="text-align:left;">This is the power of item collections. All items with the same partition key are stored together, so DynamoDB can retrieve related items with Query, using pagination when results exceed one page. No joins needed.</p><p class="paragraph" style="text-align:left;">Here&#39;s what this looks like in code:</p><div class="codeblock"><pre><code>import boto3
from boto3.dynamodb.conditions import Key

dynamodb = boto3.resource(&#39;dynamodb&#39;)
table = dynamodb.Table(&#39;ECommerce&#39;)

# Read the first page of order metadata and items; use LastEvaluatedKey for additional pages
response = table.query(
    KeyConditionExpression=Key(&#39;PK&#39;).eq(&#39;o#001&#39;)
)

# Separate order from items based on EntityType
order = None
items = []
for item in response[&#39;Items&#39;]:
    if item[&#39;EntityType&#39;] == &#39;order&#39;:
        order = item
    elif item[&#39;EntityType&#39;] == &#39;orderItem&#39;:
        items.append(item)

# Or get just the products in the order
response = table.query(
    KeyConditionExpression=Key(&#39;PK&#39;).eq(&#39;o#001&#39;) &amp; Key(&#39;SK&#39;).begins_with(&#39;p#&#39;)
)</code></pre></div><p class="paragraph" style="text-align:left;">One query, multiple entity types, no extra per-entity requests for results that fit in one page. That&#39;s the payoff for the design complexity.</p><p class="paragraph" style="text-align:left;">Still with me? This is where it starts to get interesting.</p><h3 class="heading" style="text-align:left;" id="access-pattern-5-get-invoice-for-an">Access Pattern 5: Get Invoice for an Order</h3><p class="paragraph" style="text-align:left;">The invoice belongs to the order, so it goes in the same item collection:</p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">PK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">SK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">EntityType</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">Amount</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">PaymentMethod</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">i#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">invoice</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">109.97</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">credit_card</p></td></tr></table></div><p class="paragraph" style="text-align:left;">To get the invoice for an order: <code>Query where PK = &quot;o#001&quot; AND SK begins_with &quot;i#&quot;</code></p><h3 class="heading" style="text-align:left;" id="access-pattern-6-get-orders-for-a-p">Access Pattern 6: Get Orders for a Product (Date Range)</h3><p class="paragraph" style="text-align:left;">This is our first pattern that requires a GSI. We need to query by product ID and filter by date, but our base table doesn&#39;t support that: products are partition keys for products, not for order items. (We&#39;ve hit the wall that every DynamoDB designer eventually hits.)</p><p class="paragraph" style="text-align:left;">We&#39;ll create a GSI (let&#39;s call it <code>GSI1</code>) where:</p><ul><li><p class="paragraph" style="text-align:left;">Partition key: <code>productId</code></p></li><li><p class="paragraph" style="text-align:left;">Sort key: <code>orderDate</code></p></li></ul><p class="paragraph" style="text-align:left;">Wait—but we&#39;re using single-table design with generic key names. How does this work?</p><p class="paragraph" style="text-align:left;">We add attributes to our order items: <code>GSI1PK</code> (the product ID) and <code>GSI1SK</code> (the order date). DynamoDB will index only items that have these attributes, creating what&#39;s called a <i>sparse index</i>. Items without these attributes won&#39;t appear in the GSI, and you don&#39;t need to pay to store them twice.</p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">PK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">SK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">GSI1PK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">GSI1SK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">EntityType</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">p#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">p#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">2025-03-15</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">orderItem</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">p#002</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">p#002</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">2025-03-15</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">orderItem</p></td></tr></table></div><p class="paragraph" style="text-align:left;">To get all orders containing product <code>p#001</code> in March 2025: <code>Query GSI1 where GSI1PK = &quot;p#001&quot; AND GSI1SK BETWEEN &quot;2025-03-01&quot; AND &quot;2025-03-31&quot;</code></p><h3 class="heading" style="text-align:left;" id="access-patterns-7-8-get-invoice-by-">Access Patterns 7 & 8: Get Invoice by ID, Get Payments for Invoice</h3><p class="paragraph" style="text-align:left;">Invoice by ID is interesting. Our invoice items are currently keyed by order ID, but we also need to retrieve them by invoice ID. Time for another entry in our GSI.</p><p class="paragraph" style="text-align:left;">We&#39;ll add <code>GSI1PK=i#&lt;invoiceId&gt;</code> and <code>GSI1SK=i#&lt;invoiceId&gt;</code> to our invoice items:</p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">PK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">SK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">GSI1PK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">GSI1SK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">EntityType</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">i#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">i#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">i#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">invoice</p></td></tr></table></div><p class="paragraph" style="text-align:left;">For payments, we could create separate payment items, but since our access pattern is &quot;get all payments for an invoice&quot; (not individual payments), we can store payments as a JSON attribute on the invoice. This is a valid denormalization, we&#39;re trading flexibility for simplicity.</p><h3 class="heading" style="text-align:left;" id="access-patterns-8-9-customer-invoic">Access Patterns 8 & 9: Customer Invoice and Product History</h3><p class="paragraph" style="text-align:left;">These patterns need customer ID as the partition key, which our current structure doesn&#39;t support. Time for a second GSI (GSI2):</p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">PK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">SK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">GSI2PK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">GSI2SK</p></th><th class="bh__table_header" width="20%"><p class="paragraph" style="text-align:left;">EntityType</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">i#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">c#12345</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">i#2025-03-15</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">invoice</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">p#001</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">c#12345</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">p#2025-03-15</p></td><td class="bh__table_cell" width="20%"><p class="paragraph" style="text-align:left;">orderItem</p></td></tr></table></div><p class="paragraph" style="text-align:left;">The sort key includes both a prefix (to distinguish invoices from products) and a date (for range queries).</p><p class="paragraph" style="text-align:left;">To get all invoices for customer <code>c#12345</code> in Q1 2025: <code>Query GSI2 where GSI2PK = &quot;c#12345&quot; AND GSI2SK BETWEEN &quot;i#2025-01-01&quot; AND &quot;i#2025-03-31&quot;</code></p><p class="paragraph" style="text-align:left;">To get all products ordered by customer <code>c#12345</code> in Q1 2025: <code>Query GSI2 where GSI2PK = &quot;c#12345&quot; AND GSI2SK BETWEEN &quot;p#2025-01-01&quot; AND &quot;p#2025-03-31&quot;</code></p><p class="paragraph" style="text-align:left;">If this feels like a lot of moving parts, that&#39;s because it is—but you&#39;re almost through the hard part.</p><h2 class="heading" style="text-align:left;" id="making-it-cleaner-with-multi-attrib">Making It Cleaner with Multi-Attribute Keys</h2><p class="paragraph" style="text-align:left;">Remember when I mentioned multi-attribute composite keys in GSIs? Here&#39;s where they can help. (This feature shipped in November 2025, so it&#39;s relatively new.)</p><p class="paragraph" style="text-align:left;">Without multi-attribute keys, we created synthetic keys like <code>GSI2SK = &quot;i#2025-03-15&quot;</code> by concatenating a prefix with a date. This works, but it has drawbacks:</p><ul><li><p class="paragraph" style="text-align:left;">You need to build and parse these strings in your application code (error-prone and tedious)</p></li><li><p class="paragraph" style="text-align:left;">Adding a new GSI requires backfilling synthetic keys across all existing items</p></li><li><p class="paragraph" style="text-align:left;">The data model is harder to understand at a glance</p></li></ul><p class="paragraph" style="text-align:left;">With multi-attribute keys, you can define a GSI where the sort key is composed of multiple actual attributes. For example, instead of <code>GSI2SK = &quot;i#2025-03-15&quot;</code>, you could have:</p><ul><li><p class="paragraph" style="text-align:left;">Sort key attribute 1: <code>entityType</code> (value: &quot;invoice&quot;)</p></li><li><p class="paragraph" style="text-align:left;">Sort key attribute 2: <code>orderDate</code> (value: &quot;2025-03-15&quot;)</p></li></ul><p class="paragraph" style="text-align:left;">The query becomes more natural: <code>Query where GSI2PK = &quot;c#12345&quot; AND entityType = &quot;invoice&quot; AND orderDate BETWEEN &quot;2025-01-01&quot; AND &quot;2025-03-31&quot;</code></p><p class="paragraph" style="text-align:left;">You can use up to four attributes in the partition key and four in the sort key.</p><h3 class="heading" style="text-align:left;" id="the-critical-query-constraint">The Critical Query Constraint</h3><p class="paragraph" style="text-align:left;">Here&#39;s what you absolutely need to know: multi-attribute sort key queries must be <b>left-to-right</b>. You must specify conditions in order, and you cannot skip attributes in the middle.</p><p class="paragraph" style="text-align:left;"><b>Valid query patterns:</b></p><ul><li><p class="paragraph" style="text-align:left;">✅ First attribute only: <code>entityType = &#39;invoice&#39;</code></p></li><li><p class="paragraph" style="text-align:left;">✅ First two attributes: <code>entityType = &#39;invoice&#39; AND orderDate = &#39;2025-03-15&#39;</code></p></li><li><p class="paragraph" style="text-align:left;">✅ First attribute equality + second attribute range: <code>entityType = &#39;invoice&#39; AND orderDate BETWEEN &#39;2025-01-01&#39; AND &#39;2025-03-31&#39;</code></p></li></ul><p class="paragraph" style="text-align:left;"><b>Invalid query patterns:</b></p><ul><li><p class="paragraph" style="text-align:left;">❌ Skipping first attribute: <code>orderDate = &#39;2025-03-15&#39;</code> (can&#39;t skip entityType)</p></li><li><p class="paragraph" style="text-align:left;">❌ Range on first, equality on second: <code>entityType &gt; &#39;invoice&#39; AND orderDate = &#39;2025-03-15&#39;</code> (inequality must be last)</p></li><li><p class="paragraph" style="text-align:left;">❌ Querying out of order: <code>orderDate = &#39;2025-03-15&#39; AND entityType = &#39;invoice&#39;</code></p></li></ul><p class="paragraph" style="text-align:left;">This constraint directly affects how you order attributes in your key definition. Put the attributes you&#39;ll always filter on first, with the most common range query attribute last.</p><p class="paragraph" style="text-align:left;">This feature is particularly useful when:</p><ul><li><p class="paragraph" style="text-align:left;">You&#39;re adding a GSI using attributes already present on existing items (no application backfill of synthetic key attributes required)</p></li><li><p class="paragraph" style="text-align:left;">Your data naturally has multiple hierarchical dimensions</p></li><li><p class="paragraph" style="text-align:left;">You want your schema to be more self-documenting</p></li></ul><p class="paragraph" style="text-align:left;">One thing to keep in mind: multi-attribute keys only work with GSIs, not with the base table or LSIs. And you still need to think carefully about data distribution: a multi-attribute partition key that results in a few &quot;hot&quot; partitions will cause throttling just like a single-attribute key would.</p><h2 class="heading" style="text-align:left;" id="the-final-schema">The Final Schema</h2><p class="paragraph" style="text-align:left;">Here&#39;s what our complete design looks like.</p><p class="paragraph" style="text-align:left;"><b>Base Table:</b></p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="12%"><p class="paragraph" style="text-align:left;">PK</p></th><th class="bh__table_header" width="12%"><p class="paragraph" style="text-align:left;">SK</p></th><th class="bh__table_header" width="12%"><p class="paragraph" style="text-align:left;">EntityType</p></th><th class="bh__table_header" width="12%"><p class="paragraph" style="text-align:left;">CustomerId</p></th><th class="bh__table_header" width="12%"><p class="paragraph" style="text-align:left;">ProductId</p></th><th class="bh__table_header" width="12%"><p class="paragraph" style="text-align:left;">OrderDate</p></th><th class="bh__table_header" width="12%"><p class="paragraph" style="text-align:left;">Amount</p></th><th class="bh__table_header" width="12%"><p class="paragraph" style="text-align:left;">...</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">c#12345</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">c#12345</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">customer</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">p#001</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">p#001</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">product</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">order</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">c#12345</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">2025-03-15</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">p#001</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">orderItem</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">c#12345</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">p#001</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">2025-03-15</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">o#001</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">i#001</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">invoice</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">c#12345</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">2025-03-15</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;">109.97</p></td><td class="bh__table_cell" width="12%"><p class="paragraph" style="text-align:left;"></p></td></tr></table></div><p class="paragraph" style="text-align:left;"><b>GSI1</b> (for product-based and invoice-based queries):</p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="33%"><p class="paragraph" style="text-align:left;">GSI1PK</p></th><th class="bh__table_header" width="33%"><p class="paragraph" style="text-align:left;">GSI1SK</p></th><th class="bh__table_header" width="33%"><p class="paragraph" style="text-align:left;">...</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">p#001</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">2025-03-15</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">...</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">i#001</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">i#001</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">...</p></td></tr></table></div><p class="paragraph" style="text-align:left;"><b>GSI2</b> (for customer-based queries):</p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="33%"><p class="paragraph" style="text-align:left;">GSI2PK</p></th><th class="bh__table_header" width="33%"><p class="paragraph" style="text-align:left;">GSI2SK</p></th><th class="bh__table_header" width="33%"><p class="paragraph" style="text-align:left;">...</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">c#12345</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">i#2025-03-15</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">...</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">c#12345</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">p#2025-03-15</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">...</p></td></tr></table></div><p class="paragraph" style="text-align:left;"><b>Access Pattern Summary:</b></p><div style="padding:14px 15px 14px;"><table class="bh__table" width="100%" style="border-collapse:collapse;"><tr class="bh__table_row"><th class="bh__table_header" width="33%"><p class="paragraph" style="text-align:left;">#</p></th><th class="bh__table_header" width="33%"><p class="paragraph" style="text-align:left;">Access Pattern</p></th><th class="bh__table_header" width="33%"><p class="paragraph" style="text-align:left;">Key Condition</p></th></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">1</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Get customer by ID</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">PK = &quot;c#12345&quot;, SK = &quot;c#12345&quot;</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">2</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Get product by ID</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">PK = &quot;p#001&quot;, SK = &quot;p#001&quot;</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">3</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Get order by ID</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">PK = &quot;o#001&quot;, SK = &quot;o#001&quot;</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">4</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Get products in order</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">PK = &quot;o#001&quot;, SK begins_with &quot;p#&quot;</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">5</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Get invoice for order</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">PK = &quot;o#001&quot;, SK begins_with &quot;i#&quot;</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">6</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Get orders for product</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">GSI1: GSI1PK = &quot;p#001&quot;, GSI1SK between dates</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">7</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Get invoice by ID</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">GSI1: GSI1PK = &quot;i#001&quot;, GSI1SK = &quot;i#001&quot;</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">8</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Get invoices for customer</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">GSI2: GSI2PK = &quot;c#12345&quot;, GSI2SK between &quot;i#...&quot;</p></td></tr><tr class="bh__table_row"><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">9</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">Get products for customer</p></td><td class="bh__table_cell" width="33%"><p class="paragraph" style="text-align:left;">GSI2: GSI2PK = &quot;c#12345&quot;, GSI2SK between &quot;p#...&quot;</p></td></tr></table></div><h2 class="heading" style="text-align:left;" id="design-principles-to-remember">Design Principles to Remember</h2><p class="paragraph" style="text-align:left;">After walking through this example, let me distill some principles that apply to any DynamoDB design. These are worth bookmarking.</p><p class="paragraph" style="text-align:left;"><b>1. Start with access patterns.</b> Never start by drawing an ERD and then trying to fit it into DynamoDB. List your queries first, then design a schema that supports them. This feels backwards if you&#39;re used to relational databases, but it&#39;s the right approach for DynamoDB.</p><p class="paragraph" style="text-align:left;"><b>2. Use composite keys to create item collections.</b> Items that are often queried together should share a partition key. The sort key distinguishes them and controls their order.</p><p class="paragraph" style="text-align:left;"><b>3. Add indexes only when necessary.</b> Each GSI duplicates your data and adds cost. Make sure you actually need the access pattern before creating an index. A surprising number of &quot;necessary&quot; indexes turn out to be premature optimization.</p><p class="paragraph" style="text-align:left;"><b>4. Embrace denormalization.</b> Storing the same data in multiple places (like customer ID on both orders and order items) is normal and expected. Storage is cheap, round trips are expensive.</p><p class="paragraph" style="text-align:left;"><b>5. Use sparse indexes.</b> If a GSI only needs to index certain item types, only add the GSI key attributes to those items. This keeps the index smaller and cheaper.</p><p class="paragraph" style="text-align:left;"><b>6. Plan for growth.</b> Choose partition keys that will distribute load evenly as your data grows. A partition key with low cardinality (like &quot;status&quot; with values &quot;active/inactive&quot;) will create hot partitions. You&#39;ll regret it later.</p><p class="paragraph" style="text-align:left;"><b>7. Consider using multi-attribute keys.</b> Especially for GSIs on existing tables, they can simplify your schema and avoid application backfills of synthetic keys when the required attributes already exist. This is one of those features that, once you use it, you wonder how you lived without it.</p><p class="paragraph" style="text-align:left;"><b>8. Remember transactions cost double.</b> DynamoDB transactions (TransactWriteItems, TransactGetItems) consume 2x the capacity units of regular operations. If your access patterns involve frequent transactional writes across multiple items, factor this into your capacity planning and cost estimates.</p><p class="paragraph" style="text-align:left;"><b>9. Use TTL for time-bound data.</b> For time-series data, session records, logs, or anything with natural expiration, enable Time to Live. DynamoDB deletes expired items asynchronously without charging source-Region write capacity. On MREC global tables, replicated TTL deletes consume write capacity in replica Regions. You&#39;ll need a TTL attribute (typically a Unix timestamp), so plan for it in your schema.</p><h2 class="heading" style="text-align:left;" id="recent-features-that-affect-design-">Recent Features That Affect Design Decisions</h2><p class="paragraph" style="text-align:left;">DynamoDB has evolved significantly in the past year or so. Here are some features worth knowing about, they might change how you approach certain design decisions.</p><h3 class="heading" style="text-align:left;" id="warm-throughput">Warm Throughput</h3><p class="paragraph" style="text-align:left;">DynamoDB now exposes a &quot;<a class="link" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/warm-throughput.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=dynamodb-database-design-in-2026" target="_blank" rel="noopener noreferrer nofollow">warm throughput</a>&quot; value that shows how many read and write operations your table can readily handle. More importantly, you can pre-warm your table to handle anticipated traffic spikes. This is useful if you know a traffic surge is coming (say, a product launch or Black Friday) and want to avoid throttling during the critical window.</p><h3 class="heading" style="text-align:left;" id="configurable-maximum-throughput-for">Configurable Maximum Throughput for On-Demand Tables</h3><p class="paragraph" style="text-align:left;"><a class="link" href="https://newsletter.simpleaws.dev/p/dynamodb-scaling-provisioned-on-demand?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=dynamodb-database-design-in-2026" target="_blank" rel="noopener noreferrer nofollow">On-demand tables</a> now support configurable maximum throughput. This lets you set a ceiling on how much capacity a table can consume, which is helpful for cost control and for protecting downstream services from being overwhelmed.</p><h3 class="heading" style="text-align:left;" id="multi-region-strong-consistency">Multi-Region Strong Consistency</h3><p class="paragraph" style="text-align:left;"><a class="link" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GlobalTables.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=dynamodb-database-design-in-2026" target="_blank" rel="noopener noreferrer nofollow">Global tables now support multi-region strong consistency</a>. Previously, global tables only offered eventual consistency: you&#39;d write in one region, and it would eventually replicate to others. With multi-region strong consistency, you can get strong consistency across regions, which is crucial for applications like inventory management or financial transactions. This is a big deal for applications that previously couldn&#39;t use global tables due to consistency requirements. Check the supported MRSC Region combinations and feature restrictions before choosing it; MRSC does not support TTL or transaction operations.</p><h3 class="heading" style="text-align:left;" id="configurable-pitr-periods">Configurable PITR Periods</h3><p class="paragraph" style="text-align:left;"><a class="link" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/PointInTimeRecovery_Howitworks.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=dynamodb-database-design-in-2026" target="_blank" rel="noopener noreferrer nofollow">Point-in-time recovery (PITR)</a> now lets you configure the recovery period from 1 to 35 days (it used to be fixed at 35 days). This is helpful for compliance scenarios that require shorter data retention. The change came in January 2025, and it&#39;s one of those &quot;finally!&quot; features.</p><h3 class="heading" style="text-align:left;" id="zero-etl-integrations">Zero-ETL Integrations</h3><p class="paragraph" style="text-align:left;">DynamoDB now has <a class="link" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/RedshiftforDynamoDB-zero-etl.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=dynamodb-database-design-in-2026" target="_blank" rel="noopener noreferrer nofollow">zero-ETL integrations</a> with Amazon Redshift and SageMaker Lakehouse. Changes to your DynamoDB table are automatically replicated to these analytics services, so you can run complex queries and ML workloads without building ETL pipelines. If you&#39;ve ever struggled with the &quot;DynamoDB is great for OLTP but how do I run analytics?&quot; question, this is your answer.</p><h2 class="heading" style="text-align:left;" id="common-questions">Common Questions</h2><p class="paragraph" style="text-align:left;">Let me anticipate some questions you might have. These come up every time I discuss DynamoDB design.</p><h3 class="heading" style="text-align:left;" id="why-use-one-table-instead-of-multip">Why use one table instead of multiple ones?</h3><p class="paragraph" style="text-align:left;">Because related data stays together. In DynamoDB, there&#39;s no join operation: if you need data from multiple tables, you need to make multiple API calls. Single-table design lets you fetch related entities in one query.</p><p class="paragraph" style="text-align:left;">That said, multi-table design is perfectly valid when your access patterns don&#39;t require cross-entity queries. AWS now explicitly supports both approaches. Don&#39;t let anyone tell you it&#39;s &quot;wrong.&quot;</p><h3 class="heading" style="text-align:left;" id="is-there-only-one-correct-design">Is there only one correct design?</h3><p class="paragraph" style="text-align:left;">No. There are usually multiple valid designs, and the &quot;best&quot; one depends on your specific access patterns, expected data volume, and team familiarity with DynamoDB. A good design satisfies all your access patterns efficiently, but there&#39;s rarely only one way to achieve that.</p><h3 class="heading" style="text-align:left;" id="how-do-i-handle-new-access-patterns">How do I handle new access patterns?</h3><p class="paragraph" style="text-align:left;">If the new pattern can be supported by your existing keys or indexes, great, you&#39;re done. If not, you&#39;ll need to add a GSI. With multi-attribute composite keys, you might not need to backfill data, you can use existing attributes as key components.</p><p class="paragraph" style="text-align:left;">If your data model fundamentally can&#39;t support the new pattern (like needing to filter by an attribute that doesn&#39;t exist), you might need to add that attribute and backfill it across existing items. This is annoying but manageable.</p><h3 class="heading" style="text-align:left;" id="should-i-use-dynamo-db-for-everythi">Should I use DynamoDB for everything?</h3><p class="paragraph" style="text-align:left;">No. DynamoDB is excellent for high-scale, low-latency access patterns that you can define in advance. It&#39;s not great for ad-hoc analytics, complex reporting, or applications where you don&#39;t know your access patterns upfront. If your stakeholders say &quot;we&#39;ll figure out the queries later,&quot; run towards relational and don&#39;t look back.</p><p class="paragraph" style="text-align:left;">For analytics on DynamoDB data, consider the zero-ETL integrations with Redshift or exporting to S3 and using Athena.</p><h3 class="heading" style="text-align:left;" id="is-dynamo-db-really-this-complex">Is DynamoDB really this complex?</h3><p class="paragraph" style="text-align:left;">Not really. What&#39;s complex is unlearning relational database habits. Once you internalize the &quot;access patterns first&quot; approach, DynamoDB design becomes surprisingly methodical. You&#39;re not normalizing and then optimizing, you&#39;re designing for your specific queries from the start.</p><p class="paragraph" style="text-align:left;">The good news: once your design is done, operating DynamoDB is significantly simpler than managing a relational database cluster. No sharding, no replication configuration, no maintenance windows. Well, none that you manage, <a class="link" href="https://newsletter.simpleaws.dev/p/dynamodb-scaling-provisioned-on-demand?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=dynamodb-database-design-in-2026" target="_blank" rel="noopener noreferrer nofollow">AWS handles all of it</a>.</p><h2 class="heading" style="text-align:left;" id="wrapping-up">Wrapping Up</h2><p class="paragraph" style="text-align:left;">DynamoDB database design is fundamentally different from relational database design. Instead of normalizing data and writing queries later, you start with the queries and design your schema to support them.</p><p class="paragraph" style="text-align:left;">The key concepts are:</p><ul><li><p class="paragraph" style="text-align:left;">Partition keys for data distribution and direct lookups</p></li><li><p class="paragraph" style="text-align:left;">Sort keys for ordering and range queries within a partition</p></li><li><p class="paragraph" style="text-align:left;">Item collections for co-locating related entities</p></li><li><p class="paragraph" style="text-align:left;">GSIs for alternative access patterns</p></li><li><p class="paragraph" style="text-align:left;">Single-table design for data locality (when your patterns benefit from it)</p></li></ul><p class="paragraph" style="text-align:left;">Recent features like multi-attribute composite keys, configurable throughput, and warm throughput make DynamoDB more flexible and easier to operate. But the core design principles remain the same: know your access patterns, structure your data to serve them, and embrace denormalization.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>Partitions, Sharding, and Split-for-Heat in DynamoDB</title>
  <description>Explore DynamoDB partition throughput, hot-key throttling, and write-sharding tradeoffs, with key-design and counter examples awaiting revision.</description>
  <link>https://newsletter.simpleaws.dev/p/partitions-sharding-split-for-heat-dynamodb</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/partitions-sharding-split-for-heat-dynamodb</guid>
  <pubDate>Sat, 15 Nov 2025 23:34:04 +0000</pubDate>
  <atom:published>2025-11-15T23:34:04Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Architecture]]></category>
    <category><![CDATA[Databases]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">It&#39;s 3 am, your phone buzzes. CloudWatch alarm: DynamoDB throttling. You check the metrics: your table has 10,000 Write Capacity Units (WCUs) provisioned, but you&#39;re barely using 3,000. The graphs show plenty of headroom. So why are requests getting rejected?</p><p class="paragraph" style="text-align:left;">Hopefully this doesn&#39;t happen to you (especially not at 3 am). But if it does, you should know the problem isn&#39;t your table&#39;s total capacity, but that every DynamoDB table is split into physical partitions, and each partition has a hard ceiling of 1,000 WCUs per second. When one partition key gets hammered, that specific partition throttles, even if your other 99 partitions are sitting idle.</p><p class="paragraph" style="text-align:left;">Understanding this partition-level reality is the difference between a system that scales smoothly under load and one that falls apart during your biggest traffic spike of the year. Let&#39;s get down to that. Review note: the key-design examples, scaling explanation, counter correctness and cost model below need a coordinated revision; treat the code as illustrative.</p><h2 class="heading" style="text-align:left;" id="the-1000-wcu-ceiling-you-cant-see">The 1,000 WCU Ceiling You Can&#39;t See</h2><p class="paragraph" style="text-align:left;">DynamoDB&#39;s architecture is built on physical partitions. These are the fundamental storage and throughput units that actually hold your data. Each partition is subject to three hard limits:</p><ul><li><p class="paragraph" style="text-align:left;"><b>1,000 Write Capacity Units per second</b></p></li><li><p class="paragraph" style="text-align:left;"><b>3,000 Read Capacity Units per second</b></p></li><li><p class="paragraph" style="text-align:left;"><b>10 GB of storage</b></p></li></ul><p class="paragraph" style="text-align:left;">These limits are per partition, not per table. When you provision 10,000 WCUs for your table, DynamoDB distributes that capacity across multiple partitions. If your table has 10 physical partitions, each partition gets roughly 1,000 WCUs to work with (the distribution isn&#39;t always perfectly even, but that&#39;s the general idea).</p><p class="paragraph" style="text-align:left;">Your partition key (PK) determines which physical partition stores each item. If your application writes heavily to a small number of partition keys (maybe you&#39;re tracking views on a viral post, or incrementing a global counter, or processing a massive batch of orders for a single customer) those writes all target the same partition. Once that partition hits its 1,000 WCU ceiling, requests start getting throttled with <code>ProvisionedThroughputExceededException</code> errors.</p><p class="paragraph" style="text-align:left;">If your CloudWatch metrics show the table is only using 30% of its provisioned capacity, that&#39;s because the other partitions are fine, they&#39;re not receiving traffic. But DynamoDB can&#39;t magically move capacity from an idle partition to a hot one in real time (it can over time, we&#39;ll get to that). The 1,000 WCU limit is a physical constraint of the partition itself.</p><p class="paragraph" style="text-align:left;">This is called a <b>hot partition</b>, and it&#39;s the most common scaling bottleneck in DynamoDB. The table-level capacity you see in the console is essentially a fiction, what matters is the distribution of your workload across partition keys.</p><p class="paragraph" style="text-align:left;">Let me give you a concrete example. Imagine you&#39;re building a real-time analytics system that tracks page views. You decide to use the PageID as your partition key. Sounds reasonable, right? Most pages get modest traffic. But then one article goes viral on social media, and that single PageID is now receiving 5,000 writes per second. The partition holding that key can only handle 1,000 WPS, the other 4,000 writes get throttled, even though your table has 10,000 WCUs provisioned and the vast majority of that capacity is unused. The table isn&#39;t the bottleneck, the partition is.</p><p class="paragraph" style="text-align:left;">To make this worse, there&#39;s no CloudWatch metric that directly shows you partition-level consumption (AWS added Contributor Insights to help with this, but it&#39;s not real-time and requires explicit enablement). You have to infer hot partitions from patterns in your throttling errors and your application&#39;s access patterns.</p><p class="paragraph" style="text-align:left;">The partition distribution also explains why certain write patterns create more problems than others. If you&#39;re loading data where the partition keys are sequential or heavily time-based, like using auto-incrementing order IDs or timestamps as partition keys, you create what&#39;s called a &quot;rolling hot partition.&quot; All your writes concentrate on the newest partition key values, hammering a single partition continuously while the others sit idle. DynamoDB can&#39;t split the load because there&#39;s only one active write boundary.</p><h3 class="heading" style="text-align:left;" id="global-secondary-index-propagation">Global Secondary Index Propagation</h3><p class="paragraph" style="text-align:left;">Here&#39;s a related complexity that may catch you off guard: <b>Global Secondary Index (GSI) throttling propagates to the base table</b>. When you write an item, DynamoDB must update the base table and every GSI that includes that item. The total WCU cost is the sum of the base table write plus all GSI updates (each GSI write is calculated based on the projected attribute size, rounded up to 1 KB).</p><p class="paragraph" style="text-align:left;">If a GSI is under-provisioned and can&#39;t handle the write volume, it throttles. When a GSI throttles, it blocks the base table write even if the base table has plenty of unused capacity. This is why you might see throttling on a table that appears to have headroom: you&#39;d need to look at the GSI metrics. Tip: Always monitor <code>ConsumedWriteCapacityUnits</code> for every GSI, not just the base table.</p><h2 class="heading" style="text-align:left;" id="dynamo-db-adaptive-capacity">DynamoDB Adaptive Capacity</h2><p class="paragraph" style="text-align:left;">DynamoDB has an internal mechanism called <b>Adaptive Capacity</b> that tries to handle uneven access patterns automatically. When Adaptive Capacity detects that a partition is receiving disproportionate traffic, it kicks off a process called <b>Split-for-Heat</b>.</p><p class="paragraph" style="text-align:left;">DynamoDB continuously monitors partition usage. When it identifies a partition that&#39;s consistently hitting or exceeding its throughput limits, it automatically splits that partition into two new partitions. The items are redistributed based on their sort key (SK) values. This split effectively doubles the available write capacity for that segment of data: two partitions means 2,000 WCUs instead of 1,000.</p><p class="paragraph" style="text-align:left;">DynamoDB also handles <b>Split-for-Size</b>. When a partition hits the 10 GB hard limit for storage, it&#39;s automatically split regardless of throughput concerns. Between these two mechanisms, DynamoDB can theoretically scale to handle any workload without manual intervention.</p><p class="paragraph" style="text-align:left;">The problem is that this takes several minutes to complete.</p><p class="paragraph" style="text-align:left;">In case you forgot, CloudWatch metrics are aggregated at one-minute intervals. Auto-scaling (if you&#39;re using provisioned mode) requires two consecutive minutes of exceeding the target utilization threshold before it even triggers a scaling event. Then the actual <code>UpdateTable</code> operation takes several minutes to complete as DynamoDB provisions additional capacity and potentially splits partitions. During all of this, your application is throttling.</p><p class="paragraph" style="text-align:left;"><b>Burst capacity</b> provides some buffer here. DynamoDB banks unused throughput for up to 300 seconds. For every second your table doesn&#39;t use its full provisioned capacity, that unused capacity gets saved in a credits bucket. If you provision 1,000 WCUs but only use 500, you&#39;re banking 500 WCUs per second. When a sudden spike hits, DynamoDB can pull from this banked capacity to handle traffic that temporarily exceeds your provisioned limit. This is why you might see brief periods where consumption exceeds provisioned capacity without throttling: you&#39;re spending your burst credits. But burst capacity is designed to handle short spikes, and it runs out quickly under sustained load.</p><p class="paragraph" style="text-align:left;">For a sustained traffic spike (a product launch, a viral moment, a coordinated attack) this lag time translates directly into dropped requests, failed transactions, and angry customers. Adaptive Capacity will eventually save you, but &quot;eventually&quot; can mean 5-10 minutes of degraded performance (and please tell me you&#39;re doing load shedding, otherwise this can turn into a cascading failure).</p><p class="paragraph" style="text-align:left;">There are also failure modes where Adaptive Capacity can&#39;t help at all. If you&#39;re using a Local Secondary Index (LSI), DynamoDB can&#39;t split the partition because LSIs enforce a 10 GB item collection limit per partition key. The problem here is that Local Secondary Indexes enforce a 10 GB limit per partition key value: all items sharing a PK, plus their LSI projections, must fit within 10 GB. When this limit is approached, DynamoDB can&#39;t split the partition since that would break the LSI&#39;s contiguous storage requirement. In my experience it&#39;s very rare that you&#39;d hit this limit, but it&#39;s important to know it&#39;s there.</p><h2 class="heading" style="text-align:left;" id="design-keys-that-dont-create-heat">Design Keys That Don&#39;t Create Heat</h2><p class="paragraph" style="text-align:left;">Before we talk about advanced patterns like write sharding, let&#39;s cover designing partition keys that naturally avoid hot spots. The fundamental principle is <b>high cardinality with wide dispersion</b>.</p><p class="paragraph" style="text-align:left;">Your partition key should have many possible unique values (high cardinality), and your application&#39;s access pattern should distribute writes roughly evenly across those values (wide dispersion). If both conditions are met, DynamoDB can spread your workload across many partitions, and no single partition becomes a bottleneck.</p><p class="paragraph" style="text-align:left;">Here are the anti-patterns that create hot partitions:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Sequential or auto-incrementing IDs</b>: Using <code>ORDER#1</code>, <code>ORDER#2</code>, <code>ORDER#3</code> as partition keys means all new writes target the highest number, creating a rolling hot spot.</p></li><li><p class="paragraph" style="text-align:left;"><b>Timestamp-based partition keys</b>: <code>TIMESTAMP#2025-09-29T15:30:00</code> suffers the same problem: writes cluster around the current timestamp.</p></li><li><p class="paragraph" style="text-align:left;"><b>Low-cardinality attributes</b>: Using <code>Region</code> (when you only have 3 regions) as a partition key concentrates writes on whichever region is most active.</p></li></ul><p class="paragraph" style="text-align:left;">Good partition key choices include:</p><ul><li><p class="paragraph" style="text-align:left;"><b>UUIDs or randomly generated IDs</b>: <code>USER#a7f3c891-4d2e-4b29-8f34-d9e21ba7c04f</code> provides excellent distribution. Every user gets a unique partition key with no clustering.</p></li><li><p class="paragraph" style="text-align:left;"><b>Hashed values</b>: If you must use sequential IDs, hash them first. <code>ORDER#&#123;md5(order_id)&#125;</code> spreads sequential orders across the hash space.</p></li><li><p class="paragraph" style="text-align:left;"><b>Composite keys with random components</b>: For time-series data, append a random shard suffix: <code>SENSOR#&#123;sensor_id&#125;#&#123;random_int&#125;</code> (we&#39;ll explore this pattern in depth shortly).</p></li></ul><p class="paragraph" style="text-align:left;">Keep in mind that even UUIDs can create temporary hot spots if your write pattern is heavily time-based. UUIDv1 embeds timestamp information, which can cluster writes. UUIDv4 (purely random) is better for high-write scenarios.</p><p class="paragraph" style="text-align:left;">Important: Don&#39;t use auto-incrementing integers as your partition key for high-volume writes. The newest integer value will always be the hot spot.</p><p class="paragraph" style="text-align:left;">Here&#39;s what good partition key design looks like in code:</p><div class="codeblock"><pre><code>import uuid
import hashlib
import random

# Bad: Sequential, creates hot partition at boundary
def create_order_bad(order_counter):
    pk = f&quot;ORDER#&#123;order_counter&#125;&quot;
    return pk

# Good: Random UUID provides wide dispersion  
def create_order_good():
    pk = f&quot;ORDER#&#123;uuid.uuid4()&#125;&quot;
    return pk

# Better for time-series: Add shard suffix
def create_sensor_reading(sensor_id):
    shard = random.randint(0, 9)  # 10 shards
    pk = f&quot;SENSOR#&#123;sensor_id&#125;#&#123;shard&#125;&quot;
    return pk

# Good: Hash sequential IDs to spread them
def create_order_hashed(order_id):
    hash_val = hashlib.md5(str(order_id).encode()).hexdigest()[:8]
    pk = f&quot;ORDER#&#123;hash_val&#125;&quot;
    return pk</code></pre></div><p class="paragraph" style="text-align:left;">High-cardinality partition keys prevent hot spots for most workloads. But what if your use case inherently concentrates writes? What if you&#39;re tracking global counters, processing a viral post&#39;s engagement metrics, or handling a flash sale where thousands of customers are buying the same product SKU?</p><p class="paragraph" style="text-align:left;">That&#39;s when partition key design alone isn&#39;t enough, and we need to throw some “sharding magic” into the mix.</p><h2 class="heading" style="text-align:left;" id="write-sharding-in-dynamo-db">Write Sharding in DynamoDB</h2><p class="paragraph" style="text-align:left;">If you know high-volume traffic is coming, you can&#39;t rely on Adaptive Capacity&#39;s multi-minute lag time. Ideally your keys naturally spread writes, but if they don&#39;t, you need a way to manually control how write operations are spread. That technique is called <b>write sharding</b>.</p><p class="paragraph" style="text-align:left;">The idea is that instead of using a single partition key for a high-contention entity, you create N synthetic partition keys by appending a shard identifier. When writing, you randomly select one of the N shards. This spreads writes across N logical partition keys, which DynamoDB can distribute across physical partitions. It does not guarantee one physical partition or 1,000 WCUs of capacity per shard.</p><p class="paragraph" style="text-align:left;">The tradeoff is that reads become more complex, since to retrieve the complete data for that entity you now have to query all N shards in parallel (scatter) and then aggregate the results on the client side (gather). You&#39;re gaining write throughput, but at the cost of read complexity.</p><h3 class="heading" style="text-align:left;" id="implementing-write-sharding">Implementing Write Sharding</h3><div class="codeblock"><pre><code>import random
import boto3

dynamodb = boto3.resource(&#39;dynamodb&#39;)
table = dynamodb.Table(&#39;Analytics&#39;)

SHARD_COUNT = 10  # Creates 10 logical shards; physical partition placement and capacity are not guaranteed

def increment_page_views(page_id: str, count: int = 1):
    &quot;&quot;&quot;
    Increment page view counter using write sharding.
    Randomly selects one of N shards to distribute write load.
    &quot;&quot;&quot;
    # Randomly select a shard (0 to SHARD_COUNT - 1)
    shard_id = random.randint(0, SHARD_COUNT - 1)
    
    # Create sharded partition key
    pk = f&quot;PAGE#&#123;page_id&#125;#SHARD#&#123;shard_id:02d&#125;&quot;
    
    # Atomic increment using UpdateExpression
    table.update_item(
        Key=&#123;&#39;PK&#39;: pk, &#39;SK&#39;: &#39;VIEWS&#39;&#125;,
        UpdateExpression=&#39;ADD view_count :inc&#39;,
        ExpressionAttributeValues=&#123;&#39;:inc&#39;: count&#125;
    )</code></pre></div><p class="paragraph" style="text-align:left;">With this design, each write hits a random shard amongst the 10 shards that we have. If you&#39;re processing 5,000 writes per second and they&#39;re evenly distributed across 10 shards, each shard receives roughly 500 writes per second, which is hopefully manageable with the 1,000 WCUs that each partition has (remember each write consumes 1 WCU per 1 KB of data written, rounded up). If it can&#39;t, you can either aim for more shards, or rely on burst capacity (temporary use of unused table throughput, subject to partition limits) and Adaptive Capacity (using some capacity from other partitions of the same table) while you wait for Split-for-Heat (DynamoDB splitting the shard into multiple physical partitions behind the scenes) to catch up.</p><h3 class="heading" style="text-align:left;" id="scatter-gather-query">Scatter-Gather Query</h3><p class="paragraph" style="text-align:left;">Reading the total requires querying all shards:</p><div class="codeblock"><pre><code>import concurrent.futures
from typing import Dict

def get_page_view_total(page_id: str) -&gt; int:
    &quot;&quot;&quot;
    Retrieve total page views across all shards.
    Uses parallel queries (scatter) then sums results (gather).
    &quot;&quot;&quot;
    
    def query_single_shard(shard_id: int) -&gt; int:
        &quot;&quot;&quot;Query one shard and return its count&quot;&quot;&quot;
        pk = f&quot;PAGE#&#123;page_id&#125;#SHARD#&#123;shard_id:02d&#125;&quot;
        
        try:
            response = table.get_item(
                Key=&#123;&#39;PK&#39;: pk, &#39;SK&#39;: &#39;VIEWS&#39;&#125;
            )
            return response.get(&#39;Item&#39;, &#123;&#125;).get(&#39;view_count&#39;, 0)
        except Exception as e:
            # Log error but don&#39;t fail entire operation
            print(f&quot;Error querying shard &#123;shard_id&#125;: &#123;e&#125;&quot;)
            return 0
    
    # Scatter: Execute parallel queries to all shards
    with concurrent.futures.ThreadPoolExecutor(max_workers=SHARD_COUNT) as executor:
        futures = [executor.submit(query_single_shard, i) 
                   for i in range(SHARD_COUNT)]
        results = [f.result() for f in concurrent.futures.as_completed(futures)]
    
    # Gather: Sum all shard counts
    total = sum(results)
    return total</code></pre></div><h3 class="heading" style="text-align:left;" id="the-economic-tradeoff-of-sharding">The Economic Tradeoff of Sharding</h3><p class="paragraph" style="text-align:left;">Sharding also affects your DynamoDB bill, not just your <span style="text-decoration:line-through;">sanity</span> code complexity. Reading a sharded counter requires N queries instead of 1, so if you have 10 shards and each query reads 0.1 KB, you&#39;re consuming 10 RCUs instead of 1 RCU per read (assuming strongly consistent reads) even though the total data read is well below the 4 KB limit for a single RCU.</p><p class="paragraph" style="text-align:left;">If your counter is read 1,000 times per second, sharding increases your read capacity consumption from 500 RCUs/sec to 5,000 RCUs/sec. At $0.00013 per RCU-hour in provisioned mode, that&#39;s roughly $285/month versus $28/month in read costs alone. Ouch 💸.</p><p class="paragraph" style="text-align:left;">A counter isn&#39;t the best example, since in that case you usually pre-compute it (i.e. keep the counter as a separate entity, and update it with strong consistency every time you write new data). But overall it serves to show how much sharding can cost you. Still, it&#39;s usually better than getting throttled!</p><h3 class="heading" style="text-align:left;" id="choosing-the-right-shard-count">Choosing the Right Shard Count</h3><p class="paragraph" style="text-align:left;">For most use cases, I suggest you start with N=10 shards. As you get traffic, monitor your CloudWatch metrics (specifically <code>WriteThrottleEvents for write throttling</code> and <code>ConsumedWriteCapacityUnits</code> per shard if you&#39;ve instrumented that). If you&#39;re still seeing throttles, increase N. If your read performance is suffering and throttling is rare, you might be over-sharded and can reduce N. The caveat here is that increasing N is pretty simple: You just change the value in the code and start writing to new shards 11, 12, etc. To decrease N you&#39;ll have to rewrite every single entity, since you&#39;ll need to replace the value of <code>&#123;shard&#125;</code> in the PK <code>ENTITY#&#123;shard&#125;</code>. And you can&#39;t just overwrite that field, PKs are read-only, so you need to atomically create a new object and delete the old one. The script is pretty easy, but it&#39;s 1 read, 1 write and 1 delete in a transaction per entity in your table. Ouch again 💸💸. </p><p class="paragraph" style="text-align:left;">One more consideration: you don&#39;t have to shard your entire table. Shard only the hot entities. If you have a leaderboard table tracking scores for millions of users, but only the top 100 users receive heavy write traffic, shard those 100 partition keys. The rest can use normal, single-partition-key design.</p><h2 class="heading" style="text-align:left;" id="conclusion">Conclusion</h2><p class="paragraph" style="text-align:left;">Your table capacity doesn&#39;t matter if one partition key gets all the traffic. Physical partitions max out at 1,000 WCUs, regardless of table capacity. Adaptive capacity can rebalance throughput within table and partition limits; sudden spikes still require workload-aware planning.</p><p class="paragraph" style="text-align:left;">The first step is to design partitions well. This is the most critical thing in DynamoDB, actually, so I suggest you read on <a class="link" href="https://newsletter.simpleaws.dev/p/dynamodb-database-design?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=partitions-sharding-and-split-for-heat-in-dynamodb" target="_blank" rel="noopener noreferrer nofollow">DynamoDB Design</a> and <a class="link" href="https://newsletter.simpleaws.dev/p/dynamodb-transactions?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=partitions-sharding-and-split-for-heat-in-dynamodb" target="_blank" rel="noopener noreferrer nofollow">DynamoDB Transactions</a>. Understanding <a class="link" href="https://newsletter.simpleaws.dev/p/dynamodb-scaling-provisioned-on-demand?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=partitions-sharding-and-split-for-heat-in-dynamodb" target="_blank" rel="noopener noreferrer nofollow">How DynamoDB Scales</a> is also important. Then, if you need to prepare for traffic spikes, shard your entities.</p><p class="paragraph" style="text-align:left;">DynamoDB scales incredibly well, but only if you design for how it actually works, not how you wish it worked (or worse, for how relational databases work). Which is why I somehow end up writing so much about DynamoDB.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>Why DynamoDB Failed in October 2025</title>
  <description>Understand the DynamoDB DNS race condition behind the October 2025 us-east-1 outage, based on AWS’s postmortem and separate design hypotheses.</description>
      <enclosure url="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/a03e213b-783e-47b8-b5d2-a1d9b3483131/AWS_DynamoDB_failure__1_.png" length="53857" type="image/png"/>
  <link>https://newsletter.simpleaws.dev/p/why-dynamodb-failed-in-october-2025</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/why-dynamodb-failed-in-october-2025</guid>
  <pubDate>Thu, 23 Oct 2025 20:08:03 +0000</pubDate>
  <atom:published>2025-10-23T20:08:03Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Architecture]]></category>
    <category><![CDATA[Operations]]></category>
    <category><![CDATA[Databases]]></category>
    <category><![CDATA[Networking]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">Monday, October 20th, many teams woke up to their pagers instead of their alarm clocks. AWS was down, and a big part of the internet went down with it. The cause? DNS, obviously. It&#39;s always DNS. But let&#39;s dive deeper into it, because it&#39;s pretty interesting how DynamoDB (the affected service, which resulted in a cascading failure) manages DNS.</p><p class="paragraph" style="text-align:left;">This is based on the <a class="link" href="https://aws.amazon.com/message/101925/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=why-dynamodb-failed-in-october-2025" target="_blank" rel="noopener noreferrer nofollow">detailed postmortem</a> AWS released just three days after the incident. Kudos to the AWS team, last time this happened (2023, with AWS Lambda) it took them 4 months to release a <a class="link" href="https://aws.amazon.com/message/061323/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=why-dynamodb-failed-in-october-2025" target="_blank" rel="noopener noreferrer nofollow">postmortem</a>.</p><p class="paragraph" style="text-align:left;">Back to this outage though. Yes, it was DNS. Specifically, it was a failure in the DNS of <a class="link" href="https://newsletter.simpleaws.dev/p/dynamodb-scaling-provisioned-on-demand?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=why-dynamodb-failed-in-october-2025" target="_blank" rel="noopener noreferrer nofollow">DynamoDB</a>, a serverless NoSQL database that&#39;s a foundational service for AWS (meaning many other AWS services depend on it). In this outage, the <code>dynamodb.us-east-1.amazonaws.com</code> address started returning an empty DNS record, making other services incapable of connecting to DynamoDB. When I say “other services”, I mean both your own Lambda function trying to access your table, and other AWS services that use DynamoDB internally. Let&#39;s take a look into how DynamoDB manages its DNS to understand how it failed and why.</p><h2 class="heading" style="text-align:left;" id="how-dynamo-db-manages-dns">How DynamoDB Manages DNS</h2><div class="image"><img alt="DynamoDB DNS Planner distributes plans to three independent DNS Enactors across Availability Zones, which update Route 53 records." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/a03e213b-783e-47b8-b5d2-a1d9b3483131/AWS_DynamoDB_failure__1_.png?t=1761243193"/><div class="image__source"><span class="image__source_text"><p>Diagram showing DNS management in DynamoDB</p></span></div></div><p class="paragraph" style="text-align:left;">For availability reasons, there are two components:</p><ul><li><p class="paragraph" style="text-align:left;">The <b>DNS Planner</b> monitors the health and capacity of the many load balancers that need to receive the traffic, and periodically creates a new DNS plan consisting of a set of load balancers and weights. It doesn&#39;t apply the plan, it just creates it.</p></li><li><p class="paragraph" style="text-align:left;">The <b>DNS Enactor</b> applies the required changes to Amazon Route53 following the DNS plan. This component operates three completely independent copies, one in each Availability Zone. Each instance of the Enactor applies the following:</p><ul><li><p class="paragraph" style="text-align:left;">Receive a new plan from the DNS Planner</p></li><li><p class="paragraph" style="text-align:left;">Update the Route53 records according to the received plan, using Route53 transactions. This includes retry logic.</p></li><li><p class="paragraph" style="text-align:left;">Delete plans significantly older than the plan it has just applied</p></li></ul></li></ul><p class="paragraph" style="text-align:left;">With 3 Enactors running in parallel, race conditions should be expected. They&#39;re dealt with via eventual consistency: A DNS Enactor may enact an old plan, but since the updates are expected to happen quickly and there&#39;s a cleanup step, the next Enactor that runs can update the records with the newest plan.</p><h2 class="heading" style="text-align:left;" id="how-dynamo-db-dns-failed">How DynamoDB DNS Failed</h2><p class="paragraph" style="text-align:left;">The failure resulted from several unlikely events happening at the same time:</p><ul><li><p class="paragraph" style="text-align:left;">DNS Enactor #1 experienced significant delays due to lock contention</p></li><li><p class="paragraph" style="text-align:left;">DNS Planner continued producing newer generations of plans while the delayed Enactor was still working</p></li><li><p class="paragraph" style="text-align:left;">DNS Enactor #2 rapidly applied one of the newer plans</p></li></ul><p class="paragraph" style="text-align:left;">Here&#39;s the timeline, and how those three independent events combined into a failure scenario:</p><ol start="1"><li><p class="paragraph" style="text-align:left;">Enactor 1 started applying a DNS plan. Lots of lock contention, this took a while</p></li><li><p class="paragraph" style="text-align:left;">DNS Planner generated many newer plans, making obsolete the plan Enactor 1 was still applying</p></li><li><p class="paragraph" style="text-align:left;">Enactor 2 started applying a newer and valid DNS plan</p></li><li><p class="paragraph" style="text-align:left;">Enactor 1 continued applying its old plan, overwriting the DNS records Enactor 2 had just written</p></li><li><p class="paragraph" style="text-align:left;">Enactor 2 finished applying the newer and valid plan</p></li><li><p class="paragraph" style="text-align:left;">Enactor 1 finished applying the plan it had (by now old and invalid), effectively overwriting the newer and valid plan Enactor 2 had applied</p></li><li><p class="paragraph" style="text-align:left;">Enactor 2 executed its cleanup routine: Delete plans significantly older than the newer plan it had applied. This deleted the older plan that had just become active for the regional endpoint, removing that endpoint&#39;s IP addresses</p></li><li><p class="paragraph" style="text-align:left;">Further executions of Enactor instances could not handle the inconsistent state of empty DNS records, so failed to apply newer and valid plans</p></li></ol><p class="paragraph" style="text-align:left;">With no DNS records, DNS queries for <code>dynamodb.us-east-1.amazonaws.com</code> were returning no results, and nobody could connect to DynamoDB. This wasn&#39;t a problem for the Enactors (they don&#39;t connect to DynamoDB), but it was an inconsistent state and they didn&#39;t know how to deal with it, so couldn&#39;t apply new DNS plans. And that&#39;s how DynamoDB broke.</p><div class="image"><img alt="Sequence diagram of a delayed DNS Enactor applying an old plan while another Enactor applies a newer plan and runs cleanup, leaving inconsistent DNS state." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/f7449959-af96-4b28-88df-8e5d0f776250/AWS_DynamoDB_failure.png?t=1761246819"/><div class="image__source"><span class="image__source_text"><p>Flow diagram of the events that led to the DynamoDB failure</p></span></div></div><h2 class="heading" style="text-align:left;" id="how-to-fix-dynamo-db">How To Fix DynamoDB</h2><p class="paragraph" style="text-align:left;">So, now we know how DynamoDB broke. And since many AWS services are dependent on it, this took down a significant portion of AWS, and a significant portion of the internet with it.</p><p class="paragraph" style="text-align:left;">The above sections were based on information confirmed by AWS via their outage <a class="link" href="https://aws.amazon.com/message/101925/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=why-dynamodb-failed-in-october-2025" target="_blank" rel="noopener noreferrer nofollow">postmortem</a>. What follows is my own ideas, not endorsed or sanctioned by AWS. In all honesty, these ideas are likely incorrect (if they were, I&#39;d be working at AWS). There are so many details to take into account when working at the staggering scale of AWS that the vast majority of us have never faced and will never face, and I&#39;m sure to do a bad job at it. However, I&#39;ll do my best to explain why most ideas that sound good will likely not work.</p><h3 class="heading" style="text-align:left;" id="idea-1-single-enactor">Idea 1: Single Enactor</h3><p class="paragraph" style="text-align:left;">You can completely eliminate race conditions if you have a single Enactor. This would definitely prevent the failure that occured in this occasion, but it would expose DynamoDB DNS to Availability Zone failures.</p><p class="paragraph" style="text-align:left;">Imagine a scenario where the Availability Zone that hosts the Enactor fails. This also means a significant portion of the Load Balancers that run behind DynamoDB will be failing, and without a functioning Enactor your DNS records won&#39;t be updated, sending traffic to failing endpoints.</p><p class="paragraph" style="text-align:left;">You could argue that this may be fixed by running multiple copies of the Enactor in a primary and standby architecture. The problem with this is that you can&#39;t assume the primary Enactor will always fail in a detectable way. If it starts failing silently, you won&#39;t execute your failover strategy and will need to live with stale DNS records, which at this scale of tens of thousands of servers means losing a significant portion of your capacity simply because you can&#39;t communicate with new instances, and will only realize it when it becomes a significant problem.</p><p class="paragraph" style="text-align:left;">The only way to detect a silent failure is with a parallel process that checks the results. Meaning, a second Enactor running in parallel. And we&#39;re back to the original problem of dealing with multiple Enactors acting concurrently.</p><h3 class="heading" style="text-align:left;" id="idea-2-continuously-validate-the-pl">Idea 2: Continuously Validate the Plan</h3><p class="paragraph" style="text-align:left;">A key point of the problem DynamoDB experienced was that the Enactor that was enacting the old plan only performed once the validity (or rather staleness) check of the plan. If it had checked again, it would have realized the plan was stale and no longer valid, and would have stopped applying it.</p><p class="paragraph" style="text-align:left;">The problem with this is how fast and how often these updates are expected to occur. Remember, we&#39;re thinking in a scale that completely baffles every system you and I have ever built. The DynamoDB us-east-1 endpoint gets in a single hour more traffic than any system I&#39;ve ever built across its entire lifetime.</p><p class="paragraph" style="text-align:left;">This change might actually work. We could even have some sort of timeout, or validity timestamp for the plan. Honestly, I don&#39;t know what would be the problem with this idea, but I&#39;m guessing there&#39;s something. I&#39;ll try to ask someone at re:Invent, and update you on the answer.</p><h3 class="heading" style="text-align:left;" id="idea-3-make-an-inconsistent-state-a">Idea 3: Make an Inconsistent State a Valid State</h3><p class="paragraph" style="text-align:left;">That sounds self-contradicting, and perhaps it is. But ultimately, this idea proposes not trying to prevent this failure mode, but rather accepting that this or something like this <span style="text-decoration:line-through;">may</span> <b>will</b> happen again, and automating the remediation.</p><p class="paragraph" style="text-align:left;">The way I see it, the Enactor that ran right after all DNS records had been cleaned up did know what the desired state was: That of the plan it had received from the DNS Planner. It didn&#39;t know how to deal with the current state of empty DNS records, which in its “mind” didn&#39;t make sense. But it knew what the desired state was. It could have just applied that.</p><p class="paragraph" style="text-align:left;">Or it could have triggered an alarm notifying that it could or would not update the DNS records, which wouldn&#39;t have automatically solved this, but would have saved the DynamoDB team some time trying to find the root cause.</p><p class="paragraph" style="text-align:left;">This is what I would do, if I worked at AWS. I&#39;ll accept some AWS credits for the idea 😀.</p><h2 class="heading" style="text-align:left;" id="closing-thoughts">Closing Thoughts</h2><p class="paragraph" style="text-align:left;"> If you were expecting some sort of multi-region advice, you can read my <a class="link" href="https://newsletter.simpleaws.dev/p/disaster-recovery-strategies-aws?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=why-dynamodb-failed-in-october-2025" target="_blank" rel="noopener noreferrer nofollow">Disaster Recovery</a> article. This isn&#39;t about what you and I can do. And it&#39;s not really about what AWS should do, like I said we do not have the right experience to work on these systems.</p><p class="paragraph" style="text-align:left;">This is about using our heads and trying to learn our own lessons from this. And honestly, about having fun. The outage wasn&#39;t fun, but learning about cool tech stuff certainly is.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>Aurora DSQL: The PostgreSQL-Compatible Database That Will Break Your App (And How to Fix It)</title>
  <description>A launch-era look at Aurora DSQL’s PostgreSQL differences, optimistic concurrency, IAM authentication, schema design, and application migration.</description>
  <link>https://newsletter.simpleaws.dev/p/aurora-dsql-the-postgresql-compatible-database-that-will-break-your-app-and-how-to-fix-it</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/aurora-dsql-the-postgresql-compatible-database-that-will-break-your-app-and-how-to-fix-it</guid>
  <pubDate>Thu, 12 Jun 2025 19:00:00 +0000</pubDate>
  <atom:published>2025-06-12T19:00:00Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Architecture]]></category>
    <category><![CDATA[Databases]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;"><a class="link" href="https://aws.amazon.com/rds/aurora/dsql/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aurora-dsql-the-postgresql-compatible-database-that-will-break-your-app-and-how-to-fix-it" target="_blank" rel="noopener noreferrer nofollow">Aurora DSQL</a> is the subject of this June 2025 launch-era review. Historical note (September 2026): Aurora DSQL now supports foreign keys, sequences and identity columns. The compatibility and migration discussion below reflects the original release, and the code examples still need a coordinated refresh. Marketed as the “<i>Fastest serverless distributed SQL database for always available applications</i>“, it promises PostgreSQL compatibility with virtually unlimited scale. But here&#39;s what AWS doesn&#39;t tell you upfront: it&#39;s going to break your existing PostgreSQL application. Not maybe. Not sometimes. It will break it (and that&#39;s actually by design).</p><p class="paragraph" style="text-align:left;">I&#39;ve been really excited about this since I heard about it at re:Invent 2024, but my reservations started when I talked to the team behind it and they told me there are no Foreign Keys. So now that it&#39;s on General Availability, I decided to spend a bit of time diving deep into Aurora DSQL and discovering all the ways this &quot;compatible&quot; database service differs from what you&#39;d expect. The good news? Once you understand what you&#39;re dealing with, DSQL becomes incredibly powerful. The bad news? You&#39;re going to need to rethink how you build database applications. It&#39;s true, there are no Foreign Keys.</p><h2 class="heading" style="text-align:left;" id="what-aurora-dsql-actually-is">What Aurora DSQL Actually Is</h2><p class="paragraph" style="text-align:left;">Aurora DSQL isn&#39;t just Aurora with auto-scaling bolted on (that would be too easy). It&#39;s a fundamentally different beast that happens to speak PostgreSQL&#39;s language. Think of it as a distributed, serverless SQL database that borrowed PostgreSQL&#39;s syntax but threw out most of the operational model you&#39;re used to.</p><p class="paragraph" style="text-align:left;">The architecture consists of four main layers that scale independently:</p><ul><li><p class="paragraph" style="text-align:left;">A relay and connectivity layer handling client connections.</p></li><li><p class="paragraph" style="text-align:left;">Compute and database engines processing your queries.</p></li><li><p class="paragraph" style="text-align:left;">A distributed transaction log managing concurrency and isolation.</p></li><li><p class="paragraph" style="text-align:left;">A distributed storage layer.</p></li></ul><p class="paragraph" style="text-align:left;">A control plane orchestrates everything across three Availability Zones, automatically scaling each component based on demand (which sounds great until you see the DPU bills).</p><p class="paragraph" style="text-align:left;">Here&#39;s where it gets interesting: DSQL uses Optimistic Concurrency Control instead of traditional locking. No more deadlocks, but also no more assuming your transaction will just work. Every write operation can fail at commit time if another transaction touched the same data, and your application needs to handle that gracefully. This isn&#39;t a edge case, it&#39;s normal operation.</p><p class="paragraph" style="text-align:left;">The service comes in two flavors: single-Region clusters that operate <a class="link" href="https://newsletter.simpleaws.dev/p/disaster-recovery-strategies-aws?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aurora-dsql-the-postgresql-compatible-database-that-will-break-your-app-and-how-to-fix-it" target="_blank" rel="noopener noreferrer nofollow">active-active</a> across three AZs within one AWS Region (meaning it&#39;s <a class="link" href="https://newsletter.simpleaws.dev/p/understanding-aws-high-availability-scalability?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aurora-dsql-the-postgresql-compatible-database-that-will-break-your-app-and-how-to-fix-it" target="_blank" rel="noopener noreferrer nofollow">highly available</a> out of the box), and multi-Region clusters that synchronously replicate across multiple regions with a &quot;witness&quot; region handling log consensus (much better than <a class="link" href="https://newsletter.simpleaws.dev/p/aurora-global-database-disaster-recovery-aws?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aurora-dsql-the-postgresql-compatible-database-that-will-break-your-app-and-how-to-fix-it" target="_blank" rel="noopener noreferrer nofollow">Aurora Global Databases</a>). The single-Region setup targets 99.99% availability, while multi-Region promises 99.999% (those extra nines cost extra, obviously).</p><p class="paragraph" style="text-align:left;">What makes this serverless is that you don&#39;t provision instances or manage scaling policies. DSQL automatically partitions your data based on primary key ranges and access patterns, spins up compute resources as needed, and bills you in Distributed Processing Units (DPUs) for actual usage. It&#39;s infrastructure abstraction taken to its logical extreme (for better and worse). At least it&#39;s truly truly serverless, not like <a class="link" href="https://newsletter.simpleaws.dev/p/is-amazon-aurora-serverless-actually-serverless?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aurora-dsql-the-postgresql-compatible-database-that-will-break-your-app-and-how-to-fix-it" target="_blank" rel="noopener noreferrer nofollow">Aurora Serverless</a>.</p><h2 class="heading" style="text-align:left;" id="the-three-things-that-will-surprise">The Three Things That Will Surprise You</h2><h3 class="heading" style="text-align:left;" id="1-optimistic-concurrency-control-wi">1. Optimistic Concurrency Control Will Change How You Code</h3><p class="paragraph" style="text-align:left;">Forget everything you know about database transactions. DSQL doesn&#39;t use locks during transaction execution, it validates conflicts only at commit time. This means any write transaction can fail with a serialization error, even if it ran perfectly during execution.</p><p class="paragraph" style="text-align:left;">When conflicts occur, you&#39;ll see PostgreSQL serialization errors with SQLSTATE &#39;40001&#39; or DSQL-specific &#39;OC000&#39; and &#39;OC001&#39; error codes. Your application must catch these and retry the entire transaction. Not some transactions. All transactions. Here&#39;s what robust retry logic looks like:</p><div class="codeblock"><pre><code>public void executeWithRetry(Runnable transactionLogic) &#123;
    int maxRetries = 3;
    int attempt = 0;
    
    while (attempt &lt; maxRetries) &#123;
        try &#123;
            transactionLogic.run();
            return; // Success
        &#125; catch (SQLException e) &#123;
            if (e.getSQLState().startsWith(&quot;OC&quot;) || 
                e.getSQLState().equals(&quot;40001&quot;)) &#123;
                attempt++;
                if (attempt &gt;= maxRetries) throw e;
                
                // Exponential backoff with jitter
                Thread.sleep((long) (Math.pow(2, attempt) * 100 + Math.random() * 100));
            &#125; else &#123;
                throw e; // Non-retryable error
            &#125;
        &#125;
    &#125;
&#125;
</code></pre></div><p class="paragraph" style="text-align:left;">The key insight: every operation must be idempotent. You can&#39;t just increment a counter and retry, you need to structure operations as &quot;set counter to X if current value is Y&quot; patterns. This fundamentally changes how you design business logic (trust me, it&#39;s a bigger shift than it sounds).</p><h3 class="heading" style="text-align:left;" id="2-postgre-sql-compatibility-has-som">2. PostgreSQL &quot;Compatibility&quot; Has Some Creative Interpretations</h3><p class="paragraph" style="text-align:left;">DSQL speaks PostgreSQL&#39;s wire protocol and supports a substantial subset of SQL, but &quot;compatible&quot; doesn&#39;t mean &quot;identical&quot;. The list of unsupported features reads like a PostgreSQL greatest hits album: <b>no foreign keys</b>, no triggers, no sequences, no PL/pgSQL, no user-defined functions except pure SQL ones. To be honest I can personally do without all of those, except foreign keys, that&#39;s the one thing I miss.</p><p class="paragraph" style="text-align:left;">The isolation level is fixed at Repeatable Read (you can&#39;t change it), transactions have size limits of 10 MiB and 3,000 row modifications, and you can&#39;t mix DDL and DML in the same transaction. Each transaction can contain exactly one DDL statement, period.</p><p class="paragraph" style="text-align:left;">Here&#39;s where it gets tricky for migrations. Take this seemingly innocent PostgreSQL table:</p><div class="codeblock"><pre><code>-- This works in PostgreSQL
CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    customer_id INTEGER REFERENCES customers(id),
    total NUMERIC(10,2),
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE TRIGGER update_customer_stats 
    AFTER INSERT ON orders 
    FOR EACH ROW 
    EXECUTE FUNCTION update_customer_totals();
</code></pre></div><p class="paragraph" style="text-align:left;">In DSQL, you&#39;d need to rewrite this as:</p><div class="codeblock"><pre><code>-- DSQL version requires major changes
CREATE TABLE orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    customer_id UUID,  -- No foreign key constraint
    total FLOAT,       -- NUMERIC not supported in keys
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Trigger logic moves to application layer (as it should!!)
-- Foreign key validation becomes application responsibility (this is annoying, but it&#39;s manageable)
</code></pre></div><p class="paragraph" style="text-align:left;">The absence of foreign keys means referential integrity becomes your application&#39;s job. No more relying on the database to prevent orphaned records (we&#39;ve all been there, and it&#39;s not fun to debug in production).</p><p class="paragraph" style="text-align:left;">The trick here is that foreign keys do exist in your data model, they&#39;re just no longer enforced by the database engine. You can still write relationships in your code, but if you want them enforced and want to catch the related bugs, you must do that at the application layer. Give it a long, hard thought, and you&#39;ll see that it&#39;s not the end of the world, just a quality of life tradeoff. But seriously, give it some thought. It took me 20 minutes of conversation to come to this conclusion.</p><h3 class="heading" style="text-align:left;" id="3-ddl-operations-are-distributed-tr">3. DDL Operations Are Distributed Transactions (With Consequences)</h3><p class="paragraph" style="text-align:left;">Schema changes in DSQL aren&#39;t simple metadata updates, they&#39;re strongly consistent distributed transactions across all cluster nodes. This has profound implications for how you manage schema migrations.</p><p class="paragraph" style="text-align:left;">Every DDL operation runs as a distributed transaction, which means it can fail due to concurrency conflicts just like regular transactions. You might see errors like &quot;ERROR: schema has been updated by another transaction, retry as needed (OC001)&quot; during migrations.</p><p class="paragraph" style="text-align:left;">The restrictions are strict: one DDL statement per transaction, no mixing DDL with DML, and schema changes can take time to propagate. But here&#39;s the superpower: <code>CREATE INDEX ASYNC</code> runs in the background without locking your table.</p><div class="codeblock"><pre><code>-- This runs immediately and returns a job_id
CREATE INDEX ASYNC idx_orders_customer 
ON orders(customer_id);

-- Monitor progress
SELECT * FROM sys.jobs WHERE job_id = &#39;your_job_id&#39;;

-- Wait for completion in scripts
SELECT sys.wait_for_job(&#39;your_job_id&#39;);
</code></pre></div><p class="paragraph" style="text-align:left;">Unlike PostgreSQL where <code>CREATE INDEX</code> can lock your table for hours on large datasets, DSQL&#39;s async approach lets you add indexes to active tables without downtime. This is genuinely game-changing for large-scale applications (just don&#39;t forget to check the job status).</p><h2 class="heading" style="text-align:left;" id="getting-aurora-dsql-actually-workin">Getting Aurora DSQL Actually Working</h2><p class="paragraph" style="text-align:left;">Let&#39;s get our hands dirty with real setup, because since I spent time researching this I&#39;ve decided you need to spend time on this as well. Also, it&#39;s important. DSQL requires IAM authentication (no traditional passwords), SSL connections (non-negotiable), and specific client configurations that differ from standard PostgreSQL.</p><h3 class="heading" style="text-align:left;" id="prerequisites-and-iam-setup">Prerequisites and IAM Setup</h3><p class="paragraph" style="text-align:left;">First, you need IAM permissions for both cluster management and database connections. Here&#39;s the minimum policy for getting started:</p><div class="codeblock"><pre><code>&#123;
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
        &#123;
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: [
                &quot;dsql:CreateCluster&quot;,
                &quot;dsql:GetCluster&quot;,
                &quot;dsql:ListClusters&quot;,
                &quot;dsql:DbConnectAdmin&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;
        &#125;
    ]
&#125;
</code></pre></div><p class="paragraph" style="text-align:left;">For production environments, you&#39;ll also need customer-managed KMS keys and VPC endpoint permissions, but this gets you connected (security hardening comes later).</p><h3 class="heading" style="text-align:left;" id="creating-your-first-cluster">Creating Your First Cluster</h3><p class="paragraph" style="text-align:left;">Single-region cluster creation is straightforward via the AWS CLI:</p><div class="codeblock"><pre><code># Create a basic single-region cluster
aws dsql create-cluster \
    --region us-east-1 \
    --deletion-protection-enabled \
    --tags Key=Environment,Value=development

# For multi-region (requires coordination)
aws dsql create-cluster \
    --region us-east-1 \
    --multi-region-properties &#39;&#123;
        &quot;witnessRegion&quot;: &quot;us-west-2&quot;,
        &quot;clusters&quot;: [&quot;arn:aws:dsql:us-east-2:account:cluster/peer-cluster&quot;]
    &#125;&#39;
</code></pre></div><p class="paragraph" style="text-align:left;">The cluster creation returns an ARN and endpoint URL. Save that endpoint, you&#39;ll need it for every connection (and it&#39;s not changing like RDS endpoints do during failovers).</p><h3 class="heading" style="text-align:left;" id="connecting-to-dsql-using-iam-authen">Connecting to DSQL Using IAM Authentication</h3><p class="paragraph" style="text-align:left;">DSQL connections require IAM tokens instead of passwords, and these tokens have dual expiry mechanics that will trip you up if you haven&#39;t used them before. The token expires (default 15 minutes, maximum 1 week), but the database connection also expires after one hour regardless of token validity.</p><p class="paragraph" style="text-align:left;">Generate tokens using the AWS CLI:</p><div class="codeblock"><pre><code># Generate admin token valid for 1 hour
aws dsql generate-db-connect-admin-auth-token \
    --hostname your-cluster-endpoint.dsql.amazonaws.com \
    --region us-east-1 \
    --expires-in 3600

# Generate token for specific database role
aws dsql generate-db-connect-auth-token \
    --hostname your-cluster-endpoint.dsql.amazonaws.com \
    --region us-east-1 \
    --username myuser \
    --expires-in 3600
</code></pre></div><p class="paragraph" style="text-align:left;">Now connect with psql (note the required SSL mode):</p><div class="codeblock"><pre><code>PGSSLMODE=require psql \
    --dbname postgres \
    --username admin \
    --host your-cluster-endpoint.dsql.amazonaws.com \
    --password  # Paste the IAM token when prompted
</code></pre></div><p class="paragraph" style="text-align:left;">For application connections, here&#39;s a Python example using psycopg:</p><div class="codeblock"><pre><code>import boto3
import psycopg
from urllib.parse import quote_plus

def get_dsql_connection():
    client = boto3.client(&#39;dsql&#39;, region_name=&#39;us-east-1&#39;)
    
    token = client.generate_db_connect_admin_auth_token(
        hostname=&#39;your-cluster-endpoint.dsql.amazonaws.com&#39;,
        expiresIn=3600
    )
    
    conn_string = (
        f&quot;postgresql://admin:&#123;quote_plus(token)&#125;@&quot;
        f&quot;your-cluster-endpoint.dsql.amazonaws.com:5432/postgres&quot;
        f&quot;?sslmode=require&quot;
    )
    
    return psycopg.connect(conn_string)
</code></pre></div><p class="paragraph" style="text-align:left;">Still with me? The connection complexity is real, but it&#39;s the price of IAM integration (and honestly it&#39;s more secure than password-based auth once you get the patterns down).</p><h3 class="heading" style="text-align:left;" id="schema-design-patterns">Schema Design Patterns</h3><p class="paragraph" style="text-align:left;">Once connected, your first decision is primary key strategy. For write-heavy tables, avoid sequential integers like the plague, they create hotspots in DSQL&#39;s distributed storage:</p><div class="codeblock"><pre><code>-- Good: Random distribution for high-write tables
CREATE TABLE events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID,
    event_type VARCHAR(50),
    data JSONB,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Bad: Sequential keys create write hotspots
CREATE TABLE events_bad (
    id SERIAL PRIMARY KEY,  -- This will bottleneck
    user_id INTEGER,
    event_type VARCHAR(50),
    data JSONB,
    created_at TIMESTAMP
);
</code></pre></div><p class="paragraph" style="text-align:left;">Create indexes asynchronously to avoid blocking operations:</p><div class="codeblock"><pre><code>-- Start async index creation
CREATE INDEX ASYNC idx_events_user_type 
ON events(user_id, event_type);

-- Check progress
SELECT job_id, status, progress_percentage 
FROM sys.jobs 
WHERE operation_type = &#39;CREATE_INDEX&#39;;
</code></pre></div><p class="paragraph" style="text-align:left;">Key sizing limits apply: primary keys can&#39;t exceed 1 KiB total size, individual columns in keys max out at 255 bytes, and primary keys can include up to 8 columns. Plan accordingly (<code>VARCHAR(320)</code> email fields won&#39;t work in composite keys).</p><h2 class="heading" style="text-align:left;" id="where-aurora-dsql-gets-expensive-an">Where Aurora DSQL Gets Expensive (And How to Avoid It)</h2><p class="paragraph" style="text-align:left;">DSQL&#39;s pricing model using Distributed Processing Units (DPUs) can surprise you. Unlike provisioned databases where you pay for capacity whether you use it or not, DSQL bills for actual computation, I/O, and storage operations. This sounds great until you realize inefficient queries now directly impact your AWS bill instead of just wasting already wasted capacity.</p><h3 class="heading" style="text-align:left;" id="understanding-dpu-consumption-patte">Understanding DPU Consumption Patterns</h3><p class="paragraph" style="text-align:left;">DPUs measure normalized compute units across different operations. Here&#39;s what drives costs:</p><ul><li><p class="paragraph" style="text-align:left;"><b>WriteDPU</b>: Write operations including inserts, updates, deletes</p></li><li><p class="paragraph" style="text-align:left;"><b>ReadDPU</b>: Query processing for SELECT operations</p></li><li><p class="paragraph" style="text-align:left;"><b>ComputeDPU</b>: CPU-intensive operations like complex joins and aggregations</p></li><li><p class="paragraph" style="text-align:left;"><b>MultiRegionWriteDPU</b>: Additional overhead for multi-region write replication</p></li></ul><p class="paragraph" style="text-align:left;">Monitor these with CloudWatch:</p><div class="codeblock"><pre><code># Set up CloudWatch alarms for DPU spikes
aws cloudwatch put-metric-alarm \
    --alarm-name &quot;DSQL-HighDPU&quot; \
    --alarm-description &quot;Aurora DSQL high DPU usage&quot; \
    --metric-name TotalDPU \
    --namespace AWS/AuroraDSQL \
    --statistic Average \
    --period 300 \
    --threshold 1000 \
    --comparison-operator GreaterThanThreshold \
    --dimensions Name=ResourceId,Value=your-cluster-id
</code></pre></div><p class="paragraph" style="text-align:left;">A single inefficient query scanning millions of rows can generate hundreds of DPUs in minutes. Query optimization isn&#39;t just about performance anymore, it&#39;s about keeping your AWS bill reasonable (ask me how I learned this).</p><h3 class="heading" style="text-align:left;" id="primary-key-design-impact-on-perfor">Primary Key Design Impact on Performance and Cost</h3><p class="paragraph" style="text-align:left;">Remember how I mentioned UUIDs for write-heavy tables? Here&#39;s why it matters for your wallet. Sequential primary keys concentrate writes on single partitions (remember that the D in DSQL stands for Distributed), forcing DSQL to scale those specific partitions while others sit idle. This inefficient resource utilization shows up in your DPU costs.</p><div class="codeblock"><pre><code>-- This table design will cost you more
CREATE TABLE audit_log (
    id BIGINT PRIMARY KEY,  -- Sequential writes = hotspot
    action VARCHAR(100),
    timestamp TIMESTAMP,
    user_id UUID
);

-- This distributes load and costs
CREATE TABLE audit_log_optimized (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),  -- Distributes writes
    action VARCHAR(100),
    timestamp TIMESTAMP,
    user_id UUID
);

-- Create covering index for common queries
CREATE INDEX ASYNC idx_audit_user_time 
ON audit_log_optimized(user_id, timestamp);
</code></pre></div><p class="paragraph" style="text-align:left;">The covering index is an additional trick that prevents expensive primary key lookups for common query patterns, reducing ReadDPUs significantly.</p><h3 class="heading" style="text-align:left;" id="connection-pools-in-aurora-dsql">Connection Pools in Aurora DSQL</h3><p class="paragraph" style="text-align:left;">Traditional connection pools assume connections stay valid until explicitly closed or the database terminates them. DSQL breaks this assumption with dual expiry: IAM tokens expire, and connections automatically terminate after one hour.</p><p class="paragraph" style="text-align:left;">Your connection pool needs to handle both scenarios:</p><div class="codeblock"><pre><code>import time
from dataclasses import dataclass
from typing import Optional

@dataclass
class DSQLConnection:
    connection: psycopg.Connection
    created_at: float
    token_expires_at: float
    
    def is_valid(self) -&gt; bool:
        now = time.time()
        # Check both token expiry and DSQL&#39;s 1-hour limit
        return (now &lt; self.token_expires_at and 
                now &lt; self.created_at + 3540)  # 59 minutes

class DSQLConnectionPool:
    def __init__(self, min_connections=2, max_connections=10):
        self.min_connections = min_connections
        self.max_connections = max_connections
        self.connections = []
        self.dsql_client = boto3.client(&#39;dsql&#39;)
    
    def get_connection(self) -&gt; psycopg.Connection:
        # Remove expired connections
        self.connections = [c for c in self.connections if c.is_valid()]
        
        if not self.connections:
            return self._create_new_connection()
        
        return self.connections.pop().connection
    
    def _create_new_connection(self) -&gt; psycopg.Connection:
        token = self.dsql_client.generate_db_connect_admin_auth_token(
            hostname=&#39;your-cluster-endpoint.dsql.amazonaws.com&#39;,
            expiresIn=3600  # 1 hour
        )
        
        conn = psycopg.connect(
            f&quot;postgresql://admin:&#123;quote_plus(token)&#125;@&quot;
            f&quot;your-cluster-endpoint.dsql.amazonaws.com:5432/postgres&quot;
            f&quot;?sslmode=require&quot;
        )
        
        return conn
</code></pre></div><p class="paragraph" style="text-align:left;">This complexity is manageable with the right abstractions, but it&#39;s definitely not plug-and-play with existing connection pooling libraries (most will need configuration adjustments at minimum).</p><h3 class="heading" style="text-align:left;" id="query-optimization-for-occ-and-tran">Query Optimization for OCC and Transaction Limits</h3><p class="paragraph" style="text-align:left;">DSQL&#39;s transaction limits (10 MiB data modified, 3,000 rows affected) require batching strategies for large operations:</p><div class="codeblock"><pre><code>def batch_insert_with_retry(records, batch_size=1000):
    for i in range(0, len(records), batch_size):
        batch = records[i:i+batch_size]
        
        execute_with_retry(lambda: insert_batch(batch))

def insert_batch(records):
    with get_connection() as conn:
        with conn.cursor() as cur:
            # Use COPY for efficient bulk inserts
            with cur.copy(&quot;COPY events (user_id, event_type, data) FROM STDIN&quot;) as copy:
                for record in records:
                    copy.write_row([record.user_id, record.event_type, record.data])
</code></pre></div><p class="paragraph" style="text-align:left;">The COPY command is significantly more DPU-efficient than individual INSERT statements for bulk operations, but you still need to respect the transaction size limits.</p><h2 class="heading" style="text-align:left;" id="the-missing-pieces-you-need-to-know">The Missing Pieces You Need to Know</h2><p class="paragraph" style="text-align:left;">Let&#39;s talk about the operational aspects AWS doesn&#39;t emphasize in the marketing materials. DSQL is powerful, but it requires new operational patterns and monitoring approaches.</p><h3 class="heading" style="text-align:left;" id="monitoring-and-alerting-that-actual">Monitoring and Alerting That Actually Matters</h3><p class="paragraph" style="text-align:left;">Standard PostgreSQL monitoring tools won&#39;t help you with DSQL 🫠. You need CloudWatch dashboards focused on DSQL-specific metrics:</p><div class="codeblock"><pre><code># Create custom CloudWatch dashboard
aws cloudwatch put-dashboard \
    --dashboard-name &quot;Aurora-DSQL-Operations&quot; \
    --dashboard-body &#39;&#123;
        &quot;widgets&quot;: [
            &#123;
                &quot;type&quot;: &quot;metric&quot;,
                &quot;properties&quot;: &#123;
                    &quot;metrics&quot;: [
                        [&quot;AWS/AuroraDSQL&quot;, &quot;TotalDPU&quot;, &quot;ResourceId&quot;, &quot;your-cluster-id&quot;],
                        [&quot;AWS/AuroraDSQL&quot;, &quot;OccConflicts&quot;, &quot;ResourceId&quot;, &quot;your-cluster-id&quot;],
                        [&quot;AWS/AuroraDSQL&quot;, &quot;QueryTimeouts&quot;, &quot;ResourceId&quot;, &quot;your-cluster-id&quot;]
                    ],
                    &quot;period&quot;: 300,
                    &quot;stat&quot;: &quot;Average&quot;,
                    &quot;region&quot;: &quot;us-east-1&quot;,
                    &quot;title&quot;: &quot;DSQL Performance Metrics&quot;
                &#125;
            &#125;
        ]
    &#125;&#39;
</code></pre></div><p class="paragraph" style="text-align:left;">Critical metrics to monitor:</p><ul><li><p class="paragraph" style="text-align:left;"><b>OccConflicts</b>: High values indicate data contention requiring application logic review</p></li><li><p class="paragraph" style="text-align:left;"><b>QueryTimeouts</b>: Suggests queries hitting DSQL&#39;s transaction time limits</p></li><li><p class="paragraph" style="text-align:left;"><b>TotalDPU</b>: Your primary cost indicator and performance baseline</p></li><li><p class="paragraph" style="text-align:left;"><b>CommitLatency</b>: Increased latency can indicate replication or storage issues</p></li></ul><p class="paragraph" style="text-align:left;">Set aggressive alarms on these metrics. OCC conflicts above 5% of total transactions usually indicate design problems that will only get worse under load.</p><h3 class="heading" style="text-align:left;" id="backup-and-recovery-strategy-its-di">Backup and Recovery Strategy (It&#39;s Different)</h3><p class="paragraph" style="text-align:left;">DSQL integrates with AWS Backup, not native database snapshots. This means backup policies, retention, and restoration work through the AWS Backup service:</p><div class="codeblock"><pre><code># Create backup plan for DSQL
aws backup create-backup-plan \
    --backup-plan &#39;&#123;
        &quot;BackupPlanName&quot;: &quot;dsql-daily-backups&quot;,
        &quot;Rules&quot;: [&#123;
            &quot;RuleName&quot;: &quot;daily-backup&quot;,
            &quot;TargetBackupVault&quot;: &quot;default&quot;,
            &quot;ScheduleExpression&quot;: &quot;cron(0 2 * * ? *)&quot;,
            &quot;Lifecycle&quot;: &#123;
                &quot;DeleteAfterDays&quot;: 30
            &#125;
        &#125;]
    &#125;&#39;
</code></pre></div><p class="paragraph" style="text-align:left;">Multi-region cluster restoration requires coordination between AWS Backup and DSQL APIs, and it&#39;s currently only supported via CLI (not the console). Plan your disaster recovery procedures accordingly, and test them. The restore process creates new clusters rather than in-place restoration, so test a lot.</p><h3 class="heading" style="text-align:left;" id="security-configuration-for-producti">Security Configuration for Production</h3><p class="paragraph" style="text-align:left;"><a class="link" href="https://newsletter.simpleaws.dev/p/ec2-s3-vpc-endpoint-secure?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aurora-dsql-the-postgresql-compatible-database-that-will-break-your-app-and-how-to-fix-it" target="_blank" rel="noopener noreferrer nofollow">VPC endpoints</a> are essential for production deployments of anything that doesn&#39;t live in your VPCs, and Aurora DSQL (unlike regular Aurora) doesn&#39;t go into your VPC. DSQL requires separate endpoints for management operations and database connections:</p><div class="codeblock"><pre><code># Create VPC endpoint for database connections
aws ec2 create-vpc-endpoint \
    --vpc-id vpc-12345678 \
    --service-name $(aws dsql get-vpc-endpoint-service-name \
                    --cluster-identifier your-cluster-arn \
                    --query ServiceName --output text) \
    --vpc-endpoint-type Interface \
    --subnet-ids subnet-12345678 subnet-87654321 \
    --security-group-ids sg-12345678 \
    --private-dns-enabled
</code></pre></div><h3 class="heading" style="text-align:left;" id="migration-planning-reality-check">Migration Planning Reality Check</h3><p class="paragraph" style="text-align:left;">Here&#39;s the hard truth: DSQL isn&#39;t a drop-in replacement for PostgreSQL, despite the compatibility marketing. Successful migrations require systematic assessment and application refactoring.</p><p class="paragraph" style="text-align:left;">Assessment framework:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Dependency Analysis</b>: Catalog usage of unsupported features (foreign keys, triggers, sequences, PL/pgSQL)</p></li><li><p class="paragraph" style="text-align:left;"><b>Transaction Pattern Review</b>: Identify long-running transactions, mixed DDL/DML operations, large batch operations</p></li><li><p class="paragraph" style="text-align:left;"><b>Query Complexity Evaluation</b>: Find queries that might hit transaction time limits or benefit from different indexing strategies</p></li><li><p class="paragraph" style="text-align:left;"><b>Connection Management Audit</b>: Assess current pooling strategies and connection lifecycle management</p></li></ol><p class="paragraph" style="text-align:left;">For applications heavily dependent on PostgreSQL-specific features, consider DSQL for new <a class="link" href="https://newsletter.simpleaws.dev/p/microservices-aws-migrating-from-monolith?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aurora-dsql-the-postgresql-compatible-database-that-will-break-your-app-and-how-to-fix-it" target="_blank" rel="noopener noreferrer nofollow">microservices</a> rather than migrating existing monoliths. The serverless scaling and operational simplicity make it excellent for greenfield projects designed with its constraints in mind.</p><p class="paragraph" style="text-align:left;">The learning curve is real, but once you adjust to the OCC model and embrace the serverless operational paradigm, DSQL delivers on its scalability promises. Just don&#39;t expect it to be PostgreSQL with better marketing (because it&#39;s not, and that&#39;s actually the point).</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>Microservices vs. Agentic AI (Part 4): Agentic Microservices</title>
  <description>Explore hybrid AI-agent and microservice patterns, including tool APIs, orchestration, identity propagation, consistency, and team skills.</description>
  <link>https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-4-agentic-microservices</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-4-agentic-microservices</guid>
  <pubDate>Mon, 12 May 2025 16:20:41 +0000</pubDate>
  <atom:published>2025-05-12T16:20:41Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Artificial Intelligence]]></category>
    <category><![CDATA[Architecture]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">This is part 4 of this Microservices vs Agentic AI series. In the first two parts of this series we laid out the core distinctions between <b>Microservice</b> and <b>Agentic AI</b> architectures. <a class="link" href="https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-1?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">Part 1</a> traced their separate reasons for being: Microservices emerged to solve the challenges of monolithic scale by decomposing applications along <b>business domain</b> lines. Agentic AI, on the other hand, leverages the recent power of <b>Large Language Models (LLMs)</b> to tackle complex <b>tasks</b> through autonomous reasoning and functional decomposition. This led us to see how their fundamental approaches to autonomy and specialization naturally diverge.</p><p class="paragraph" style="text-align:left;"><a class="link" href="https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-2?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">Part 2</a> then took us into the thick of their runtime behaviors. We contrasted the predictable, deterministic logic common in Microservice communication and state management (using Sagas for eventual consistency) with the intelligent, context-rich flows, operational memory, and inherent non-determinism that characterize Agentic AI systems and their unique pattern needs (and the unique challenges they bring).</p><p class="paragraph" style="text-align:left;">Following that, <a class="link" href="https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-3?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">Part 3</a> led us deep into the operational realities. We considered the demands and challenges of operating agentic systems, compared to the more understood load-based, horizontal scaling of microservices. We discussed the distinct resilience challenges posed by infrastructure failures versus cognitive ones, the critical differences in observability needs (seeing the &quot;why&quot; of AI decisions), and the current state of tooling and the MLOps/LLMOps frontier. We also briefly touched upon the cost structures of both architectures, with a focus on understanding LLM token consumption.</p><p class="paragraph" style="text-align:left;">Now that we&#39;ve established this comprehensive picture of their individual characteristics, from foundational philosophy through runtime execution to operational demands, I&#39;d like to propose something different for this fourth and final article on this series. We&#39;re going to explore how these two architectural paradigms can be combined, and what that means for system design. Beyond the technical integration, we&#39;ll examine the<b> mindset shifts</b> required from the teams involved. And finally, we&#39;ll look towards <b>future considerations</b>, especially the continued operational importance of security and ethics. The goal is to put everything together and offer a complete perspective. Let&#39;s hope I can pull that off.</p><h2 class="heading" style="text-align:left;" id="hybrid-architecture-patterns-in-pra">Hybrid Architecture Patterns in Practice</h2><p class="paragraph" style="text-align:left;">Let me kick this off by saying I am not recommending you build a hybrid system with both microservices and AI agents just so you can “get the benefits of both patterns” or something like that. That&#39;s the completely wrong approach to architecture. I&#39;m not saying either that you should never combine them. The purpose of this section is to highlight the areas to pay special attention to if you ever find the need to add agentic capabilities to some parts of a microservices architecture, or vice versa. Basically, to warn you of where things will likely get extra difficult, the points where the complexities of each pattern overlap.</p><h3 class="heading" style="text-align:left;" id="common-integration-patterns">Common Integration Patterns</h3><p class="paragraph" style="text-align:left;">These are the most obvious ways in which these two patterns would combine:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Agent as Intelligent Facade:</b> The agent provides a natural language (or other intuitive) interface, understanding user intent and translating it into calls to existing backend microservice APIs. <i>Example:</i> A user asks the OmniMart chatbot, &quot;Can I get free shipping on order 12345 to my alternate address?&quot;. The agent parses this, calls the <code>Order Service</code> API for order details, the <code>Customer Profile Service</code> API for addresses, and potentially a <code>Shipping Policy Service</code> API (or consults a KB), synthesizes the answer, and responds naturally. Basically the agent is the user interface, and the microservices are called as tools by that agent instead of (or in addition to) as a backend by a typical frontend.</p></li><li><p class="paragraph" style="text-align:left;"><b>Microservice as Reliable Tool:</b> Agents offload complex, sensitive, or mission-critical business logic execution to dedicated microservices, treating them as reliable tools. <i>Example:</i> The OmniMart financial planning agent uses a validated <code>PortfolioRiskCalculationService</code> microservice instead of trying to implement complex financial math via prompts. The agent focuses on goal setting and strategy, the microservice provides reliable calculation. This looks very similar to the above, but the core distinction is that in <b>Agent as Intelligent Facade</b> you start from the actions (microservices) and then expose them via an agent. In <b>Microservice as Reliable Tool</b> the core of the system is the agent, and microservices just solve things that LLMs are bad at.</p></li><li><p class="paragraph" style="text-align:left;"><b>Event-Driven Trigger:</b> Microservices emit business events (e.g., using Amazon EventBridge) that trigger agentic processes for deeper analysis or complex follow-up actions. <i>Example:</i> OmniMart <code>Fraud Detection Service</code> (a microservice using programmatic rules) detects a potentially suspicious pattern and emits a <code>MediumRiskTransactionEvent</code>. An <b>Investigation Agent</b> subscribed to the topic receives the event, then uses tools to pull data from multiple other services (user history, location data, etc.), performs more complex reasoning, and decides whether to escalate to a human analyst or automatically block the transaction via another tool call. This way we&#39;d be using agents and microservices interchangeably, where components of an <a class="link" href="https://newsletter.simpleaws.dev/p/event-driven-architecture-aws-benefits-use-cases?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">Event-Driven Architecture</a> emit events and those are handled by other components, and any component can be a microservice or an agent. One thing to keep in mind is that agents are non-deterministic, and that will complicate <a class="link" href="https://newsletter.simpleaws.dev/p/distributed-transactions-event-driven-architectures?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">distributed transactions</a>.</p></li><li><p class="paragraph" style="text-align:left;"><b>Agent Orchestrating Microservices:</b> An agent&#39;s plan involves coordinating a sequence of actions across multiple microservices. <i>Example:</i> The OmniMart returns agent, after interacting with the customer and approving an exception based on complex logic, calls the <code>Order Service</code> API (tool) to mark the item for return and the <code>Logistics Service</code> API (tool) to schedule a pickup. This is pretty similar to <b>Microservice as Reliable Tool</b>, but the microservices aren&#39;t just potentially useful tools for the agent, the tools are the end goal and the purpose of the agent is to orchestrate them. It would be like a very smart <a class="link" href="https://newsletter.simpleaws.dev/p/event-driven-architecture-patterns?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">orchestrator pattern</a>.</p></li></ul><h3 class="heading" style="text-align:left;" id="challenges-of-the-hybrid-world">Challenges</h3><p class="paragraph" style="text-align:left;">While all of the above sounds like a great idea to implement, it comes with a few operational challenges:</p><ul><li><p class="paragraph" style="text-align:left;"><b>End-to-End Observability:</b> Tracing a logical operation becomes significantly harder when it crosses between the agent&#39;s reasoning steps (often opaque) and multiple microservice calls (potentially asynchronous). Correlating logs and metrics across these two types of components requires meticulous instrumentation, context propagation (passing trace IDs), and potentially specialized tooling beyond standard distributed tracing offered by AWS X-Ray alone. Debugging failures that span the boundaries of microservices and agents are going to be particularly challenging, even more so than failures that just span multiple microservices.</p></li><li><p class="paragraph" style="text-align:left;"><b>Security Context Propagation:</b> How is the user&#39;s identity or authorization context securely passed from the initial interaction with the agent through its subsequent calls to backend microservices? Mechanisms like secure token exchange or carefully managed IAM permissions for agent tools are needed, adding complexity. The key problem here is that, as I&#39;ve said many times in this series, agentic AI is still a new thing, and we don&#39;t have the generally accepted patterns and shared best practices that we can count on for microservices.</p></li><li><p class="paragraph" style="text-align:left;"><b>Deployment Dependencies & Contracts:</b> Changes to a microservice API used as a tool directly impact the agent. This requires careful versioning, communication between teams, and potentially updating the agent&#39;s tool description, prompts, or even retraining its ability to use the tool correctly. The annoying part here is that changes that don&#39;t add capabilities, e.g. a reduction in latency, would typically not affect the caller of a microservice, but if the caller is an AI agent then these aspects matter in tool selection, and need to be reflected in the tool&#39;s description. We&#39;ve inadvertently introduced more coupling than what we&#39;re used to when dealing with APIs, and broken a fundamental tenet of microservices: callers only depend on the exposed interfaces and contracts, not on implementation details.</p></li><li><p class="paragraph" style="text-align:left;"><b>Consistency Management:</b> If an agent orchestrates actions that modify state across multiple microservices, ensuring overall transactional integrity becomes very challenging. Implementing a Saga pattern <i>driven by the agent&#39;s logic</i> is complex and potentially fragile compared to service-level Sagas (remember that LLMs are non-deterministic!). Careful design is needed to avoid leaving backend systems in inconsistent states if the agent&#39;s workflow fails mid-process. To be fair, a perfect distributed system would already be designed to recover from any kind of failure, so we already know how to handle this. But of course, no system is really perfect, and those imperfections might become more evident with LLM hallucinations having a much higher probability of occurrence than network failures.</p></li></ul><h2 class="heading" style="text-align:left;" id="people-and-process-the-socio-techni">People and Process: The Socio-Technical Shift</h2><p class="paragraph" style="text-align:left;">This section is intended to help those who already work with microservices and would like to add AI capabilities to your software. It&#39;s based on my experiences at <a class="link" href="https://caylent.com/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">Caylent</a> building really interesting Generative AI applications, and a few discussions with some friends who tried to build GenAI teams.</p><h3 class="heading" style="text-align:left;" id="evolving-team-structures-and-skills">Evolving Team Structures and Skillsets</h3><p class="paragraph" style="text-align:left;">Microservice development often relies on cross-functional teams blending at least backend and infrastructure, and ideally with some decent domain expertise. Building sophisticated agentic systems requires a broader skillset across the team:</p><ul><li><p class="paragraph" style="text-align:left;"><b>AI/ML Expertise:</b> The obvious skills here are model understanding, evaluation and optimization. But the real AI/ML experts (not just GenAI experts) also bring a lot of relevant experience. Remember that GenAI and LLMs are just a special flavor of ML, and while the uses are different, a lot of the building blocks are similar enough. Look out especially for <b>MLOps</b> expertise, and knowledge of how to prepare and manage <b>data</b>. If you get <b>LLMOps</b>, you&#39;ve hit the jackpot.</p></li><li><p class="paragraph" style="text-align:left;"><b>Prompt Engineering:</b> This sounds completely trivial now that we have reasoning models, and fortunately that ridiculous hype of “everyone will just be a prompt engineer” died really quickly. You don&#39;t need a prompt engineer. But you do need someone to understand how prompting works, including <a class="link" href="https://newsletter.simpleaws.dev/p/amazon-bedrock-prompt-caching?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">prompt caching</a>, <a class="link" href="https://caylent.com/blog/reducing-gen-ai-cost-5-strategies?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">cost optimization</a>, and other stuff related to prompts. At least one person in the team should have a good idea about this.</p></li><li><p class="paragraph" style="text-align:left;"><b>Software and Cloud Engineering:</b> Do not try to build software without software engineers, and do not deploy to the cloud without cloud engineering. I know a lot of brilliant people who I&#39;m pretty sure could code an LLM from scratch, but who don&#39;t know <a class="link" href="https://newsletter.simpleaws.dev/p/aws-lambda-underlying-architecture?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">how AWS Lambda works</a> or <a class="link" href="https://newsletter.simpleaws.dev/p/understanding-aws-high-availability-scalability?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">how to achieve high availability and scalability</a>. Generative AI applications need generative AI skills, but they also need application skills.</p></li><li><p class="paragraph" style="text-align:left;"><b>Data Science/Engineering:</b> This is a bit more situational, and will depend on the exact skillset of your AI/ML experts and how you manage data. Creating and using a Knowledge Base should be easy enough with GenAI expertise, even the very fine details like choosing the embeddings size, optimizing the vector database, or fine-tuning the embeddings model. However, if you need to do some data processing, you&#39;ll need someone with data science and/or data engineering skills. Any cloud engineer can stand up a data pipeline on AWS, and any software engineer can write the ETL code (it&#39;s just the MapReduce pattern). But without the right conceptual and domain knowledge you&#39;ll likely be processing useless data and reaching wrong conclusions.</p></li><li><p class="paragraph" style="text-align:left;"><b>Domain Expertise:</b> This is always super important. But in my experience, it&#39;s even more important for Generative AI applications. We&#39;re collectively still discovering where GenAI is most useful, and where it sounds useful on paper but isn&#39;t reliable or cheap enough yet. I work at a consulting company, so helping companies figure out the business impact of our work is second nature at this point. But I&#39;ve found that while building blindly is always bad, building GenAI stuff blindly is even worse than usual. Btw I dumped a lot of thoughts on this in the <a class="link" href="https://caylent.com/blog/the-2025-outlook-on-generative-ai-whitepaper?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">2025 Outlook on GenAI whitepaper</a> I co-wrote with our head of AI.</p></li><li><p class="paragraph" style="text-align:left;"><b>UI/UX Design:</b> In many cases, AI is redefining the way users interact with software. In many cases we&#39;ve traded screens filled with buttons for a chatbot that provides easier access to the same functionality. Sometimes that&#39;s not the best decision. On some occasions you need a combination of both, with either a dashboard on screen and a chat to ask more questions, or a chatbot that can output graphs instead of just text. Sometimes we&#39;re just making backend changes but the whole expectation shifts a bit (remember non-determinism!!). And sometimes we want to do away with screens entirely, e.g. with <a class="link" href="https://caylent.com/blog/introducing-amazon-nova-sonic?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">Speech-to-Speech models</a>. There are no clear patterns yet, so you need someone capable of understanding and especially designing how users interact.</p></li></ul><p class="paragraph" style="text-align:left;">Smaller teams will have a hard time including all the skills I listed above, and you&#39;re going to need to make compromises. When you do, be aware of what you&#39;ll need and why, and I&#39;d suggest don&#39;t hesitate to rely on consultants or fractional resources, either for fewer hours a day or for just a few weeks.</p><p class="paragraph" style="text-align:left;">Organizations that need to field teams like this multiple times will likely gravitate towards centralized expertise, just like we already do with cloud or security: Cloud Center of Excellence, Security Operations Center, platform engineering with best practices. I believe that&#39;s a good investment for the long term. However, for the shorter term the practice that I&#39;ve seen have the biggest impact is to create a culture of sharing knowledge. Many times an engineer with some knowledge can get you 80% of the way there, and you can bring in the experts for a few hours to help push the rest of the way. Moreover, sharing the lessons from different projects helps everyone grow. So right now I&#39;d put my focus there: Build cross-functional teams as good as you can, and create a culture of sharing knowledge across the entire company.</p><h2 class="heading" style="text-align:left;" id="conclusion">Conclusion</h2><p class="paragraph" style="text-align:left;">And with that, our deep dive comparison into Microservice and Agentic AI architectures comes to an end. Across these four parts, we&#39;ve journeyed from their foundational principles and distinct origins (<a class="link" href="https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-1?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">Part 1</a>), through their contrasting runtime dynamics concerning communication, state, and predictability (<a class="link" href="https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-2?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">Part 2</a>), into the practical operational realities of scale, resilience, observability, tooling, deployment, and cost (<a class="link" href="https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-3?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-4-agentic-microservices" target="_blank" rel="noopener noreferrer nofollow">Part 3</a>), and finally, explored the integration points and challenges in hybrid systems and what all of this means for the humans involved (Part 4, this one).</p><p class="paragraph" style="text-align:left;">While both architectural styles leverage decomposition and operate as distributed systems, they are fundamentally different tools designed for different primary purposes. Microservices offer a mature, robust paradigm for structuring large applications around business domains, optimizing for engineering lifecycle agility, operational scalability, and reliability through well-understood patterns and a rich tooling ecosystem. Agentic AI provides a powerful, though operationally less mature, paradigm for automating complex tasks, enabling autonomous reasoning and action, and creating intelligent interactions by orchestrating LLMs, tools, and knowledge.</p><p class="paragraph" style="text-align:left;">This series was never intended as a guide for choosing one <i>over</i> the other as if they were direct competitors for every problem. My initial goal was to uncover useful patterns and lessons from microservices that could be applied to Agentic AI, under the assumption that since both patterns are based on decomposition, there would be a lot of similarities (in fact, I said multiple times that agents were just LLM-based microservices). I&#39;ve failed at that goal: I found a lot more differences than similarities, and few transferrable lessons.</p><p class="paragraph" style="text-align:left;">But even if I failed at my original goal, I hope exploring both patterns, with their respective strengths, weaknesses, unique characteristics, and operational demands, still yielded some value to you. My hope is that understanding these nuances allows you to avoid misapplying principles from one domain to the other, to set realistic expectations, and, most importantly, to identify opportunities where their strengths can be combined.</p><p class="paragraph" style="text-align:left;">As LLMs keep percolating the software world, I don&#39;t think I&#39;m too crazy if I predict that the future of many complex systems will likely be hybrid, blending the deterministic reliability of microservices for core business logic and data management with the adaptive intelligence of agents for sophisticated interaction, automation, and reasoning. So this should all be useful as we move towards that future.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>Microservices vs. Agentic AI (Part 3): Operations and Costs</title>
  <description>Comparing Microservice &amp; Agentic AI operations. Deep dive into scale/resilience implications, observability, tooling, MLOps, and cost structures. Part 3.</description>
  <link>https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-3</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-3</guid>
  <pubDate>Mon, 05 May 2025 00:19:40 +0000</pubDate>
  <atom:published>2025-05-05T00:19:40Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Artificial Intelligence]]></category>
    <category><![CDATA[Architecture]]></category>
    <category><![CDATA[Operations]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">In the first two parts of this series we established a clear picture of the fundamental differences between <b>Microservice</b> and <b>Agentic AI</b> architectures. <a class="link" href="https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-1?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">Part 1</a> traced their origins and motivations, revealing how Microservices evolved to solve software lifecycle and scaling challenges by decomposing applications along <b>business domain</b> lines, while Agentic AI leverages <b>Large Language Model (LLM)</b> breakthroughs to automate complex <b>tasks</b> through autonomous reasoning and action. <a class="link" href="https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-2?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">Part 2</a> explored the consequences for runtime behavior, contrasting Microservices&#39; reliance on defined APIs, &quot;dumb pipes,&quot; deterministic logic, and established patterns for eventual data consistency with Agentic AI&#39;s use of intelligent flows, contextual memory, cognitive reliability patterns, and inherent non-determinism.</p><p class="paragraph" style="text-align:left;">These foundational and runtime differences inevitably lead to distinct operational challenges and realities. Now, in this third part, we&#39;re going to dive into the operations side: How do we effectively operate, scale, and ensure the resilience of these systems in production? What are the real-world complexities surrounding observability, tooling, deployment, and lifecycle management? How do their cost structures differ, and what optimization strategies are available, particularly on AWS? Our goal remains not to prescribe one architecture over the other, but to understand the operational landscape of each, learning from their contrasts to make better-informed design and operational decisions. And to have fun while thinking about complex stuff.</p><h2 class="heading" style="text-align:left;" id="scale-and-resilience-implications">Scale and Resilience Implications</h2><p class="paragraph" style="text-align:left;">Operating distributed systems at scale always involves managing scalability and resilience, but the specifics differ significantly. Let&#39;s use this as the first angle of comparison in this part.</p><h3 class="heading" style="text-align:left;" id="complexities-of-scaling-ai-agents">Complexities of Scaling AI Agents</h3><p class="paragraph" style="text-align:left;">Microservice scalability requires good engineering, but it follows relatively well-understood operational patterns. The primary goal is handling request load for specific services, which involves configuring <b>horizontal scaling</b> mechanisms. On AWS, this means setting up Auto Scaling Groups for EC2 instances, configuring Service Auto Scaling for <a class="link" href="https://newsletter.simpleaws.dev/p/ecs-basics-and-tips?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">Amazon ECS</a> tasks, or Kubernetes Horizontal Pod Autoscaler for <a class="link" href="https://newsletter.simpleaws.dev/p/kubernetes-aws-basics-best-practices?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">EKS</a> deployments alongside Cluster Autoscaler or Karpenter for node capacity, or relying on the inherent <a class="link" href="https://newsletter.simpleaws.dev/p/architecting-with-aws-lambda-architecture-design?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">concurrency scaling of AWS Lambda</a>. Monitoring focuses on metrics like CPU utilization, memory usage, request counts, or queue lengths to trigger scaling events. And yeah, this is all more or less automated, but we still need to understand the fine details.</p><p class="paragraph" style="text-align:left;">Agentic AI scaling presents a similar picture, even if the names are different. Teams must monitor and manage capacity across multiple components. The main difference is tool maturity. Here are the main parts you need to scale:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Foundation Model (FM) Throughput:</b> The core reasoning engine itself can become a bottleneck. Ensuring adequate throughput and acceptable latency for LLM inference (e.g., via Amazon Bedrock or Amazon SageMaker endpoints) is critical. For example you can use <a class="link" href="https://newsletter.simpleaws.dev/p/amazon-bedrock-deep-dive?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow"><b>Amazon Bedrock Provisioned Throughput</b></a>, where you purchase dedicated inference capacity in model units, billed hourly for supported models and potentially achieve lower per-token costs at scale, but this requires accurate capacity planning and commitment. This part is very similar to scaling microservices, but the tooling isn&#39;t as mature yet: You get either self-hosted models e.g. in SageMaker (which is comparable to EC2) or a very black box service like Bedrock or external APIs (comparable to <a class="link" href="https://newsletter.simpleaws.dev/p/architecting-with-aws-lambda-best-practices?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">Lambda</a>).</p></li><li><p class="paragraph" style="text-align:left;"><b>Tool Scalability:</b> Each external <b>Tool</b> an agent calls (often an <a class="link" href="https://newsletter.simpleaws.dev/p/aws-lambda-20-advanced-tips?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">AWS Lambda function</a> or another API) must scale independently to handle the aggregated load from potentially many concurrent agent instances. For this part you can directly use the same techniques as with microservices (in fact, as we&#39;ll see later in this article, Tools can easily be microservices). Unless you&#39;re consuming external tools, but in that case it&#39;s the same as when a microservice uses one.</p></li><li><p class="paragraph" style="text-align:left;"><b>Knowledge Base (KB) Scalability:</b> For agents using <b>RAG</b>, the vector database (e.g., Amazon OpenSearch Service, <a class="link" href="https://newsletter.simpleaws.dev/p/managed-relational-databases-aws-rds-aurora?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">Amazon Aurora</a> with pgvector) must handle the concurrent query load generated during the retrieval step. This requires appropriate provisioning (instance sizes, shard counts for OpenSearch) or leveraging serverless options like OpenSearch Serverless or <a class="link" href="https://newsletter.simpleaws.dev/p/is-amazon-aurora-serverless-actually-serverless?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">Aurora Serverless</a> and monitoring query latency and resource utilization. You should treat this part just like scaling a regular database (after all, this is a regular database).</p></li><li><p class="paragraph" style="text-align:left;"><b>Orchestration Layer Scalability:</b> The platform or framework managing the agent&#39;s execution flow or multi-agent coordination must also scale. For managed platforms like Bedrock Agents, AWS handles this scaling automatically, though it&#39;s always important to understand <a class="link" href="https://docs.aws.amazon.com/general/latest/gr/bedrock.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs#limits_bedrock" target="_blank" rel="noopener noreferrer nofollow">the limits</a>. For custom frameworks deployed on compute like ECS or Lambda, standard application scaling techniques apply but need to account for the specific load patterns of agent orchestration (take into special account the time waiting for inference).</p></li></ul><p class="paragraph" style="text-align:left;">Operationally, managing agentic scaling requires a more complex baseline: you can&#39;t just track overall request and database loads, at a minimum you&#39;ll need to look at FM utilization, tool performance, KB query latency, and the health of the orchestration layer itself. Predicting bottlenecks requires understanding the entire execution graph of the agentic task, just like for microservices, but that execution graph tends to be bigger and the tooling is less mature.</p><h3 class="heading" style="text-align:left;" id="handling-cognitive-failures-operati">Handling Cognitive Failures Operationally</h3><p class="paragraph" style="text-align:left;">Microservice resilience focuses on surviving infrastructure failures using patterns like Retries, Timeouts, Circuit Breakers (configured in the application or service mesh; AWS App Mesh reaches end of support on September 30, 2026), Bulkheads, and Redundancy. Operationally, handling these involves monitoring infrastructure health, configuring resilience patterns appropriately, and having automated recovery mechanisms (like instance/container replacement via Auto Scaling). Debugging often involves analyzing logs and traces (using <a class="link" href="https://newsletter.simpleaws.dev/p/observability-with-aws-x-ray?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">AWS X-Ray</a>) to identify the failing network hop or service instance reporting an error.</p><p class="paragraph" style="text-align:left;">Agentic AI must handle these infrastructure failures (especially for tool calls) but adds the significant operational challenge of dealing with <b>cognitive failures</b>: the AI producing incorrect, biased, unsafe, or nonsensical outputs (hallucinations) or failing to follow instructions or plan effectively. Detecting and debugging these is operationally different and a lot more difficult:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Detection:</b> While infrastructure failures throw clear error signals, cognitive failures might result in a successfully completed task with a subtly wrong or harmful outcome. Detecting this often requires implementing semantic validation checks on the agent&#39;s output (typically with <a class="link" href="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">Guardrails</a> that can block the response generation), continuous monitoring against predefined quality metrics or &#39;golden datasets&#39;, or even incorporating human feedback loops. Simple health checks are insufficient, and LLMs don&#39;t reliably return accurate response codes (200 for OK, 500 for error, etc).</p></li><li><p class="paragraph" style="text-align:left;"><b>Debugging:</b> Imagine an agent consistently failing to extract the correct information using a tool, and imagine you&#39;ve reliably identified this failure. Debugging a microservice failure might involve checking the tool&#39;s logs for errors. Debugging the agent failure, however, requires a multi-faceted investigation: examining the <i>exact</i> prompt sent to the LLM, the specific context provided (including any RAG results), the LLM&#39;s generated reasoning trace or intermediate &#39;thoughts&#39; (if logged, and not all providers give you this), the parameters passed to the tool, the tool&#39;s actual response, and how the agent interpreted that response. This requires significantly richer logging and specialized debugging skills focused on the interplay between prompt, context, model behavior, and tool interaction. Standard distributed tracing often lacks the semantic depth required, and again we&#39;re faced with the lack of tool maturity.</p></li><li><p class="paragraph" style="text-align:left;"><b>Mitigation Overhead:</b> Implementing AI-specific resilience patterns like <b>Reflection</b> involves managing the logic for self-critique (e.g. have the agent review its plan against constraints or use a separate LLM call to evaluate its own response) and the retry mechanisms, adding latency and cost (more LLM calls). Setting up and monitoring human-in-the-loop workflows for exceptions adds significant process overhead. In the lab, these techniques work very well in most cases (no matter how much people like to criticize LLMs). In the real world, the level of work required, and the cost and especially latency introduced, often make them not really viable. Plus, you&#39;ll find a lot less people who know what <a class="link" href="https://arxiv.org/abs/2303.11366?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">Reflection</a> is compared to the people who know what a <a class="link" href="https://martinfowler.com/bliki/CircuitBreaker.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">Circuit Breaker</a> is.</p></li></ul><h2 class="heading" style="text-align:left;" id="observability-and-tooling-maturity-">Observability and Tooling Maturity (May 2025)</h2><p class="paragraph" style="text-align:left;">I&#39;ve already mentioned this a lot in the section above, but I think it&#39;s worth it to go beyond “the tooling is less mature”, and discuss what capabilities are missing. Just note that this whole field is so new that I forgive all the tool makers, even if I keep calling out their tools.</p><h3 class="heading" style="text-align:left;" id="seeing-the-why-not-just-the-what">Seeing the &quot;Why&quot;, not Just the What</h3><p class="paragraph" style="text-align:left;">The need for robust observability is amplified for agentic systems because of their non-determinism and the opacity of LLM reasoning. The standard <a class="link" href="https://peter.bourgon.org/blog/2017/02/21/metrics-tracing-and-logging.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">three pillars of observability</a> provide a baseline:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Metrics (Amazon CloudWatch):</b> Useful for tracking infrastructure health (Lambda, databases), tool invocation rates/errors, and critically, LLM usage metrics (token counts, inference latency, as provided by Bedrock or SageMaker).</p></li><li><p class="paragraph" style="text-align:left;"><b>Logs (Amazon CloudWatch Logs, Amazon OpenSearch Service):</b> Essential, but need to capture far more than typical application logs. Effective agent logs must include: the final prompt sent to the LLM (with RAG context), the model configuration used (model ID, temperature), the raw LLM response (including any intermediate reasoning or planned steps), details of tool calls (chosen tool, parameters, response), data retrieved from KBs, and the final output or action taken. Capturing this level of detail systematically is critical, but trust me, logs can get really noisy.</p></li><li><p class="paragraph" style="text-align:left;"><b>Traces (AWS X-Ray):</b> Useful for tracing requests across agent tool calls <i>if</i> those tools are instrumented microservices or Lambda functions. However, standard tracing tools generally cannot provide visibility <i>inside</i> the LLM&#39;s reasoning process or easily correlate distributed trace spans back to the specific semantic context or reasoning step within the agent that initiated them. I know for a fact that AWS is working on improving this, but we&#39;ll have to wait a bit more.</p></li></ul><p class="paragraph" style="text-align:left;"><a class="link" href="https://www.honeycomb.io/blog/time-to-version-observability-signs-point-to-yes?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">Observability 2.0</a> (which involves wide events as a single source of truth) is, to my knowledge, still absent in the Agentic AI space. I&#39;ll update this article if I hear something different.</p><p class="paragraph" style="text-align:left;">The primary observability challenge for Agentic AI is achieving visibility into the <b>&quot;Why?&quot;</b> behind an agent&#39;s behavior. This requires specialized approaches, potentially integrating detailed logging with tracing tools or using specific features within agentic frameworks or platforms designed for visualizing execution graphs and reasoning steps. We can use standards like OpenTelemetry for LLM calls, but we&#39;re still lacking any standards specifically for representing and observing agent reasoning paths, making deep debugging and performance analysis harder than in the more standardized microservice world. Consequently, when planning an agentic system, teams must budget significant effort not just for development, but for building custom observability solutions or carefully evaluating the introspection capabilities offered by managed platforms.</p><h3 class="heading" style="text-align:left;" id="tooling-maturity-and-platform-depen">Tooling Maturity and Platform Dependence</h3><p class="paragraph" style="text-align:left;">This observability gap reflects a broader difference in ecosystem maturity. The microservice world benefits from years of development and standardization efforts, particularly via the <b>Cloud Native Computing Foundation (CNCF)</b> centered around <b>Kubernetes</b>. This provides a rich ecosystem of mature, often interchangeable, open-source and commercial tools for orchestration (Kubernetes itself via Amazon EKS, or alternative orchestrators like Amazon ECS), service mesh (Istio or Linkerd), CI/CD, security scanning, monitoring, logging, and tracing. This standardization grants significant <b>platform portability</b> and flexibility, and more importantly, it turns microservice operations into a known and mostly solved problem (even if the solution is hard and expensive).</p><p class="paragraph" style="text-align:left;">The operational tooling landscape for Agentic AI (MLOps/LLMOps) is much younger, more fragmented, and lacks comparable standardization. While excellent libraries exist for <i>building</i> agents, the tools for <b>deploying, managing, observing, and securing complex multi-agent systems at scale</b> are still rapidly evolving. There is no &quot;Kubernetes for Agents&quot; providing a universally accepted, platform-agnostic operational control plane.</p><p class="paragraph" style="text-align:left;">This gap forces difficult operational choices:</p><ul><li><p class="paragraph" style="text-align:left;"><b>DIY Approach:</b> Teams can build agentic systems using open-source frameworks and fundamental AWS services (Lambda, Step Functions, SQS, EventBridge, SageMaker for model hosting, OpenSearch for KBs). This offers maximum flexibility and avoids lock-in but requires significant engineering effort to build and maintain the custom orchestration, state management, observability, and deployment infrastructure reliably.</p></li><li><p class="paragraph" style="text-align:left;"><b>Integrated Platform Approach:</b> Leveraging platforms like <b>Amazon Bedrock Agents</b> provides pre-built, managed capabilities for agent creation, orchestration, tool integration, knowledge bases, and potentially simplified deployment and logging. This dramatically accelerates development but often results in significant <b>platform dependence</b>, tying the architecture and operational model to AWS-specific services and abstractions, potentially limiting future portability or requiring rework if migrating later. You&#39;re reading a newsletter called Simple AWS, so you know I&#39;m cool with being locked in with AWS, but it&#39;s always important to call this out.</p></li></ul><p class="paragraph" style="text-align:left;">This gap in tooling was also one of my main drivers for writing this series. I was hoping to find lessons from microservices that we could apply to AI agents, but most things weren&#39;t that useful or easy to transfer to this domain.</p><p class="paragraph" style="text-align:left;">Architects must therefore factor MLOps/LLMOps tooling maturity and the resulting platform dependence trade-offs into their risk assessment and strategic platform choices when designing agentic systems.</p><h2 class="heading" style="text-align:left;" id="deployment-and-lifecycle-ml-ops-and">Deployment and Lifecycle: MLOps and LLMOps</h2><p class="paragraph" style="text-align:left;">MLOps and LLMOps aren&#39;t terms that most architects know about, but I&#39;ll give you a tip: ML engineers/architects who&#39;ve been doing traditional ML since before ChatGPT know a lot of useful things about these topics; you should talk to them. I&#39;ve been doing that a lot lately, and this section is a result of that.</p><h3 class="heading" style="text-align:left;" id="managing-the-ai-artifact-lifecycle">Managing the AI Artifact Lifecycle</h3><p class="paragraph" style="text-align:left;">Microservice deployment typically follows mature <b>Continuous Integration and Continuous Delivery (CI/CD)</b> practices focused on code artifacts. Automated pipelines (using tools like the <a class="link" href="https://newsletter.simpleaws.dev/p/ci-cd-pipeline-with-aws-codestar?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">AWS CodePipeline, CodeBuild and CodeDeploy</a>, or alternatives) handle building container images or function packages, running automated tests (unit, integration), deploying via strategies like Blue/Green or Canary, and monitoring the release.</p><p class="paragraph" style="text-align:left;">Deploying and managing Agentic AI systems requires a slightly different set of practices called <b>MLOps (Machine Learning Operations)</b> and <b>LLMOps</b>, encompassing a more complex set of artifacts:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Models:</b> Versioning, deploying new or fine-tuned FMs, A/B testing performance, monitoring for drift or regressions.</p></li><li><p class="paragraph" style="text-align:left;"><b>Prompts:</b> Treating prompts as code: version control, automated testing (evaluating behavioral impact of prompt changes), safe rollout strategies. Prompt engineering is iterative and requires tight feedback loops.</p></li><li><p class="paragraph" style="text-align:left;"><b>Knowledge Bases:</b> Pipelines for updating KB data, re-indexing vector stores, validating data quality, and managing different KB versions.</p></li><li><p class="paragraph" style="text-align:left;"><b>Tools:</b> Standard CI/CD for the underlying code implementing agent tools.</p></li><li><p class="paragraph" style="text-align:left;"><b>Agent Configuration/Orchestration:</b> Versioning and deploying the definitions that tie models, prompts, tools, and workflows together. With Bedrock you can treat this as infrastructure.</p></li><li><p class="paragraph" style="text-align:left;"><b>Evaluation:</b> CI/CD pipelines <i>must</i> integrate rigorous automated evaluation suites that test the <i>behavior</i> and <i>quality</i> of agent responses against predefined benchmarks or criteria, going far beyond simple code compilation or unit tests for tools. I&#39;ll write more about this in another article.</p></li></ul><p class="paragraph" style="text-align:left;">This composite lifecycle, managing interconnected changes across code, models, data, and prompts, is inherently more complex and requires specialized MLOps/LLMOps tooling and processes that are still maturing. It&#39;s not that we don&#39;t know how to manage each of these parts individually. The problem is that the behavior of the system is determined by the combination of all of these parts, so we need to manage them as a single set, not independent variables.</p><h3 class="heading" style="text-align:left;" id="deployment-independence">Deployment Independence</h3><p class="paragraph" style="text-align:left;">The fact that these artifacts are so interconnected makes independent deployments more difficult. While a microservice team can confidently deploy their updated service if the API contract holds (that&#39;s one of the core points of microservices), updating an agent&#39;s prompt might subtly change its interaction with multiple tools or its interpretation of context, requiring broader behavioral testing. Swapping an underlying FM version often requires re-evaluating all associated prompts and tool interactions. This tighter coupling, especially within integrated platforms, makes the practical reality of independent deployment for agentic systems currently less attainable than the ideal achieved in well-architected microservice systems. Again, maturity.</p><h2 class="heading" style="text-align:left;" id="ai-vs-microservices-cost-may-2025">AI vs Microservices Cost (May 2025)</h2><p class="paragraph" style="text-align:left;">Cost management is another area where the paradigms diverge significantly. In theory you could run your own models or open source ones in your own infrastructure, and the comparison becomes moot. In practice you&#39;ll often find yourself using proprietary models, and that&#39;s where you need to understand inference costs and tokenomics.</p><h3 class="heading" style="text-align:left;" id="contrasting-cost-models-g-bseconds-">Contrasting Cost Models: GB-seconds and Tokens</h3><p class="paragraph" style="text-align:left;">Microservice costs on AWS are primarily driven by <b>infrastructure consumption</b>, scaling relatively predictably with usage: compute time (EC2, Fargate, Lambda), database capacity and I/O (RDS, DynamoDB), data storage (S3, EBS), network throughput (ELB, API Gateway, Data Transfer), and messaging volume (SQS, SNS, EventBridge). Costs can be tracked and optimized using standard cloud practices and tools like AWS Cost Explorer, and optimized with <a class="link" href="https://newsletter.simpleaws.dev/p/aws-savings-plans-reserved-instances?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">Savings Plans and Reserved Instances</a>. If we used serverless compute, this is measured in GB-seconds (hence the title).</p><p class="paragraph" style="text-align:left;">Agentic AI costs include these infrastructure components (for tools, KBs, orchestration compute) but are frequently dominated by a factor unique to AI: <b>Foundation Model inference costs</b>, typically priced <b>per token</b>. These are the main points you need to understand:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Token Calculation:</b> Costs apply to <i>both</i> input tokens (the prompt, including instructions, context, RAG results, chat history) <i>and</i> output tokens (the generated response, reasoning steps, or tool parameters). Longer interactions, more complex reasoning, or verbose outputs directly increase costs.</p></li><li><p class="paragraph" style="text-align:left;"><b>Model Variance:</b> Costs per token vary dramatically between different LLMs. Larger, state-of-the-art models like Claude 3.7 Sonnet can be orders of magnitude more expensive than smaller, faster models like Llama 4 Scout 17B.</p></li><li><p class="paragraph" style="text-align:left;"><b>Task Complexity Impact:</b> A simple agent query might use a few hundred tokens. A complex multi-step task involving extensive reasoning, RAG lookups, and multiple tool interactions could consume tens or even hundreds of thousands of tokens for a single user request. This makes cost highly sensitive to workflow design.</p></li></ul><p class="paragraph" style="text-align:left;">This token-based pricing makes agentic system costs potentially much more <b>variable</b> and harder to predict than typical microservice costs, which scale more directly with request volume or provisioned infrastructure. A small change in user input could trigger a much longer reasoning path, drastically increasing the token count and cost for that single interaction. Implementing robust cost monitoring, potentially with per-user or per-task tracking and alerts specifically for token consumption becomes more important than ever.</p><h3 class="heading" style="text-align:left;" id="optimization-strategies-infrastruct">Optimization Strategies: Infrastructure Tuning vs. AI Efficiency</h3><p class="paragraph" style="text-align:left;">Microservice cost optimization primarily focuses on <b>infrastructure efficiency</b>: right-sizing compute instances and Lambda memory, using <a class="link" href="https://newsletter.simpleaws.dev/p/aws-savings-plans-reserved-instances?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">AWS Savings Plans or Reserved Instances</a> for baseline load, leveraging cheaper Spot Instances where applicable, optimizing database performance, and implementing <a class="link" href="https://newsletter.simpleaws.dev/p/elasticache-redis-cache-rds?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">data caching with services like Amazon ElastiCache</a>.</p><p class="paragraph" style="text-align:left;">Agentic AI also requires these infrastructure optimizations for its non-AI components, but <i>must</i> also prioritize <b>AI efficiency</b>:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Strategic Model Selection (Cascading):</b> This is often the most impactful optimization. Use the simplest, cheapest model suitable for each step. Design workflows that route simple tasks to cheap models and only invoke powerful, expensive models when complex reasoning is truly needed.</p></li><li><p class="paragraph" style="text-align:left;"><b>Prompt Engineering:</b> Meticulously craft prompts to be concise, reducing input tokens. Guide the model to produce shorter, focused outputs where appropriate to reduce output tokens.</p></li><li><p class="paragraph" style="text-align:left;"><b>Managing FM Costs:</b> For predictable high volumes, evaluate <b>Amazon Bedrock Provisioned Throughput</b> to potentially lower per-token costs compared to on-demand usage, although this requires accurate capacity planning.</p></li><li><p class="paragraph" style="text-align:left;"><b>Efficient Workflow Design:</b> Minimize unnecessary LLM calls within an agent&#39;s workflow. Optimize the number of reasoning steps or tool interactions required.</p></li><li><p class="paragraph" style="text-align:left;"><b>Effective Caching:</b> <a class="link" href="https://newsletter.simpleaws.dev/p/amazon-bedrock-prompt-caching?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-3-operations-and-costs" target="_blank" rel="noopener noreferrer nofollow">Cache LLM responses</a> (if determinism allows and inputs repeat), knowledge base lookups, or results from read-only tools when cache keys preserve access scope and expiry meets freshness requirements to avoid redundant computation and inference costs.</p></li></ul><p class="paragraph" style="text-align:left;">For agentic systems, tuning prompts and strategically selecting models often gets you greater cost savings than purely optimizing the underlying Lambda function memory. That&#39;s because in agentic systems and in any generative AI systems costs are typically dominated by inference. So, follow the usual advice: Start by optimizing the biggest thing you can find.</p><h2 class="heading" style="text-align:left;" id="part-3-conclusion">Part 3 Conclusion</h2><p class="paragraph" style="text-align:left;">So far in this series we&#39;ve navigated the complex relationship between Microservice and Agentic AI architectures. We started by discussing their distinct origins and foundational principles, moved through the runtime dynamics regarding communication, state, and predictability, and now we&#39;ve examined the practical realities of operating these systems.</p><p class="paragraph" style="text-align:left;">While both patterns employ decomposition to manage complexity, they are fundamentally different tools designed for different primary purposes. Microservices offer a mature, robust paradigm for structuring large applications around business domains, optimizing for engineering lifecycle agility, operational scalability, and reliability through well-understood patterns and tooling. Agentic AI provides a powerful, rapidly evolving paradigm for automating complex tasks, enabling autonomous reasoning and action, and creating intelligent interactions by orchestrating LLMs, tools, and knowledge.</p><p class="paragraph" style="text-align:left;">We&#39;ve gone through all the comparisons I considered worth exploring. In the next part, part 4 of this series, we&#39;ll explore hybrid architectures and how we can apply both patterns at the same time. Stay tuned!</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>Microservices vs. Agentic AI (Part 2): Communication, State, Patterns, and Predictability</title>
  <description>Comparing Microservice &amp; Agentic AI runtime behavior. Deep dive into communication patterns, state management (Sagas vs Memory), design patterns, &amp; non-determinism. Part 2.</description>
  <link>https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-2</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-2</guid>
  <pubDate>Fri, 18 Apr 2025 01:02:33 +0000</pubDate>
  <atom:published>2025-04-18T01:02:33Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Artificial Intelligence]]></category>
    <category><![CDATA[Architecture]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">In Part 1 of this series I talked about the origins and foundational principles distinguishing <b>Microservices</b> from modern <b>Agentic AI</b>. I argued that microservices stem from software engineering needs, decomposing large applications along <b>business domain</b> lines to enhance agility and scalability. Agentic AI, fueled by <b>Large Language Model (LLM)</b> breakthroughs, focuses on decomposing complex <b>tasks</b> to orchestrate autonomous reasoning and action. Their different motivations shape their approaches to decomposition, specialization, and even the focus of <b>autonomy</b> itself (Team/Deployment vs. Operational/Decision-Making).</p><p class="paragraph" style="text-align:left;">With that foundation laid, we now shift focus to how these architectural differences play out in <b>runtime behavior</b>. How do services communicate, versus how agents interact and collaborate? How do we manage the persistent records of a business versus the operational context of a reasoning engine? What patterns have emerged to solve common runtime problems on both sides, and how do they compare? And how does the fundamental nature of their core components impact system predictability? Exploring and contrasting these runtime dynamics is necessary to understand the practical trade-offs and design considerations inherent in each approach, which is what architecture is all about.</p><h2 class="heading" style="text-align:left;" id="interaction-styles-of-microservices">Interaction Styles of Microservices and Agentic AI</h2><p class="paragraph" style="text-align:left;">Both microservices and AI agents are distributed system, and in any distributed system the key is how those components communicate with each other. This isn&#39;t necessarily dictated by what the component do, but rather by the granularity of their responsibilities, and the expectations set upon the interactions.</p><h3 class="heading" style="text-align:left;" id="microservice-communication-defined-">Microservice Communication: Defined APIs and Simple Transport</h3><p class="paragraph" style="text-align:left;">Microservices interact primarily through well-defined Application Programming Interfaces (APIs). The common mechanisms include:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Synchronous Communication:</b> A service makes a request to another service&#39;s API and waits for a response. REST over HTTP/S is simple and stateless. For higher performance or scenarios requiring bidirectional streaming, gRPC (using HTTP/2 and Protocol Buffers) is a frequent choice, with the main alternative being websockets. This pattern is good for interactions where the caller requires immediate data or confirmation to proceed, though it has the disadvantage of introducing temporal coupling between the services, and can make some failure scenarios harder to recover from.</p></li><li><p class="paragraph" style="text-align:left;"><b>Asynchronous Communication:</b> This is a more complex pattern, but usually preferred in microservice architectures. Services communicate indirectly without waiting for an immediate response, typically via messaging systems. This promotes loose coupling and improves resilience, as components don&#39;t need to be available simultaneously. Common patterns and their associated AWS services include:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Message Queues (Amazon SQS):</b> One service sends a message to a queue; another service polls the queue and processes the message later. Excellent for decoupling tasks and load leveling (<a class="link" href="https://newsletter.simpleaws.dev/p/sqs-throttle-database-writes-dynamodb?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow">throttling</a>).</p></li><li><p class="paragraph" style="text-align:left;"><b>Publish/Subscribe (</b><a class="link" href="https://newsletter.simpleaws.dev/p/aws-sns-decoupling-components-in-cloud-architectures?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow"><b>Amazon SNS</b></a><b>):</b> A service publishes a message (event) to a topic, and multiple subscribing services receive a copy independently. Ideal for broadcasting state changes or events, and for fanning out an event to multiple components. Can be used in conjunction with queues.</p></li><li><p class="paragraph" style="text-align:left;"><b>Event Buses (Amazon EventBridge):</b> A central bus receives events from various sources (other microservices, or even AWS services) and routes them to target services based on defined rules. Facilitates building <a class="link" href="https://newsletter.simpleaws.dev/p/event-driven-architecture-patterns?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow">event-driven architectures</a>.</p></li><li><p class="paragraph" style="text-align:left;"><b>Streaming Platforms (Amazon MSK for Apache Kafka, </b><a class="link" href="https://newsletter.simpleaws.dev/p/real-time-data-processing-pipeline-kinesis-and-lambda?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow"><b>Amazon Kinesis Data Streams</b></a><b>):</b> For handling high-volume, ordered streams of events, often used for real-time data processing or feeding analytics pipelines.</p></li></ul></li></ul><p class="paragraph" style="text-align:left;">A central philosophy guiding microservice communication is <b>&quot;smart endpoints, dumb pipes.&quot;</b> The &quot;smarts,&quot; the business logic, resides entirely within the microservices themselves (the endpoints). The communication infrastructure (network protocols and message brokers, the &quot;pipes&quot;) should ideally act as simple, reliable transport mechanisms. This principle arose partly as a reaction against older SOA patterns where heavyweight Enterprise Service Buses (ESBs) sometimes contained significant business logic, transformation rules, or complex routing, creating a central point of complexity and potential failure. Intermediaries like an <a class="link" href="https://newsletter.simpleaws.dev/p/why-you-should-put-api-gateway-in-front-of-everything?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow"><b>API Gateway</b></a> (Amazon API Gateway) manage external API exposure, request routing, authentication, and rate limiting. <b>Load Balancers</b> (like AWS Elastic Load Balancer, both Application Load Balancers and Network Load Balancers) distribute traffic across service instances. <b>Service Meshes</b> (like Istio/Linkerd, and to some extent Amazon ECS Service Connect; AWS App Mesh reaches end of support on September 30, 2026) handle network-level concerns like reliable service-to-service communication, discovery, observability, retries, and circuit breaking. These intermediaries primarily provide infrastructure capabilities, aiming to keep application logic out of the communication fabric itself.</p><h3 class="heading" style="text-align:left;" id="agentic-communication-context-tools">Agentic Communication: Context, Tools, and Orchestration</h3><p class="paragraph" style="text-align:left;">Agentic AI systems exhibit more diverse interaction patterns, reflecting the agent&#39;s need to reason, access information, and act upon the world:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Agent-Tool Interaction:</b> This is a fundamental part of agentic architectures. An agent, guided by its reasoning process (an LLM call), determines it needs to perform an action or get external information and invokes a predefined <b>Tool</b>. Technically, this is often implemented as a standard API call (REST or RPC) to a function (like an AWS Lambda function) or another service. The agent&#39;s LLM often needs to generate the correct parameters for the API call based on the task context and the tool&#39;s description, and that&#39;s a whole other discussion.</p></li><li><p class="paragraph" style="text-align:left;"><b>Agent-Knowledge Base Interaction:</b> Agents often need external information not present in their training data. <b>Retrieval-Augmented Generation (RAG)</b> is the common pattern. The agent (or a process acting on its behalf) takes the user query or intermediate thought, converts it to an embedding, searches a vector database (like those powered by Amazon OpenSearch Service with k-NN, or Amazon Aurora with the pgvector extension) for relevant text chunks, retrieves those chunks, and then includes them as context within the prompt sent to the LLM to generate a more informed response. This is a specialized information retrieval interaction.</p></li><li><p class="paragraph" style="text-align:left;"><b>Agent-Agent Collaboration:</b> In multi-agent systems, which are one form of agentic AI, coordination and ensuring the right agents are called is critical. This often involves patterns more complex than simple API calls:</p><ul><li><p class="paragraph" style="text-align:left;"><i>Orchestrator/Supervisor:</i> A central agent manages the workflow, assigns tasks to worker agents, gathers results, and decides the next steps, often communicating via internal function calls or messages.</p></li><li><p class="paragraph" style="text-align:left;"><i>Broker:</i> A routing agent directs messages or tasks between specialized agents based on content or required skills, typically using asynchronous messaging. This function can also be performed by the Supervisor.</p></li><li><p class="paragraph" style="text-align:left;"><i>Event-Driven:</i> Agents publish events reflecting their state changes or completed actions (e.g. to Amazon EventBridge), and other agents subscribe and react autonomously.</p></li><li><p class="paragraph" style="text-align:left;">Frameworks like LangGraph allow defining these interactions as state machines or graphs, managed by the framework&#39;s runtime. Platforms like <b>Amazon Bedrock Agents</b> provide managed orchestration capabilities.</p></li></ul></li></ul><p class="paragraph" style="text-align:left;">Critically, the communication payload in agentic architectures often includes rich <b>context</b>, intermediate reasoning steps, goals, or detailed instructions, not just simple data. The &quot;dumb pipes&quot; philosophy doesn&#39;t neatly apply. The agent&#39;s reasoning <i>before</i> calling other agents injects intelligence and business logic into the communication. Intermediaries like Orchestrators or frameworks explicitly contain workflow logic. The interaction is less about simple data transport and more about orchestrating an intelligent, context-aware process.</p><h2 class="heading" style="text-align:left;" id="managing-state-in-microservices-and">Managing State in Microservices and AI Agents</h2><p class="paragraph" style="text-align:left;">State is anything that is used for more than one request, and that should be persisted across requests and accessible to all nodes (a critical thing for <a class="link" href="https://newsletter.simpleaws.dev/p/understanding-aws-high-availability-scalability?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow">high availability and scalability</a>). This is true for both microservices and AI agents, but the way these architectures handle state and data persistence is another area of significant divergence.</p><h3 class="heading" style="text-align:left;" id="microservices-decentralized-busines">Microservices: Decentralized Business Data & Eventual Consistency</h3><p class="paragraph" style="text-align:left;">The mantra here is <b>decentralized data management</b>. Each microservice owns its data, tailored to its needs (sometimes using different data stores like <a class="link" href="https://newsletter.simpleaws.dev/p/managed-relational-databases-aws-rds-aurora?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow">Amazon RDS</a> for relational data, <a class="link" href="https://newsletter.simpleaws.dev/p/dynamodb-database-design?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow">Amazon DynamoDB</a> for NoSQL, etc.). This avoids the coupling and schema evolution nightmares of shared databases, and decouples other services that consume this data from how the data is stored. State primarily represents the authoritative, persistent records of <b>business entities and transactions</b>.</p><p class="paragraph" style="text-align:left;">The major challenge this approach introduces is maintaining <b>data consistency</b> across these independent databases when a single business process modifies data owned by multiple services. Because <a class="link" href="https://newsletter.simpleaws.dev/p/distributed-transactions-event-driven-architectures?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow">distributed transactions</a> using protocols like two-phase commit are complex and tend to make overall availability a bit more difficult, the prevalent approach is <b>eventual consistency</b>. This acknowledges that updates across services won&#39;t be instantaneous but will converge over time, requiring applications to tolerate temporary inconsistencies (e.g. an order status might briefly differ between the Order service and a Reporting service).</p><p class="paragraph" style="text-align:left;">Handling this requires specific patterns:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Saga Pattern:</b> This pattern manages distributed business transactions through a sequence of local transactions within each participating service. If any step fails, predefined <b>compensating transactions</b> are executed to undo the effects of preceding successful steps, effectively rolling back the business operation. For example, if booking a flight succeeds but charging the credit card fails, the compensation action is to cancel the flight booking. Sagas can be implemented via:</p><ul><li><p class="paragraph" style="text-align:left;"><i>Choreography:</i> Services react to events published by others (e.g. using Amazon SNS or EventBridge). This is highly decoupled but can be hard to track.</p></li><li><p class="paragraph" style="text-align:left;"><i>Orchestration:</i> A central coordinator (potentially implemented using <a class="link" href="https://newsletter.simpleaws.dev/p/aws-step-functions-complex-multi-step-workflow-advanced-strategies?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow">AWS Step Functions</a>) explicitly tells each service what local transaction to execute and handles failure/compensation logic. This is easier to manage but introduces a coordinator dependency.</p></li></ul></li><li><p class="paragraph" style="text-align:left;"><b>Transactional Outbox Pattern:</b> This pattern ensures that events corresponding to a database change are published reliably. The service atomically commits both the data change and an event record describing the change to its local database within the same transaction. A separate process then reads these event records from the &quot;outbox&quot; table and reliably publishes them to a message broker (like Amazon SNS, EventBridge, or MSK). This prevents the state where the database is updated but the corresponding event notification fails to send.</p></li></ul><h3 class="heading" style="text-align:left;" id="agentic-ai-memory-knowledge-and-con">Agentic AI: Memory, Knowledge, and Contextual Coherence</h3><p class="paragraph" style="text-align:left;">Agentic AI&#39;s focus is different. It&#39;s less about owning persistent business data and more about managing the <b>operational state and context</b> needed for reasoning and task execution. Key elements include:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Memory:</b> This is essential for maintaining context during interactions.</p><ul><li><p class="paragraph" style="text-align:left;"><b>Short-Term Memory:</b> Holds information relevant to the current conversation turn or task execution sequence (e.g. recent user messages, agent&#39;s intermediate thoughts or plans). This might be managed in memory or stored transiently. Context window limitations of LLMs are a major factor here, and even though they&#39;ve grown a lot lately (Gemini 2.5 pro has a 1M tokens context window), you still need to be mindful of input tokens and the associated cost.</p></li><li><p class="paragraph" style="text-align:left;"><b>Long-Term Memory:</b> Allows agents to recall information across multiple interactions or sessions (e.g. user preferences, past conversation summaries, learned facts). This requires persistence, potentially using databases like DynamoDB or, increasingly, vector databases for semantic retrieval of relevant past experiences.</p></li></ul></li><li><p class="paragraph" style="text-align:left;"><b>Knowledge Bases (for RAG):</b> Agents frequently access external information repositories via <b>Retrieval-Augmented Generation</b>. The RAG process typically involves embedding a query, searching a vector database (OpenSearch or Amazon Aurora with pgvector) to find relevant document chunks, and injecting these chunks into the LLM prompt as context. This external knowledge isn&#39;t state the agent <i>owns</i>, but state it <i>accesses</i> to generate informed responses. Updating these knowledge bases isn&#39;t really part of the agentic architecture itself, but it&#39;s worth mentioning that they are updated.</p></li></ul><p class="paragraph" style="text-align:left;">The primary &quot;consistency&quot; challenge here is ensuring the agent&#39;s internal state (<b>memory</b>) remains <b>coherent</b> and relevant, and that the external <b>knowledge bases</b> are <b>accurate and up-to-date</b>. An agent reasoning with inconsistent memory or outdated knowledge will produce poor results. While multi-agent systems modifying shared state could face distributed consistency issues, patterns for managing <i>inter-agent state consistency</i> seem less standardized than microservice data patterns, likely because it&#39;s a much less prevalent pattern (at least according to my own experience, and likely because agents are so new). However, when an agent uses a <b>tool</b> that interacts with a traditional database or microservice, it <i>must</i> engage with that system&#39;s consistency mechanisms. For instance, if an agent orchestrates actions via tools that modify multiple backend microservices, implementing or invoking a <b>Saga</b> might be necessary to ensure the overall business operation completes reliably or compensates correctly.</p><h3 class="heading" style="text-align:left;" id="caching-strategies">Caching Strategies</h3><p class="paragraph" style="text-align:left;">Both architectures benefit from caching, and it&#39;s interesting to explore how they use it. Microservices use caches like <a class="link" href="https://newsletter.simpleaws.dev/p/elasticache-redis-cache-rds?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow">Amazon ElastiCache</a> (Redis, Memcached) primarily to store frequently accessed <b>domain data</b> or API responses, reducing latency and database load. Agentic AI caching focuses on reducing <b>latency</b> and <b>cost</b> (especially LLM token costs). Common targets include <b>prompts</b> (check out <a class="link" href="https://newsletter.simpleaws.dev/p/amazon-bedrock-prompt-caching?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow">Amazon Bedrock Prompt Caching</a>), <b>LLM responses</b> (if inputs are identical and some level of determinism is acceptable), <b>knowledge base lookup results</b> (which are the same idea as caching database responses), and outputs from <b>read-only tool calls when cache keys preserve access scope and expiry meets freshness requirements</b> (same idea as caching microservice responses). The performance and cost-saving goals are similar, but the nature of the cached content differs. Still, caching is caching, and there&#39;s a lot to learn from 15 years of microservice experience, and 50 or so years of cache experience (the earliest reference I could find is from <i>Structured Computer Organization</i> by Andrew S. Tanenbaum, 1976).</p><h2 class="heading" style="text-align:left;" id="shared-and-differing-design-pattern">Shared and Differing Design Patterns</h2><p class="paragraph" style="text-align:left;">Both Microservice and Agentic AI architectures, being distributed systems, inevitably face similar fundamental challenges. However, the different nature of the components (deterministic code vs. probabilistic reasoning engines) and the core objectives (application structure vs. task automation) lead to divergent, though sometimes conceptually related, patterns. Let&#39;s explore how each architecture addresses a few shared problems, and consider whether they also share solutions.</p><h3 class="heading" style="text-align:left;" id="dynamic-component-discovery">Dynamic Component Discovery</h3><p class="paragraph" style="text-align:left;">In any dynamic distributed environment, particularly those leveraging cloud elasticity or container orchestration, component instances are ephemeral. Services or agents are created, destroyed, scaled up or down, and their network locations (like IP addresses and ports) are not fixed. This presents a fundamental problem: how does a component reliably find the correct network address of another component it needs to communicate with at runtime?</p><p class="paragraph" style="text-align:left;">The microservice ecosystem relies heavily on <b>Service Discovery</b> mechanisms operating at the network infrastructure level. Typically, this involves a <b>Service Registry</b> (like AWS Cloud Map, Consul, etcd, or integrated <a class="link" href="https://newsletter.simpleaws.dev/p/kubernetes-aws-basics-best-practices?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow">Kubernetes</a> service discovery). When a microservice instance starts, it registers its network location and health status with the registry. When another service needs to call it, it queries the registry using a stable logical service name to resolve the current, healthy IP address(es) and port(s). Load balancers often integrate directly with these registries to know where to distribute traffic. The focus is squarely on resolving logical service names to physical network endpoints.</p><p class="paragraph" style="text-align:left;">Agentic systems also need discovery, but often at a different level of abstraction. While an agent might need to discover the network endpoint of a <b>Tool</b> (which could use standard service discovery), a more common challenge is “discovering” the <i>right tool or agent for a specific task</i>. This frequently involves the core <b>LLM&#39;s reasoning capabilities</b>. The agent is provided with descriptions of available tools (including their purpose and parameters). Based on its current goal or sub-task, the LLM analyzes these descriptions to semantically determine the most appropriate tool to invoke. Agentic <b>Frameworks</b> might also provide registries or mechanisms for tool/agent lookup, but the selection process often involves this layer of semantic understanding or functional matching rather than just resolving a network address. In multi-agent systems, finding another agent might involve explicit routing by an <b>Orchestrator</b> or <b>Broker</b>, or discovery based on capabilities advertised within the framework.</p><p class="paragraph" style="text-align:left;">Both architectures solve the &quot;finding the right component&quot; problem, but operate at different conceptual levels. Microservices know which component they&#39;re looking for, and they primarily rely on network-level service discovery to resolve logical names to physical addresses for potentially identical service instances. AI Agents first need to determine which component they&#39;re looking for, relying on semantic reasoning or framework-level capabilities to discover the functionally appropriate tool or specialized agent needed for a specific step in a task, and only after that they fall into the same problem of microservice discovery.</p><h3 class="heading" style="text-align:left;" id="managing-interaction-complexity-rou">Managing Interaction Complexity & Routing</h3><p class="paragraph" style="text-align:left;">As the number of independent components (services or agents) grows, direct peer-to-peer communication becomes increasingly complex to manage, test, and secure. Systems need ways to route requests efficiently, manage external access, and control internal interaction flows without creating a tangled mess.</p><p class="paragraph" style="text-align:left;">In a microservices architecture this is addressed through patterns that manage traffic flow and centralize certain concerns, primarily at the network edge or infrastructure level. The <a class="link" href="https://newsletter.simpleaws.dev/p/why-you-should-put-api-gateway-in-front-of-everything?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow"><b>API Gateway</b></a> pattern provides a single, managed entry point for external clients, handling concerns like request routing to appropriate backend services, authentication/authorization, rate limiting, request and/or response transformation, and sometimes response aggregation. Internally, <b>Load Balancers</b> distribute traffic across available instances based on load and health. <b>Service Meshes</b> can provide more sophisticated internal traffic management, like fine-grained routing rules or traffic splitting for canary releases. These solutions focus on managing network traffic and enforcing policies at architectural boundaries.</p><p class="paragraph" style="text-align:left;">Managing interaction complexity in Agentic AI architectures often involves embedding logic within specialized agents or frameworks that control the workflow. The <b>Orchestrator/Supervisor</b> agent centralizes the control logic for a multi-step task or multi-agent collaboration. The <b>Broker</b> agent (which may be a separate agent, or may be the same Supervisor agent) intelligently routes messages or tasks to other agents based on content or required skills. <b>Routing Workflow</b> patterns explicitly define paths for different types of tasks to reach specialized handlers or agents. These patterns manage the <i>logical flow of execution</i> and collaboration, often making decisions based on the state of the task or the content of messages.</p><p class="paragraph" style="text-align:left;">Both paradigms use intermediary patterns to manage interaction complexity. However, microservice intermediaries (API Gateway, LB) primarily function as <i>infrastructure components</i>, routing network traffic and enforcing generic policies without deep application awareness. Agentic intermediaries (Orchestrator, Broker) often encapsulate significant <i>application or workflow logic</i>, actively directing the sequence of operations and making content-aware routing decisions. The intelligence sits closer to, or within, the interaction management layer in agentic systems.</p><h3 class="heading" style="text-align:left;" id="ensuring-reliability-handling-failu">Ensuring Reliability & Handling Failure</h3><p class="paragraph" style="text-align:left;">Partial failures are a fact of life in distributed systems. Network connections can drop, components can crash or become slow, external dependencies can fail. Systems must be designed to tolerate these faults and continue operating reliably, or at least fail gracefully.</p><p class="paragraph" style="text-align:left;">In microservices, reliability focuses heavily on <b>infrastructure resilience</b> and handling failures between deterministic code units. Key patterns include: <b>Redundancy</b> (running multiple instances), <b>Health Checks</b> (detecting unhealthy instances for removal from load balancing), <b>Timeouts</b> (preventing indefinite waits on dependencies), <b>Retries</b> (handling transient network errors), <b>Circuit Breakers</b> (stopping requests to failing services to prevent cascading failures and allow recovery), and <b>Bulkheads</b> (isolating resources to limit the blast radius of dependency failures). Asynchronous communication also contributes significantly to resilience by decoupling components. Data consistency (or at least detection of inconsistencies) is also an important aspect, but I already talked about it. The key point is that these patterns primarily address component unavailability or network issues.</p><p class="paragraph" style="text-align:left;">Reliability in Agentic AI is twofold. First, it must handle the same infrastructure failures as microservices, especially when <b>Tools</b> interact with external systems (requiring robust tool design and standard patterns like <b>Retries</b> and <b>Timeouts</b> applied either by the agent logic or within the tool). Second, and uniquely, it must handle <b>AI-specific or cognitive failures</b>: LLM hallucinations, poor reasoning or planning, inability to use a tool correctly, misinterpreting results. Solutions for this include AI-specific patterns like <b>Reflection/Self-Correction</b> (where an agent reviews and attempts to fix its own output or plan based on predefined criteria or checks), <b>Evaluations</b> (external validation of agent performance, either by code or by another LLM call), adaptive <b>Re-planning</b> (finding alternative paths if a step fails), and robust <b>Error Handling</b> within the agent&#39;s core logic to catch and manage exceptions from both tools and the LLM. <b>Guardrails</b> act as safety nets against undesirable outputs, and they can be applied either to the final response or to intermediate responses.</p><h2 class="heading" style="text-align:left;" id="the-predictability-divide-determini">The Predictability Divide: Deterministic Logic vs. Probabilistic Reasoning</h2><p class="paragraph" style="text-align:left;">This difference perhaps most profoundly shapes the operational reality and design philosophy of Generative AI applications in general, not limited to Agents.</p><p class="paragraph" style="text-align:left;">Microservices, like most traditional software, are built upon a foundation of <b>determinism</b>. Given a specific input and state, a correctly functioning service is expected to produce the exact same output and undergo the exact same state transition every single time. This predictability is fundamental to how we:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Test:</b> Unit tests, integration tests, and end-to-end tests largely rely on asserting that actual outputs match expected outputs for given inputs.</p></li><li><p class="paragraph" style="text-align:left;"><b>Debug:</b> Reproducing a bug typically involves recreating the specific inputs and state conditions that reliably trigger the erroneous behavior.</p></li><li><p class="paragraph" style="text-align:left;"><b>Reason about Reliability:</b> We build confidence by testing predictable components and using patterns (like retries for transient network errors) that manage failures within an otherwise deterministic system.</p></li></ul><p class="paragraph" style="text-align:left;">It&#39;s worth noting that distributed systems are never truly 100% deterministic. Unexpected network and infrastructure failures can and will occur, and they will impact the system. These are mostly solved problems, even if the solutions are rather complex (distributed transactions, eventual consistency, etc). Moreover, we can test for these unpredictable failures using <a class="link" href="https://newsletter.simpleaws.dev/p/chaos-engineering-on-aws?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow">chaos engineering</a>, for example with <a class="link" href="https://newsletter.simpleaws.dev/p/chaos-engineering-using-aws-fault-injection-simulator?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-2-communication-state-patterns-and-predictability" target="_blank" rel="noopener noreferrer nofollow">AWS Fault Injection Simulator</a>. In almost any form of traditional software we adapt the application layer to handle these failures and test for them at the infrastructure layer.</p><h3 class="heading" style="text-align:left;" id="agentic-ai-embracing-and-managing-n">Agentic AI: Embracing (and Managing) Non-Determinism</h3><p class="paragraph" style="text-align:left;">Agentic systems leveraging LLMs introduce <b>inherent non-determinism</b>. This stems primarily from the probabilistic nature of how LLMs generate text: they predict the most likely next token (or word part) based on the preceding context, often employing sampling strategies (like adjusting the &#39;temperature&#39; parameter) to introduce variability and avoid repetitive outputs. This means:</p><ul><li><p class="paragraph" style="text-align:left;">The same prompt might yield slightly different phrasing, sentence structure, or even minor variations in the reasoning path on subsequent runs.</p></li><li><p class="paragraph" style="text-align:left;">In rare cases, it can lead to <b>hallucinations</b>: confident-sounding but factually incorrect or nonsensical outputs.</p></li></ul><p class="paragraph" style="text-align:left;">This non-determinism is not just unavoidable, it&#39;s often entirely desirable. Any form of creativity by an LLM is due to the same factors that cause hallucinations, and the whole reason why we build Generative AI applications and use LLMs is because of these factors. However, even if we&#39;re explicitly searching for this non-determinism, its presence fundamentally changes how we operate applications:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Testing:</b> Exact output matching is often impossible or irrelevant. Testing must shift focus to <b>behavioral validation</b>: Does the agent achieve the intended goal? Does the output satisfy key criteria (checked via rules, another LLM, or human evaluation)? Is the agent robust to slight variations in input phrasing? Testing often requires evaluating performance over multiple runs or using statistical measures, and determining correctness via semantic analysis instead of direct comparison. Equivalence classes for inputs are often impossible to predict just by analysis, and likely impractical even to calculate via statistical observations. For these reasons, significant investment in robust <b>evaluation frameworks</b> becomes necessary, and I promise to write an article about it soon.</p></li><li><p class="paragraph" style="text-align:left;"><b>Debugging:</b> When an agent misbehaves, pinpointing the cause is harder. Was it the prompt, the specific context window contents, retrieved RAG data, the inherent randomness of the model for that query, or a genuine hallucination? Reproducing the error can be difficult, even if you manage to recreate the exact state. Debugging necessitates capturing and analyzing the <i>entire reasoning context</i>: the exact prompt sent to the LLM, the retrieved knowledge snippets, any tool calls and responses, and the intermediate &quot;thoughts&quot; or plans generated by the agent (requiring detailed logging or specialized observability tools). To make matters even more fun, there are no tools that can really observe the internal state of a model, so we can only capture what the model outputs.</p></li><li><p class="paragraph" style="text-align:left;"><b>Reliability:</b> Building reliable agentic systems means managing this uncertainty. It requires good <b>input validation</b>, strong <b>output validation</b> (checking against rules or desired formats, and very robust evals), incorporating <b>self-correction</b> mechanisms (like the Reflection pattern), implementing clear <b>fallback behaviors</b> when high confidence cannot be achieved, and using <b>Guardrails</b> to prevent harmful or undesirable actions. Human oversight (called human-in-the-loop) for critical tasks is often a necessary reliability component, even if introduces delays and causes more work.</p></li></ul><h2 class="heading" style="text-align:left;" id="conclusion-runtime-realities-shape-">Conclusion: Runtime Realities Shape Design</h2><p class="paragraph" style="text-align:left;">The runtime behaviors of Microservices and Agentic AI systems present some similarities, but also differences due to the fundamental nature of their components. Microservices communicate via relatively simple transport mechanisms between endpoints containing deterministic business logic, manage persistent business data with established patterns for eventual consistency, and rely on infrastructure-focused resilience patterns. Agentic AI involves more complex, context-rich communication flows often managed by intelligent intermediaries, focuses state management on operational context and memory, requires unique patterns for cognitive reliability, and must contend with inherent non-determinism resulting from its AI core.</p><p class="paragraph" style="text-align:left;">In part 1 we concluded the different focus (especially the <i>why</i>) of these two architecture patterns results in different ways to solve the same problems, due to their different priorities. Here in part 2 we found that some solutions apply equally well to both patterns, but in many cases AI Agents add one more layer to the problem, necessitating newer solutions on top of the known patterns. In part 3 we&#39;ll analyze deployment, observability and security, and consider hybrid implementations that combine both patterns.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>Microservices vs. Agentic AI (Part 1): Decomposing Applications vs. Orchestrating Tasks</title>
  <description>Compare microservices and agentic AI: business-domain boundaries versus task orchestration, specialization, autonomy, and team ownership.</description>
  <link>https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-1</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/microservices-vs-agentic-ai-part-1</guid>
  <pubDate>Mon, 14 Apr 2025 22:19:03 +0000</pubDate>
  <atom:published>2025-04-14T22:19:03Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Artificial Intelligence]]></category>
    <category><![CDATA[Architecture]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">Modern system design is about dealing with complexity. As applications grow, monolithic approaches can become bottlenecks, affecting development speed, scalability, and resilience. That&#39;s where we start applying architectural patterns focused on <b>decomposition</b>, like <a class="link" href="https://newsletter.simpleaws.dev/p/microservices-aws-migrating-from-monolith?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-1-decomposing-applications-vs-orchestrating-tasks" target="_blank" rel="noopener noreferrer nofollow"><b>Microservices</b></a> (though remember it&#39;s not the only option).</p><p class="paragraph" style="text-align:left;">When you start talking about AI Applications, especially related to Generative AI, at some point you face the same problem: That big ball of mud becomes really hard to maintain, and especially to operate. <b>Agentic AI</b> seemingly comes to solve that problem, <b>decomposing</b> your AI application into individual agents.</p><p class="paragraph" style="text-align:left;">Both patterns involve breaking down larger systems into smaller, interacting units, and both find strong support within cloud ecosystems like AWS. So it makes sense to think they&#39;re similar, or even the same thing applied to different contexts. That assumption is what prompted me to dive into this comparison, hoping to find lessons from microservices architecture that I could apply to Agentic AI.</p><p class="paragraph" style="text-align:left;">But I was wrong.</p><p class="paragraph" style="text-align:left;">Looking past these surface similarities reveals fundamental differences in the origins, the core purpose, and the design philosophies of these patterns. And while there are shared lessons, the main lesson is that <b>they are not the same</b>.</p><p class="paragraph" style="text-align:left;">This article is the first in a series dedicated to a deep comparison of these two architectural patterns. I&#39;ll explore the underlying principles and practical consequences of each, in an attempt to build a clear understanding of the distinct problems each paradigm solves best and the unique ways they approach system design. Here in Part 1, I establish the foundation, examining their contrasting histories and motivations, their vastly different approaches to decomposition and specialization, and the distinct meanings they assign to autonomy and independence. Getting these fundamentals right is necessary before we can effectively analyze their runtime behaviors and operational realities in subsequent parts.</p><h2 class="heading" style="text-align:left;" id="introduction-beyond-surface-similar">Introduction: Beyond Surface Similarities</h2><p class="paragraph" style="text-align:left;">The appeal of both Microservices and Agentic AI lies in their promise to manage complexity through decomposition. Microservices carve up large, monolithic applications into collections of smaller, independent services. Agentic AI breaks down complex tasks or goals into sequences of reasoning and action executed by potentially multiple intelligent agents. Both yield distributed systems operating on cloud infrastructure.</p><p class="paragraph" style="text-align:left;">And both have become very popular, in a memetic and fashion-like way that&#39;s unrelated to their actual usefulness. They&#39;re the cool kids on the bloc, and if you use them you&#39;re cool and knowledgeable and your seniority magically increases.</p><p class="paragraph" style="text-align:left;">And that&#39;s where the resemblance ends. The <i>reasons</i> driving this decomposition, the <i>nature</i> of the resulting components, and the fundamental <i>challenges</i> being addressed diverge significantly. Microservices emerged as a software architecture pattern focused squarely on improving the software development lifecycle – enhancing team agility, enabling independent deployment, achieving better scalability, and increasing the resilience of large applications. It&#39;s about structuring code and teams more effectively. Agentic AI, especially its current LLM-driven form, is an AI system design pattern focused on achieving sophisticated, goal-oriented, autonomous behavior. It aims to automate complex processes involving reasoning, planning, learning, and interaction with the environment. It&#39;s about orchestrating intelligence.</p><p class="paragraph" style="text-align:left;">They both divide complexity into more manageable pieces, but they do it for <i>different reasons</i>, and they divide <i>different complexities</i>.</p><p class="paragraph" style="text-align:left;">Misunderstanding this core distinction, or trying to apply principles from one directly to the other without careful consideration, can lead down unproductive architectural paths. Let&#39;s begin by examining their roots so we can understand the <i>why</i>.</p><h2 class="heading" style="text-align:left;" id="different-problems-different-origin">Different Problems, Different Origins</h2><p class="paragraph" style="text-align:left;">An architecture pattern&#39;s history often reveals its soul. The core problems it was conceived to solve heavily influence its structure and principles. The why determines the how.</p><h3 class="heading" style="text-align:left;" id="microservices-basically-res-tbased-">Microservices: Basically REST-based SOA to fight Monoliths</h3><p class="paragraph" style="text-align:left;">Microservices represent an “evolutionary refinement” (I&#39;m feeling fancy 🤣) of earlier distributed system ideas, particularly <b>Service-Oriented Architecture (SOA)</b>. SOA became prominent in the early 2000s, aiming for better enterprise integration and component reuse via standardized services that communicated via the SOAP protocol over a centralized Enterprise Service Bus (ESB). It had a pretty significant impact in enterprise architecture, but certain SOA implementations grew way too complex and sometimes introduced performance or governance bottlenecks.</p><p class="paragraph" style="text-align:left;">Around the early 2010s, practices honed at web-scale companies like Netflix and Amazon facing the extreme limitations of monolithic applications began to coalesce into the Microservices pattern. This approach embraced SOA&#39;s service orientation but adopted a more pragmatic, decentralized philosophy. It emphasized simpler communication styles (often via <a class="link" href="https://martinfowler.com/articles/richardsonMaturityModel.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-1-decomposing-applications-vs-orchestrating-tasks" target="_blank" rel="noopener noreferrer nofollow">REST</a>/HTTP), favored direct service-to-service interaction or lightweight messaging (&quot;dumb pipes&quot;) over heavy middleware, advocated for <b>decentralized data management</b> (each service owns its data), and placed a very high premium on <b>automation</b> (CI/CD) and <b>independent deployability</b>.</p><p class="paragraph" style="text-align:left;">The short explanation of microservices is that you decompose a use case into separate behaviors that use separate data, and implement the management of those units of data and the execution of those behaviors into a single deployment unit with a well-defined interface, called a microservice. The entire application is the result of combining the behaviors of these microservices in certain ways.</p><p class="paragraph" style="text-align:left;">The motivation was explicitly to combat the pains of the monolith: development bottlenecks slowing down releases, inefficient scaling of the entire application for localized load, technology lock-in slowing down innovation, poor fault isolation leading to cascading failures, and difficulties coordinating large teams on a single codebase. Microservices were a direct answer to critical <b>software engineering lifecycle and operational scaling challenges</b>.</p><p class="paragraph" style="text-align:left;">It&#39;s worth noting at this point that we have learned two critical lessons since: One, that we can build scalable monoliths with good code practices focused on separation of concerns. Two, we can build distributed services which are separated at the application layer but share the data layer, and while they don&#39;t offer all the advantages of microservices the get us most of them and are much easier to implement and maintain. Yes, in case you didn&#39;t know, if your “microservices” all access the same database they&#39;re not really microservices, and that&#39;s ok!</p><p class="paragraph" style="text-align:left;">Also, modern cloud technologies have made monoliths, distributed services and microservices much easier to build, deploy, and especially operate.</p><h3 class="heading" style="text-align:left;" id="agentic-ai-managing-ll-ms-for-compl">Agentic AI: Managing LLMs for Complex Tasks</h3><p class="paragraph" style="text-align:left;">The current surge in <b>Agentic AI</b> architectures is much more recent and has a different catalyst: the dramatic arrival of powerful <b>Large Language Models (LLMs)</b> in the early 2020s, and their limitations. Early models like GPT-4 demonstrated impressive capabilities not just in language generation, but also in rudimentary reasoning, instruction following, and even planning. And on top of their real use cases, they also became a fad, greatly increasing their adoption.</p><p class="paragraph" style="text-align:left;">However, it quickly became apparent that complex, real-world tasks often require more than a single LLM interaction. Tasks might involve multiple steps, require access to up-to-the-minute external information, necessitate interaction with other software systems or APIs, or need context maintained over extended interactions. This gap between LLM potential and practical task complexity spurred the use of multiple techniques and improvements like <b>Retrieval-Augmented Generation (RAG)</b>, multi-turn conversations, and hybrid architectures combining regular code with LLM calls to solve a problem. This ultimately led to the development of agentic frameworks and architectures, where the core idea became to orchestrate LLM calls, augmenting the model&#39;s reasoning capabilities with external <b>Tools</b> (functions or APIs the agent can call), <b>Memory</b> (short-term context and long-term information retention), and access to external <b>Knowledge Bases</b> (often via RAG, using vector databases like Amazon OpenSearch or <a class="link" href="https://newsletter.simpleaws.dev/p/managed-relational-databases-aws-rds-aurora?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-1-decomposing-applications-vs-orchestrating-tasks" target="_blank" rel="noopener noreferrer nofollow">Amazon Aurora</a> for Postgres with pgvector).</p><p class="paragraph" style="text-align:left;">This modern wave actually builds upon decades of theoretical work in AI on autonomous agents (Distributed AI, Multi-Agent Systems, Belief-Desire-Intention models), but this time with LLMs as readily available reasoning engines, often orchestrated by platforms like <a class="link" href="https://newsletter.simpleaws.dev/p/amazon-bedrock-deep-dive?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-1-decomposing-applications-vs-orchestrating-tasks" target="_blank" rel="noopener noreferrer nofollow">Amazon Bedrock</a>, or open-source frameworks. The primary motivation is <b>extending AI capabilities</b> to <b>automate complex, multi-step tasks</b> requiring planning, reasoning, and interaction, thereby achieving a higher degree of <b>autonomous, goal-oriented behavior</b>.</p><h2 class="heading" style="text-align:left;" id="the-decomposition-divide-structurin">The Decomposition Divide: Structuring Applications vs. Structuring Tasks</h2><p class="paragraph" style="text-align:left;">As you can see, it&#39;s the same idea of decomposing a problem and creating autonomous units that can solve individual parts of it, but with a completely different reason why we&#39;re decomposing the problem in the first place. Why does the reason matter? Because it will determine how we structure the components and what we optimize the solution for.</p><h3 class="heading" style="text-align:left;" id="microservice-decomposition-business">Microservice Decomposition: Business Capabilities and DDD</h3><p class="paragraph" style="text-align:left;">Microservices decompose the <b>structure of a software application</b>. The goal is to partition the application into independent units that align with business functions, promoting agility, scalability and maintainability via independent components that can be owned separately. This decomposition is often guided by principles from <b>Domain-Driven Design (DDD)</b>:</p><ul><li><p class="paragraph" style="text-align:left;"><b>By Business Capability:</b> Services map directly to what the business <i>does</i> (e.g., <code>Catalog Management</code>, <code>Pricing Engine</code>, <code>Shipping Logistics</code>). Each service contains all logic and data needed for its capability. This ensures technology serves business needs directly, and makes owning the data easier.</p></li><li><p class="paragraph" style="text-align:left;"><b>By Subdomain (DDD):</b> The overall business domain is analyzed to identify distinct subdomains (e.g., <code>Product</code>, <code>Order</code>, <code>Customer</code>). Each subdomain with its specific language and model becomes a <b>Bounded Context</b>. Microservices are often designed to align with these Bounded Contexts, ensuring conceptual integrity and clear boundaries. Defining these boundaries often involves identifying Aggregates (entities treated as a unit) within the domain.</p></li><li><p class="paragraph" style="text-align:left;"><b>By Transaction:</b> Occasionally, services might be designed around encapsulating entire high-level business transactions. The idea of these functional microservices is to abstract away the implementation details of a distinct part of a process, along with the ownership of the data involved.</p></li></ul><p class="paragraph" style="text-align:left;">The guiding principles are <b>High Cohesion</b> within a service (related things stay together) and <b>Loose Coupling</b> between services (dependencies are minimized and managed through clearly defined APIs). Getting these boundaries right, often based on domain analysis, is essential for realizing the benefits. This often involves understanding the stable structures within the business domain itself, and understanding the data that is stored, and which processes use it and how.</p><h3 class="heading" style="text-align:left;" id="agentic-ai-decomposition-functional">Agentic AI Decomposition: Functional Roles and Workflows</h3><p class="paragraph" style="text-align:left;">Agentic AI decomposes a <b>complex task or goal</b> into manageable steps or functions. The goal is to create an executable plan or workflow that leverages the reasoning capabilities of LLMs and the specific functionalities of tools or specialized agents. The decomposition is driven by the logic of the task itself:</p><ul><li><p class="paragraph" style="text-align:left;"><b>By Functional Steps:</b> Breaking down the task sequentially (e.g., Step 1: Understand request, Step 2: Search knowledge base, Step 3: Call Tool A, Step 4: Synthesize result, Step 5: Format output).</p></li><li><p class="paragraph" style="text-align:left;"><b>By Role/Skill:</b> Assigning different agents specific roles based on required skills or tools (e.g., a <code>Planner Agent</code> creates the task breakdown, <code>Research Agents</code> use specific search tools, an <code>Executor Agent</code> calls action APIs, an <code>Evaluator Agent</code> checks the quality). This mimics human team specialization.</p></li><li><p class="paragraph" style="text-align:left;"><b>By Workflow Pattern:</b> Implementing structures like a linear <b>Pipeline</b>, a centrally managed <b>Orchestrator</b> directing workers, concurrent execution via <b>Parallelization</b>, or dynamic message routing via a <b>Broker</b>. Frameworks like LangGraph allow defining these complex execution flows.</p></li></ul><p class="paragraph" style="text-align:left;">Here, the focus is on orchestrating the <i>process</i> of reasoning and action needed to achieve the goal.</p><h3 class="heading" style="text-align:left;" id="comparing-philosophies-domain-stabi">Comparing Philosophies: Domain Stability vs. Task Logic</h3><p class="paragraph" style="text-align:left;">Microservice decomposition anchors itself in the relatively stable concepts of the business domain. The architecture reflects the business structure, aiming for long-term maintainability. Agentic AI decomposition anchors itself in the logic required to complete a specific task or goal. This logic might be more dynamic, changing as the understanding of the task evolves, new tools become available, or the capabilities of the underlying LLMs change.</p><p class="paragraph" style="text-align:left;">Trying to apply DDD Bounded Context thinking rigidly to every step of an agent&#39;s workflow is likely inappropriate. While high-level agents might correspond to broad business domains, the fine-grained decomposition within an agent&#39;s process is typically functional or task-oriented. Understanding this difference is fundamental because choosing the wrong decomposition strategy can lead to poorly defined boundaries, increased coupling (either between services or between agent steps), and ultimately defeat the purpose of decomposition in either context. For microservices, it leads to systems hard to change independently. For agents, it can lead to brittle or inefficient task execution.</p><p class="paragraph" style="text-align:left;">The functional microservices style of decomposition is the one that looks the most similar to Agentic AI. At a glance, the overall goal of a functional microservice is to abstract behind an API a set of steps or a transaction within an entire workflow, and workflows are realized by combining multiple functional microservices. Taken at face value, that&#39;s pretty much how you decompose a task into AI agents, and I believe now that was one of the core reasons for my initial idea that microservices and AI agents were very similar. Looking a bit deeper into it, a functional microservice also owns all the data that is involved in the transaction, and manages access to it. It encapsulates not just the behavior of executing the transaction, but also the transaction itself as an object of the system. AI agents operate at a much finer-grained level, and it would seem logical for a single functional microservice to implement its behavior using a fleet of AI agents.</p><h2 class="heading" style="text-align:left;" id="specialization-domain-expertise-vs-">Specialization: Domain Expertise vs. Functional Skill</h2><p class="paragraph" style="text-align:left;">The key point here is that while both architectures aim for specialized components, they specialize along different axes.</p><h3 class="heading" style="text-align:left;" id="microservice-specialization-owning-">Microservice Specialization: Owning a Business Domain Slice</h3><p class="paragraph" style="text-align:left;">A microservice specializes in a specific <b>segment of the business domain</b>. The <code>AuthenticationService</code> knows everything about authentication protocols, credential storage, and session management. The <code>RecommendationService</code> understands recommendation algorithms, owns user preference modeling, and stores item metadata. This <b>domain-oriented specialization</b> allows teams to build deep expertise and optimize data models and logic for that specific business capability.</p><h3 class="heading" style="text-align:left;" id="agent-specialization-specific-tasks">Agent Specialization: Specific Tasks and Tools</h3><p class="paragraph" style="text-align:left;">An AI agent typically specializes in a specific <b>function, task, role, or skill</b>. For example:</p><ul><li><p class="paragraph" style="text-align:left;">An agent might be highly optimized for <b>natural language conversation</b> and managing dialogue state.</p></li><li><p class="paragraph" style="text-align:left;">Another might specialize in <b>interpreting and executing code</b> via a specific tool.</p></li><li><p class="paragraph" style="text-align:left;">A third might be expert at <b>RAG</b>, efficiently querying multiple vector databases and synthesizing retrieved information.</p></li><li><p class="paragraph" style="text-align:left;">Yet another could be a <code>ValidatorAgent</code> specialized in checking the outputs of other agents against specific rules or constraints.</p></li></ul><p class="paragraph" style="text-align:left;">This <b>task/role/skill-oriented specialization</b> aims to improve the agent&#39;s performance (accuracy, latency, reliability) and cost for its specific function, often by tailoring its prompts, carefully selecting and fine-tuning its underlying model (if applicable), or giving it access to a specific, curated set of tools and knowledge.</p><h3 class="heading" style="text-align:left;" id="implications-for-design-and-boundar">Implications for Design and Boundaries</h3><p class="paragraph" style="text-align:left;">This difference reinforces the boundary definition contrast. Microservice boundaries encapsulate domain knowledge. Agent boundaries encapsulate specific skills or steps within a process. An agent specialized in using the <code>WeatherAPI</code> tool has a boundary defined by that functional capability, which is conceptually different from a <code>UserProfileService</code> boundary defined by the scope of user-related business data and logic.</p><h2 class="heading" style="text-align:left;" id="autonomy-and-independence-empowerin">Autonomy and Independence: Empowering Teams vs. Empowering Decisions</h2><p class="paragraph" style="text-align:left;">Autonomy and independence are advertised as benefits of both approaches, but the emphasis and practical realization differ significantly.</p><h3 class="heading" style="text-align:left;" id="microservice-autonomy-independent-d">Microservice Autonomy: Independent Deployment and Governance</h3><p class="paragraph" style="text-align:left;">Microservice autonomy is primarily about <b>empowering development teams</b> and enabling <b>operational independence</b> for deployable units:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Team Autonomy & Decentralized Governance:</b> Teams owning services can often choose their own technology stacks (within organizational bounds) and manage their service&#39;s evolution.</p></li><li><p class="paragraph" style="text-align:left;"><b>Independent Deployability:</b> This is the cornerstone. The ability to deploy a single service update without impacting others, enabled by well-defined APIs and robust automation (like CI/CD pipelines running on <a class="link" href="https://newsletter.simpleaws.dev/p/ci-cd-pipeline-with-aws-codestar?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-1-decomposing-applications-vs-orchestrating-tasks" target="_blank" rel="noopener noreferrer nofollow">AWS CodePipeline</a>), is a massive driver of agility.</p></li></ul><p class="paragraph" style="text-align:left;">The service runs independently, but the key autonomy is about the team&#39;s control over its development and release lifecycle.</p><h3 class="heading" style="text-align:left;" id="agentic-autonomy-task-execution-det">Agentic Autonomy: Task Execution Details</h3><p class="paragraph" style="text-align:left;">An AI agent perceives its environment (user input, tool output), reasons based on its goals and internal state (memory, model knowledge), and autonomously chooses and executes actions (calling tools, generating responses) to achieve its objectives. This is <b>runtime, decision-making autonomy</b>.</p><p class="paragraph" style="text-align:left;">An AI agent also serves to encapsulate the execution details of the given task, centralizing management of the prompt, model, knowledge bases and tools used to execute that specific task.</p><h3 class="heading" style="text-align:left;" id="independence-realities-encapsulatio">Independence Realities: Encapsulation, Deployment Limits, and Ownership</h3><ul><li><p class="paragraph" style="text-align:left;"><b>Encapsulation:</b> Both architectures heavily rely on encapsulation through interfaces (APIs for microservices; agent interactions for agents). This is a strong parallel and a shared best practice that enables modularity. However, the granularity of the behaviors that each architecture encapsulates are different.</p></li><li><p class="paragraph" style="text-align:left;"><b>Deployment Independence:</b> This deployment comparison reflects the tooling landscape in April 2025. Microservices, especially when built using containers and orchestrated by platforms like <a class="link" href="https://newsletter.simpleaws.dev/p/migrate-nodejs-app-from-ec2-to-scalable-ecs-guide?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-1-decomposing-applications-vs-orchestrating-tasks" target="_blank" rel="noopener noreferrer nofollow">Amazon ECS</a> or Kubernetes on <a class="link" href="https://newsletter.simpleaws.dev/p/nodejs-app-on-kubernetes-aws-eks?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-1-decomposing-applications-vs-orchestrating-tasks" target="_blank" rel="noopener noreferrer nofollow">Amazon EKS</a>, achieve a high degree of practical independent deployment across various environments. Agentic AI systems are conceptually modular, but in practice there is no standardized, platform-agnostic operational tooling for complex agent lifecycles. This heavily contrasts with the &quot;deploy anywhere&quot; flexibility that containerized microservices have. This platform dependence is a significant practical constraint, even on the more mature platforms like <a class="link" href="https://newsletter.simpleaws.dev/p/amazon-bedrock-deep-dive?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=microservices-vs-agentic-ai-part-1-decomposing-applications-vs-orchestrating-tasks" target="_blank" rel="noopener noreferrer nofollow">Amazon Bedrock</a>.</p></li><li><p class="paragraph" style="text-align:left;"><b>Team Ownership:</b> The &quot;you build it, you run it&quot; model provides clear ownership for microservices, and as mentioned above, existing tools provide excellent support for this. For agentic systems, ownership is often ambiguous. Is it the prompt engineering team, the machine learning team, the tool-building team, or a central AI platform team? Clear ownership models are still evolving, complicated by the diverse skillsets required (AI/ML, Prompting, Software Engineering, Data Engineering).</p></li></ul><h2 class="heading" style="text-align:left;" id="conclusion-laying-the-foundation-pr">Conclusion: Laying the Foundation (Preview of Part 2)</h2><p class="paragraph" style="text-align:left;">This initial exploration reveals that Microservices and Agentic AI, despite sharing the concept of decomposition, are built on different foundations to solve different core problems. Microservices refine software architecture principles to tackle application complexity and improve the development lifecycle, anchoring themselves in stable business domains. Agentic AI tackles complex tasks through autonomous behavior, anchoring itself in reasoning processes, functional roles, and workflow orchestration. Their distinct motivations dictate different decomposition strategies, specialization types, and degrees of autonomy and independence.</p><p class="paragraph" style="text-align:left;">Ultimately, we need to remember why we&#39;re using an architecture pattern: You use microservices to achieve independent ownership and deployment of functionality around business domains. You use Agentic AI to implement specific complex behaviors that resolve specialized tasks in complex processes.</p><p class="paragraph" style="text-align:left;">And more importantly, we need to remember that this is not an A or B decision. You can use both of these patterns at the same time, or you can use neither.</p><p class="paragraph" style="text-align:left;">In Part 2 we&#39;ll build upon this foundation, examining how these different architecture patterns result in different communication techniques, state management approaches, design patterns, and the critical dimension of predictability at runtime.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>AWS Organizations Deep Dive part 2</title>
  <description>Building a secure and sane multi-account strategy, part 2</description>
      <enclosure url="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/51782faf-7dba-4bc7-b462-58df5fd61e5e/AWS_Organizations__1_.png" length="157809" type="image/png"/>
  <link>https://newsletter.simpleaws.dev/p/aws-organizations-best-practices</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/aws-organizations-best-practices</guid>
  <pubDate>Fri, 28 Mar 2025 22:00:21 +0000</pubDate>
  <atom:published>2025-03-28T22:00:21Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Security]]></category>
    <category><![CDATA[Architecture]]></category>
    <category><![CDATA[Operations]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">When you start with AWS you usually create a single account, get familiar with some services, and deploy a few workloads. Then your usage grows and everything becomes a lot more difficult. AWS Organizations makes those problems a bit easier to deal with.</p><p class="paragraph" style="text-align:left;">In this series we&#39;re going to talk about AWS Organizations, with a focus on how to implement a secure and sane multi-account strategy (sane as in it doesn&#39;t drive you mad). This article is part 2 of the series, where cover best practices and suggestions. Part 1 can be read <a class="link" href="https://newsletter.simpleaws.dev/p/aws-organizations?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-organizations-deep-dive-part-2" target="_blank" rel="noopener noreferrer nofollow">here</a>, which covers the key features of Organizations.</p><p class="paragraph" style="text-align:left;">These best practices are based on the <a class="link" href="https://docs.aws.amazon.com/whitepapers/latest/organizing-your-aws-environment/organizing-your-aws-environment.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-organizations-deep-dive-part-2" target="_blank" rel="noopener noreferrer nofollow">Organizing Your AWS Environment Using Multiple Accounts whitepaper by AWS</a>, and on my own experience as a cloud architect, consultant, and developer of a landing zone product (not my own, I was an employee).</p><h2 class="heading" style="text-align:left;" id="multi-account-structure-best-practi">Multi-Account Structure Best Practices</h2><p class="paragraph" style="text-align:left;">Designing your overall AWS multi-account structure involves balancing isolation and collaboration, and deciding how to manage provisioning and operations at scale. What&#39;s the right balance between central control and team autonomy? How do you structure your accounts to minimize overhead while maximizing security?</p><p class="paragraph" style="text-align:left;">First, use AWS accounts as your primary isolation boundaries. Accounts provide hard separation not just for access control, but also for fault isolation and service quotas. It&#39;s generally recommended to separate different environments (prod vs. dev) and different projects or teams into different accounts. This way, if one account has issues or hits limits, it doesn&#39;t affect others.</p><p class="paragraph" style="text-align:left;">When deciding between centralization and decentralization, consider what aspects benefit from economies of scale versus what needs team autonomy. And especially consider the reasons things have for changing and who is responsible for making those changes. Typically, it makes sense to centralize foundational components like security, identity management, and networking. These benefit from consistent implementation across workloads, performed by expert specialists. Meanwhile, application workloads can be decentralized to the teams responsible for them, giving those teams greater agility.</p><p class="paragraph" style="text-align:left;">This approach creates some trade-offs you&#39;ll need to navigate. Centralization provides consistency and easier compliance but can create bottlenecks if teams must wait for central groups to make every change. Decentralization improves agility but may lead to inconsistent implementations or duplicate efforts. This is the fundamental dichotomy between governance and agility, and finding the right balance between freedom and restrictions is very tricky. The ideal solution is called Platform Engineering, where internal teams work not on serving application teams directly but on developing self-service tools that application teams can leverage. This ensures that guardrails and best practices are always in place, without requiring manual work from internal teams and without creating delays for application teams. Of course, reaching this point requires significant organizational maturity and a considerable investment, but the resulting developer experience really pays off. It&#39;s a very long topic though, so let&#39;s get back to Organizations and how you&#39;d structure accounts.</p><p class="paragraph" style="text-align:left;">For example, you might have a central Identity account that manages authentication for all accounts, a Network account that manages VPCs and transit gateways, and a Security account that aggregates findings from all accounts. But individual application teams would manage their own workload accounts within guardrails established by these central services.</p><h3 class="heading" style="text-align:left;" id="creating-new-aws-accounts">Creating New AWS Accounts</h3><p class="paragraph" style="text-align:left;">As your account numbers grow, you&#39;ll need efficient ways to provision and configure new accounts. You have two main options:</p><ol start="1"><li><p class="paragraph" style="text-align:left;">Use AWS Organizations APIs like <code>CreateAccount</code> to programmatically create accounts from the management account. Then use AWS CloudFormation StackSets to deploy baseline infrastructure to new accounts automatically. Many organizations build an internal &quot;account vending machine&quot; that handles both the account creation and post-creation configuration.</p></li><li><p class="paragraph" style="text-align:left;">Implement event-driven automation where new account creation triggers Lambda functions to apply your organization&#39;s baseline configurations automatically.</p></li></ol><p class="paragraph" style="text-align:left;">For networking across accounts, consider whether you want isolated networks per account or a more connected approach. Many organizations use a hub-and-spoke model with a transit gateway in a central Network account that connects to VPCs in various workload accounts. You can use AWS Resource Access Manager to share network resources like subnets or transit gateway attachments between accounts.</p><p class="paragraph" style="text-align:left;">As your account numbers grow, central visibility becomes another important aspect. Implement cross-account monitoring using CloudWatch to aggregate logs and metrics, and set up a centralized logging account where all CloudTrail logs, Config logs, and other operational logs are collected. Use AWS Organizations features like CloudTrail Organization Trail to centrally log the configured event types across accounts to an S3 bucket; data events must be enabled explicitly, so you can access all events from a single place (and so they can&#39;t be deleted to hide an attacker&#39;s steps!).</p><p class="paragraph" style="text-align:left;">Also, please maintain documentation about your account structure, including an internal registry of accounts with metadata like owner, purpose, and OU membership. Establish clear processes for requesting new accounts and reviewing the account inventory regularly to identify unused or misconfigured accounts. This is very tedious, but it helps a lot when dealing with audits or making org-wide changes.</p><h2 class="heading" style="text-align:left;" id="landing-zones-and-control-tower">Landing Zones and Control Tower</h2><p class="paragraph" style="text-align:left;">Setting up a well-designed multi-account environment from scratch involves many complex tasks: creating accounts, establishing organizational units, configuring logging, implementing security controls, and more. AWS provides a few solutions to help you establish this foundation quickly and according to best practices defined by them and by yourself.</p><p class="paragraph" style="text-align:left;"><a class="link" href="https://docs.aws.amazon.com/controltower/latest/userguide/what-is-control-tower.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-organizations-deep-dive-part-2" target="_blank" rel="noopener noreferrer nofollow">AWS Control Tower</a> is a managed service that automates the setup of a secure and opinionated multi-account AWS environment. It&#39;s essentially AWS&#39;s opinion on how to set up a multi-account landing zone with the right guardrails and structure in place. When you implement Control Tower, it automatically creates a baseline environment with:</p><ol start="1"><li><p class="paragraph" style="text-align:left;">An organization in AWS Organizations with well-structured OUs</p></li><li><p class="paragraph" style="text-align:left;">A Log Archive account for centralized logging</p></li><li><p class="paragraph" style="text-align:left;">An Audit account for security tooling</p></li><li><p class="paragraph" style="text-align:left;">Pre-configured controls, with SCPs and RCPs for prevention, AWS Config rules for detection, and CloudFormation Hooks for proactive checks</p></li><li><p class="paragraph" style="text-align:left;">A dashboard to manage and monitor your environment</p></li></ol><p class="paragraph" style="text-align:left;">Under the hood, Control Tower is orchestrating many AWS services on your behalf. It&#39;s creating and configuring the organization structure in AWS Organizations, setting up roles for cross-account access, enabling CloudTrail across accounts, establishing S3 buckets for log storage, and implementing AWS Config rules to validate compliance.</p><p class="paragraph" style="text-align:left;">Control Tower provides an Account Factory that lets you provision new accounts with a few clicks or via API. These accounts come pre-configured with standard baselines: CloudTrail is enabled, AWS Config is running, the required IAM roles are created, and security guardrails are applied. This standardization ensures all accounts meet your security and operational requirements from day one.</p><p class="paragraph" style="text-align:left;">Control Tower supports preventive, detective, and proactive controls. Proactive controls check resources before deployment using CloudFormation Hooks; the first two types work as follows:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Preventative guardrails</b> that use SCPs or resource control policies to prevent disallowed actions, like preventing the creation of S3 buckets with public access or disabling regions you don&#39;t want to use.</p></li><li><p class="paragraph" style="text-align:left;"><b>Detective guardrails</b> that use AWS Config rules to check for compliance and flag violations, like detecting unencrypted EBS volumes or resources missing required tags.</p></li></ol><p class="paragraph" style="text-align:left;">Control Tower isn&#39;t just for new AWS environments. It can be enabled in existing organizations, though this requires careful planning to avoid disrupting current operations. The service maintains a concept of &quot;drift&quot;: when resources under Control Tower management are changed outside of Control Tower&#39;s processes, it can detect and help you remediate this drift.</p><p class="paragraph" style="text-align:left;">If you already have an Organization set up without Control Tower, it&#39;s typically worth the effort of enabling Control Tower if you&#39;re expanding and creating new accounts, or suddenly face some regulation or compliance requirements. You should create an OU with accounts that are not managed by Control Tower yet, move all your accounts there, apply Control Tower to your Organization except for that OU, and then slowly move the accounts back to their original OUs (now managed by Control Tower). By doing it one account at a time you can do all the necessary prep work for each account and resolve any problems causing minimum disruption to your activities. Examples of disruption can be a separately configured automated remediation terminating EC2 instances that lack a required tag, which is easy to fix by setting up the tag in your Auto Scaling Group configuration, but is not something you want to happen in production or to all your accounts at the same time.</p><p class="paragraph" style="text-align:left;">If Control Tower is too prescriptive for your needs, you might consider a custom landing zone. A landing zone is essentially the foundational structure for your AWS environment, including the account structure, network design, security controls, and access management. You can build this yourself using AWS services like Organizations, CloudFormation StackSets, and custom automation.</p><p class="paragraph" style="text-align:left;">The <a class="link" href="https://aws.amazon.com/solutions/implementations/landing-zone-accelerator-on-aws/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-organizations-deep-dive-part-2" target="_blank" rel="noopener noreferrer nofollow">AWS Landing Zone Accelerator</a> is an open-source solution built on CDK that helps you deploy a customizable landing zone aligned with AWS best practices. It has more flexibility than Control Tower but requires more effort to implement and maintain.</p><p class="paragraph" style="text-align:left;">When deciding between Control Tower and a custom landing zone, consider factors like speed of deployment, customization needs, operational complexity, and your existing environment. Control Tower offers faster setup but less flexibility, while custom approaches give you complete control but require more engineering effort.</p><p class="paragraph" style="text-align:left;">For most organizations just starting their multi-account journey, Control Tower provides the simplest path to a well-architected foundation. You can always extend it with customizations as your needs evolve, and the Control Tower team is always working on making it more flexible.</p><h2 class="heading" style="text-align:left;" id="account-segmentation-purpose-driven">Account Segmentation: Purpose-Driven Account Types</h2><p class="paragraph" style="text-align:left;">AWS&#39;s multi-account best practices include creating several purpose-driven accounts to support governance and infrastructure needs in addition to your application workload accounts. But why is this segmentation so important, and how does it enhance your security posture?</p><div class="image"><img alt="AWS Organizations account structure: a management account oversees Security and Infrastructure OUs containing log archive, security tooling, network, identity, shared services, and backup accounts." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/51782faf-7dba-4bc7-b462-58df5fd61e5e/AWS_Organizations__1_.png?t=1743195624"/><div class="image__source"><span class="image__source_text"><p>Support AWS Accounts</p></span></div></div><p class="paragraph" style="text-align:left;">The most fundamental account is the <b>Management Account</b> (also called the payer account). This is the first account that hosts your AWS Organization and serves as the billing center for all other accounts. Because of its privileged position, you should strictly limit what runs in this account – ideally, it should be used only for managing AWS Organizations, SCPs, and consolidated billing. Avoid deploying actual workloads here to reduce security exposure (remember that SCPs don&#39;t affect this account).</p><p class="paragraph" style="text-align:left;">A <b>Log Archive Account</b> provides centralized, immutable storage for logs and audit data from all other accounts. CloudTrail logs, Config snapshots, and other operational logs should be collected here. Under the hood, this account typically contains specially configured S3 buckets with strict access controls and retention policies. Apply strong safeguards like SCPs that deny deletion of any S3 objects here, to enforce that logs are append-only.</p><p class="paragraph" style="text-align:left;">A <b>Security Audit Account</b> (sometimes called Security Tooling account) is used by your security team to deploy and run security tools that need organization-wide visibility. This account typically becomes the delegated administrator for services like Security Hub, GuardDuty, and IAM Access Analyzer. Behind the scenes, it&#39;s using cross-account roles to gather security information from other accounts and maintaining a centralized database of findings.</p><p class="paragraph" style="text-align:left;">For organizations with complex networking needs, a <b>Shared Network Account</b> centralizes network infrastructure management. This account might host a transit gateway, VPN connections, Direct Connect gateways, and shared VPCs that other accounts can use. By centralizing network management, you ensure consistent configuration of routing, security groups, and network ACLs across your environment. A simple trick to save money if you use a lot of NAT Gateways (e.g. for dev environments) is to place them here and access them through transit gateway or shared subnets.</p><p class="paragraph" style="text-align:left;">An <b>Identity Account</b> can be used to manage identity providers and directory services, especially if you&#39;re integrating with on-premises identity systems. This account can be the delegated administrator for IAM Identity Center and may host AWS Managed Microsoft AD or AD Connector. The organization instance of IAM Identity Center remains in the management account.</p><p class="paragraph" style="text-align:left;">Some organizations also create specialized accounts like:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Infrastructure Tooling Account</b> for operational tools like CI/CD systems</p></li><li><p class="paragraph" style="text-align:left;"><b>Shared Services Account</b> for common services that multiple teams use</p></li><li><p class="paragraph" style="text-align:left;"><b>Backup Account</b> for centralized management of AWS Backup across accounts</p></li></ul><p class="paragraph" style="text-align:left;">These foundation accounts typically fall into the Security OU (Log Archive, Security Tooling) and Infrastructure OU (Network, Identity, Shared Services) as discussed earlier. Not every organization needs all of these accounts initially, you can start with just the Management, Log Archive, and Security Tooling accounts and add others as you need them.</p><p class="paragraph" style="text-align:left;">For application accounts, AWS recommends segmenting by purpose: separate accounts for each production workload (to contain blast radius), separate accounts for development/testing of those workloads, and possibly accounts for sandbox/experimentation. These accounts would typically live in the Workloads OU, often further split between Production and Non-Production sub-OUs.</p><p class="paragraph" style="text-align:left;">This account segmentation provides clear separation of duties and stronger security. Your security logs are in an account that application administrators can&#39;t modify, ensuring their integrity. Compliance requirements like PCI DSS or HIPAA can be addressed by isolating sensitive workloads in dedicated accounts with stricter controls. And if a particular service requires specialized oversight, it can get its own dedicated account.</p><p class="paragraph" style="text-align:left;">When implementing account segmentation, use a clear naming convention to make the purpose of each account immediately evident. For example, prefix account names with their purpose: &quot;Security-Audit&quot;, &quot;Security-LogArchive&quot;, &quot;Infra-Network&quot;, &quot;Workload-AppX-Prod&quot;, &quot;Workload-AppX-Dev&quot;, etc.</p><p class="paragraph" style="text-align:left;">Note that migrating a workload to another account is pretty complex, to the point that I&#39;m referring to it as migrating and not just moving. It&#39;s worth to spend a few minutes or even hours considering the right account structure before deploying stuff, it will save you weeks down the line.</p><h2 class="heading" style="text-align:left;" id="monitoring-and-observability-in-mul">Monitoring and Observability in Multi-Account Environments</h2><p class="paragraph" style="text-align:left;">When you have dozens or hundreds of AWS accounts, maintaining visibility across all of them becomes a significant challenge. How do you ensure that you can detect issues, monitor performance, and maintain security across your entire AWS footprint? It&#39;s easy, you just manually log into each AWS account and… nah, I&#39;m just kidding.</p><p class="paragraph" style="text-align:left;">The key is implementing a comprehensive monitoring and observability strategy that aggregates data from all accounts while maintaining appropriate access controls. This strategy should cover operational metrics, application performance, security events, and compliance status.</p><p class="paragraph" style="text-align:left;">AWS offers several services designed for cross-account monitoring. CloudWatch cross-account observability allows you to view and analyze metrics, logs, and traces from multiple accounts in a single account. This feature uses a source-destination model: source accounts share their monitoring data with a monitoring account, which can then create dashboards and alarms across the entire organization.</p><p class="paragraph" style="text-align:left;">Behind the scenes, this sharing is implemented through resource-based policies that permit the monitoring account to access CloudWatch data from source accounts. Configure a monitoring-account sink and source-account links in each Region. AWS Organizations and CloudFormation StackSets can automate onboarding accounts in selected OUs, but merely designating a monitoring account does not create those links.</p><p class="paragraph" style="text-align:left;">For security monitoring, AWS Security Hub provides a comprehensive view of your security posture across accounts. It collects security findings from services like GuardDuty, Inspector, Macie, and IAM Access Analyzer and aggregates them in a central dashboard. When configured as the delegated administrator, your Security account can view and manage Security Hub for all accounts in your organization.</p><p class="paragraph" style="text-align:left;">AWS Config Aggregator performs a similar function for configuration compliance. It allows you to view the configuration of resources across all accounts in your organization, making it easier to identify misconfigurations or policy violations. You can deploy organization-wide AWS Config rules that check for compliance with your security and operational standards.</p><p class="paragraph" style="text-align:left;">For log aggregation, a central logging strategy is essential. This typically involves:</p><ol start="1"><li><p class="paragraph" style="text-align:left;">Setting up an organization-wide CloudTrail that records API activity from all accounts to a central S3 bucket in your Log Archive account</p></li><li><p class="paragraph" style="text-align:left;">Configuring VPC Flow Logs from all accounts to be sent to a central location</p></li><li><p class="paragraph" style="text-align:left;">Aggregating application and system logs via CloudWatch Logs with cross-account subscriptions or third-party logging solutions</p></li></ol><p class="paragraph" style="text-align:left;">When implementing this centralized logging, consider the volume of data. Large organizations may generate terabytes of logs daily. Set up appropriate retention policies and consider using tools like Amazon Athena, OpenSearch, or third-party SIEM solutions to make this data searchable and actionable.</p><p class="paragraph" style="text-align:left;">One challenge in a multi-account environment is determining who should receive alerts. In a single account this is straightforward, but with many accounts, you need to route alerts to the right teams. This is where proper account metadata becomes important. Maintain a registry of account owners and responsibilities, and use this information to configure alert routing. Keep in mind that alerting everyone of everything quickly leads to alert fatigue.</p><p class="paragraph" style="text-align:left;">CloudWatch Metrics Insights and CloudWatch Logs Insights allow you to query across multiple accounts, making it easier to identify patterns or issues that span your organization. These tools can help you answer questions like &quot;Which applications are experiencing the highest error rates?&quot; or &quot;Where is most of my network traffic coming from?&quot; Moreover, nowadays they&#39;re powered by AI, so you can almost literally ask those questions.</p><h2 class="heading" style="text-align:left;" id="security-and-compliance-in-multi-ac">Security and Compliance in Multi-Account Environments</h2><p class="paragraph" style="text-align:left;">While multi-account setups enhance security isolation by default, they also introduce complexity in ensuring consistent security across your organization. I mean, sure, I&#39;ve been babbling about a Security account and share security stuff, but you have to admit that would be easier to implement for a single account than for a dozen accounts with different security needs. Still, it can be done, and the results are often much better (the key being the different security needs).</p><p class="paragraph" style="text-align:left;">Start with centralized logging and monitoring. Ensure every account sends logs to a central, secure location – typically the Log Archive account. This includes CloudTrail logs for API calls, AWS Config snapshots, VPC Flow Logs, and CloudWatch Logs for critical systems. Enable a CloudTrail organization trail that automatically logs events from all accounts in your organization, so you&#39;re not relying on each account to set up CloudTrail individually.</p><p class="paragraph" style="text-align:left;">Behind the scenes, this organization trail creates a resource-based policy on the destination S3 bucket that allows each member account&#39;s CloudTrail service to write to it. This ensures that even if a particular account is compromised, an attacker can&#39;t easily cover their tracks by disabling logging or deleting logs.</p><p class="paragraph" style="text-align:left;">Implement detective controls that work across your organization. Use AWS Security Hub and Amazon GuardDuty in all accounts, designating your Security account as the aggregator for findings. This gives your security team centralized visibility into potential issues across your entire cloud footprint. Similarly, deploy AWS Config rules or conformance packs to check each account against security best practices, like ensuring encryption is enabled or detecting public S3 buckets.</p><p class="paragraph" style="text-align:left;">For identity and access management, use AWS IAM Identity Center for centralized human access as discussed earlier. This gives you a single source of truth for users and makes it easy to revoke access when someone leaves or changes roles. For the root user in each account, implement the safeguards mentioned earlier – ideally removing root credentials from member accounts entirely and implementing an SCP that denies all actions performed by the root user.</p><p class="paragraph" style="text-align:left;">Network security requires special attention in a multi-account environment. If accounts are connected via VPC peering or a transit gateway, ensure you maintain appropriate segmentation with security groups, NACLs, and possibly AWS Network Firewall at central chokepoints. One account shouldn&#39;t be able to freely access another&#39;s internal resources unless explicitly intended. Use AWS Firewall Manager to deploy consistent security group rules or WAF configurations across accounts.</p><p class="paragraph" style="text-align:left;">Apply the principle of least privilege not just within accounts but across accounts. The team managing security tooling should have only audit access to application accounts, not full administrative privileges. Application developers shouldn&#39;t have access to modify central logging or security accounts. By using accounts as separation boundaries, you physically enforce this segregation – developers simply have no credentials in the Log Archive account.</p><p class="paragraph" style="text-align:left;">For incident response, plan for cross-account scenarios. If one account is compromised, how will you investigate and contain the issue? Ensure you have cross-account CloudTrail enabled so you have logs even if an attacker tries to clear local evidence. Prepare an SCP &quot;kill switch&quot; that can quickly quarantine a compromised account by denying all or most API actions. You can even create a Quarantine OU for this purpose.</p><p class="paragraph" style="text-align:left;">Encryption and data security should be consistent across accounts. Use AWS Config to verify that all accounts meet your encryption standards. For example, checking that S3 buckets have default encryption enabled and EBS volumes are encrypted. For KMS keys that need to be used across accounts, consider centralizing key management in a security account or implementing consistent key policies across accounts.</p><p class="paragraph" style="text-align:left;">For continuous compliance, treat your multi-account environment as a constantly audited domain. Existing AWS Audit Manager customers can continue using it; it stopped accepting new customers on April 30, 2026. For new deployments, evaluate another supported compliance-assessment tool. Use AWS Organizations Tag Policies to ensure resources are consistently tagged across accounts, which helps with both cost allocation and security attribution.</p><p class="paragraph" style="text-align:left;">Finally, establish clear governance processes around account management. Require approval for creating new accounts and ensure they fit in your OU structure with all baseline controls applied. Periodically review your account inventory to identify accounts that are no longer needed or are misplaced in the wrong OU.</p><h2 class="heading" style="text-align:left;" id="conclusion-and-next-steps">Conclusion and Next Steps</h2><p class="paragraph" style="text-align:left;">So, lots of best practices. The question now is, should you do <b>all</b> of them? And the answer isn&#39;t “it depends”. It&#39;s “Yes… eventually”. Here&#39;s some general guidance on when you should do each of these things:</p><ul><li><p class="paragraph" style="text-align:left;">Creating an Organization: Right away.</p></li><li><p class="paragraph" style="text-align:left;">Creating all the Accounts I mentioned: Why not? They&#39;re free</p></li><li><p class="paragraph" style="text-align:left;">IAM Identity Center, basic SCPs, CloudTrail Org Trail: Yes, all of those</p></li><li><p class="paragraph" style="text-align:left;">Shared resources like CI/CD: When you need them, build them as shared</p></li><li><p class="paragraph" style="text-align:left;">Networking: This is somewhat more complex and you don&#39;t need it right away</p></li><li><p class="paragraph" style="text-align:left;">All that other security stuff: Not before you have at least one part-time security person</p></li><li><p class="paragraph" style="text-align:left;">Monitoring and observability: Do logs right away, build the rest progressively as you need it</p></li></ul><p class="paragraph" style="text-align:left;">The best moment to do all of this is right before you need it, but that&#39;s often hard to get exactly right. So, do the basics (they&#39;re pretty easy to implement) and grow the rest progressively. Just don&#39;t forget about it. The absolute last thing you ever want to do is migrate a workload to another AWS account, so plan accordingly and implement everything properly before you reach that point.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>AWS Organizations Deep Dive</title>
  <description>Building a secure and sane multi-account strategy, part 1</description>
      <enclosure url="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/c96fe7da-859d-49a9-b23a-87ba64823d89/image.png" length="129451" type="image/png"/>
  <link>https://newsletter.simpleaws.dev/p/aws-organizations</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/aws-organizations</guid>
  <pubDate>Fri, 21 Mar 2025 22:01:43 +0000</pubDate>
  <atom:published>2025-03-21T22:01:43Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Security]]></category>
    <category><![CDATA[Architecture]]></category>
    <category><![CDATA[Operations]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">When you start with AWS you usually create a single account, get familiar with some services, and deploy a few workloads. But as your AWS footprint grows, managing everything in one account becomes unwieldy and risky. What if a developer accidentally deletes a production database? What if someone creates resources in an expensive region? What if your root credentials get compromised? (Yes, that still happens, I saved someone from that last year).</p><p class="paragraph" style="text-align:left;">AWS Organizations solves these problems by enabling you to manage multiple AWS accounts centrally. The trick is governing those accounts with consistent policies, tracking costs collectively, implementing proper isolation between different environments and workloads, and doing all of that without it becoming an even bigger pain in the backend.</p><p class="paragraph" style="text-align:left;">In this series we&#39;re going to talk about AWS Organizations, with a focus on how to implement a secure and sane multi-account strategy (sane as in it doesn&#39;t drive you mad). This article is part 1 of the series, where I&#39;ll cover the key features of Organizations. In part 2, which will be published next week, I&#39;ll cover best practices and suggestions.</p><h2 class="heading" style="text-align:left;" id="root-user-security-and-management">Root User Security and Management</h2><p class="paragraph" style="text-align:left;">Each AWS account has a highly privileged root user; in member accounts, applicable SCPs can still restrict its actions. These god-like permissions make the root user your biggest security risk.</p><p class="paragraph" style="text-align:left;">There are very few tasks that require root credentials. For standalone accounts, examples include changing the root email address or closing the account; changing the account name does not require root. With AWS Organizations and the required permissions, member-account email updates and closure can be managed centrally. Some billing activities like enabling IAM access to the billing console and specific security features like enabling MFA delete on S3 buckets also require the root user. But beyond that, you really don&#39;t need it.</p><p class="paragraph" style="text-align:left;">Here&#39;s what you would do if you were using a single account:</p><ol start="1"><li><p class="paragraph" style="text-align:left;">Enable multi-factor authentication (MFA) for the root user.</p></li><li><p class="paragraph" style="text-align:left;">Use a strong, randomly generated password and never reuse it on other systems.</p></li><li><p class="paragraph" style="text-align:left;">Don&#39;t create access keys for the root user.</p></li><li><p class="paragraph" style="text-align:left;">Only use the root user for the few tasks that absolutely require it. Treat the root user as a &quot;break-glass&quot; credential that stays locked away until absolutely needed.</p></li><li><p class="paragraph" style="text-align:left;">Use a corporate group email for the root user so multiple trusted admins receive security notices and can recover the account if needed.</p></li></ol><p class="paragraph" style="text-align:left;">I even wrote an article on <a class="link" href="https://newsletter.simpleaws.dev/p/7-must-do-security-best-practices-for-your-aws-account?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-organizations-deep-dive" target="_blank" rel="noopener noreferrer nofollow">single account security</a> a couple of years ago.</p><p class="paragraph" style="text-align:left;">Well, in a multi-account environment with AWS Organizations you can do all of that and more. AWS Organizations allows the management account to centralize root access by removing root credentials from member accounts. If you use Organizations to create AWS Accounts they don&#39;t even come with a root password. Plus, Service Control Policies can restrict a member account&#39;s root user, subject to the documented SCP exceptions.</p><p class="paragraph" style="text-align:left;">But I&#39;m getting ahead of myself. Let&#39;s talk about what you can do with Organizations.</p><h2 class="heading" style="text-align:left;" id="organizational-units-o-us-design-an">Organizational Units (OUs): Design and Structure</h2><p class="paragraph" style="text-align:left;">Organizational Units (OUs) are logical groupings of accounts within AWS Organizations. They&#39;re the containers that let you apply management policies (like Service Control Policies) to multiple accounts simultaneously in a hierarchical fashion. The key to effective OUs is structuring them based on common functions or compliance requirements, not mirroring your org chart.</p><div class="image"><img alt="Example AWS Organizations hierarchy: the root contains Dev, Prod, and Sales OUs, with nested Test, PreProd, and EMEA OUs and five member accounts." class="image__image" style="border-radius:0px 0px 0px 0px;border-style:solid;border-width:0px 0px 0px 0px;box-sizing:border-box;border-color:#E5E7EB;" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/c96fe7da-859d-49a9-b23a-87ba64823d89/image.png?t=1742580839"/><div class="image__source"><span class="image__source_text"><p>Example Organizational Units structure</p></span></div></div><p class="paragraph" style="text-align:left;">A well-planned hierarchy typically starts with a few foundational OUs (which I didn&#39;t include in the diagram up there because I forgot):</p><ul><li><p class="paragraph" style="text-align:left;"><b>Security</b>: Contains accounts for centralized security services such as log archiving, security tooling, and audit functions.</p></li><li><p class="paragraph" style="text-align:left;"><b>Shared</b>: Shared services like networking, identity management, and possibly shared CI/CD systems that are used organization-wide.</p></li><li><p class="paragraph" style="text-align:left;"><b>Workloads</b>: Contains accounts for business application workloads. These are often subdivided by environment (e.g., Production and Non-Production sub-OUs).</p></li><li><p class="paragraph" style="text-align:left;"><b>Sandbox</b>: Experimental accounts where developers can try things without worrying about affecting someone else. Even non-production accounts like Dev need to be somewhat stable and have some rules. Sandboxes usually don&#39;t have many rules except for security and to prevent excessively expensive experiments.</p></li></ul><p class="paragraph" style="text-align:left;">You can add more OUs as needed, such as a Policy Staging OU for testing new policies before broader deployment, or a Suspended OU to contain decommissioned accounts with an SCP that denies all actions.</p><p class="paragraph" style="text-align:left;">OUs can be nested up to five levels deep, enabling hierarchical policy inheritance. This means that policies attached at a parent OU automatically apply to all child OUs and accounts beneath it. For example, you can attach broad security baseline SCPs at a top-level OU, and then apply more specific rules to sub-OUs as required.</p><p class="paragraph" style="text-align:left;">When designing your OU structure, remember that OUs are primarily a tool for policy management, not arbitrary grouping. Create them when you need to apply different policies or controls to a set of accounts. And avoid making your hierarchy too deep or overly reflective of organizational changes, which tend to happen more frequently than policy needs change.</p><h2 class="heading" style="text-align:left;" id="consolidated-billing-streamlining-f">Consolidated Billing: Streamlining Financial Management</h2><p class="paragraph" style="text-align:left;">One of the most immediate benefits of AWS Organizations is consolidated billing. Instead of managing separate bills for each AWS account, you get a single monthly bill that covers all accounts in your organization.</p><p class="paragraph" style="text-align:left;">When you set up AWS Organizations, all member accounts automatically have their usage rolled up to the management account&#39;s bill. This simplifies finance operations and makes it easier to process payments.</p><p class="paragraph" style="text-align:left;">Another major advantage is that your combined usage qualifies for <b>volume pricing discounts</b>. AWS treats all the usage across your organization as one pool for services with tiered pricing, like data transfer or S3 requests. This may lead to lower overall costs than if each account operated independently. I mean, volume pricing discounts are not easy to reach, but putting every dollar into the same bill helps. And remember that you can negotiate private pricing with AWS if your bill is large enough.</p><p class="paragraph" style="text-align:left;">Even more significant is the ability to share <a class="link" href="https://newsletter.simpleaws.dev/p/aws-savings-plans-reserved-instances?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-organizations-deep-dive" target="_blank" rel="noopener noreferrer nofollow">Reserved Instances (RIs) and Savings Plans</a> across accounts. If one account purchases a heavy RI and isn&#39;t fully utilizing it, other accounts can automatically benefit from the discounted rate. This maximizes the value of your commitments and avoids the common problem of &quot;stranded&quot; discounts.</p><p class="paragraph" style="text-align:left;">While the management account is responsible for paying the consolidated bill, you can still track costs by individual account. The management account can see a breakdown of charges incurred by each account, which is useful for internal chargeback to departments or projects.</p><p class="paragraph" style="text-align:left;">There are some limitations to be aware of. AWS Support fees are typically not pooled under Organizations. Each account&#39;s support plan is billed separately and calculated based on that account&#39;s usage, though Enterprise Support customers have options for aggregated support.</p><p class="paragraph" style="text-align:left;">Also, if an account leaves your organization, it loses access to historical cost data from when it was a member. This can complicate matters if you&#39;re reorganizing or spinning off accounts, so plan accordingly.</p><h2 class="heading" style="text-align:left;" id="cost-explorer-and-multi-account-cos">Cost Explorer and Multi-Account Cost Management</h2><p class="paragraph" style="text-align:left;">AWS Cost Explorer provides fantastic visualization and analysis capabilities. For multi-account environments, the management account has access to all cost data for all member accounts in the organization, and you can view it all in Cost Explorer.</p><p class="paragraph" style="text-align:left;">Within Cost Explorer you can apply the Account filter to view spending for specific accounts or compare spending across accounts. You can also create custom reports that group accounts in meaningful ways, like by environment or business unit.</p><p class="paragraph" style="text-align:left;">Member accounts, by default, can only see their own costs. If you want teams managing member accounts to have visibility into their spending, you&#39;ll need to grant them billing access through the &quot;Activate IAM Access to Billing&quot; setting and appropriate IAM policies.</p><p class="paragraph" style="text-align:left;">For more advanced cost allocation, AWS offers Cost Categories. These let you define custom groupings for cost reporting. For example you can create a cost category called &quot;Environment&quot; that groups certain accounts as &quot;Production&quot; and others as &quot;Development.&quot; This is particularly helpful because Cost Explorer doesn&#39;t natively understand the OU hierarchy or tags from AWS Organizations.</p><p class="paragraph" style="text-align:left;">When implementing cost management across accounts, consider centralizing cost monitoring through the management account, creating read-only billing dashboard roles that team leads or finance can assume. Enable AWS Cost and Usage Report (CUR) and have it delivered to an S3 bucket. The CUR contains granular data for all usage and costs, including each account&#39;s consumption, which is invaluable for advanced analysis.</p><p class="paragraph" style="text-align:left;">Use consistent resource tagging across accounts for shared resources, and enable them as cost allocation tags in the Billing Console. Set up AWS Budgets on both a per-account basis and a rolled-up basis. Create budgets that track each account&#39;s monthly spend with notifications to account owners if they approach limits.</p><p class="paragraph" style="text-align:left;">Leverage data from Cost Explorer or CUR to allocate charges to business units. An account-based model simplifies this since each account&#39;s bill is clearly defined.</p><p class="paragraph" style="text-align:left;">Unfortunately there&#39;s no sane way to enforce tags on AWS resources. That I know of. It&#39;s a billion-dollar problem, so if you have a reliable solution please let me know.</p><h2 class="heading" style="text-align:left;" id="accessing-your-aws-accounts-in-an-o">Accessing Your AWS Accounts in an Organization</h2><p class="paragraph" style="text-align:left;">Managing access across multiple AWS accounts can get messy quickly if you try to create and maintain separate IAM users in each account. So how do you provide secure, manageable access to dozens or hundreds of accounts? AWS IAM Identity Center is the way.</p><p class="paragraph" style="text-align:left;">IAM Identity Center lets you connect your corporate identity provider (like Active Directory, Okta, or other SAML 2.0 providers) or use its built-in directory to manage users. You then assign these identities different levels of access to your AWS accounts.</p><p class="paragraph" style="text-align:left;">Setting up IAM Identity Center is pretty straightforward. First enable it from within the AWS Organizations management account. Next choose your identity source, either the built-in Identity Center directory, Active Directory, or an external identity provider. Then create permission sets that define what users can do in your AWS accounts. Finally assign users some permission sets in some accounts.</p><p class="paragraph" style="text-align:left;">Permission sets are essentially IAM roles with predefined policies (like AdministratorAccess or ReadOnlyAccess) or custom permission policies you create. Once your permission sets are defined, you assign them to specific users or groups for specific AWS accounts. For example, you might give your DevOps team administrator access to development accounts but only read-only access to production accounts. Or you might give your finance team billing access to all accounts.</p><p class="paragraph" style="text-align:left;">When users need to work in AWS, they log into the AWS access portal with their corporate credentials. They&#39;re presented with a list of accounts they can access and the roles they can assume in each. Clicking on a role takes them directly to that account with those permissions, without requiring separate passwords for each account.</p><div class="image"><img alt="AWS access portal listing eight accounts, with one account expanded to show the FullAdmin permission set and an Access keys link." class="image__image" style="border-radius:0px 0px 0px 0px;border-style:solid;border-width:0px 0px 0px 0px;box-sizing:border-box;border-color:#E5E7EB;" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/742e64f6-de2e-4066-a9c9-d95acd37bf7d/image.png?t=1742585344"/><div class="image__source"><span class="image__source_text"><p>Example list of AWS accounts</p></span></div></div><p class="paragraph" style="text-align:left;">Behind the scenes, IAM Identity Center creates the corresponding IAM roles in each account for each permission set assignment. It also handles the authentication and federation process, making it seamless for users to switch between accounts.</p><p class="paragraph" style="text-align:left;">This approach has several major benefits:</p><ol start="1"><li><p class="paragraph" style="text-align:left;">You eliminate the need to create and manage IAM users in each account separately.</p></li><li><p class="paragraph" style="text-align:left;">Users have a single set of credentials to remember and secure.</p></li><li><p class="paragraph" style="text-align:left;">When someone leaves your organization, you can revoke access in one place instead of hunting through multiple accounts.</p></li><li><p class="paragraph" style="text-align:left;">Access assignments can follow your account structure, but must be maintained as accounts are added or moved.</p></li></ol><p class="paragraph" style="text-align:left;">IAM Identity Center should be the primary way that humans access AWS accounts. Period.</p><p class="paragraph" style="text-align:left;">You can also get programmatic access for humans, in the form of short-lived credentials that you can paste in your CLI or .env file. Just click on Access keys 🔑 and you&#39;ll see this:</p><div class="image"><img alt="AWS access portal credentials dialog for FullAdmin, showing CLI setup, environment variables, profile configuration, and individual credentials with sensitive values redacted." class="image__image" style="border-radius:0px 0px 0px 0px;border-style:solid;border-width:0px 0px 0px 0px;box-sizing:border-box;border-color:#E5E7EB;" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/82d13fa1-1172-4678-b68d-e29224798650/image.png?t=1742585755"/><div class="image__source"><span class="image__source_text"><p>Temporary credentials</p></span></div></div><p class="paragraph" style="text-align:left;">For occasional access, this is the easiest way. For regular access you can <a class="link" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-organizations-deep-dive" target="_blank" rel="noopener noreferrer nofollow">Configure IAM Identity Center authentication with the AWS CLI</a>. For programmatic access not meant for humans, e.g. for a web server, you shouldn&#39;t use this, nor should you use long-term credentials. Instead you should set up an <a class="link" href="https://newsletter.simpleaws.dev/p/aws-iam-permissions-a-comprehensive-guide?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-organizations-deep-dive" target="_blank" rel="noopener noreferrer nofollow">IAM Role</a>.</p><h2 class="heading" style="text-align:left;" id="cross-account-programmatic-access">Cross-Account Programmatic Access</h2><p class="paragraph" style="text-align:left;">IAM Identity Center handles human access to your accounts, but you also need to manage how services and applications in one account access resources in another. The answer is usually IAM roles with trust relationships, or sometimes Resource Access Manager.</p><p class="paragraph" style="text-align:left;">The main way is to create an IAM role in Account B that grants access to certain resources and set the role&#39;s trust policy to allow Account A to assume it. Then a service or application in Account A can assume that role to get temporary credentials for Account B.</p><p class="paragraph" style="text-align:left;">For accounts created through AWS Organizations, it creates an administrative access role named OrganizationAccountAccessRole by default, unless you choose another name. Invited accounts do not receive it automatically. This role enables management-account administrators to access the member account; consolidated billing does not depend on this role.</p><p class="paragraph" style="text-align:left;">Beyond this default administrative role, you&#39;ll want to create more narrowly scoped roles for specific cross-account access needs. For example a deployment role that your CI/CD pipelines can assume to deploy resources</p><p class="paragraph" style="text-align:left;">Note that if you&#39;re setting this up for a resource like an AWS Lambda function in account A, the role in account B can&#39;t have a trust relationship for the function. The role in account B can trust a specific IAM role in account A, such as the Lambda execution role, or delegate trust to account A subject to permissions and conditions. Account A delegates this capability of assuming the role in account B to its principals, in this case the IAM Role that the Lambda function has assigned to it. So, when the Lambda function in account A accesses a resource in account B, this is what&#39;s actually happening:</p><ol start="1"><li><p class="paragraph" style="text-align:left;">The Lambda function in account A gets temporary credentials for the role in account A that it has assigned to it. This role has permissions to assume the role in account B.</p></li><li><p class="paragraph" style="text-align:left;">The Lambda function uses these credentials to assume the role in account B, receiving temporary credentials scoped for account B.</p></li><li><p class="paragraph" style="text-align:left;">The Lambda function uses those credentials to access the intended resource in account B.</p></li></ol><p class="paragraph" style="text-align:left;">Nobody in account A can assume the role in account B unless they use credentials that have this explicit permission (via an IAM Policy applied to the user or role).</p><p class="paragraph" style="text-align:left;">For resource-specific access, you can use <b>resource-based policies</b> instead of roles. For instance, an S3 bucket in Account A can include a bucket policy that grants read/write permissions to a specific role in Account B. Similar cross-account resource policies exist for services like KMS, SNS, and SQS.</p><p class="paragraph" style="text-align:left;"><b><a class="link" href="https://docs.aws.amazon.com/ram/latest/userguide/what-is.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-organizations-deep-dive" target="_blank" rel="noopener noreferrer nofollow">AWS Resource Access Manager (RAM)</a></b> is another service that works with Organizations to let you share resources across accounts. Instead of giving Account A&#39;s IAM principal direct access to Account B&#39;s subnet, you can share the subnet from Account B to Account A using RAM. This simplifies resource sharing, especially for networking and infrastructure components.</p><p class="paragraph" style="text-align:left;">For service-level delegation, AWS Organizations allows certain services to designate a member account as a <b>delegated administrator</b>. This means a specific account (other than the management account) can manage that service&#39;s organization-wide settings. For example, you could delegate GuardDuty administration to your security account, allowing that account to enable/disable GuardDuty for all accounts and aggregate findings without using the management account.</p><p class="paragraph" style="text-align:left;"><b>Tip 1:</b> Limit the use of the management account to organizational tasks only and don&#39;t use it for routine operations in member accounts.</p><p class="paragraph" style="text-align:left;"><b>Tip 2:</b> Establish a clear access model, and use that consistently.</p><p class="paragraph" style="text-align:left;"><b>Tip 3:</b> Use external IDs in trust policies when delegating access to third parties, adding an additional verification check to the role assumption process.</p><p class="paragraph" style="text-align:left;"><b>Tip 4:</b> Automate the provisioning of standard cross-account roles. When new accounts are created, automatically set up the roles they need to participate in your environment.</p><h2 class="heading" style="text-align:left;" id="service-control-policies-sc-ps">Service Control Policies (SCPs)</h2><p class="paragraph" style="text-align:left;">Service Control Policies (SCPs) act as guardrails that define the maximum allowed actions for accounts in your organization. They&#39;re written in JSON format similar to IAM policies, but instead of granting permissions, they filter which permissions can be exercised. A user or role in an account affected by an SCP can only perform actions that are allowed by both the IAM policy and all applicable SCPs, and that are not explicitly denied in any IAM policy or any SCP.</p><p class="paragraph" style="text-align:left;">Let&#39;s say a developer has an IAM policy that grants them permission to launch any EC2 instance type. If you apply an SCP that denies the ability to launch expensive instance types like <code>x1e.32xlarge</code>, that developer won&#39;t be able to launch those instances regardless of their IAM permissions. The effective permissions are the intersection of IAM permissions and SCP allowances. To understand more about how IAM permissions are applied, read my <a class="link" href="https://newsletter.simpleaws.dev/p/aws-iam-permissions-a-comprehensive-guide?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-organizations-deep-dive" target="_blank" rel="noopener noreferrer nofollow">IAM Permissions Deep Dive</a>.</p><p class="paragraph" style="text-align:left;">By default, when you enable SCPs (you must have Organizations set to &quot;all features&quot; mode, not just consolidated billing), AWS provides a policy called FullAWSAccess attached to every OU. This default policy allows all actions, ensuring existing operations aren&#39;t disrupted until you start implementing custom SCPs.</p><p class="paragraph" style="text-align:left;">There are two main strategies for implementing SCPs:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Deny list strategy</b>: Keep the default FullAWSAccess policy and add SCPs that explicitly deny specific actions or services you want to restrict. This is simpler to start with since it won&#39;t accidentally block legitimate activities.</p></li><li><p class="paragraph" style="text-align:left;"><b>Allow list strategy</b>: Replace the default policy with SCPs that explicitly allow only the services and actions you want to permit. This is more secure but requires more maintenance, as new AWS services will be denied by default until added to your allow list.</p></li></ol><p class="paragraph" style="text-align:left;">SCPs can be attached at the organization root level, to specific OUs, or to individual accounts. Policies are inherited down the hierarchy, so SCPs attached to a parent OU apply to all child OUs and accounts beneath it. Multiple SCPs can apply to the same account, and their effects are cumulative. If any policy denies an action, it&#39;s denied regardless of what other policies say.</p><p class="paragraph" style="text-align:left;">Importantly, SCPs do not restrict principals in the management account. This makes sense since you wouldn&#39;t want to accidentally lock yourself out of your organization&#39;s central account. They also don&#39;t interfere with service-linked roles that AWS services use to perform operations on your behalf.</p><p class="paragraph" style="text-align:left;">A few interesting things you can do with SCPs:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Restricting regions</b>: You can deny all actions in AWS regions that your company doesn&#39;t use, effectively geo-fencing your accounts to approved regions for compliance or cost control.</p></li><li><p class="paragraph" style="text-align:left;"><b>Blocking risky services</b>: If certain AWS services aren&#39;t approved in your organization, an SCP can prevent their use entirely.</p></li><li><p class="paragraph" style="text-align:left;"><b>Enforcing security controls</b>: SCPs can prevent anyone from disabling security services or changing critical security settings, like turning off CloudTrail logging or disabling encryption.</p></li><li><p class="paragraph" style="text-align:left;"><b>Quarantining compromised accounts</b>: If you detect a security issue, you can quickly attach an SCP with a <code>Deny *</code> statement to restrict principals in the member account. This is not complete isolation: management-account principals, service-linked roles, and external principals with resource-policy access are outside that SCP restriction, and existing resources can continue generating charges.</p></li></ol><p class="paragraph" style="text-align:left;"><b>Tip:</b> Start with a deny list approach, beginning with a small set of risky actions that you know should never be allowed. Use <a class="link" href="https://asecure.cloud/l/scp?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-organizations-deep-dive" target="_blank" rel="noopener noreferrer nofollow">these SCP templates</a> as a starting point.</p><p class="paragraph" style="text-align:left;"><b>Tip 2:</b> Always test SCPs in a limited scope before broad deployment. Create a Policy Staging OU or test account, apply the SCP there, and verify that it works as expected without blocking needed functionality.</p><p class="paragraph" style="text-align:left;"><b>Tip 3:</b> Use the IAM Access Analyzer or policy simulator to validate your SCPs and understand their impact before applying them. Document each SCP&#39;s purpose and which accounts it affects. When developers encounter &quot;Access Denied&quot; errors due to SCPs, they need to understand why.</p><p class="paragraph" style="text-align:left;"><b>Tip 4:</b> Regularly review your SCPs as AWS releases new services or your organization&#39;s requirements change. SCPs aren&#39;t micromanagement tools, they&#39;re guardrails that establish boundaries while still allowing teams to innovate within those boundaries. Use them a lot, but be careful not to stifle innovation. That&#39;s what we have sandbox accounts for, after all.</p><h2 class="heading" style="text-align:left;" id="conclusion-of-part-1">Conclusion of Part 1</h2><p class="paragraph" style="text-align:left;">This article was getting pretty long, so I&#39;m going to stop it here. I&#39;ll post Part 2 next week, with best practices and suggestions.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>Amazon Bedrock Prompt Caching</title>
  <description>A March 2025 look at Amazon Bedrock prompt caching: reusable prefixes, checkpoints, API examples, and token-cost calculations, with implementation caveats.</description>
  <link>https://newsletter.simpleaws.dev/p/amazon-bedrock-prompt-caching</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/amazon-bedrock-prompt-caching</guid>
  <pubDate>Fri, 07 Mar 2025 15:00:00 +0000</pubDate>
  <atom:published>2025-03-07T15:00:00Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Artificial Intelligence]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">I spend a lot of time working with large language models in production, and one persistent problem is that they&#39;re expensive 💸 . Prompt caching can help a lot with that, especially when you&#39;re reusing the context in a conversation (something that we take for granted in ChatGPT or Claude.ai, but is actually expensive).</p><p class="paragraph" style="text-align:left;">This March 2025 article explores Amazon Bedrock&#39;s explicit prompt caching, checkpoint mechanics, cache lifetime, APIs, and economics. Model support, cache lifetimes, and prices have since changed; the historical code snippets require schema and response-handling repairs before use.</p><h2 class="heading" style="text-align:left;" id="technical-architecture-of-bedrock-p">Technical Architecture of Bedrock Prompt Caching</h2><p class="paragraph" style="text-align:left;">Prompt caching isn&#39;t just a simple text storage mechanism. There&#39;s a whole infrastructure dedicated to preserving neural network states.</p><p class="paragraph" style="text-align:left;">When a large language model processes text, it&#39;s not just reading characters, it&#39;s building an internal representation of everything it reads. This includes attention patterns, token relationships, and other neural activations. Normally, when you send a prompt, the model rebuilds this entire representation from scratch every time, even if 90% of the prompt is identical to previous requests.</p><p class="paragraph" style="text-align:left;">What actually happens with Bedrock prompt caching is that the service captures this internal neural state at specific points (checkpoints) and stores it in an ephemeral cache. This is fundamentally more complex than just storing the text string, it&#39;s preserving the actual computed representation inside the model.</p><p class="paragraph" style="text-align:left;">The cache itself lives in AWS-managed infrastructure, completely isolated within service boundaries to maintain security between different AWS accounts. Your cached content isn&#39;t accessible to other users, and their cached content isn&#39;t accessible to you. The architecture maintains security by keeping the cached state tied to your specific Bedrock resources and API credentials.</p><p class="paragraph" style="text-align:left;">The caching layer integrates directly with Bedrock&#39;s execution environment. If you&#39;re familiar with <a class="link" href="https://newsletter.simpleaws.dev/p/aws-lambda-underlying-architecture?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=amazon-bedrock-prompt-caching" target="_blank" rel="noopener noreferrer nofollow">how AWS Lambda works</a>, you can think of a similar execution model. Just as Lambda runs on AWS Lambda Workers (which are essentially EC2 instances), Bedrock also runs on compute infrastructure optimized for inference. The prompt caching system sits between your API calls and this infrastructure, intercepting and storing neural states when instructed.</p><p class="paragraph" style="text-align:left;">From a performance standpoint, setting up a cache creates a small overhead on first write. But that investment pays off dramatically on subsequent reads, with massive reductions in processing time and cost. The whole system is optimized for bursts of activity with similar prompt content, which fits the pattern of many LLM applications like conversation agents, document analysis tools, coding assistants, or anything where you keep asking things about something you already shared.</p><h2 class="heading" style="text-align:left;" id="cache-checkpoint-mechanics">Cache Checkpoint Mechanics</h2><p class="paragraph" style="text-align:left;">Cache checkpoints are the actual mechanism that makes prompt caching work. But what exactly is a checkpoint? It&#39;s a specific position in a prompt where Bedrock saves the model&#39;s entire internal state up to that point. Think of it as a bookmark in the model&#39;s thought process, the model can jump back to that exact state later without reprocessing everything that came before.</p><p class="paragraph" style="text-align:left;">You can&#39;t just place checkpoints anywhere you want. They follow specific token thresholds that vary by model. For Anthropic&#39;s Claude 3.5 model, you need approximately 1,024 tokens of combined conversation before you can set the first checkpoint. Why this minimum threshold? Because the overhead of caching very small prompts would outweigh the benefits, there wouldn&#39;t be enough tokens saved to justify the cache management cost.</p><p class="paragraph" style="text-align:left;">Different models have different checkpoint configurations:</p><ul><li><p class="paragraph" style="text-align:left;">Claude 3.5 allows up to 4 checkpoints per conversation</p></li><li><p class="paragraph" style="text-align:left;">Amazon&#39;s smaller Nova models typically support just 1 checkpoint</p></li><li><p class="paragraph" style="text-align:left;">The placement options vary by model, some allow checkpoints in system messages, user messages, and tool sections, while others restrict placement</p></li></ul><p class="paragraph" style="text-align:left;">The checkpoint creation process is pretty fascinating if you understand what&#39;s happening during model inference, which is why I&#39;m diving so deep into this stuff (I had a lot of fun researching it!). When Bedrock identifies a cache point in your prompt that meets the token threshold, it does something akin to taking a snapshot of the entire neural network&#39;s state at that moment. This state captures all the attention patterns, embeddings, and internal representations that the model has built up to that point. This complex state is extracted and stored with a unique identifier associated with your AWS account and the specific model.</p><p class="paragraph" style="text-align:left;">I should emphasize one technical limitation: checkpoints are tied to the exact prefix they follow. If you change even a single character before the checkpoint, the cache can&#39;t be used. Why? The model&#39;s internal state depends on the precise sequence of tokens it has processed, any change, no matter how small, would result in a different neural state. This strictness is necessary because the model&#39;s computations (not output) are deterministic based on the input.</p><p class="paragraph" style="text-align:left;">Another important detail is how checkpoints interact with the model&#39;s context window. Cached content absolutely still counts toward the total context window for the model. If you cache a 2,000-token document and the model has a 100,000-token context window, you&#39;ll still have only 98,000 tokens available for additional content before the model starts “forgetting” stuff and quality goes to the floor. The cache doesn&#39;t magically expand the context window, it just prevents redundant processing of the same tokens, and prevents you from having to pay for that redundant processing.</p><h2 class="heading" style="text-align:left;" id="cache-lifetime-and-management">Cache Lifetime and Management</h2><p class="paragraph" style="text-align:left;">In Bedrock&#39;s implementation, the prompt cache is ephemeral by design, with a default Time To Live (TTL) of 5 minutes. The TTL timer works on a sliding window basis, each time the cache is successfully hit (used), the 5-minute timer resets. This keeps frequently used cache entries alive while allowing rarely used ones to expire naturally. Why 5 minutes specifically? It&#39;s likely the AWS team found this to be the sweet spot that covers most interactive use cases without wasting storage on infrequently accessed data. I&#39;ll ask them if I get the chance.</p><p class="paragraph" style="text-align:left;">When a cache expires, the stored model state is discarded completely. There&#39;s no way to retrieve it after expiration, and no background persistence occurs. If you need the same content again after expiration, you&#39;ll need to reprocess the full prompt and recreate the cache from scratch.</p><p class="paragraph" style="text-align:left;">What&#39;s particularly interesting about Amazon&#39;s implementation is that it&#39;s clearly optimized for interactive workloads rather than long-running processes. The 5-minute window aligns with typical human interaction patterns in conversation systems and document analysis workflows, where bursts of activity occur within short timeframes. For systems with longer idle periods between related requests, you&#39;ll need to implement strategies to manage cache retention, such as periodic &quot;keepalive&quot; requests or prompt restructuring to minimize the impact of cache misses. Yes, both of these are real things we do in prod.</p><h2 class="heading" style="text-align:left;" id="bedrock-api-integration">Bedrock API Integration</h2><p class="paragraph" style="text-align:left;">Understanding how to integrate prompt caching with Bedrock&#39;s APIs is where the rubber meets the road. There are three primary integration points: the Converse API for multi-turn conversations, the InvokeModel API for single-turn completions, and automatic integration with Bedrock Agents.</p><p class="paragraph" style="text-align:left;">For the Converse API, which handles chat-style interactions, you mark cache points within the message structure. Here&#39;s a Python example:</p><div class="codeblock"><pre><code>import boto3

bedrock = boto3.client(&quot;bedrock-runtime&quot;)
document_text = &quot;Very long reference document that you don&#39;t want to process repeatedly...&quot; 

messages = [&#123;&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: []&#125;]
# Add the long context to the user message
messages[0][&quot;content&quot;].append(&#123;&quot;text&quot;: document_text&#125;)
# Mark cache checkpoint after the large context
messages[0][&quot;content&quot;].append(&#123;&quot;cachePoint&quot;: &#123;&quot;type&quot;: &quot;default&quot;&#125;&#125;)
# Add the user&#39;s actual question after the checkpoint
messages[0][&quot;content&quot;].append(&#123;&quot;text&quot;: &quot;What are the key points in this document?&quot;&#125;)

response = bedrock.converse(
    modelId=&quot;anthropic.claude-3-5-sonnet-20241022-v2:0&quot;,
    messages=messages
)</code></pre></div><p class="paragraph" style="text-align:left;">This structure tells Bedrock to process the <code>document_text</code> once, cache the resulting model state, and then process the question. On subsequent requests, resend the same cacheable document prefix and cache checkpoint; Bedrock can reuse its cached computation. The beauty of this approach is that it integrates directly with the conversation structure without requiring special caching APIs.</p><p class="paragraph" style="text-align:left;">For the InvokeModel API, which handles single-turn prompts, caching is enabled by including appropriate fields in the request body. The exact structure depends on the model being used. For Anthropic models, it typically involves adding a <code>cache_control</code> field:</p><div class="codeblock"><pre><code>response = bedrock.invoke_model(
    modelId=&quot;anthropic.claude-3-5-sonnet-20241022-v2:0&quot;,
    body=json.dumps(&#123;
        &quot;prompt&quot;: &quot;System: You are a helpful assistant.\n\nHuman: &quot; + document_text,
        &quot;cache_control&quot;: &#123;&quot;type&quot;: &quot;ephemeral&quot;&#125;,
        &quot;max_tokens&quot;: 1000
    &#125;)
)</code></pre></div><p class="paragraph" style="text-align:left;">Here&#39;s a trick that&#39;s not immediately obvious: To verify that caching is working properly, you should examine the response metadata. Bedrock includes cache-related metrics in the usage data:</p><div class="codeblock"><pre><code># Extract usage metrics from response
usage = response[&quot;usage&quot;]
cached_read_tokens = usage.get(&quot;cacheReadInputTokens&quot;, 0)
cached_write_tokens = usage.get(&quot;cacheWriteInputTokens&quot;, 0)

print(f&quot;Tokens read from cache: &#123;cached_read_tokens&#125;&quot;)
print(f&quot;Tokens written to cache: &#123;cached_write_tokens&#125;&quot;)</code></pre></div><p class="paragraph" style="text-align:left;">On the first request you&#39;ll see a high value for cacheWriteInputTokens (the tokens being cached) and zero for cacheReadInputTokens. On subsequent requests that use the same cached content, you&#39;ll see the opposite pattern, high cacheReadInputTokens and zero cacheWriteInputTokens. This provides a clear signal that the caching system is working as expected.</p><p class="paragraph" style="text-align:left;">For Bedrock Agents, enabling prompt caching is even simpler, you just toggle a setting in the agent configuration:</p><div class="codeblock"><pre><code>response = bedrock_agent.update_agent(
    agentId=&quot;your-agent-id&quot;,
    promptOverrideConfiguration=&#123;
        &quot;promptCachingEnabled&quot;: True
    &#125;
)</code></pre></div><p class="paragraph" style="text-align:left;">With this setting enabled, the agent automatically manages cache checkpoints without requiring any additional code in your application logic. This is particularly useful for complex agent workflows where manually placing checkpoints would be cumbersome.</p><h2 class="heading" style="text-align:left;" id="how-much-did-bedrock-prompt-caching">How Much Did Bedrock Prompt Caching Cost in March 2025?</h2><p class="paragraph" style="text-align:left;">Now let&#39;s talk about money. The economics of prompt caching are where this feature really shines for applications with repetitive content. Here&#39;s the detailed cost structure:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Cache Write (First-time Processing):</b> When content is processed and written to the cache for the first time, you pay a small premium over regular processing. For third-party models like Anthropic&#39;s Claude on Bedrock, the cache write cost is approximately 25% higher than the standard input token price. For Amazon&#39;s own models, there&#39;s currently (as of March 2025) no extra charge for cache writes.</p></li><li><p class="paragraph" style="text-align:left;"><b>Cache Read (Subsequent Reuse):</b> The big savings come from cache reads. When you reuse cached content, you pay only about 10% of the normal input token price, a 90% discount compared to processing those tokens from scratch.</p></li><li><p class="paragraph" style="text-align:left;"><b>Storage Costs:</b> There are no separate storage fees for keeping data in the cache. You only pay the read/write token fees described above.</p></li></ol><p class="paragraph" style="text-align:left;">This pricing structure reflects the real computational cost difference between processing tokens from scratch and reusing pre-computed representations, which I talked in detail about at the beginning of the article.</p><p class="paragraph" style="text-align:left;">Let&#39;s see some example numbers.</p><p class="paragraph" style="text-align:left;">Imagine you&#39;re building a financial document analysis system that allows users to upload quarterly reports (average 30,000 tokens) and ask multiple questions about them. Each user session involves around 8 questions about the same document. Without caching, each question would require reprocessing the entire document.</p><p class="paragraph" style="text-align:left;">Let&#39;s calculate the costs using the newly-released Claude 3.7 Sonnet pricing (Claude 3.5 Sonnet has the same price):</p><ul><li><p class="paragraph" style="text-align:left;">Input tokens (no cache): $0.003 / 1k tokens</p></li><li><p class="paragraph" style="text-align:left;">Output tokens: $0.015 / 1k tokens (same price with or without cache)</p></li><li><p class="paragraph" style="text-align:left;">Input tokens (cache write): $0.00375 / 1k tokens (25% more expensive)</p></li><li><p class="paragraph" style="text-align:left;">Input tokens (cache read): $0.0003 / 1k tokens (90% cheaper)</p></li></ul><p class="paragraph" style="text-align:left;"><b>Without Caching:</b></p><ul><li><p class="paragraph" style="text-align:left;">Document processing per question: 30,000 tokens × $0.003/1000 = $0.09 per question</p></li><li><p class="paragraph" style="text-align:left;">Total for 8 questions: 8 × $0.09 = $0.72</p></li></ul><p class="paragraph" style="text-align:left;"><b>With Caching:</b></p><ul><li><p class="paragraph" style="text-align:left;">First question (cache write): 30,000 tokens × $0.00375/1000 = $0.1125</p></li><li><p class="paragraph" style="text-align:left;">Subsequent 7 questions: 7 × 30,000 tokens × $0.0003/1000 = $0.063</p></li><li><p class="paragraph" style="text-align:left;">Total: $0.1125 + $0.063 = $0.1755</p></li></ul><p class="paragraph" style="text-align:left;">That&#39;s a 75.6% cost reduction for a single user session. Now scale that to an enterprise with thousands of users and documents, and the savings add up to a lot. For a system processing 10,000 documents per month with this pattern, you&#39;d save over $5,400 monthly.</p><p class="paragraph" style="text-align:left;">But it&#39;s not always that simple. Some scenarios where caching might not help much:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Single-use content:</b> If each document is only analyzed once, the cache write premium actually increases your cost by 25% 🫠.</p></li><li><p class="paragraph" style="text-align:left;"><b>Tiny prompts:</b> For very small prompts below the minimum token threshold caching doesn&#39;t activate, so you don&#39;t save anything (nor pay extra).</p></li><li><p class="paragraph" style="text-align:left;"><b>Long gaps between requests:</b> If users typically wait more than 5 minutes between questions, the cache expires and you lose the benefit. Of course you still pay for the cache write, so again your costs increase by 25%.</p></li><li><p class="paragraph" style="text-align:left;"><b>High cache miss rate:</b> If your application frequently generates slightly different versions of prompts that can&#39;t share cache entries, you&#39;ll pay for cache writes without getting read benefits.</p></li></ul><p class="paragraph" style="text-align:left;">To effectively measure your actual savings, implement logging for the <code>cacheReadInputTokens</code> and <code>cacheWriteInputTokens</code> metrics from your API responses. Over time, this data can help you optimize your caching strategy and quantify the ROI.</p><p class="paragraph" style="text-align:left;">There&#39;s another economic benefit worth mentioning: by reducing response latency, caching can also improve user experience. This secondary economic benefit doesn&#39;t show up directly in the token costs but can significantly impact your app, especially since end users’ most common complaint about AI applications (in my experience) is that they take too long to respond.</p><h2 class="heading" style="text-align:left;" id="performance-optimization">Performance Optimization</h2><p class="paragraph" style="text-align:left;">As I mentioned, prompt caching can have a significant impact on performance. According to AWS, cached content can be processed up to 85% faster than uncached content. This translates directly to lower latency for your users.</p><p class="paragraph" style="text-align:left;">The performance gain scales with the size of the cached content, following an interesting pattern. Caching a few hundred tokens might save tens of milliseconds, while caching thousands of tokens can reduce response times by seconds or even tens of seconds for very large prompts. This non-linear relationship occurs because the token processing time in LLMs isn&#39;t perfectly linear, there are fixed overheads and optimizations that vary based on the total workload.</p><p class="paragraph" style="text-align:left;">Here&#39;s a technical insight you might not find in the documentation: the performance benefit isn&#39;t just from skipping token processing, it also comes from avoiding the initial model loading and warmup. When a model starts processing a prompt, there&#39;s a &quot;ramp-up&quot; period where tensor operations aren&#39;t fully optimized. By jumping straight to a cached state, you skip this ramp-up, giving an additional performance boost beyond the raw token processing time.</p><h3 class="heading" style="text-align:left;" id="tips-to-optimize-prompt-structure-f">Tips to optimize prompt structure for maximum performance</h3><ol start="1"><li><p class="paragraph" style="text-align:left;">Place cache checkpoints at logical boundaries in your prompt, such as after system instructions or reference documents but before user queries</p></li><li><p class="paragraph" style="text-align:left;">Ensure that static content comes before dynamic content in your prompts</p></li><li><p class="paragraph" style="text-align:left;">Meet the minimum token threshold for your model before inserting a checkpoint</p></li><li><p class="paragraph" style="text-align:left;">Structure multi-turn conversations to leverage previously cached content</p></li></ol><p class="paragraph" style="text-align:left;">A pitfall I&#39;ve encountered is what I call &quot;cache fragmentation&quot;, creating slightly different versions of similar prompts that can&#39;t benefit from the same cache entry. For example, if you include timestamps or request IDs in your prompt prefix, you&#39;ll create a unique cache entry for each request, effectively nullifying the benefits of caching. To avoid this, standardize your prompt templates and ensure that fixed content is consistent across requests.</p><p class="paragraph" style="text-align:left;">For example, here&#39;s a simple prompt that contains variable data where you&#39;d tend to place it:</p><div class="codeblock"><pre><code>You will be presented with 3 articles about AWS. You must answer the user&#39;s questions about them. The user is a cloud engineer, so make sure you adopt an appropriate tone in your responses.</code></pre></div><p class="paragraph" style="text-align:left;">See the variables? <b>3</b> articles about <b>AWS</b>, and the user is a <b>cloud engineer</b>. So if you later need to send 4 articles instead of 3, or they&#39;re about AI instead of AWS, or the user is a sales rep instead of an engineer, you can&#39;t reuse this from the cache. Here&#39;s how you can rewrite it instead:</p><div class="codeblock"><pre><code>You will be presented with some articles about a specific topic, which I&#39;ll specify at the end of this prompt. You must answer the user&#39;s questions about them. The user has a certain role, which I&#39;ll specify at the end of this prompt, so make sure you adopt an appropriate tone in your responses.

[checkpoint]

topic: AWS
user role: cloud engineer</code></pre></div><p class="paragraph" style="text-align:left;">Now you can put a checkpoint at <code>[checkpoint]</code> and reuse everything that comes before it as a cached prompt. Of course this example is way too brief to cache, but I hope you get the idea.</p><h2 class="heading" style="text-align:left;" id="implementation-patterns-for-common-">Implementation Patterns for Common Scenarios</h2><p class="paragraph" style="text-align:left;">Different use cases require different implementation approaches for prompt caching. Let&#39;s look at some common patterns I&#39;ve found effective:</p><h3 class="heading" style="text-align:left;" id="multi-turn-conversations">Multi-Turn Conversations</h3><p class="paragraph" style="text-align:left;">For conversational applications, cache the system prompt and conversation history to avoid reprocessing previous exchanges. In a multi-turn implementation:</p><div class="codeblock"><pre><code>def handle_conversation(conversation_id, user_message):
    # Retrieve conversation history
    history = get_conversation_history(conversation_id)
    
    # Structure messages with cache points
    messages = [
        &#123;&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: SYSTEM_PROMPT&#125;,
        &#123;&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &#123;&quot;cachePoint&quot;: &#123;&quot;type&quot;: &quot;default&quot;&#125;&#125;&#125;
    ]
    
    # Add previous exchanges with a cache point after history
    for msg in history:
        messages.append(msg)
    
    if history:
        messages.append(&#123;&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &#123;&quot;cachePoint&quot;: &#123;&quot;type&quot;: &quot;default&quot;&#125;&#125;&#125;)
    
    # Add the new user message
    messages.append(&#123;&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_message&#125;)
    
    # Call Bedrock with caching enabled
    response = bedrock.converse(
        modelId=MODEL_ID,
        messages=messages
    )
    
    # Save the exchange to history
    update_conversation_history(conversation_id, 
                               &#123;&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_message&#125;,
                               &#123;&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: response[&quot;output&quot;]&#125;)
    
    return response[&quot;output&quot;]</code></pre></div><p class="paragraph" style="text-align:left;">This approach places checkpoints after the system prompt and conversation history, allowing the model to skip redundant processing of earlier messages. One thing to watch out for is the context window limit, if the history grows too large, you&#39;ll need to summarize or truncate it while maintaining the cache checkpoints. And if you summarize it, make sure you drop the cache by starting a new conversation! I&#39;ve made that mistake of adding the summary but also keeping the old stuff 🤦.</p><h3 class="heading" style="text-align:left;" id="document-qa">Document Q&A</h3><p class="paragraph" style="text-align:left;">For document-centric applications (i.e. the first thing that comes to mind when I say RAG), cache the document content to enable fast querying:</p><div class="codeblock"><pre><code>def document_qa(document_id, query):
    # Retrieve document content
    document = get_document_content(document_id)
    
    # Structure request with document caching
    messages = [
        &#123;&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: [
            &#123;&quot;text&quot;: &quot;I want to ask questions about this document:\n\n&quot; + document&#125;,
            &#123;&quot;cachePoint&quot;: &#123;&quot;type&quot;: &quot;default&quot;&#125;&#125;,
            &#123;&quot;text&quot;: query&#125;
        ]&#125;
    ]
    
    # Call Bedrock with caching enabled
    response = bedrock.converse(
        modelId=MODEL_ID,
        messages=messages
    )
    
    return response[&quot;output&quot;]</code></pre></div><p class="paragraph" style="text-align:left;">A non-obvious optimization here: for very large documents, you might need to split them into chunks with multiple cache points. Claude 3.5 supports up to 4 checkpoints, so you could structure a long document with checkpoints after each quarter, allowing partial reuse even if some sections change. If nothing changes, don&#39;t bother, one huge checkpoint and 4 smaller checkpoints have the same performance.</p><h3 class="heading" style="text-align:left;" id="coding-assistant">Coding Assistant</h3><p class="paragraph" style="text-align:left;">For coding assistants that analyze codebases:</p><div class="codeblock"><pre><code>def code_assistant(repository_id, file_paths, query):
    # Retrieve code files
    code_context = &quot;&quot;
    for path in file_paths:
        code = get_file_content(repository_id, path)
        code_context += f&quot;File: &#123;path&#125;\n```\n&#123;code&#125;\n```\n\n&quot;
    
    # Structure request with code caching
    messages = [
        &#123;&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;You are a coding assistant that helps with programming tasks.&quot;&#125;,
        &#123;&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: [
            &#123;&quot;text&quot;: &quot;Here is the code to analyze:\n\n&quot; + code_context&#125;,
            &#123;&quot;cachePoint&quot;: &#123;&quot;type&quot;: &quot;default&quot;&#125;&#125;,
            &#123;&quot;text&quot;: query&#125;
        ]&#125;
    ]
    
    # Call Bedrock with caching enabled
    response = bedrock.converse(
        modelId=MODEL_ID,
        messages=messages
    )
    
    return response[&quot;output&quot;]</code></pre></div><p class="paragraph" style="text-align:left;">An important note for code analysis: because code often changes incrementally, consider version-specific caching strategies. For example, you might include a git commit hash in your cache key to ensure you&#39;re not using outdated cached representations for modified code.</p><p class="paragraph" style="text-align:left;">To be honest I haven&#39;t found a good way to use cache for this. My conclusion is that I&#39;d need to be able to predict which files are less likely to change so I can place them in the cache. This sounds partially doable with some pre-processing of the files and the user&#39;s query, but I haven&#39;t tested it, nor explored other ideas. Too busy writing!</p><h2 class="heading" style="text-align:left;" id="debugging-cache-issues">Debugging Cache Issues</h2><p class="paragraph" style="text-align:left;">When things go wrong with prompt caching, diagnosing the problem can be tricky without knowing what to look for. Here are the most common issues I&#39;ve encountered and how to resolve them:</p><h3 class="heading" style="text-align:left;" id="identifying-cache-misses">Identifying Cache Misses</h3><p class="paragraph" style="text-align:left;">The first step in debugging is determining whether your cache is being used at all. The most reliable method is to check the response metrics:</p><div class="codeblock"><pre><code>def is_cache_hit(response):
    usage = response.get(&quot;usage&quot;, &#123;&#125;)
    read_tokens = usage.get(&quot;cacheReadInputTokens&quot;, 0)
    return read_tokens &gt; 0</code></pre></div><p class="paragraph" style="text-align:left;">If this function returns False, your cache isn&#39;t being hit. The most common reasons are:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Cache expiration:</b> The 5-minute TTL elapsed between requests</p></li><li><p class="paragraph" style="text-align:left;"><b>Prompt mismatch:</b> The prefix text doesn&#39;t exactly match the cached version</p></li><li><p class="paragraph" style="text-align:left;"><b>Token threshold not met:</b> You&#39;re trying to cache a segment smaller than the minimum requirement</p></li><li><p class="paragraph" style="text-align:left;"><b>Cache point not properly placed:</b> The cachePoint marker is missing or incorrectly formatted</p></li></ol><h2 class="heading" style="text-align:left;" id="conclusion-please-use-prompt-cachin">Conclusion: Please Use Prompt Caching Often (not always)</h2><p class="paragraph" style="text-align:left;">Prompt caching should be a standard consideration in your design process. It&#39;s particularly valuable for applications with these characteristics:</p><ul><li><p class="paragraph" style="text-align:left;">Multi-turn conversations with consistent system prompts</p></li><li><p class="paragraph" style="text-align:left;">Document-centric analysis where users ask multiple questions about the same content</p></li><li><p class="paragraph" style="text-align:left;">Coding assistants that need to reference the same codebase repeatedly (though this needs a bit more work)</p></li><li><p class="paragraph" style="text-align:left;">Any workflow where large static context is combined with smaller dynamic queries</p></li></ul><p class="paragraph" style="text-align:left;">When not to use it: Workloads without a sufficiently long, reusable prompt prefix or enough cache hits to justify the write cost. Supported InvokeModel requests can benefit too.</p><p class="paragraph" style="text-align:left;">Tips when using caching:</p><ul><li><p class="paragraph" style="text-align:left;">Design your prompts with caching in mind from the beginning. For example, move variables to the end.</p></li><li><p class="paragraph" style="text-align:left;">Monitor cache performance and cost metrics to validate your approach. Remember that writing the cache costs 25% more, so if you&#39;re not benefitting you&#39;re overpaying.</p></li><li><p class="paragraph" style="text-align:left;">Plan for cache misses and expirations. They will happen more than you think.</p></li></ul><p class="paragraph" style="text-align:left;">That&#39;s it. Sorry I took a bit long to say that, but I hope you found the inner workings interesting!</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

      <item>
  <title>AWS Batch Reference Architecture</title>
  <description>In-depth discussion of AWS Batch and reference architecture with 3 compute environments for High Performance Computing (HPC), On-Demand and Spot.</description>
      <enclosure url="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/ee3eafd9-59d8-49c6-ac35-c085b4c4fdad/AWS_Batch_Ref_Arch.png" length="162522" type="image/png"/>
  <link>https://newsletter.simpleaws.dev/p/aws-batch-reference-architecture</link>
  <guid isPermaLink="true">https://newsletter.simpleaws.dev/p/aws-batch-reference-architecture</guid>
  <pubDate>Sun, 23 Feb 2025 15:11:01 +0000</pubDate>
  <atom:published>2025-02-23T15:11:01Z</atom:published>
    <dc:creator>Guille Ojeda</dc:creator>
    <category><![CDATA[Architecture]]></category>
    <category><![CDATA[Containers]]></category>
    <category><![CDATA[Data]]></category>
  <content:encoded><![CDATA[
    <div class='beehiiv'><style>
  .bh__table, .bh__table_header, .bh__table_cell { border: 1px solid #C0C0C0; }
  .bh__table_cell { padding: 5px; background-color: #FFFFFF; }
  .bh__table_cell p { color: #2D2D2D; font-family: 'Helvetica',Arial,sans-serif !important; overflow-wrap: break-word; }
  .bh__table_header { padding: 5px; background-color:#F1F1F1; }
  .bh__table_header p { color: #2A2A2A; font-family:'Trebuchet MS','Lucida Grande',Tahoma,sans-serif !important; overflow-wrap: break-word; }
</style><div class='beehiiv__body'><p class="paragraph" style="text-align:left;">Batch computing is how we process things when there’s no need for immediate interactive responses. Instead of spinning up compute capacity on demand, we gather tasks and process them asynchronously, typically at a moment that suits our operational or business schedule (which really means whenever it&#39;s cheaper). It&#39;s pretty straightforward: queue some jobs, run them whenever, collect results later. As usual, the devil is in the details. AWS Batch can keep you away from the devil by offering a managed orchestration layer for containerized batch workloads, which is awesome! In fact, I already wrote a <a class="link" href="https://newsletter.simpleaws.dev/p/aws-batch-computing?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-batch-reference-architecture" target="_blank" rel="noopener noreferrer nofollow">deep dive on AWS Batch</a>.</p><p class="paragraph" style="text-align:left;">In this article I&#39;d like to examine an advanced AWS Batch reference architecture, so we can take the discussion beyond the complex features and into how they play out in the real world. I&#39;ll show you multi-step pipelines, concurrency controls, HPC (high-performance computing) features, cost-saving strategies, job orchestration patterns, network isolation, advanced security measures, monitoring, logging, and all that stuff.</p><h2 class="heading" style="text-align:left;" id="conceptual-overview-and-goals">Conceptual Overview and Goals</h2><p class="paragraph" style="text-align:left;">Let’s begin with a mental model of what we’re trying to achieve. AWS Batch is a fully managed service that abstracts away the complexity of provisioning compute resources for asynchronous workloads. Jobs run in <i>compute environments</i>, are queued in <i>job queues</i>, and are configured using <i>job definitions</i>.</p><p class="paragraph" style="text-align:left;">When a job is submitted, AWS Batch pulls it from the job queue, finds a suitable compute environment, provisions resources if needed, and schedules the job to run. Well, at least that&#39;s the happy path, which works for 80% of use cases. If you read <a class="link" href="https://newsletter.simpleaws.dev/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-batch-reference-architecture" target="_blank" rel="noopener noreferrer nofollow">Simple AWS</a> I&#39;m guessing you&#39;re the person they call on for the other 20%, so let&#39;s talk about the hard stuff.</p><p class="paragraph" style="text-align:left;">Here’s how we&#39;re going to use those features in a real pipeline:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Data Ingestion</b>: CSV files land in an S3 bucket, typically once per day.</p></li><li><p class="paragraph" style="text-align:left;"><b>Pre-Processing</b>: A job cleans up and validates each CSV, storing intermediate outputs in another S3 location.</p></li><li><p class="paragraph" style="text-align:left;"><b>HPC Analysis</b>: We run a multi-node parallel job that uses GPU-accelerated instances to perform data-intensive computations.</p></li><li><p class="paragraph" style="text-align:left;"><b>Post-Processing</b>: A final job merges the HPC results, updates DynamoDB, and sends a Slack notification.</p></li></ol><p class="paragraph" style="text-align:left;">It sound straightforward, but there’s a lot of details in there: specialized HPC compute, custom AMIs (optionally, I won&#39;t include them in this example but you can add them), ephemeral storage for large scratch space, Spot Instances for cost savings, and job dependencies or external orchestration for multi-step logic. And of course we need logging and monitoring, and at least half decent security.</p><p class="paragraph" style="text-align:left;">I&#39;ll show you how each of these topics works, then I&#39;ll give you a CloudFormation template to show you how to deploy this.</p><h2 class="heading" style="text-align:left;" id="brief-aws-batch-recap">Brief AWS Batch Recap</h2><p class="paragraph" style="text-align:left;">I won&#39;t bore you with the basics, here&#39;s my previous <a class="link" href="https://newsletter.simpleaws.dev/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-batch-reference-architecture" target="_blank" rel="noopener noreferrer nofollow">AWS Batch Deep Dive</a> for that (not to bore you, hopefully!). Instead, let me set the stage for the rest of this article by refreshing a couple of advanced things:</p><h3 class="heading" style="text-align:left;" id="multiple-compute-environments-for-d"><b>Multiple Compute Environments for Different Workloads</b></h3><p class="paragraph" style="text-align:left;">In large-scale setups, we often create multiple compute environments. One might be optimized for On-Demand instances for critical or latency-sensitive jobs, another might use Spot Instances for cost-savings on non-urgent tasks, and yet another might be Fargate-based for smaller ephemeral jobs that require minimal overhead. AWS Batch allows you to attach multiple compute environments to a single job queue, using priorities or other scheduling logic to decide which environment is used.</p><h3 class="heading" style="text-align:left;" id="fair-share-scheduling"><b>Fair-Share Scheduling</b></h3><p class="paragraph" style="text-align:left;">AWS Batch provides an advanced scheduling policy called fair-share. This feature is valuable when multiple teams or users share a single queue. You can assign “share identifiers” to each user or workload, and AWS Batch ensures that resources are distributed fairly according to assigned weights. This prevents one job from hogging the entire cluster, which is particularly useful in HPC or multi-tenant environments.</p><h3 class="heading" style="text-align:left;" id="multi-node-parallel-jobs"><b>Multi-Node Parallel Jobs</b></h3><p class="paragraph" style="text-align:left;">Beyond standard single-container tasks, AWS Batch supports multi-node parallel jobs that can span multiple EC2 instances for tightly coupled, parallel workloads. This is often used for HPC tasks like MPI-based simulations or large-scale machine learning training. When you enable multi-node parallel, AWS Batch provisions a set of instances that function together as a mini-cluster, typically placed in a cluster placement group for high-bandwidth networking. We’ll see how that fits into a real scenario.</p><h3 class="heading" style="text-align:left;" id="extensive-retries-and-dependencies"><b>Extensive Retries and Dependencies</b></h3><p class="paragraph" style="text-align:left;">Jobs can depend on other jobs’ success. A single job might wait for multiple upstream jobs to complete before it starts. You can also configure complex retry behavior, distinguishing between certain failure states (for example you might retry if the job was interrupted by Spot reclamation, but not if the application itself returned an error that indicates a code bug). Not as powerful as <a class="link" href="https://newsletter.simpleaws.dev/p/aws-step-functions-complex-multi-step-workflow-advanced-strategies?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-batch-reference-architecture" target="_blank" rel="noopener noreferrer nofollow">Step Functions</a>, but pretty close, and more than enough for most situations.</p><h2 class="heading" style="text-align:left;" id="batch-architecture-overview-multi-s">Batch Architecture Overview: Multi-Step Pipeline</h2><p class="paragraph" style="text-align:left;">For our scenario we&#39;ll imagine we have a multi-step data processing pipeline that handles daily data ingestion, transformation, optional HPC-style analytics, and final post-processing. Our pipeline is designed around an end-to-end flow:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Data Ingestion</b></p><ul><li><p class="paragraph" style="text-align:left;">A scheduled or external process uploads CSV files to an “incoming” <a class="link" href="https://newsletter.simpleaws.dev/p/amazon-s3-storage-classes-explained?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-batch-reference-architecture" target="_blank" rel="noopener noreferrer nofollow">Amazon S3 bucket</a>.</p></li></ul></li><li><p class="paragraph" style="text-align:left;"><b>Pre-Processing</b></p><ul><li><p class="paragraph" style="text-align:left;">A single-container job reads each CSV, performs data validation (removing incomplete rows or normalizing columns), and writes the cleaned result to an “intermediate” S3 bucket.</p></li></ul></li><li><p class="paragraph" style="text-align:left;"><b>HPC Analysis</b></p><ul><li><p class="paragraph" style="text-align:left;">A multi-node job uses GPU-accelerated instances (for example <a class="link" href="https://aws.amazon.com/ec2/instance-types/p5/?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-batch-reference-architecture" target="_blank" rel="noopener noreferrer nofollow">p5 instances</a>) to perform CPU- or GPU-intensive computations on the cleaned data. It might be training a machine learning model or running a simulation, generating partial outputs in either S3 or a shared file system like FSx for Lustre.</p></li></ul></li><li><p class="paragraph" style="text-align:left;"><b>Post-Processing</b></p><ul><li><p class="paragraph" style="text-align:left;">Another job merges partial HPC results, updates a <a class="link" href="https://newsletter.simpleaws.dev/p/dynamodb-database-design?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-batch-reference-architecture" target="_blank" rel="noopener noreferrer nofollow">DynamoDB</a> table or data warehouse, and sends a Slack notification that the day’s run is complete.</p></li></ul></li></ol><p class="paragraph" style="text-align:left;">We’ll set up three compute environments:</p><ul><li><p class="paragraph" style="text-align:left;"><b>High-Priority On-Demand</b> (for tasks that must never be interrupted).</p></li><li><p class="paragraph" style="text-align:left;"><b>Spot Environment</b> (for workloads that tolerate interruptions).</p></li><li><p class="paragraph" style="text-align:left;"><b>HPC Environment</b> (GPU-optimized, possibly with multi-node parallel support).</p></li></ul><p class="paragraph" style="text-align:left;">We’ll also create two job queues:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Main Queue</b>: Points to On-Demand first, then Spot as fallback. This handles all “normal” single-container tasks like data ingestion and post-processing.</p></li><li><p class="paragraph" style="text-align:left;"><b>HPC Queue</b>: Points only to the HPC environment. This is for large HPC or GPU tasks.</p></li></ul><div class="image"><img alt="AWS Batch pipeline: users upload CSV files to S3, Lambda submits jobs, and Batch routes work to HPC, Spot, and On-Demand compute environments." class="image__image" style="" src="https://media.beehiiv.com/cdn-cgi/image/fit=scale-down,format=auto,onerror=redirect,quality=80/uploads/asset/file/ee3eafd9-59d8-49c6-ac35-c085b4c4fdad/AWS_Batch_Ref_Arch.png?t=1740323254"/><div class="image__source"><span class="image__source_text"><p>Architecture diagram of AWS Batch with 3 compute environments</p></span></div></div><h2 class="heading" style="text-align:left;" id="batch-compute-environments-and-job-">Batch Compute Environments and Job Queues</h2><h3 class="heading" style="text-align:left;" id="multiple-environments-for-different">Multiple Environments for Different Purposes</h3><p class="paragraph" style="text-align:left;">A single AWS Batch compute environment can handle many scenarios, but dividing them based on workload or cost strategy gives you better control. We&#39;ll use three compute environments:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>High-Priority On-Demand Environment</b><br>This environment is used for time-sensitive or critical jobs. We set it up with On-Demand instances only, so tasks here never risk interruption from a Spot termination. We can use instance types with a good balance of CPU, memory, and network throughput, like <code>m</code> and <code>c</code> families.</p></li><li><p class="paragraph" style="text-align:left;"><b>Spot-Optimized Environment</b><br>This environment is for workloads that tolerate interruptions and some wait times. We can use a wide range of instance types, all of them on Spot Instances. We also use the allocation strategy <code>SPOT_CAPACITY_OPTIMIZED</code> so AWS Batch tries to select instance pools less likely to be reclaimed. This environment is just plain cheaper.</p></li><li><p class="paragraph" style="text-align:left;"><b>HPC Environment with Multi-Node Support</b><br>Our HPC environment will use specialized instance types like p5 (with GPUs) for machine learning workloads. We enable multi-node parallel job support here, so large tasks that need multiple nodes can run seamlessly. We could also include cluster placement groups in the configuration if we need low-latency networking. Elastic Fabric Adapter (EFA) also helps with latency.</p></li></ol><p class="paragraph" style="text-align:left;">Each environment needs to be assigned an AWS Batch service role that allows the service to manage EC2 instances in our account. We also configure networking (subnets, security groups) carefully. Since we want all jobs to run in private subnets, we need to make sure we have all the VPC endpoints we need, for S3 and other AWS services.</p><h3 class="heading" style="text-align:left;" id="job-queues-and-their-priorities">Job Queues and Their Priorities</h3><p class="paragraph" style="text-align:left;">We&#39;ll create two job queues:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Main Queue:</b> High priority. This queue references both the On-Demand environment (with the lowest numeric order in <code>ComputeEnvironmentOrder</code>) and the Spot environment as a secondary. When a job enters this queue, AWS Batch first attempts to run it on On-Demand. If that environment is fully utilized or hits its max vCPU limit, it the job will spill over to the Spot environment if it&#39;s marked as “Spot-friendly.”</p></li><li><p class="paragraph" style="text-align:left;"><b>HPC Queue:</b> Dedicated for multi-node GPU-based tasks. This queue is mapped only to the HPC environment.</p></li></ul><p class="paragraph" style="text-align:left;">At this stage we have a logical separation: critical jobs go to the main queue and can use On-Demand, while HPC tasks go to the HPC queue. Everything else that’s flexible about interruptions can run on Spot.</p><h2 class="heading" style="text-align:left;" id="deploying-the-environment-with-clou">Deploying the Environment with CloudFormation</h2><p class="paragraph" style="text-align:left;">Below is an example CloudFormation template that demonstrates many of these concepts. It creates:</p><ul><li><p class="paragraph" style="text-align:left;">A simple VPC with one public subnet, one private subnet, and a NAT Gateway.</p></li><li><p class="paragraph" style="text-align:left;">IAM roles for AWS Batch.</p></li><li><p class="paragraph" style="text-align:left;">Three Compute Environments: On-Demand, Spot, HPC.</p></li><li><p class="paragraph" style="text-align:left;">Two Job Queues: Main and HPC.</p></li><li><p class="paragraph" style="text-align:left;">Three sample Job Definitions (pre-processing, HPC, post-processing) with placeholders for container images and commands.</p></li></ul><p class="paragraph" style="text-align:left;">This template is intended as a starting point, so you might add details (like cluster placement groups or EFA) later. This illustrative template needs coordinated configuration, permissions, GPU-sizing, and monitoring repairs and deployment validation before use. In production you’d likely store parameters (like container repository URIs) in a Parameter Store or pass them as CloudFormation parameters</p><div class="codeblock"><pre><code>AWSTemplateFormatVersion: &quot;2010-09-09&quot;
Description: &quot;AWS Batch Reference Architecture with On-Demand, Spot, HPC Environments&quot;

Parameters:
  VpcCidr:
    Type: String
    Default: &quot;10.0.0.0/16&quot;
    Description: &quot;CIDR block for the VPC&quot;

  PreprocessorImage:
    Type: String
    Default: &quot;123456789012.dkr.ecr.us-east-1.amazonaws.com/data-preprocessor:latest&quot;

  HpcAnalysisImage:
    Type: String
    Default: &quot;123456789012.dkr.ecr.us-east-1.amazonaws.com/hpc-analysis:gpu-latest&quot;

  PostprocessorImage:
    Type: String
    Default: &quot;123456789012.dkr.ecr.us-east-1.amazonaws.com/data-postprocessor:latest&quot;

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: &quot;BatchRefArchVPC&quot;

  InternetGateway:
    Type: AWS::EC2::InternetGateway

  AttachInternetGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: &quot;10.0.0.0/24&quot;
      AvailabilityZone: !Select [0, !GetAZs &quot;&quot;]
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: &quot;BatchRefArchPublicSubnet1&quot;

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: &quot;0.0.0.0/0&quot;
      GatewayId: !Ref InternetGateway

  PublicSubnet1RouteTableAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  NatEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: VPC

  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatEIP.AllocationId
      SubnetId: !Ref PublicSubnet1

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: &quot;10.0.1.0/24&quot;
      AvailabilityZone: !Select [1, !GetAZs &quot;&quot;]
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: &quot;BatchRefArchPrivateSubnet1&quot;

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC

  PrivateSubnet1RouteTableAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  PrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: &quot;0.0.0.0/0&quot;
      NatGatewayId: !Ref NatGateway

  BatchSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: &quot;Security Group for AWS Batch Instances&quot;
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: -1
          CidrIp: &quot;10.0.0.0/16&quot;
      Tags:
        - Key: Name
          Value: &quot;BatchRefArchSG&quot;

  BatchServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: &quot;2012-10-17&quot;
        Statement:
          - Effect: Allow
            Principal:
              Service: &quot;batch.amazonaws.com&quot;
            Action: &quot;sts:AssumeRole&quot;
      ManagedPolicyArns:
        - &quot;arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole&quot;
      Tags:
        - Key: Name
          Value: &quot;BatchServiceRole&quot;

  BatchInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: &quot;2012-10-17&quot;
        Statement:
          - Effect: Allow
            Principal:
              Service: &quot;ec2.amazonaws.com&quot;
            Action: &quot;sts:AssumeRole&quot;
      ManagedPolicyArns:
        - &quot;arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly&quot;
        - &quot;arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy&quot;
      Path: &quot;/&quot;
      Tags:
        - Key: Name
          Value: &quot;BatchInstanceRole&quot;

  BatchInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref BatchInstanceRole
      InstanceProfileName: &quot;BatchInstanceProfile&quot;

  BatchJobRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: &quot;2012-10-17&quot;
        Statement:
          - Effect: Allow
            Principal:
              Service: &quot;ecs-tasks.amazonaws.com&quot;
            Action: &quot;sts:AssumeRole&quot;
      Path: &quot;/&quot;
      ManagedPolicyArns:
        - &quot;arn:aws:iam::aws:policy/AmazonS3FullAccess&quot;
        - &quot;arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess&quot;
      Tags:
        - Key: Name
          Value: &quot;BatchJobRole&quot;

  SpotFleetRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: &quot;2012-10-17&quot;
        Statement:
          - Effect: Allow
            Principal:
              Service: &quot;spotfleet.amazonaws.com&quot;
            Action: &quot;sts:AssumeRole&quot;
      ManagedPolicyArns:
        - &quot;arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetTaggingRole&quot;

  OnDemandComputeEnv:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      ComputeEnvironmentName: &quot;OnDemandCE&quot;
      Type: MANAGED
      ServiceRole: !GetAtt BatchServiceRole.Arn
      ComputeResources:
        Type: EC2
        AllocationStrategy: BEST_FIT_PROGRESSIVE
        MinvCpus: 0
        MaxvCpus: 32
        DesiredvCpus: 0
        InstanceTypes:
          - &quot;m5.xlarge&quot;
          - &quot;c5.xlarge&quot;
        Subnets:
          - !Ref PrivateSubnet1
        SecurityGroupIds:
          - !Ref BatchSecurityGroup
        InstanceRole: !GetAtt BatchInstanceProfile.Arn
        Tags:
          Name: &quot;OnDemandCEInstances&quot;
      State: ENABLED

  SpotComputeEnv:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      ComputeEnvironmentName: &quot;SpotCE&quot;
      Type: MANAGED
      ServiceRole: !GetAtt BatchServiceRole.Arn
      ComputeResources:
        Type: EC2
        AllocationStrategy: SPOT_CAPACITY_OPTIMIZED
        MinvCpus: 0
        MaxvCpus: 64
        DesiredvCpus: 0
        InstanceTypes:
          - &quot;m5.large&quot;
          - &quot;c5.large&quot;
          - &quot;r5.large&quot;
        Subnets:
          - !Ref PrivateSubnet1
        SecurityGroupIds:
          - !Ref BatchSecurityGroup
        InstanceRole: !GetAtt BatchInstanceProfile.Arn
        SpotIamFleetRole: !GetAtt SpotFleetRole.Arn
        Tags:
          Name: &quot;SpotCEInstances&quot;
      State: ENABLED

  HPCComputeEnv:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      ComputeEnvironmentName: &quot;HPCCE&quot;
      Type: MANAGED
      ServiceRole: !GetAtt BatchServiceRole.Arn
      ComputeResources:
        Type: EC2
        AllocationStrategy: BEST_FIT_PROGRESSIVE
        MinvCpus: 0
        MaxvCpus: 64
        DesiredvCpus: 0
        InstanceTypes:
          - &quot;p5.48xlarge&quot;
        Subnets:
          - !Ref PrivateSubnet1
        SecurityGroupIds:
          - !Ref BatchSecurityGroup
        InstanceRole: !GetAtt BatchInstanceProfile.Arn
        Tags:
          Name: &quot;HPCCEInstances&quot;
      State: ENABLED

  MainJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      JobQueueName: &quot;MainQueue&quot;
      Priority: 10
      ComputeEnvironmentOrder:
        - Order: 1
          ComputeEnvironment: !Ref OnDemandComputeEnv
        - Order: 2
          ComputeEnvironment: !Ref SpotComputeEnv
      State: ENABLED

  HPCJobQueue:
    Type: AWS::Batch::JobQueue
    Properties:
      JobQueueName: &quot;HPCQueue&quot;
      Priority: 5
      ComputeEnvironmentOrder:
        - Order: 1
          ComputeEnvironment: !Ref HPCComputeEnv
      State: ENABLED

  PreprocessingJobDef:
    Type: AWS::Batch::JobDefinition
    Properties:
      JobDefinitionName: &quot;preprocessing-job-def&quot;
      Type: &quot;container&quot;
      ContainerProperties:
        Image: !Ref PreprocessorImage
        Vcpus: 2
        Memory: 4096
        Command:
          - &quot;python&quot;
          - &quot;/app/preprocess.py&quot;
        JobRoleArn: !Ref BatchJobRole
      RetryStrategy:
        Attempts: 3
      Timeout:
        AttemptDurationSeconds: 3600

  HPCAnalysisJobDef:
    Type: AWS::Batch::JobDefinition
    Properties:
      JobDefinitionName: &quot;hpc-analysis-job-def&quot;
      Type: &quot;multinode&quot;
      NodeProperties:
        NumNodes: 4
        MainNode: 0
        NodeRangeProperties:
          - TargetNodes: &quot;0:3&quot;
            Container:
              Image: !Ref HpcAnalysisImage
              Vcpus: 32
              Memory: 122880
              Command:
                - &quot;mpirun&quot;
                - &quot;/app/hpc_analysis.sh&quot;
              JobRoleArn: !Ref BatchJobRole
      RetryStrategy:
        Attempts: 2
      Timeout:
        AttemptDurationSeconds: 86400

  PostprocessingJobDef:
    Type: AWS::Batch::JobDefinition
    Properties:
      JobDefinitionName: &quot;postprocessing-job-def&quot;
      Type: &quot;container&quot;
      ContainerProperties:
        Image: !Ref PostprocessorImage
        Vcpus: 2
        Memory: 4096
        Command:
          - &quot;python&quot;
          - &quot;/app/postprocess.py&quot;
        JobRoleArn: !Ref BatchJobRole
      RetryStrategy:
        Attempts: 1
      Timeout:
        AttemptDurationSeconds: 7200

Outputs:
  MainQueueName:
    Description: &quot;Name of the main AWS Batch queue&quot;
    Value: !Ref MainJobQueue

  HPCQueueName:
    Description: &quot;Name of the HPC AWS Batch queue&quot;
    Value: !Ref HPCJobQueue</code></pre></div><h3 class="heading" style="text-align:left;" id="template-highlights">Template Highlights</h3><ul><li><p class="paragraph" style="text-align:left;"><b>Network</b>: Creates a VPC with a public subnet (for NAT) and a private subnet (where our Batch instances run).</p></li><li><p class="paragraph" style="text-align:left;"><b>Security Groups</b>: Opens all traffic within the VPC’s CIDR. For HPC or debugging, you might refine these rules.</p></li><li><p class="paragraph" style="text-align:left;"><b>IAM Roles</b>:</p><ul><li><p class="paragraph" style="text-align:left;"><b>BatchServiceRole</b> is for the AWS Batch service to manage resources.</p></li><li><p class="paragraph" style="text-align:left;"><b>BatchInstanceRole</b> is attached to the EC2 instances for ECS agent usage, logging, and ECR access.</p></li><li><p class="paragraph" style="text-align:left;"><b>BatchJobRole</b> is for the containers themselves, granting them (for example) S3 and DynamoDB permissions.</p></li></ul></li><li><p class="paragraph" style="text-align:left;"><b>Compute Environments</b>:</p><ul><li><p class="paragraph" style="text-align:left;"><b>OnDemandComputeEnv</b> with <code>MaxvCpus: 32</code> for critical jobs.</p></li><li><p class="paragraph" style="text-align:left;"><b>SpotComputeEnv</b> with <code>MaxvCpus: 64</code> for cheaper runs.</p></li><li><p class="paragraph" style="text-align:left;"><b>HPCComputeEnv</b> to handle multi-node GPU-based tasks with up to 64 vCPUs.</p></li></ul></li><li><p class="paragraph" style="text-align:left;"><b>Job Queues</b>:</p><ul><li><p class="paragraph" style="text-align:left;"><b>MainQueue</b> references On-Demand first, then Spot.</p></li><li><p class="paragraph" style="text-align:left;"><b>HPCQueue</b> references the HPC environment exclusively.</p></li></ul></li><li><p class="paragraph" style="text-align:left;"><b>Job Definitions</b>: Pre-Processing, HPC Analysis (multi-node), and Post-Processing. Each is minimal in this template, but you can tweak ephemeral storage, environment variables, etc.</p></li></ul><p class="paragraph" style="text-align:left;">The following job submissions illustrate the intended pipeline flow; validate the repaired environment before running them.</p><h2 class="heading" style="text-align:left;" id="submitting-and-orchestrating-jobs">Submitting and Orchestrating Jobs</h2><h3 class="heading" style="text-align:left;" id="single-queue-approach-with-dependen">Single-Queue Approach with Dependencies</h3><p class="paragraph" style="text-align:left;">You can orchestrate these tasks entirely within AWS Batch by chaining job submissions:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Submit Preprocessing Job</b> (to <code>MainQueue</code>)</p></li></ol><div class="codeblock"><pre><code>aws batch submit-job \
  --job-name &quot;my-preproc-job&quot; \
  --job-queue &quot;MainQueue&quot; \
  --job-definition &quot;preprocessing-job-def&quot;</code></pre></div><p class="paragraph" style="text-align:left;">Suppose AWS Batch returns <code>jobId=abc123</code>.</p><ol start="2"><li><p class="paragraph" style="text-align:left;"><b>Submit HPC Job</b> (to <code>HPCQueue</code>), dependent on the above:</p></li></ol><div class="codeblock"><pre><code>aws batch submit-job \
  --job-name &quot;my-hpc-job&quot; \
  --job-queue &quot;HPCQueue&quot; \
  --job-definition &quot;hpc-analysis-job-def&quot; \
  --depends-on jobId=abc123</code></pre></div><p class="paragraph" style="text-align:left;">Now the HPC job won’t start until the pre-processing job finishes successfully.</p><ol start="3"><li><p class="paragraph" style="text-align:left;"><b>Submit Post-Processing Job</b> (to <code>MainQueue</code>) after HPC completes:</p></li></ol><div class="codeblock"><pre><code>aws batch submit-job \
  --job-name &quot;my-postproc-job&quot; \
  --job-queue &quot;MainQueue&quot; \
  --job-definition &quot;postprocessing-job-def&quot; \
  --depends-on jobId=def456</code></pre></div><p class="paragraph" style="text-align:left;">Where <code>def456</code> is the HPC job’s ID.</p><p class="paragraph" style="text-align:left;">This simple chaining of jobs is enough for linear sequences. If the HPC job fails, the post-processing job is never triggered. You can combine that with the built-in retry logic to handle certain failures automatically.</p><h3 class="heading" style="text-align:left;" id="aws-step-functions-for-advanced-log">AWS Step Functions for Advanced Logic</h3><p class="paragraph" style="text-align:left;">If you need branching logic (e.g. if HPC fails you want to run a fallback job on On-Demand, or if HPC logic splits into multiple parallel tasks), it’s usually easier to manage all that in <a class="link" href="https://newsletter.simpleaws.dev/p/aws-step-functions-complex-multi-step-workflow-advanced-strategies?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-batch-reference-architecture" target="_blank" rel="noopener noreferrer nofollow">AWS Step Functions</a> (this is basically moving from a choreographed transaction to an orchestrated transaction, see my <a class="link" href="https://newsletter.simpleaws.dev/p/distributed-transactions-event-driven-architectures?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-batch-reference-architecture" target="_blank" rel="noopener noreferrer nofollow">distributed transactions</a> article if you don&#39;t remember what those terms mean). You can define a state machine with steps like:</p><ul><li><p class="paragraph" style="text-align:left;"><b>Step 1</b>: Submit the pre-processing job, wait for success.</p></li><li><p class="paragraph" style="text-align:left;"><b>Step 2</b>: Submit HPC, wait for success.</p></li><li><p class="paragraph" style="text-align:left;"><b>Step 3</b>: Submit post-processing, wait for success.</p></li><li><p class="paragraph" style="text-align:left;"><b>Step 4</b>: If it fails, do X. If it succeeds, do Y.</p></li></ul><p class="paragraph" style="text-align:left;">Step Functions can also handle partial failures or Spot interruptions of single-node jobs by re-submitting the job or switching job queues automatically. It can also notify you, update status somewhere, and anything else you need.</p><h2 class="heading" style="text-align:left;" id="cost-optimization-strategies">Cost Optimization Strategies</h2><h3 class="heading" style="text-align:left;" id="use-spot-for-non-urgent-steps">Use Spot for Non-Urgent Steps</h3><p class="paragraph" style="text-align:left;">The CloudFormation template sets <code>Order: 1</code> for the On-Demand environment in the Main Queue, which means the job first tries On-Demand. If your job is truly flexible, you can reverse that priority or skip On-Demand entirely to keep costs down. Single-node HPC tasks can use Spot with suitable checkpointing, but AWS Batch multi-node parallel jobs do not support Spot compute environments.</p><h3 class="heading" style="text-align:left;" id="diversify-instances">Diversify Instances</h3><p class="paragraph" style="text-align:left;">In the Spot environment I included c5.large, m5.large, and r5.large instance types. That’s just an example. The more instance types you allow, the more capacity pools you can tap into. This lowers the chance of Spot interruption.</p><h3 class="heading" style="text-align:left;" id="right-sizing">Right-Sizing</h3><p class="paragraph" style="text-align:left;">Monitor how much CPU and memory your containers actually use (via CloudWatch). If they rarely exceed 50% usage, you might reduce requested resources in the job definition, letting AWS Batch fit more containers per instance and lower overall cost.</p><h3 class="heading" style="text-align:left;" id="hpc-vs-on-demand">HPC vs. On-Demand</h3><p class="paragraph" style="text-align:left;">Some HPC tasks are so time-critical that you can’t risk Spot interruptions. In that case, HPC might run solely on On-Demand GPU instances. For single-node HPC jobs that can rerun, Spot can offer savings; the multi-node parallel job in this example requires On-Demand compute. Evaluate your job’s tolerance for restarts or delays.</p><h2 class="heading" style="text-align:left;" id="storage-custom-am-is-and-network-co">Storage, Custom AMIs, and Network Considerations</h2><h3 class="heading" style="text-align:left;" id="ephemeral-storage">Ephemeral Storage</h3><p class="paragraph" style="text-align:left;">If your HPC or pre-processing job expands data temporarily, consider using ephemeral storage. For Fargate jobs, ephemeralStorage.sizeInGiB expands task ephemeral storage. The EC2 jobs in this example need storage configured on their instances and mounted into containers. For bigger HPC tasks that rely on shared data you might want to use FSx for Lustre or EFS, mapped as volumes in your container.</p><h3 class="heading" style="text-align:left;" id="host-volumes-and-custom-am-is">Host Volumes and Custom AMIs</h3><p class="paragraph" style="text-align:left;">If you have large Docker images (like a big HPC environment with CUDA libraries), it might be worth building a custom AMI that pre-pulls these images or pre-installs drivers. You’ll save container startup time, particularly if your job definitions request ephemeral volumes or if you run many short HPC jobs in bursts.</p><h3 class="heading" style="text-align:left;" id="network">Network</h3><p class="paragraph" style="text-align:left;">In the template I put all compute in a single private subnet. For HPC jobs, you might want to create a separate HPC subnet or enable cluster placement groups. You can do that by editing the HPC environment’s <code>ComputeResources</code> block. If you need a lot more network throughput, you can use <a class="link" href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/efa.html?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-batch-reference-architecture" target="_blank" rel="noopener noreferrer nofollow">Elastic Fabric Adapter (EFA)</a>. You can set that up via a Launch Template that includes <code>efaEnabled</code> and an instance type that supports EFA.</p><h2 class="heading" style="text-align:left;" id="monitoring-logging-and-debugging">Monitoring, Logging, and Debugging</h2><h3 class="heading" style="text-align:left;" id="cloud-watch-metrics">CloudWatch Metrics</h3><p class="paragraph" style="text-align:left;">AWS Batch publishes metrics like <code>RunningJobs</code>, <code>PendingJobs</code>, and <code>DesiredvCpus</code> for each compute environment. Keep an eye on them. If you see a large backlog of <code>PendingJobs</code>, it might be a resource shortage or a misconfiguration in your environment. If HPC is pegged at its max vCPU, consider raising the limit or splitting tasks differently.</p><h3 class="heading" style="text-align:left;" id="cloud-watch-logs">CloudWatch Logs</h3><p class="paragraph" style="text-align:left;">Each container’s stdout/stderr can be streamed to CloudWatch Logs. You can view logs by job ID, and for multi-node parallel jobs, each node gets its own log stream. This is often the first place to check if something fails unexpectedly. If you rely on ephemeral storage or host volumes, logs might reveal out-of-disk issues or insufficient ephemeral memory.</p><h3 class="heading" style="text-align:left;" id="alarms-and-automation">Alarms and Automation</h3><p class="paragraph" style="text-align:left;">You can set CloudWatch Alarms for job failures, job queue length, or HPC environment capacity. A simple approach is to alarm if the number of jobs in a queue remains above a certain threshold for X minutes. Another approach is to alarm on repeated HPC job failures, which might indicate infrastructure or application errors. Combine these alarms with <a class="link" href="https://newsletter.simpleaws.dev/p/aws-sns-decoupling-components-in-cloud-architectures?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-batch-reference-architecture" target="_blank" rel="noopener noreferrer nofollow">Amazon SNS</a> or Slack notifications so you know quickly if your pipeline is stuck.</p><h2 class="heading" style="text-align:left;" id="putting-it-all-together-an-example-">Putting It All Together: An Example Workflow</h2><p class="paragraph" style="text-align:left;">This is the complete flow for our example:</p><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>CSV Upload: </b>A daily script or external tool puts a CSV file into <code>s3://my-ingest-bucket/data/YYYYMMDD/input.csv</code>. That triggers a small <a class="link" href="https://newsletter.simpleaws.dev/p/aws-lambda-underlying-architecture?utm_source=simpleaws&utm_medium=newsletter&utm_campaign=aws-batch-reference-architecture" target="_blank" rel="noopener noreferrer nofollow">AWS Lambda function</a> that calls <code>aws batch submit-job</code> for the <code>preprocessing-job-def</code> in <code>MainQueue</code>.</p></li><li><p class="paragraph" style="text-align:left;"><b>Pre-Processing</b><br>The container pulls <code>input.csv</code>, processes it, writes <code>cleaned.csv</code> to <code>s3://my-intermediate-bucket/data/YYYYMMDD/cleaned.csv</code>. The job finishes, returning success. Let&#39;s assume its <code>jobId</code> is <code>abc123</code>.</p></li><li><p class="paragraph" style="text-align:left;"><b>HPC Analysis</b><br>We immediately submit the HPC job with <code>depends-on jobId=abc123</code>. That job runs in <code>HPCQueue</code>, which spawns up to 4 <code>p5.48xlarge</code> instances (<b>note: THAT IS A LOT OF MONEY. </b>the original February 2025 example used $98.32/hour per On-Demand instance; verify current pricing for your Region). The container script uses MPI to coordinate across nodes, reading from <code>my-intermediate-bucket</code>, generating partial results in <code>my-hpc-outputs-bucket</code>, and concluding after some hours (note: Now you&#39;re poor. Seriously, it&#39;s a lot of money). The HPC environment tears down the GPU instances when the job finishes, saving you from AWS-induced homelessness.</p></li><li><p class="paragraph" style="text-align:left;"><b>Post-Processing</b><br>When the HPC job succeeds, we run a job in <code>MainQueue</code> with <code>depends-on jobId=def456</code> (the HPC job’s ID). It merges partial HPC outputs, updates a DynamoDB table with final metrics, and notifies Slack. If this job fails or times out, we get a CloudWatch Alarm. If it completes, we’re done for the day.</p></li><li><p class="paragraph" style="text-align:left;"><b>Cleanup and Monitoring</b><br>The environments scale back to 0. Logs and metrics are available in CloudWatch and CloudWatch Logs.</p></li></ol><h2 class="heading" style="text-align:left;" id="operational-tips-and-troubleshootin">Operational Tips and Troubleshooting for Batch</h2><p class="paragraph" style="text-align:left;">One of the biggest differences between a proof-of-concept environment and a production one is how you handle the inevitable problems:</p><h3 class="heading" style="text-align:left;" id="quotas-and-limits">Quotas and Limits</h3><p class="paragraph" style="text-align:left;">Check the applied EC2 On-Demand and Spot quotas for your account, Region, and instance family. Size quota requests for the full EC2 instances that the job will launch, not just the vCPUs requested by its containers. Monitor for jobs stuck in <code>RUNNABLE</code> state. If you see that, check your account’s service quotas.</p><h3 class="heading" style="text-align:left;" id="network-configuration">Network Configuration</h3><p class="paragraph" style="text-align:left;">If your Batch jobs require frequent reads from S3, make sure that you use S3 VPC endpoints in each subnet so traffic never goes through a NAT gateway, or consider FSx for Lustre as a caching layer.</p><h3 class="heading" style="text-align:left;" id="am-is-and-ecs-agent-issues">AMIs and ECS Agent Issues</h3><p class="paragraph" style="text-align:left;">If you&#39;re using custom AMIs, always confirm that the ECS agent is installed and that it registers to the cluster. Many times a job doesn’t start because the instance never joined the ECS cluster due to an agent misconfiguration. Double-check your user data scripts or Launch Template parameters.</p><h3 class="heading" style="text-align:left;" id="long-running-jobs-and-timeouts">Long-Running Jobs and Timeouts</h3><p class="paragraph" style="text-align:left;">If a job is supposed to run for many hours, set the job definition’s <code>AttemptDurationSeconds</code> to a high enough value. AWS Batch has no job timeout by default; the one-hour timeout in this example is explicitly configured. However, don’t let jobs run forever! If a job is stuck, it’s better to fail fast, notify, and investigate.</p><h2 class="heading" style="text-align:left;" id="final-thoughts-and-next-steps">Final Thoughts and Next Steps</h2><p class="paragraph" style="text-align:left;">We’ve walked through an advanced AWS Batch reference architecture, complete with a CloudFormation template to stand up three compute environments, job queues, and job definitions. Our pipeline scenario demonstrated how pre-processing, HPC analysis, and post-processing tasks can flow together with either AWS Batch dependencies or an external orchestrator like Step Functions.</p><h3 class="heading" style="text-align:left;" id="key-takeaways">Key Takeaways</h3><ol start="1"><li><p class="paragraph" style="text-align:left;"><b>Separate Environments by Cost and Requirements</b><br>Use On-Demand for critical tasks, Spot for interruptible tasks, HPC for specialized GPU or large-memory nodes.</p></li><li><p class="paragraph" style="text-align:left;"><b>Configure Dependencies and Retries</b><br>Try to keep it simple with built-in job dependencies, and use Step Functions if you need complex branching and error-handling.</p></li><li><p class="paragraph" style="text-align:left;"><b>Optimize for Cost</b><br>Spot Instances, ephemeral storage, custom AMIs, and instance-type diversification can significantly lower your AWS bill.</p></li><li><p class="paragraph" style="text-align:left;"><b>Monitor and Fine-Tune</b><br>Collect CloudWatch metrics and logs. Configure alarms. Evaluate resource usage and scale or refine job definitions accordingly.</p></li><li><p class="paragraph" style="text-align:left;"><b>EFA and HPC</b><br>For truly HPC-level stuff, consider a Launch Template that enables Elastic Fabric Adapter (EFA). It really is fast.</p></li></ol><p class="paragraph" style="text-align:left;">That&#39;s it. Now go Batch happily, or something like that. I&#39;m not good at inspirational quotes and influencer stuff.</p><hr class="content_break"><div class="section" style="background-color:transparent;border-color:#153a71;border-radius:5px;border-style:solid;border-width:2px;margin:5.0px 5.0px 5.0px 5.0px;padding:5.0px 5.0px 5.0px 5.0px;"></div></div></div>
  ]]></content:encoded>
</item>

  </channel>
</rss>
