Hi, I'm Takashi Iwamoto (@iwamot), an AWS Community Builder (Cloud Operations).
When I looked for prior examples of building ChatOps on AWS, most of them used Lambda.
However, I had a hunch that we could get rid of Lambda by using the Custom Actions feature of Amazon Q Developer in chat applications (formerly AWS Chatbot), which was announced in November 2023.
After trying it out, I was able to build a fairly practical architecture.
In this post, I'll walk you through how I built AWS ChatOps without Lambda.
Why Avoid Lambda
Before we dive in, let me touch on why I want to avoid Lambda.
In a word: "Because it makes operations easier."
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.
Services Used & Overall Architecture
Here's what I actually built.
The main AWS services I used are these three:
- Amazon EventBridge – event detection & routing
- Amazon SNS – notification delivery
- Amazon Q Developer in chat applications – Slack notifications & Custom Actions
And the overall architecture looks like this:
- An EventBridge rule detects the CodeDeploy deployment state‑change event.
- The event is transformed with an input transformer and routed to an SNS topic.
- The message is delivered to an Amazon Q Developer Slack channel.
- The notification message and Custom Action buttons appear in Slack.
- A user picks one of the Custom Actions.
- The corresponding CodeDeploy command is executed (e.g.
continue-deployment
,stop-deployment
).
Creating & Associating Custom Actions
To show Custom Action buttons in chat apps like Slack, you first need to create the Custom Actions themselves.
There are two ways to define Custom Actions:
- Create them directly in the chat application
- Create them with the Amazon Q Developer in chat applications API
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.
If you go the API route, you'll also need to associate the Custom Actions with the chat channel yourself.
The steps are:
- Call the CreateCustomAction API to create the action.
- Call the AssociateToConfiguration API to attach it to the chat channel.
Below is how I implemented it in Terraform. (It's a bit long, so I've collapsed it.)
Definition of Custom Actions
locals {
custom_actions = {
SwitchTasks = {
button_text = "🔁 Switch task sets"
criteria = [
{
operator = "EQUALS"
value = "blue-green-deployment"
variable_name = "ActionGroup"
},
]
variables = {
"ActionGroup" = "event.metadata.additionalContext.ActionGroup"
"DeploymentId" = "event.metadata.additionalContext.DeploymentId"
"Region" = "event.metadata.additionalContext.Region"
}
command_text = "codedeploy continue-deployment --deployment-id $DeploymentId --region $Region --deployment-wait-type READY_WAIT"
}
RollbackDeployment = {
button_text = "🔙 Rollback"
criteria = [
{
operator = "EQUALS"
value = "blue-green-deployment"
variable_name = "ActionGroup"
},
]
variables = {
"ActionGroup" = "event.metadata.additionalContext.ActionGroup"
"DeploymentId" = "event.metadata.additionalContext.DeploymentId"
"Region" = "event.metadata.additionalContext.Region"
}
command_text = "codedeploy stop-deployment --deployment-id $DeploymentId --region $Region --auto-rollback-enabled true"
}
TerminateOldTask = {
button_text = "🧹 Terminate old task set"
criteria = [
{
operator = "EQUALS"
value = "blue-green-deployment"
variable_name = "ActionGroup"
},
]
variables = {
"ActionGroup" = "event.metadata.additionalContext.ActionGroup"
"DeploymentId" = "event.metadata.additionalContext.DeploymentId"
"Region" = "event.metadata.additionalContext.Region"
}
command_text = "codedeploy continue-deployment --deployment-id $DeploymentId --region $Region --deployment-wait-type TERMINATION_WAIT"
}
}
}
resource "awscc_chatbot_custom_action" "this" {
for_each = local.custom_actions
action_name = each.key
attachments = [
{
button_text = each.value.button_text
criteria = each.value.criteria
notification_type = "Custom"
variables = each.value.variables
},
]
definition = {
command_text = each.value.command_text
}
tags = [
for key, value in merge(data.aws_default_tags.this.tags, { "Name" = each.key }) : {
key = key
value = value
}
]
}
Associating the Custom Actions with the Chat Channel (implemented with null_resource)
# In this example we set custom_action_groups = ["blue-green-deployment"]
variable "custom_action_groups" {
description = "A list of Custom Action group names to associate specific AWS Chatbot actions with the specified Slack channel configuration."
type = list(string)
default = []
}
locals {
action_group_map = {
"blue-green-deployment" = ["SwitchTasks", "RollbackDeployment", "TerminateOldTask"]
}
selected_actions = distinct(flatten([
for group in var.custom_action_groups :
lookup(local.action_group_map, group, [])
]))
action_arns = {
for k, v in {
for action in local.selected_actions :
action => try(
one([
for id in data.awscc_chatbot_custom_actions.this.ids : id
if endswith(id, "/${action}")
]),
null
)
} : k => v if v != null
}
}
resource "null_resource" "associate_actions" {
for_each = local.action_arns
triggers = {
action_arn = each.value
chatbot_arn = aws_chatbot_slack_channel_configuration.this.chat_configuration_arn
}
provisioner "local-exec" {
command = "aws chatbot associate-to-configuration --resource ${self.triggers.action_arn} --chat-configuration ${self.triggers.chatbot_arn} --region us-west-2"
}
provisioner "local-exec" {
when = destroy
command = "aws chatbot disassociate-from-configuration --resource ${self.triggers.action_arn} --chat-configuration ${self.triggers.chatbot_arn} --region us-west-2"
}
}
EventBridge Rule
variable "sns_topic_arn" {
description = "The SNS topic to notify when the blue/green deployment is ready for validation."
type = string
nullable = false
}
variable "deployment_group_name" {
description = "The name of the deployment group targeted by ChatOps."
type = string
nullable = false
}
variable "green_url" {
description = "The URL for validating the green environment."
type = string
nullable = false
}
resource "aws_cloudwatch_event_rule" "this" {
description = null
event_bus_name = "default"
event_pattern = jsonencode(
{
detail = {
deploymentGroup = [
var.deployment_group_name,
]
state = [
"READY",
]
}
detail-type = [
"CodeDeploy Deployment State-change Notification",
]
source = [
"aws.codedeploy",
]
}
)
force_destroy = false
name = "${var.deployment_group_name}-deployment-ready"
name_prefix = null
role_arn = null
schedule_expression = null
state = "ENABLED"
tags = {
"Name" = "${var.deployment_group_name}-deployment-ready"
}
}
resource "aws_cloudwatch_event_target" "this" {
arn = var.sns_topic_arn
event_bus_name = "default"
force_destroy = false
input = null
input_path = null
role_arn = aws_iam_role.this.arn
rule = aws_cloudwatch_event_rule.this.name
target_id = "sns"
input_transformer {
input_paths = {
"account" = "$.account"
"deploymentGroup" = "$.detail.deploymentGroup"
"deploymentId" = "$.detail.deploymentId"
"region" = "$.region"
"time" = "$.time"
}
input_template = replace(replace(
jsonencode(
{
content = {
description = trimspace(
<<-EOT
*Deployment group:* <deploymentGroup>
*Deployment ID:* <deploymentId>
*Account:* <account>
*Region:* <region>
*Time:* <time>
EOT
)
nextSteps = [
"Check if the app works correctly at ${var.green_url}",
"If all looks good, switch task sets. If not, trigger a rollback",
"Once validated, terminate the original task set",
]
textType = "client-markdown"
title = ":large_blue_circle::large_green_circle: Deployment is now ready for validation"
}
metadata = {
additionalContext = {
ActionGroup = "blue-green-deployment"
DeploymentId = "<deploymentId>"
Region = "<region>"
}
enableCustomActions = true
}
source = "custom"
version = "1.0"
}
),
"\u003c", "<"), "\u003e", ">")
}
}
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.
Remaining Challenge
As you can see, this solution works quite well, but one issue is that we can't control the order of the Custom Action buttons.
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.
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.
Conclusion: ChatOps Without Lambda Is Possible
That was an example of ChatOps built with Amazon Q Developer in chat applications Custom Actions.
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.
I plan to expand our ChatOps coverage with this method, and I hope it inspires you to try it out yourself.
Top comments (0)