Building with AWS ChatBot: Approving CodePipeline Executions Through ChatOps

Use AWS ChatBot and EventBridge to manage your CodePipeline approval steps via ChatOps

Posted by Steven Tan on 28th March 2024

Previously, I showcased how to say "Hello World" through an AWS Chatbot integration in your Slack channel. This blog post will explore a more advanced use case that will work with AWS Chatbot.

Introducing the Use-Case

If you have ever used AWS CodePipelines with approval actions, you will be used to configuring an Amazon SNS target and then having a few emails subscribed for approval notifications. Managing and ensuring the list of subscribed emails can become tedious, accidental un-subscriptions can occur, or even emails getting lost in the inbox.

As an engineer attempting to deploy your changes into production, you may need to ping other staff members to get their attention and check their inboxes.

This workflow can be improved through ChatOps, where time-sensitive notifications are sent, reviewed, and actioned through one Slack channel.

Implementation

AWS Architecture Diagram

Implementing this solution requires three services (excluding the Amazon SNS topic and AWS Chatbot created in part 1): AWS CodePipeline Amazon EventBridge AWS Lambda

AWS CodePipeline

AWS CodePipeline Stages

When CodePipeline enters the approval action stage, it generates an event through AWS EventBridge that the Lambda function can pick up.

{
  "detail-type": ["CodePipeline Action Execution State Change"],
  "detail": {
    "type": {
      "owner": ["AWS"],
      "provider": ["Manual"],
      "category": ["Approval"]
    }
  },
  "source": ["aws.codepipeline"]
}

Slack Notifier Lambda

After the Lambda function receives the event from Amazon EventBridge, the message needs to be formatted correctly before being sent to Amazon SNS. In my example code, I have referenced the SNS Topic ARN as an environment variable but this can also be hardcoded.

import boto3
import os
import json

from typing import Dict, Any

sns = boto3.resource('sns')
topic = sns.Topic(os.getenv('SNS_TOPIC'))

def lambda_handler(event: Dict[str, Any], context):
    approval_state: str = event.get("detail", {}).get("state")
    if approval_state == "STARTED":
        print("CodePipeline Approval Requested and Waiting")
        notification: dict[str, Any] = {
            "version": "1.0",
            "source": "custom",
            "content": {
                "textType": "client-markdown",
                "description": f"@here CodePipeline '{event['detail']['pipeline']}' Stage '{event['detail']['stage']}' is awaiting approval."
            },
            "metadata": {
                "threadId": event['detail']['execution-id'],
                'additionalContext': {
                    'pipeline': event['detail']['pipeline'],
                    'stage': event['detail']['stage'],
                    'action': event['detail']['action'],
                }
            }
        }
        topic.publish(Message=json.dumps(notification))
        return
    if approval_state == "SUCCEEDED":
        print("CodePipeline Approval Request Approved")
        return
    if approval_state == "FAILED":
        print("CodePipeline Approval Request Rejected")
        return

Lambda Permissions:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "sns:Publish",
            "Resource": "$$YOUR_SNS_TOPIC_ARN$$",
            "Effect": "Allow"
        }
    ]
}

The resulting notification on Slack will look like this.

AWS CodePipeline Notification

Configuring the Slack Custom Actions

Once this notification appears on Slack, a custom action must be configured to trigger Lambda functions on the AWS account. Configuring the Slack custom action is straightforward as the notification includes the metadata required to match the notification to the specific CodePipeline Stage needing approval.

Lambda Function Code

The approval Lambda function code will take the invocation performed by AWS ChatBot, including the approval/rejection action and metadata, correlating that to the CodePipeline that needs to be approved.

The parameters it relies on are "approval", "pipeline", "stage", and "action".

from typing import Dict
import boto3

codepipeline = boto3.client('codepipeline')

def lambda_handler(event: Dict[str, str], context):
    approval: str = event.get("approval")

    if approval not in ["Approved", "Rejected"]:
        return "Approval state must be either Approved or Rejected"

    pipeline_name: str = event['pipeline']
    stage_name: str = event['stage']
    action_name: str = event['action']

    pipeline_state = codepipeline.get_pipeline_state(
        name=pipeline_name
    )
    token: str
    for stage in pipeline_state['stageStates']:
        if stage['stageName'] != stage_name:
            continue
        for action in stage['actionStates']:
            if action['actionName'] != action_name:
                continue
            token = action['latestExecution']['token']
            break

    codepipeline.put_approval_result(
        pipelineName=pipeline_name,
        stageName=stage_name,
        actionName=action_name,
        result={
            'summary': f"Execution was {approval.lower()} through Slack ChatBot",
            'status': approval
        },
        token=token
    )
    return f"Pipeline approval action for {pipeline_name} was {approval}"

The permissions required to approve/reject a CodePipeline Approval action are as follows:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "codepipeline:GetPipelineState",
                "codepipeline:PutApprovalResult"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

Slack Action Config

A Slack Custom Action must be created through the Slack UI. Once Slack receives a notification, the custom Slack action can be configured by clicking the "⋮" (ellipses) button and following the prompts.

This involves configuring the AWS account, region, and AWS Lambda function name to point the custom action to the correct target. With this information, you can use the metadata provided by the first Lambda function.

Custom Action Configuration

The only thing that changes between the two actions is the "Approved" or "Rejected" keywords, so the configuration is nearly identical. Other variables can be included, such as an approval message which is recorded in the CodePipeline action execution.

Once the custom action has been successfully configured, a CodePipeline approval or rejection action performed through the AWS ChatBot integration will look like this:

Approval

AWS CodePipeline Approval

Rejection

AWS CodePipeline Approval

Outcomes

This implementation provides the bare minimum necessities to approach CodePipeline approvals through a ChatOps discipline. This implementation can be extended to invoke CodePipelines, helping you manage your entire deployment process through Slack.