<?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.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>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 Developer in chat applications&lt;/strong&gt; (formerly AWS Chatbot), which was announced in November 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 Q 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 Q 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 Q 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;
  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;
  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;
  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;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 Q 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://media.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://media.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."&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;/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>
