<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Takashi Iwamoto</title>
    <description>The latest articles on DEV Community by Takashi Iwamoto (@iwamot).</description>
    <link>https://dev.to/iwamot</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F74296%2F76fc142f-53c5-4a5a-b29c-c618e42a6336.jpg</url>
      <title>DEV Community: Takashi Iwamoto</title>
      <link>https://dev.to/iwamot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iwamot"/>
    <language>en</language>
    <item>
      <title>Test your Step Functions workflows locally with pytest</title>
      <dc:creator>Takashi Iwamoto</dc:creator>
      <pubDate>Wed, 23 Sep 2026 13:47:08 +0000</pubDate>
      <link>https://dev.to/aws-builders/test-your-step-functions-workflows-locally-with-pytest-3hod</link>
      <guid>https://dev.to/aws-builders/test-your-step-functions-workflows-locally-with-pytest-3hod</guid>
      <description>&lt;p&gt;Before you deploy a Step Functions workflow, it is hard to check what happens when a service fails: which errors get retried, which Catch handles them, and what the next Task receives. AWS documents &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/sfn-local.html" rel="noopener noreferrer"&gt;Step Functions Local&lt;/a&gt; as unsupported. The recommended alternative, the &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/test-state-isolation.html" rel="noopener noreferrer"&gt;TestState API&lt;/a&gt;, supports mocks but tests one state per call. To test a complete path, you have to chain those calls yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/iwamot/sfnx" rel="noopener noreferrer"&gt;sfnx&lt;/a&gt; includes a local runner for testing an entire workflow with pytest. You provide mock service responses, the runner follows the definition's Retry and Catch rules, and your assertions check the calls and the result. It works with existing JSONata definitions, so there's no need to rewrite the workflow or add states to it.&lt;/p&gt;

&lt;p&gt;sfnx also has a compiler if you'd prefer to maintain the workflow itself in Python. I'll cover that option in the second half.&lt;/p&gt;

&lt;h2&gt;
  
  
  A test can run the ASL you already have
&lt;/h2&gt;

&lt;p&gt;The runner reads the same JSON that Step Functions runs. A workflow is a JSON document in the Amazon States Language (ASL), whether you write it by hand or build it in Workflow Studio, the console's visual editor. In JSONata mode, fields such as a Task's &lt;code&gt;Arguments&lt;/code&gt; can hold expressions inside &lt;code&gt;{% ... %}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For this example, I'll use AWS's &lt;a href="https://github.com/aws-samples/sample-stepfunctions-agentcore-coding-agent" rel="noopener noreferrer"&gt;coding-agent sample&lt;/a&gt;. It looks up a clinical term with Lambda, calls an AI agent if there's no direct match, routes the answer by confidence, and writes back the result. The agent runs in an Amazon Bedrock AgentCore harness, which Step Functions can &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/06/aws-step-functions-agentcore/" rel="noopener noreferrer"&gt;invoke directly as a Task&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Each Task has a &lt;strong&gt;Retry&lt;/strong&gt; policy for transient errors. If the lookup, agent call or initial write-back fails with an error handled by its &lt;strong&gt;Catch&lt;/strong&gt;, the workflow moves to a fallback Task that marks the term as open and records the reason. If the fallback also fails after its retries, the execution fails.&lt;/p&gt;

&lt;p&gt;The test below asks one question: if the agent is throttled on every attempt, how many times is it called, and what does the fallback Task receive?&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up a project
&lt;/h3&gt;

&lt;p&gt;You need &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; and Python 3.11 or later. In a new directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv init
uv add &lt;span class="nt"&gt;--dev&lt;/span&gt; &lt;span class="s2"&gt;"sfnx[testing]==2.1.0"&lt;/span&gt; pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These examples were tested with sfnx v2.1.0. Omit &lt;code&gt;==2.1.0&lt;/code&gt; to install the latest version instead.&lt;/p&gt;

&lt;p&gt;Save &lt;a href="https://raw.githubusercontent.com/iwamot/sfnx/v2.1.0/tests/samples/coding_workflow.asl.json" rel="noopener noreferrer"&gt;the JSON form of the sample's definition from sfnx v2.1.0&lt;/a&gt; as &lt;code&gt;coding-workflow.asl.json&lt;/code&gt; in the project root. It is AWS's definition, converted from the sample's YAML, with nothing added.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write the test
&lt;/h3&gt;

&lt;p&gt;One mock function handles the lookup, agent call and write-back. Save this test as &lt;code&gt;test_coding_workflow.py&lt;/code&gt; in the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sfnx.testing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;

&lt;span class="n"&gt;DEFINITION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coding-workflow.asl.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;TERM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verbatim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrane&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encoding_dictionary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MedDRA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encoding_dictionary_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v27.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_study&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ONCO-2024-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;HARNESS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:states:::bedrockagentcore:invokeHarness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;throttled_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;No direct match, the agent is throttled, and the write-back succeeds.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HARNESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BedrockAgentCore.ThrottlingException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;slow down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FunctionName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${CheckDirectFunctionArn}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matched&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_a_throttled_agent_is_retried_then_the_term_is_left_open&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DEFINITION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TERM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;throttled_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;harness_calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HARNESS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;harness_calls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;  &lt;span class="c1"&gt;# the first attempt and 3 retries
&lt;/span&gt;    &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failure_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BedrockAgentCore.ThrottlingException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failure_reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BedrockAgentCore.ThrottlingException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rationale&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it from the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest test_coding_workflow.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What the runner did
&lt;/h3&gt;

&lt;p&gt;This test runs locally without AWS credentials or calls to AWS services. &lt;code&gt;run&lt;/code&gt; follows the definition state by state. At each Task, it passes the &lt;code&gt;Resource&lt;/code&gt; and evaluated &lt;code&gt;Arguments&lt;/code&gt; to &lt;code&gt;throttled_agent&lt;/code&gt; and uses the return value as the service response.&lt;/p&gt;

&lt;p&gt;When the mock raises &lt;code&gt;Failure&lt;/code&gt;, the runner applies the definition's error handling. Here, the agent gets one initial attempt and three retries before the Catch routes execution to the fallback Task.&lt;/p&gt;

&lt;p&gt;The assertions check the four agent attempts, the fallback Task's arguments, and the final output. If the execution fails, accessing &lt;code&gt;execution.output&lt;/code&gt; raises the error, which also fails the test.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;${...}&lt;/code&gt; placeholders are left unchanged. The mock receives &lt;code&gt;${CheckDirectFunctionArn}&lt;/code&gt; as written, so you can test the definition before substituting the real ARNs.&lt;/p&gt;

&lt;p&gt;The runner skips delays: retries don't wait between attempts, and Wait states return immediately. Testing a retry policy doesn't mean waiting through its backoff intervals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing a definition from Workflow Studio
&lt;/h2&gt;

&lt;p&gt;To test your own JSONata workflow, export its definition from the console as JSON. Use each Task's &lt;code&gt;Resource&lt;/code&gt; and &lt;code&gt;Arguments&lt;/code&gt; to decide what your mock should return, or raise &lt;code&gt;Failure&lt;/code&gt; to test an error path. Then add assertions for the calls and output you expect.&lt;/p&gt;

&lt;p&gt;I also tested a small JSONata workflow built in Workflow Studio and exported without edits. It had a Lambda call with the retry policy the editor adds, a Choice, Succeed and Fail states, and a Catch.&lt;/p&gt;

&lt;p&gt;It followed the expected path in all four cases I tried: success, the Choice's other branch, a throttled call caught after retries, and an error caught without retries. I haven't tested every state type or integration available in the editor.&lt;/p&gt;

&lt;p&gt;The runner checks for unsupported states and fields before execution. It accepts JSONata mode only; a JSONPath state or an unsupported field produces an error identifying the state. See &lt;a href="https://github.com/iwamot/sfnx/blob/v2.1.0/docs/testing.md" rel="noopener noreferrer"&gt;the testing guide&lt;/a&gt; for the supported states and fields.&lt;/p&gt;

&lt;p&gt;A local run checks control flow against your mocks, not the services. It does not reproduce elapsed time, concurrency, real service behavior, or every detail of JSONata on AWS. &lt;a href="https://github.com/iwamot/sfnx/blob/v2.1.0/docs/testing.md#where-a-local-run-differs" rel="noopener noreferrer"&gt;The testing guide lists where a local run differs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to maintain the workflow in Python, sfnx can compile it
&lt;/h2&gt;

&lt;p&gt;The compiler is optional; nothing above used it. You write the workflow's intent in Python, sfnx compiles it to readable ASL in JSONata mode, and you deploy the generated definition with your usual tools.&lt;/p&gt;

&lt;p&gt;Here's how the same coding-agent workflow looks in Python. You can find &lt;a href="https://github.com/iwamot/sfnx/blob/v2.1.0/examples/coding_agent.py" rel="noopener noreferrer"&gt;the full source&lt;/a&gt; and &lt;a href="https://github.com/iwamot/sfnx/blob/v2.1.0/examples/coding_agent.asl.json" rel="noopener noreferrer"&gt;the generated definition&lt;/a&gt; in the repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define a shared retry policy once
&lt;/h3&gt;

&lt;p&gt;The original definition repeats the same Lambda retry policy on four Tasks. In Python, it is one module-level constant (excerpt; the full constant has a second retrier for timeouts and database errors):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;LAMBDA_RETRY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ErrorEquals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TooManyRequestsException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceException&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IntervalSeconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MaxAttempts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BackoffRate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each Lambda Task uses the same constant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;direct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:states:::lambda:invoke&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FunctionName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${CheckDirectFunctionArn}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verbatim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;verbatim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encoding_dictionary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dictionary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encoding_dictionary_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;LAMBDA_RETRY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To change the backoff, you edit the constant once. The compiler includes the full policy in each Task that uses it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write branches with if/elif
&lt;/h3&gt;

&lt;p&gt;The original uses a &lt;strong&gt;Choice&lt;/strong&gt; state to select the next Task based on the agent's score:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"ScoreThreshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Choices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $agentScore &amp;gt;= 0.9 %}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WriteBack"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $agentScore &amp;gt;= 0.7 %}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MarkForReview"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MarkOpen"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Python, the same decision is an &lt;code&gt;if&lt;/code&gt; / &lt;code&gt;elif&lt;/code&gt; / &lt;code&gt;else&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;autocoded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approval_required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles to a single Choice state. Each matching rule sets the &lt;code&gt;status&lt;/code&gt; variable; the default path sets it to &lt;code&gt;open&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"if_3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Choices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $score &amp;gt;= 0.9 %}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Assign"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"autocoded"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"if_4"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $score &amp;gt;= 0.7 %}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Assign"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"approval_required"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"if_4"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Assign"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"if_4"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Python version's graph has a different shape from the original: it uses one write-back Task for the accepted and review cases, passing &lt;code&gt;status&lt;/code&gt; as &lt;code&gt;target_status&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handle errors with try/except: only Task, Parallel and Map states catch
&lt;/h3&gt;

&lt;p&gt;There is an important difference from Python's &lt;code&gt;try&lt;/code&gt; / &lt;code&gt;except&lt;/code&gt;: the compiler adds Catch clauses to Task, Parallel and Map states in the &lt;code&gt;try&lt;/code&gt; block, but Pass states cannot catch errors.&lt;/p&gt;

&lt;p&gt;The example therefore parses the agent's reply in the same statement as the &lt;code&gt;task()&lt;/code&gt; call. If parsing fails, the Task's Catch handles the error. Moving the parsing to a separate statement would put it in a Pass state, where a parsing error would fail the entire execution. The &lt;a href="https://github.com/iwamot/sfnx/blob/v2.1.0/docs/language.md#the-asls-own-semantics" rel="noopener noreferrer"&gt;language reference&lt;/a&gt; covers the details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stay within the supported Python subset
&lt;/h3&gt;

&lt;p&gt;The compiler supports a subset of Python, so arbitrary library calls won't compile. Unsupported constructs produce an error with a suggested alternative. You can also use &lt;code&gt;jsonata()&lt;/code&gt; to write an expression directly in JSONata.&lt;/p&gt;

&lt;p&gt;Deployment stays with your existing tools. Resources, IAM and substitutions go through CDK, SAM, CloudFormation or the AWS CLI; the &lt;a href="https://github.com/iwamot/sfnx/blob/v2.1.0/docs/deployment.md" rel="noopener noreferrer"&gt;deployment guide&lt;/a&gt; has examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Python version behaves like the original in 12 mocked scenarios
&lt;/h2&gt;

&lt;p&gt;To check the Python version against the original, &lt;a href="https://github.com/iwamot/sfnx/blob/v2.1.0/tests/test_samples.py" rel="noopener noreferrer"&gt;this test&lt;/a&gt; runs both definitions through 12 scenarios with the same mocks. It uses AWS's definition from commit &lt;code&gt;ab76ef3&lt;/code&gt; (MIT-0) and compares every call's resource and arguments, along with the final output or error.&lt;/p&gt;

&lt;p&gt;The scenarios cover direct matches, blocked terms, each confidence band, replies without a score, a rationale or JSON, and each service failing. To reproduce them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/iwamot/sfnx.git
&lt;span class="nb"&gt;cd &lt;/span&gt;sfnx
git checkout v2.1.0
uv run pytest tests/test_samples.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both definitions produce the same calls and results for these scenarios in the local runner. That doesn't establish that they behave identically on AWS with real Lambda and AgentCore responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The compiler's trade-off: it can add states
&lt;/h2&gt;

&lt;p&gt;In this example, the compiled workflow has more states than the original. This affects workflows you compile with sfnx; testing existing ASL with the runner adds no states. The compiler emits Choice states for branches and separate states for assignments it cannot combine with another state.&lt;/p&gt;

&lt;p&gt;Some assignments are combined with an existing state: a Wait can hold the assignments after it, a Task can assign its result, and a Choice rule can set a variable. The compiler keeps states separate when combining them could change the behavior.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Coding agent&lt;/th&gt;
&lt;th&gt;AWS original&lt;/th&gt;
&lt;th&gt;Python version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;States in the definition&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;States entered, agent confident&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;States entered, lookup fails&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;States entered, 12 scenarios&lt;/td&gt;
&lt;td&gt;3 to 6&lt;/td&gt;
&lt;td&gt;6 to 13&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Across the 12 scenarios, the Python version enters 1.5 to 2.3 times as many states on the same path.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  The 12 scenarios
  &lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;AWS original&lt;/th&gt;
&lt;th&gt;Python version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;blocked&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;direct match&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent confident / unsure&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent doubtful&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent without a score / without a rationale&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent without JSON / with broken JSON&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lookup fails&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent fails&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;write-back fails&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;p&gt;These are local execution counts, not billing estimates. The runner records each state entry but excludes retry attempts from this count. Standard workflows are &lt;a href="https://aws.amazon.com/step-functions/pricing/" rel="noopener noreferrer"&gt;billed per state transition&lt;/a&gt;, including retries.&lt;/p&gt;

&lt;p&gt;What you pay depends on the path each execution takes and how many executions you run. For a Standard workflow that runs often, look at the generated graph before you adopt the compiler.&lt;/p&gt;

&lt;p&gt;For a smaller example, see &lt;a href="https://github.com/iwamot/sfnx/blob/v2.1.0/examples/hello_world.py" rel="noopener noreferrer"&gt;the Python version of the console's Hello World template&lt;/a&gt;. It shows how &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;wait()&lt;/code&gt;, &lt;code&gt;parallel()&lt;/code&gt; and &lt;code&gt;raise&lt;/code&gt; compile. The generated definition has 11 states compared with the template's 9; a successful run enters 9 states compared with 8.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one failure path
&lt;/h2&gt;

&lt;p&gt;Try adding a test for one failure path in a JSONata workflow you already have. Export the definition, mock its Tasks, and check that the workflow handles the error and passes the expected data to the next Task.&lt;/p&gt;

&lt;p&gt;If you'd also like to maintain the workflow in Python, start with a small example and inspect the generated ASL before deciding whether to use the compiler.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/iwamot/sfnx" rel="noopener noreferrer"&gt;sfnx is on GitHub&lt;/a&gt;. If you try it on a real workflow, I'd like to hear which paths you tested, and where the runner fell short.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>python</category>
      <category>serverless</category>
      <category>testing</category>
    </item>
    <item>
      <title>Connect Your Bedrock AgentCore Agent to Slack — Without Writing the Slack Part</title>
      <dc:creator>Takashi Iwamoto</dc:creator>
      <pubDate>Sun, 12 Jul 2026 14:16:03 +0000</pubDate>
      <link>https://dev.to/aws-builders/connect-your-bedrock-agentcore-agent-to-slack-without-writing-the-slack-part-2o91</link>
      <guid>https://dev.to/aws-builders/connect-your-bedrock-agentcore-agent-to-slack-without-writing-the-slack-part-2o91</guid>
      <description>&lt;p&gt;Want to use the agent you deployed on Amazon Bedrock AgentCore from Slack? You're not alone — there are already quite a few samples and blog posts that connect AgentCore agents to Slack.&lt;/p&gt;

&lt;p&gt;Reading through them, I noticed something: on the Slack side, everyone is solving the same problems. Receiving events, fetching thread history and converting it into the model's input format, rendering replies. The agents themselves all differ, but the Slack work is the same.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/iwamot/welt" rel="noopener noreferrer"&gt;Welt&lt;/a&gt; factors that common part out, so it doesn't have to be rebuilt for every agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Welt
&lt;/h2&gt;

&lt;p&gt;Welt is a general-purpose Slack frontend for agents on AgentCore. It sits between Slack and your agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Slack
  ⇅
Welt
  ⇅  plain JSON
AgentCore Runtime
  └── your agent, wired through an adapter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The division of labor is clear — Welt owns everything Slack, you own everything agent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Side&lt;/th&gt;
&lt;th&gt;Owns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Welt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slack tokens, event intake, thread-history fetch, streaming rendering of replies, file upload/download, approval buttons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Model, tools, MCP, memory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  One Wire, Any Framework
&lt;/h3&gt;

&lt;p&gt;The two sides exchange plain JSON, specified in the &lt;a href="https://github.com/iwamot/welt/blob/main/docs/wire.md" rel="noopener noreferrer"&gt;Wire Contract&lt;/a&gt;, so nothing ties Welt to any particular agent framework. An adapter maps the wire to one framework's types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Adapter&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/iwamot/welt-io-strands" rel="noopener noreferrer"&gt;welt-io-strands&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;&lt;a href="https://strandsagents.com/" rel="noopener noreferrer"&gt;Strands Agents&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://pypi.org/project/welt-io-strands/" rel="noopener noreferrer"&gt;welt-io-strands&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/iwamot/welt-io-strands-ts" rel="noopener noreferrer"&gt;welt-io-strands-ts&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;&lt;a href="https://strandsagents.com/" rel="noopener noreferrer"&gt;Strands Agents&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/@welt-io/strands" rel="noopener noreferrer"&gt;@welt-io/strands&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/iwamot/welt-io-mastra" rel="noopener noreferrer"&gt;welt-io-mastra&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;&lt;a href="https://mastra.ai/" rel="noopener noreferrer"&gt;Mastra&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.npmjs.com/package/@welt-io/mastra" rel="noopener noreferrer"&gt;@welt-io/mastra&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This article uses the Python Strands adapter; the others work the same way. And since the wire is plain JSON, an adapter can be built for any stack — if there's one you'd like, let me know in an &lt;a href="https://github.com/iwamot/welt/issues" rel="noopener noreferrer"&gt;issue&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Agent-Side Changes Are Small
&lt;/h2&gt;

&lt;p&gt;If you already have a Strands agent, connecting it to Slack takes nothing more than wiring in two welt-io-strands functions. This is Welt's biggest selling point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bedrock_agentcore.runtime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BedrockAgentCoreApp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;welt_io_strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;decode_messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;renderable_events&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BedrockAgentCoreApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.entrypoint&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decode_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="c1"&gt;# your agent, unchanged
&lt;/span&gt;    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[...])&lt;/span&gt;
    &lt;span class="c1"&gt;# yield the events Welt renders into the Slack thread
&lt;/span&gt;    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;renderable_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two functions cover the two directions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;decode_messages()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inbound&lt;/td&gt;
&lt;td&gt;Restores the Converse-shaped messages Welt sends (files from Slack uploads arrive base64-encoded) to the form Strands expects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;renderable_events()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Outbound&lt;/td&gt;
&lt;td&gt;Reduces the raw &lt;code&gt;stream_async&lt;/code&gt; events to the JSON-serializable events Welt can render&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Your agent's logic — model choice, tools, system prompt — stays untouched. And you never write a single line of Slack API code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Get on the Slack Side
&lt;/h2&gt;

&lt;p&gt;With just the code above, the Slack thread gets history, streaming, and files — with zero extra work on the agent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Thread history&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;On every turn, Welt fetches the whole thread, converts it into Converse-shaped messages, and hands it to your agent — the agent needs no conversation store of its own (if you'd rather manage history on the agent side, there's a mode that sends only the new messages)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Streaming&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The agent's reply streams into the thread in real time, with an indicator while a tool is running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Files, both ways&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Files uploaded to Slack go to the agent, and images or documents the agent (or its tools) generate are uploaded back into the thread — strands-tools' &lt;code&gt;generate_image&lt;/code&gt; works with zero extra code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Human-in-the-Loop Becomes Slack Buttons
&lt;/h3&gt;

&lt;p&gt;Welt also supports human-in-the-loop. Among the AgentCore-to-Slack integrations out there, I haven't seen many that go this far.&lt;/p&gt;

&lt;p&gt;With Strands &lt;a href="https://strandsagents.com/docs/user-guide/concepts/interrupts/" rel="noopener noreferrer"&gt;interrupts&lt;/a&gt;, a tool can pause the run and wait for human approval. Welt renders that pause as &lt;strong&gt;buttons and a text field right in the Slack thread&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6pps1fln7anes950dv2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6pps1fln7anes950dv2.png" alt="An agent asked to perform a dangerous action pausing for approval in a Slack thread, with Approve / Cancel buttons and a text field" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You describe the buttons and the text field with welt-io-strands's &lt;code&gt;interrupt_reason()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interrupt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deploy-approval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;interrupt_reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deploy to prod?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deploy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;style&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;primary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cancel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Or tell me what to do instead&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pressing a button (or typing into the field) resumes the run. Strands' stock &lt;code&gt;HumanInTheLoop&lt;/code&gt; intervention also works over Welt as-is — the stdio approval prompt simply becomes Slack buttons.&lt;/p&gt;

&lt;p&gt;The complete code, including the resume handling, is in welt-io-strands's &lt;a href="https://github.com/iwamot/welt-io-strands/tree/main/examples/agent" rel="noopener noreferrer"&gt;example agent&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/iwamot/welt#quick-start" rel="noopener noreferrer"&gt;Quick Start&lt;/a&gt; runs everything on your machine — nothing is deployed, and the only AWS dependency is the Bedrock model the agent calls:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Slack app from the &lt;code&gt;manifest.yml&lt;/code&gt; bundled in the Welt repository&lt;/li&gt;
&lt;li&gt;Clone Welt and put the two tokens in a &lt;code&gt;.env&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uv run --env-file .env main.py&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;In another terminal, run welt-io-strands's &lt;a href="https://github.com/iwamot/welt-io-strands/tree/main/examples/agent" rel="noopener noreferrer"&gt;example agent&lt;/a&gt; — with no &lt;code&gt;AGENT_ARN&lt;/code&gt; set, Welt talks to it at &lt;code&gt;localhost:8080&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By default, Welt connects to Slack over Socket Mode. There's no public endpoint to stand up, so the local &lt;code&gt;uv run&lt;/code&gt; just works.&lt;/p&gt;

&lt;p&gt;Moving to production is a change of address, not code. Deploy the agent to AgentCore Runtime with the AgentCore CLI and point &lt;code&gt;AGENT_ARN&lt;/code&gt; at it; for Welt itself, put the container image (&lt;code&gt;ghcr.io/iwamot/welt&lt;/code&gt;) on ECS or similar, and it works over Socket Mode the same way.&lt;/p&gt;

&lt;p&gt;If you'd rather not pay for an always-on process, you can also &lt;a href="https://github.com/iwamot/welt/blob/main/docs/lambda.md" rel="noopener noreferrer"&gt;run Welt on AWS Lambda&lt;/a&gt;. In that case it receives HTTP at a Function URL instead of using Socket Mode (Welt handles the signature verification), and the idle cost drops to zero.&lt;/p&gt;

&lt;p&gt;From there, make it yours. The Slack part is no longer your problem — swap the example agent for your own model, tools, MCP servers, and memory, and you have an agent that's actually useful to you and your team, living in a Slack thread everyone can talk to.&lt;/p&gt;

&lt;p&gt;Welt and its adapters are MIT-licensed open source. If you've been wanting to use your AgentCore agent from Slack, give it a try.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>slack</category>
      <category>agentcore</category>
      <category>python</category>
    </item>
    <item>
      <title>How I Made RDS Private Without Extra Cost Using Egress-Only IGW</title>
      <dc:creator>Takashi Iwamoto</dc:creator>
      <pubDate>Sun, 01 Feb 2026 06:12:32 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-i-made-rds-private-without-extra-cost-using-egress-only-igw-4a5i</link>
      <guid>https://dev.to/aws-builders/how-i-made-rds-private-without-extra-cost-using-egress-only-igw-4a5i</guid>
      <description>&lt;p&gt;I had an Amazon RDS DB instance with public access enabled due to certain requirements. When public access was no longer needed, I migrated it to a private subnet.&lt;/p&gt;

&lt;p&gt;By using an &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/egress-only-internet-gateway.html" rel="noopener noreferrer"&gt;Egress-Only Internet Gateway&lt;/a&gt; (Egress-Only IGW), I completed the migration without adding paid resources like NAT Gateways or VPC endpoints.&lt;/p&gt;

&lt;p&gt;In this article, I'll share the background and the actual migration steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;The reason I wanted to move the DB to a private subnet was that public access was no longer required.&lt;/p&gt;

&lt;p&gt;Public access had been enabled because Lambda functions in multiple AWS accounts needed to access this DB. While VPC peering was an option, I chose simple public access since IAM authentication alone seemed sufficient.&lt;/p&gt;

&lt;p&gt;Later, the requirements changed—only Lambda functions in the same account as the DB needed access. This meant I could finally migrate to a private subnet.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge: Connecting to AWS Service Endpoints
&lt;/h3&gt;

&lt;p&gt;However, there was one hurdle to overcome: "How do Lambda functions connect to AWS service endpoints?"&lt;/p&gt;

&lt;p&gt;The Lambda functions needed to access not only the DB but also AWS service endpoints—specifically Athena, STS, and CloudWatch.&lt;/p&gt;

&lt;p&gt;Attaching a VPC to the Lambda function would enable access to the DB in the private subnet. But it would also cut off the route to service endpoints.&lt;/p&gt;

&lt;p&gt;My first thought was to use paid NAT Gateways or VPC endpoints. But I wanted to complete the migration without adding costs if possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Egress-Only IGW: A Cost-Free Solution
&lt;/h3&gt;

&lt;p&gt;After some thought, I came up with the solution: Egress-Only IGW. It has the following characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Used only for IPv6 traffic&lt;/li&gt;
&lt;li&gt;Allows outbound communication from VPC to the internet (inbound connections cannot be initiated from the internet)&lt;/li&gt;
&lt;li&gt;No charge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means if the AWS service endpoints support IPv6, Lambda functions with VPC attached can connect to them for free.&lt;/p&gt;

&lt;p&gt;You can check IPv6 support for service endpoints in &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/aws-ipv6-support.html" rel="noopener noreferrer"&gt;this documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Athena, STS, and CloudWatch all support IPv6 (note: the STS global endpoint does not support IPv6; regional endpoints are required), so the following migration should be possible:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DB instance&lt;/td&gt;
&lt;td&gt;Public subnet&lt;/td&gt;
&lt;td&gt;Private subnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda → DB&lt;/td&gt;
&lt;td&gt;Internet&lt;/td&gt;
&lt;td&gt;Within VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lambda → AWS services&lt;/td&gt;
&lt;td&gt;IPv4&lt;/td&gt;
&lt;td&gt;IPv6 (via Egress-Only IGW)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Migration Steps
&lt;/h2&gt;

&lt;p&gt;With the goal set, I proceeded with the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add IPv6 support to the VPC&lt;/li&gt;
&lt;li&gt;Update Lambda function configuration&lt;/li&gt;
&lt;li&gt;Migrate DB to private subnet&lt;/li&gt;
&lt;li&gt;Delete unnecessary resources&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1. Add IPv6 Support to the VPC
&lt;/h3&gt;

&lt;p&gt;First, I added IPv6 support to the VPC, including associating CIDR blocks and updating route tables. See the &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-migrate-ipv6-add.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; for details.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Update Lambda Function Configuration
&lt;/h3&gt;

&lt;p&gt;Next, I updated the Lambda function configuration. Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allow IPv6 egress in the security group&lt;/li&gt;
&lt;li&gt;Set the environment variable &lt;code&gt;AWS_USE_DUALSTACK_ENDPOINT=true&lt;/code&gt; (&lt;a href="https://docs.aws.amazon.com/sdkref/latest/guide/feature-endpoints.html" rel="noopener noreferrer"&gt;reference&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Attach VPC and enable "Allow IPv6 traffic for dual-stack subnets" (&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html#configuration-vpc-ipv6" rel="noopener noreferrer"&gt;reference&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The environment variable tells boto3 (used by the Lambda function) to send requests to IPv4/IPv6 dual-stack service endpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Migrate DB to Private Subnet
&lt;/h3&gt;

&lt;p&gt;Then, I migrated the DB to the private subnet. This was the main task.&lt;/p&gt;

&lt;p&gt;I expected it to be simple, but you cannot directly migrate a DB from a public subnet to a private subnet. It required some extra steps.&lt;/p&gt;

&lt;p&gt;Specifically, you need to follow steps like "disable Multi-AZ → edit subnet group → enable Multi-AZ → ..." See &lt;a href="https://repost.aws/knowledge-center/rds-move-to-private-subnet" rel="noopener noreferrer"&gt;this article&lt;/a&gt; for details.&lt;/p&gt;

&lt;p&gt;After migration, I edited the DB's security group:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before: Allow access from &lt;code&gt;0.0.0.0/0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;After: Allow access from the Lambda function's security group&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Delete Unnecessary Resources
&lt;/h3&gt;

&lt;p&gt;Finally, I deleted the now-unnecessary public subnet and the regular Internet Gateway. Migration complete.&lt;/p&gt;

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

&lt;p&gt;That's how I used Egress-Only IGW to make RDS private without extra cost. It was a great opportunity to work with this feature in a real-world scenario.&lt;/p&gt;

&lt;p&gt;This example used Lambda, but the approach should apply to other services like ECS as well—assuming the target endpoints support IPv6.&lt;/p&gt;

&lt;p&gt;If you're choosing a public subnet due to cost-security trade-offs, Egress-Only IGW might be your solution. Give it a try!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>rds</category>
      <category>vpc</category>
      <category>lambda</category>
    </item>
    <item>
      <title>Migrating SOCI Index Manifest from v1 to v2 for ECS/Fargate</title>
      <dc:creator>Takashi Iwamoto</dc:creator>
      <pubDate>Sun, 01 Feb 2026 04:40:31 +0000</pubDate>
      <link>https://dev.to/aws-builders/migrating-soci-index-manifest-from-v1-to-v2-for-ecsfargate-13n6</link>
      <guid>https://dev.to/aws-builders/migrating-soci-index-manifest-from-v1-to-v2-for-ecsfargate-13n6</guid>
      <description>&lt;p&gt;At my current workplace, we use Seekable OCI (SOCI) to speed up Amazon ECS task launches on AWS Fargate.&lt;/p&gt;

&lt;p&gt;Recently, we migrated the SOCI Index Manifest version from v1 to v2. In this article, I'll explain why and how we did it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Migrated
&lt;/h2&gt;

&lt;p&gt;The reason is simple: v1 support is ending. AWS sent out the following notification:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After careful consideration, we have made the decision to discontinue the support for SOCI Index Manifest v1 for Amazon ECS customers using AWS Fargate launch mode, effective February 9, 2026. Until this date, AWS Fargate customers using Index Manifest v1 will be able to leverage existing indexes and create new indexes using Index Manifest v1 to deploy applications.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without migrating to v2, task launches would become slower.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Migrated
&lt;/h2&gt;

&lt;p&gt;The migration method is also straightforward. In short, we "deployed the new &lt;a href="https://awslabs.github.io/cfn-ecr-aws-soci-index-builder/" rel="noopener noreferrer"&gt;SOCI Index Builder&lt;/a&gt; that supports v2." We had been using a different solution with the same name, but it was deprecated without v2 support, so we switched.&lt;/p&gt;

&lt;p&gt;Here are the general steps:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Prepare the Tool
&lt;/h3&gt;

&lt;p&gt;Clone the SOCI Index Builder Git repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/awslabs/cfn-ecr-aws-soci-index-builder.git
&lt;span class="nb"&gt;cd &lt;/span&gt;cfn-ecr-aws-soci-index-builder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Edit &lt;code&gt;.taskcat.yml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Next, edit &lt;code&gt;.taskcat.yml&lt;/code&gt;. This mainly involves changing the deployment region. For example, if you only want to accelerate images in the Tokyo region ECR repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;--- a/.taskcat.yml
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/.taskcat.yml
&lt;/span&gt;&lt;span class="p"&gt;@@ -12,11 +12,5 @@&lt;/span&gt; tests:
     parameters:
       SociRepositoryImageTagFilters: "*:*"
     regions:
&lt;span class="gd"&gt;-      - us-east-1
-      - us-east-2
-      - us-west-1
-      - us-west-2
-      - eu-west-1
-      - eu-west-2
-      - eu-west-3
&lt;/span&gt;&lt;span class="gi"&gt;+      - ap-northeast-1
&lt;/span&gt;     template: templates/SociIndexBuilder.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Run &lt;code&gt;taskcat upload&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Then run &lt;code&gt;taskcat upload&lt;/code&gt;. This uploads the CloudFormation templates and Lambda function packages to S3.&lt;/p&gt;

&lt;p&gt;I recommend running taskcat via Docker rather than installing it locally, as it doesn't support Python 3.14 yet and you probably won't use it very often anyway.&lt;/p&gt;

&lt;p&gt;Here's an example of a dry run. Remove &lt;code&gt;--dry-run&lt;/code&gt; to actually upload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:/mnt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; AWS_ACCESS_KEY_ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; AWS_SECRET_ACCESS_KEY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; AWS_SESSION_TOKEN &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;GIT_PYTHON_REFRESH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;quiet &lt;span class="se"&gt;\&lt;/span&gt;
  taskcat/taskcat taskcat upload &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Create the CloudFormation Stack
&lt;/h3&gt;

&lt;p&gt;Finally, create a stack using the uploaded CloudFormation template. As documented, the easiest way is through the Management Console.&lt;/p&gt;

&lt;p&gt;For reference, here's an example of creating a stack in a single region using Terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;repository_image_tag_filters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"*:*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_object"&lt;/span&gt; &lt;span class="s2"&gt;"template"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;your-s3-bucket-name&amp;gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cfn-ecr-aws-soci-index-builder/templates/SociIndexBuilder.yml"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudformation_stack"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;capabilities&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CAPABILITY_IAM"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cfn-ecr-aws-soci-index-builder"&lt;/span&gt;
  &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;IamPermissionsBoundaryArn&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"none"&lt;/span&gt;
    &lt;span class="nx"&gt;QSS3BucketName&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_s3_object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;
    &lt;span class="nx"&gt;QSS3KeyPrefix&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"/&lt;/span&gt;&lt;span class="sr"&gt;", data.aws_s3_object.template.key)[0]}&lt;/span&gt;&lt;span class="dl"&gt;/"&lt;/span&gt;&lt;span class="s2"&gt;
    SociIndexVersion              = "&lt;/span&gt;&lt;span class="nx"&gt;V2&lt;/span&gt;&lt;span class="s2"&gt;"
    SociRepositoryImageTagFilters = join("&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;", local.repository_image_tag_filters)
  }
  template_url = format(
    "&lt;/span&gt;&lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//%s.s3.%s.amazonaws.com/%s",&lt;/span&gt;
    &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_s3_object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_s3_object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_s3_object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;That's how we migrated the SOCI Index Manifest from v1 to v2 for ECS/Fargate.&lt;/p&gt;

&lt;p&gt;If you've been using v1 and haven't migrated yet, I recommend doing so; it's not that much work.&lt;/p&gt;

&lt;p&gt;Haven't adopted SOCI at all? You can easily speed up your task launches with these four steps. Give it a try!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ecs</category>
      <category>fargate</category>
      <category>containers</category>
    </item>
    <item>
      <title>AWS ChatOps Without Lambda? Yes, You Can!</title>
      <dc:creator>Takashi Iwamoto</dc:creator>
      <pubDate>Sat, 17 May 2025 09:42:18 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-chatops-without-lambda-yes-you-can-4cjd</link>
      <guid>https://dev.to/aws-builders/aws-chatops-without-lambda-yes-you-can-4cjd</guid>
      <description>&lt;p&gt;Hi, I'm Takashi Iwamoto (&lt;a class="mentioned-user" href="https://dev.to/iwamot"&gt;@iwamot&lt;/a&gt;), an AWS Community Builder (Cloud Operations).&lt;/p&gt;

&lt;p&gt;When I looked for prior examples of building ChatOps on AWS, most of them used Lambda.&lt;/p&gt;

&lt;p&gt;However, I had a hunch that we could get rid of Lambda by using the &lt;strong&gt;&lt;a href="https://docs.aws.amazon.com/chatbot/latest/adminguide/custom-actions.html" rel="noopener noreferrer"&gt;Custom Actions&lt;/a&gt;&lt;/strong&gt; feature of &lt;strong&gt;Amazon Q&amp;nbsp;Developer in chat applications&lt;/strong&gt; (formerly AWS&amp;nbsp;Chatbot), which was announced in November&amp;nbsp;2023.&lt;/p&gt;

&lt;p&gt;After trying it out, I was able to build a fairly practical architecture.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk you through how I built AWS ChatOps without Lambda.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Avoid Lambda
&lt;/h2&gt;

&lt;p&gt;Before we dive in, let me touch on why I want to avoid Lambda.&lt;/p&gt;

&lt;p&gt;In a word: &lt;strong&gt;"Because it makes operations easier."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you don't use Lambda, you never have to worry about runtime end‑of‑life. The more ChatOps targets you have, the more the cost of those EOL updates will hurt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Services Used &amp;amp; Overall Architecture
&lt;/h2&gt;

&lt;p&gt;Here's what I actually built.&lt;/p&gt;

&lt;p&gt;The main AWS services I used are these three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon EventBridge&lt;/strong&gt; – event detection &amp;amp; routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon SNS&lt;/strong&gt; – notification delivery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon&amp;nbsp;Q&amp;nbsp;Developer in chat applications&lt;/strong&gt; – Slack notifications &amp;amp; Custom Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the overall architecture looks like this:&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;An &lt;strong&gt;EventBridge rule&lt;/strong&gt; detects the CodeDeploy deployment state‑change event.&lt;/li&gt;
&lt;li&gt;The event is transformed with an &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html" rel="noopener noreferrer"&gt;input transformer&lt;/a&gt; and routed to an &lt;strong&gt;SNS topic&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The message is delivered to an &lt;strong&gt;Amazon&amp;nbsp;Q&amp;nbsp;Developer Slack channel&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The notification message and Custom Action buttons appear in Slack.&lt;/li&gt;
&lt;li&gt;A user picks one of the Custom Actions.&lt;/li&gt;
&lt;li&gt;The corresponding CodeDeploy command is executed (e.g. &lt;code&gt;continue-deployment&lt;/code&gt;, &lt;code&gt;stop-deployment&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Creating &amp;amp; Associating Custom Actions
&lt;/h2&gt;

&lt;p&gt;To show Custom Action buttons in chat apps like Slack, you first need to create the Custom Actions themselves.&lt;/p&gt;

&lt;p&gt;There are two ways to define Custom Actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/chatbot/latest/adminguide/creating-custom-actions.html" rel="noopener noreferrer"&gt;Create them directly in the chat application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Create them with the &lt;a href="https://docs.aws.amazon.com/chatbot/latest/APIReference/Welcome.html" rel="noopener noreferrer"&gt;Amazon&amp;nbsp;Q&amp;nbsp;Developer in chat applications API&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose the API method because I wanted to manage everything as IaC. If you create the action in the chat UI, it seems that the resource is owned by AWS and not created inside your own AWS account.&lt;/p&gt;

&lt;p&gt;If you go the API route, &lt;strong&gt;you'll also need to associate the Custom Actions with the chat channel yourself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The steps are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Call the &lt;a href="https://docs.aws.amazon.com/chatbot/latest/APIReference/API_CreateCustomAction.html" rel="noopener noreferrer"&gt;CreateCustomAction&lt;/a&gt; API to create the action.&lt;/li&gt;
&lt;li&gt;Call the &lt;a href="https://docs.aws.amazon.com/chatbot/latest/APIReference/API_AssociateToConfiguration.html" rel="noopener noreferrer"&gt;AssociateToConfiguration&lt;/a&gt; API to attach it to the chat channel.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Below is how I implemented it in Terraform. (It's a bit long, so I've collapsed it.)&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Definition of Custom Actions
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;custom_actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;SwitchTasks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;button_text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"🔁 Switch task sets"&lt;/span&gt;
      &lt;span class="nx"&gt;criteria&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;operator&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EQUALS"&lt;/span&gt;
          &lt;span class="nx"&gt;value&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"blue-green-deployment"&lt;/span&gt;
          &lt;span class="nx"&gt;variable_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ActionGroup"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"ActionGroup"&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"event.metadata.additionalContext.ActionGroup"&lt;/span&gt;
        &lt;span class="s2"&gt;"DeploymentId"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"event.metadata.additionalContext.DeploymentId"&lt;/span&gt;
        &lt;span class="s2"&gt;"Region"&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"event.metadata.additionalContext.Region"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;command_text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"codedeploy continue-deployment --deployment-id $DeploymentId --region $Region --deployment-wait-type READY_WAIT"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;RollbackDeployment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;button_text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"🔙 Rollback"&lt;/span&gt;
      &lt;span class="nx"&gt;criteria&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;operator&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EQUALS"&lt;/span&gt;
          &lt;span class="nx"&gt;value&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"blue-green-deployment"&lt;/span&gt;
          &lt;span class="nx"&gt;variable_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ActionGroup"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"ActionGroup"&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"event.metadata.additionalContext.ActionGroup"&lt;/span&gt;
        &lt;span class="s2"&gt;"DeploymentId"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"event.metadata.additionalContext.DeploymentId"&lt;/span&gt;
        &lt;span class="s2"&gt;"Region"&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"event.metadata.additionalContext.Region"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;command_text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"codedeploy stop-deployment --deployment-id $DeploymentId --region $Region --auto-rollback-enabled true"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;TerminateOldTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;button_text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"🧹 Terminate old task set"&lt;/span&gt;
      &lt;span class="nx"&gt;criteria&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;operator&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EQUALS"&lt;/span&gt;
          &lt;span class="nx"&gt;value&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"blue-green-deployment"&lt;/span&gt;
          &lt;span class="nx"&gt;variable_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ActionGroup"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"ActionGroup"&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"event.metadata.additionalContext.ActionGroup"&lt;/span&gt;
        &lt;span class="s2"&gt;"DeploymentId"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"event.metadata.additionalContext.DeploymentId"&lt;/span&gt;
        &lt;span class="s2"&gt;"Region"&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"event.metadata.additionalContext.Region"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;command_text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"codedeploy continue-deployment --deployment-id $DeploymentId --region $Region --deployment-wait-type TERMINATION_WAIT"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"awscc_chatbot_custom_action"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;custom_actions&lt;/span&gt;

  &lt;span class="nx"&gt;action_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;
  &lt;span class="nx"&gt;attachments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;button_text&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button_text&lt;/span&gt;
      &lt;span class="nx"&gt;criteria&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;criteria&lt;/span&gt;
      &lt;span class="nx"&gt;notification_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Custom"&lt;/span&gt;
      &lt;span class="nx"&gt;variables&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;variables&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;definition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command_text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;command_text&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_default_tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"Name"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;
      &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Associating the Custom Actions with the Chat Channel (implemented with null_resource)
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In this example we set custom_action_groups = ["blue-green-deployment"]&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"custom_action_groups"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"A list of Custom Action group names to associate specific AWS Chatbot actions with the specified Slack channel configuration."&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;action_group_map&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"blue-green-deployment"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SwitchTasks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"RollbackDeployment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"TerminateOldTask"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;selected_actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;distinct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;custom_action_groups&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action_group_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
  &lt;span class="p"&gt;]))&lt;/span&gt;

  &lt;span class="nx"&gt;action_arns&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selected_actions&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;try&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;one&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
          &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awscc_chatbot_custom_actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;
          &lt;span class="nx"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/${action}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]),&lt;/span&gt;
        &lt;span class="kc"&gt;null&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="nx"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"null_resource"&lt;/span&gt; &lt;span class="s2"&gt;"associate_actions"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action_arns&lt;/span&gt;

  &lt;span class="nx"&gt;triggers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;action_arn&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
    &lt;span class="nx"&gt;chatbot_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_chatbot_slack_channel_configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat_configuration_arn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws chatbot associate-to-configuration --resource ${self.triggers.action_arn} --chat-configuration ${self.triggers.chatbot_arn} --region us-west-2"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;when&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;destroy&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws chatbot disassociate-from-configuration --resource ${self.triggers.action_arn} --chat-configuration ${self.triggers.chatbot_arn} --region us-west-2"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  EventBridge Rule
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"sns_topic_arn"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The SNS topic to notify when the blue/green deployment is ready for validation."&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;nullable&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"deployment_group_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The name of the deployment group targeted by ChatOps."&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;nullable&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"green_url"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The URL for validating the green environment."&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;nullable&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_event_rule"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;event_bus_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;
  &lt;span class="nx"&gt;event_pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;deploymentGroup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deployment_group_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;"READY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;detail-type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"CodeDeploy Deployment State-change Notification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"aws.codedeploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;force_destroy&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.deployment_group_name}-deployment-ready"&lt;/span&gt;
  &lt;span class="nx"&gt;name_prefix&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;role_arn&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;schedule_expression&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ENABLED"&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"Name"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.deployment_group_name}-deployment-ready"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_event_target"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;arn&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sns_topic_arn&lt;/span&gt;
  &lt;span class="nx"&gt;event_bus_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;
  &lt;span class="nx"&gt;force_destroy&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;input&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;input_path&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="nx"&gt;role_arn&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;rule&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_event_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;target_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sns"&lt;/span&gt;

  &lt;span class="nx"&gt;input_transformer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;input_paths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s2"&gt;"account"&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$.account"&lt;/span&gt;
      &lt;span class="s2"&gt;"deploymentGroup"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$.detail.deploymentGroup"&lt;/span&gt;
      &lt;span class="s2"&gt;"deploymentId"&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$.detail.deploymentId"&lt;/span&gt;
      &lt;span class="s2"&gt;"region"&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$.region"&lt;/span&gt;
      &lt;span class="s2"&gt;"time"&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"$.time"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;input_template&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trimspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOT&lt;/span&gt;&lt;span class="sh"&gt;
              *Deployment group:* &amp;lt;deploymentGroup&amp;gt;
              *Deployment ID:* &amp;lt;deploymentId&amp;gt;
              *Account:* &amp;lt;account&amp;gt;
              *Region:* &amp;lt;region&amp;gt;
              *Time:* &amp;lt;time&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;            EOT
&lt;/span&gt;            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nx"&gt;nextSteps&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
              &lt;span class="s2"&gt;"Check if the app works correctly at ${var.green_url}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s2"&gt;"If all looks good, switch task sets. If not, trigger a rollback"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="s2"&gt;"Once validated, terminate the original task set"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="nx"&gt;textType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"client-markdown"&lt;/span&gt;
            &lt;span class="nx"&gt;title&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;":large_blue_circle::large_green_circle: Deployment is now ready for validation"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;additionalContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;ActionGroup&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"blue-green-deployment"&lt;/span&gt;
              &lt;span class="nx"&gt;DeploymentId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;deploymentId&amp;gt;"&lt;/span&gt;
              &lt;span class="nx"&gt;Region&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;region&amp;gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;enableCustomActions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"custom"&lt;/span&gt;
          &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.0"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u003c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;u003e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;With this in place, Slack now shows the following notification, and the entire Blue/Green deployment can be operated using just the Custom Action buttons.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Remaining Challenge
&lt;/h2&gt;

&lt;p&gt;As you can see, this solution works quite well, &lt;strong&gt;but one issue is that we can't control the order of the Custom Action buttons&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sometimes they show up as "🧹 Terminate old task set", "🔙 Rollback", "🔁 Switch task sets", in that order. The screenshot above just happened to be in the desired order.&lt;/p&gt;

&lt;p&gt;If we could control that order, I'm sure this approach would be solid enough to become a go-to ChatOps pattern. I've already submitted a feature request to AWS Support, and I'm hopeful it will be addressed in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: ChatOps Without Lambda Is Possible
&lt;/h2&gt;

&lt;p&gt;That was an example of ChatOps built with Amazon&amp;nbsp;Q&amp;nbsp;Developer in chat applications Custom Actions.&lt;/p&gt;

&lt;p&gt;When you build it without Lambda, operations get easier. The unresolved button‑ordering issue remains, but if you can live with that, it's already perfectly usable.&lt;/p&gt;

&lt;p&gt;I plan to expand our ChatOps coverage with this method, and I hope it inspires you to try it out yourself.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>chatops</category>
      <category>terraform</category>
      <category>slack</category>
    </item>
    <item>
      <title>How to Change Network Configurations for Blue/Green Amazon ECS Services</title>
      <dc:creator>Takashi Iwamoto</dc:creator>
      <pubDate>Sat, 19 Oct 2024 15:10:57 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-to-change-network-configurations-for-bluegreen-amazon-ecs-services-42de</link>
      <guid>https://dev.to/aws-builders/how-to-change-network-configurations-for-bluegreen-amazon-ecs-services-42de</guid>
      <description>&lt;p&gt;&lt;strong&gt;If you want to change the network settings for an Amazon ECS service using blue/green deployment with AWS CodeDeploy, you need to edit the AppSpec file and trigger a new deployment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without knowing this method, you'd either have to recreate the entire ECS service or give up on changing the network settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;For ECS services using blue/green deployment with AWS CodeDeploy, you cannot directly change the network settings.&lt;/p&gt;

&lt;p&gt;When you try to make changes by calling the ECS &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_UpdateService.html" rel="noopener noreferrer"&gt;UpdateService&lt;/a&gt; API, you will encounter the following error:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;InvalidParameterException: Unable to update network parameters on services with a CODE_DEPLOY deployment controller. Use AWS CodeDeploy to trigger a new deployment.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This error message indicates that you need to trigger a new deployment with CodeDeploy, as noted in the documentation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For services using the blue/green (&lt;code&gt;CODE_DEPLOY&lt;/code&gt;) deployment controller, only the desired count, deployment configuration, health check grace period, task placement constraints and strategies, enable ECS managed tags option, and propagate tags can be updated using this API. If the network configuration, platform version, task definition, or load balancer need to be updated, create a new AWS CodeDeploy deployment. For more information, see &lt;a href="https://docs.aws.amazon.com/codedeploy/latest/APIReference/API_CreateDeployment.html" rel="noopener noreferrer"&gt;CreateDeployment&lt;/a&gt; in the &lt;em&gt;AWS CodeDeploy API Reference&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_UpdateService.html" rel="noopener noreferrer"&gt;UpdateService - Amazon Elastic Container Service&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;Now that we know we need to trigger a new deployment, the question remains: how exactly should we do that?&lt;/p&gt;

&lt;p&gt;The answer is to "edit the &lt;a href="https://docs.aws.amazon.com/codedeploy/latest/userguide/application-specification-files.html" rel="noopener noreferrer"&gt;AppSpec file&lt;/a&gt; and then trigger a new deployment." If you trigger it without editing the AppSpec file, the network settings won't change.&lt;/p&gt;

&lt;p&gt;In the case of appspec.yml, edit it as shown below.&lt;/p&gt;

&lt;blockquote&gt;

&lt;pre class="highlight yaml"&gt;&lt;code&gt;        &lt;span class="na"&gt;NetworkConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;AwsvpcConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;Subnets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subnet-1234abcd"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subnet-5678abcd"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;SecurityGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sg-12345678"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;AssignPublicIp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENABLED"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;br&gt;&lt;em&gt;&lt;a href="https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-example.html#appspec-file-example-ecs" rel="noopener noreferrer"&gt;AppSpec File example - AWS CodeDeploy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Specify the subnets, security groups, and whether or not to assign a public IP address for the ECS tasks in the AppSpec file.&lt;/p&gt;

&lt;p&gt;When doing so, you need to specify all three: &lt;code&gt;Subnets&lt;/code&gt;, &lt;code&gt;SecurityGroups&lt;/code&gt;, and &lt;code&gt;AssignPublicIp&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;All or none of the settings under &lt;code&gt;NetworkConfiguration&lt;/code&gt; must be specified. For example, if you want to specify &lt;code&gt;Subnets&lt;/code&gt;, you must also specify &lt;code&gt;SecurityGroups&lt;/code&gt; and &lt;code&gt;AssignPublicIp&lt;/code&gt;. If none is specified, CodeDeploy uses the current network Amazon ECS settings.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-structure-resources.html#reference-appspec-file-structure-resources-ecs" rel="noopener noreferrer"&gt;AppSpec 'resources' section (Amazon ECS and AWS Lambda deployments only) - AWS CodeDeploy&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After editing the AppSpec file, trigger a new deployment, and the network settings will be applied. The new ECS tasks in the green environment should start with the specified network configuration.&lt;/p&gt;

&lt;p&gt;Once the deployment is complete, you can safely remove the &lt;code&gt;NetworkConfiguration&lt;/code&gt; from the AppSpec file.&lt;/p&gt;

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

&lt;p&gt;As described, even for Amazon ECS services using blue/green deployment with CodeDeploy, you can change the network settings by editing the AppSpec file and triggering a new deployment.&lt;/p&gt;

&lt;p&gt;I almost ended up recreating the ECS service because I wasn't aware of this method. I hope this article helps those in a similar situation.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ecs</category>
      <category>codedeploy</category>
    </item>
    <item>
      <title>Implementing SLO Error Budget Monitoring with AWS Services Only</title>
      <dc:creator>Takashi Iwamoto</dc:creator>
      <pubDate>Sun, 08 Sep 2024 14:32:28 +0000</pubDate>
      <link>https://dev.to/aws-builders/implementing-slo-error-budget-monitoring-with-aws-services-only-2dd7</link>
      <guid>https://dev.to/aws-builders/implementing-slo-error-budget-monitoring-with-aws-services-only-2dd7</guid>
      <description>&lt;p&gt;In this article, I’ll introduce an SLO error budget monitoring system that I built entirely using AWS services.&lt;/p&gt;

&lt;h2&gt;
  
  
  System Overview
&lt;/h2&gt;

&lt;p&gt;This system is based on the multi-window, multi-burn-rate alerting method proposed in the &lt;em&gt;&lt;a href="https://sre.google/workbook/table-of-contents/" rel="noopener noreferrer"&gt;Site Reliability Workbook&lt;/a&gt;&lt;/em&gt;. It sends alerts to Slack when the SLO error budget burn rate exceeds a certain threshold.&lt;/p&gt;

&lt;blockquote&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;expr: (
        job:slo_errors_per_request:ratio_rate1h{job="myjob"} &amp;gt; (14.4*0.001)
      and
        job:slo_errors_per_request:ratio_rate5m{job="myjob"} &amp;gt; (14.4*0.001)
      )
    or
      (
        job:slo_errors_per_request:ratio_rate6h{job="myjob"} &amp;gt; (6*0.001)
      and
        job:slo_errors_per_request:ratio_rate30m{job="myjob"} &amp;gt; (6*0.001)
      )
severity: page

expr: (
        job:slo_errors_per_request:ratio_rate24h{job="myjob"} &amp;gt; (3*0.001)
      and
        job:slo_errors_per_request:ratio_rate2h{job="myjob"} &amp;gt; (3*0.001)
      )
    or
      (
        job:slo_errors_per_request:ratio_rate3d{job="myjob"} &amp;gt; 0.001
      and
        job:slo_errors_per_request:ratio_rate6h{job="myjob"} &amp;gt; 0.001
      )
severity: ticket
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;em&gt;&lt;a href="https://sre.google/workbook/alerting-on-slos/" rel="noopener noreferrer"&gt;https://sre.google/workbook/alerting-on-slos/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;em&gt;Site Reliability Workbook&lt;/em&gt; recommends combining multiple time windows and burn rates to create more accurate alerts. For example, if you only alert based on a short time window, you may get too many false positives. On the other hand, if you rely on a longer time window, it may take too long for an alert to trigger or recover. By using both short and long windows, the system can generate more precise alerts.&lt;/p&gt;

&lt;p&gt;For more detailed explanations of this alerting method, I encourage you to check out the &lt;em&gt;Site Reliability Workbook&lt;/em&gt;, which is available for free. Personally, I believe that strictly following these guidelines is the best way to monitor service reliability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Implemented This
&lt;/h2&gt;

&lt;p&gt;The primary motivation behind this monitoring system was to solve the problem of "alert fatigue." As an operations engineer, I was constantly bombarded with alerts that didn’t actually require any action.&lt;/p&gt;

&lt;p&gt;Previously, we monitored CPU and memory usage metrics directly, which led to alerts even when the service itself was running smoothly. For example, we would receive a Slack alert if the CPU usage of a web server exceeded 90%, regardless of whether there were any issues with availability or latency.&lt;/p&gt;

&lt;p&gt;However, even if the CPU usage was high, if the service remained available and responsive, there was no need for intervention. Over time, receiving alerts for issues that didn't require action caused fatigue and increased the risk of missing critical alerts. Missing an important alert could damage service reliability and lead to a loss of users.&lt;/p&gt;

&lt;p&gt;To address this, I decided to focus only on critical indicators—such as availability and latency—and set up an SLO-based monitoring system that tracks the error budget burn rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Used AWS Services Only
&lt;/h2&gt;

&lt;p&gt;At first, I considered using third-party monitoring tools like New Relic or Datadog. However, after investigating these tools in early 2023, I found that it was either impossible or very difficult to implement the kind of SLO monitoring I needed with their existing features. I worked with support teams from both platforms, but we couldn't come up with a viable solution.&lt;/p&gt;

&lt;p&gt;On the other hand, by combining AWS services such as Amazon CloudWatch, I could implement the necessary monitoring and at a much lower cost—around $20 per month.&lt;/p&gt;

&lt;p&gt;Since most of our workloads are hosted on AWS, building the monitoring system directly on AWS made it easier to integrate everything into a single console. AWS also offers a high degree of flexibility in terms of combining services, giving us more room to scale and customize compared to third-party tools like New Relic or Datadog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;Here’s an overview of the system’s architecture:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F15lc5plg9xpct7t87sne.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F15lc5plg9xpct7t87sne.png" alt="Diagram showing an AWS-based SLO error budget monitoring system. Key services include EventBridge, Lambda, Athena, S3, CloudWatch, RDS, and SNS. The system periodically queries access logs, aggregates data, and triggers alerts based on custom metrics." width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Although it may look a bit complex at first glance, the core workflow is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EventBridge Scheduler triggers a Lambda function at regular intervals.&lt;/li&gt;
&lt;li&gt;The Lambda function runs an Athena query to aggregate ALB access logs.&lt;/li&gt;
&lt;li&gt;The results are stored in both CloudWatch custom metrics and RDS.&lt;/li&gt;
&lt;li&gt;CloudWatch composite alarms use these metrics to trigger alerts based on the SLO conditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Lambda function is triggered by EventBridge instead of an S3 file save event. This is because when there is heavy traffic, multiple log files are saved to S3, which could result in the aggregation process being run multiple times unnecessarily.&lt;/p&gt;

&lt;p&gt;We use RDS to store the aggregated results in order to reduce Athena scan costs. For more details on these optimizations and custom metric definitions, please refer to &lt;a href="https://jawspankration2024.jaws-ug.jp/en/timetable/TT-36/" rel="noopener noreferrer"&gt;my presentation&lt;/a&gt; at &lt;a href="https://jawspankration2024.jaws-ug.jp/en/" rel="noopener noreferrer"&gt;JAWS PANKRATION 2024&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results of the Implementation
&lt;/h2&gt;

&lt;p&gt;Since we introduced this system, alert fatigue has significantly decreased. I personally noticed a reduction in the number of unnecessary alerts that required no action.&lt;/p&gt;

&lt;p&gt;Additionally, the development team has become more invested in the reliability of the services. Now, when an alert is triggered, the team takes the initiative to tune performance or even revise the SLOs when necessary.&lt;/p&gt;

&lt;p&gt;As of September 2023, this system is monitoring 7 services and 12 critical user journeys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational Tips
&lt;/h2&gt;

&lt;p&gt;To streamline operations, I developed an internal Terraform module that allows us to quickly accommodate additional monitoring requests from the development team. While the module itself is private, this is an actual example of its interface, which could be useful as a reference if you plan to implement a similar module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"slomon"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"git@github.com:enechange/terraform-modules.git//slomon-for-alb?ref=v0.53.0"&lt;/span&gt;

  &lt;span class="nx"&gt;environment_name&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod-enechange"&lt;/span&gt;
  &lt;span class="nx"&gt;alb_access_logs_s3_url&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb_access_logs_s3_url&lt;/span&gt;
  &lt;span class="nx"&gt;sns_topic_names_for_paging&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cto-incident-enechange"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;sns_topic_names_for_ticketing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cto-alert-enechange"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;critical_user_journeys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;input1_submit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;http_method&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;
      &lt;span class="nx"&gt;path&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/try/input1_submit"&lt;/span&gt;
      &lt;span class="nx"&gt;dashboard_order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

      &lt;span class="nx"&gt;slo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;availability_target&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;95.0&lt;/span&gt;
        &lt;span class="nx"&gt;latency_p95_threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;
        &lt;span class="nx"&gt;latency_p50_threshold&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Terraform module also includes Athena queries for analyzing historical data, allowing us to quickly set SLOs based on past performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Outlook
&lt;/h2&gt;

&lt;p&gt;At present, this monitoring system meets all of our needs. However, if CloudWatch's Application Signals feature evolves to support the required conditions for SLO monitoring, this custom system may no longer be necessary. In that case, we would likely transition to a fully managed solution.&lt;/p&gt;

&lt;p&gt;Likewise, if New Relic or Datadog introduce features that support the kind of SLO monitoring we need, we may consider switching to them. These APM tools generally offer better capabilities for diagnosing root causes of performance degradation compared to CloudWatch.&lt;/p&gt;

&lt;p&gt;Ultimately, we will continue to assess the best solution for our needs and adapt our system as new tools and services emerge.&lt;/p&gt;

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

&lt;p&gt;In this article, I introduced an SLO error budget monitoring system that I built using AWS services only.&lt;/p&gt;

&lt;p&gt;By implementing this system, we were able to reduce alert fatigue and increase the development team’s focus on service reliability.&lt;/p&gt;

&lt;p&gt;I hope this article provides useful insights for those dealing with similar challenges around alert fatigue.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudwatch</category>
      <category>monitoring</category>
      <category>sre</category>
    </item>
  </channel>
</rss>
