Introduction
In DevOps, continuous integration and delivery (CI/CD) pipelines are integral for efficient software development. AWS CodePipeline facilitates the orchestration of these pipelines, but keeping track of pipeline executions in real time can be challenging. This blog post will guide you through the process of automating notifications to a Discord channel using AWS Lambda whenever there is an update to a CodePipeline execution. The solution leverages serverless computing, AWS Lambda, and Infrastructure as Code (IaC) with Terraform for seamless deployment.
π‘ The GitHub link to the complete code can be found at the end of the blog.
1. Overview
AWS CodePipeline is a fully managed CI/CD service, that streamlines the process of building, testing, and deploying code changes. However, being aware of pipeline status changes in real-time is crucial for efficient collaboration and issue resolution.
2. Setting Up Discord Integration
Before diving into the technical details, you need to set up Discord integration. This involves creating a Discord webhook, which will be the communication link for posting messages to your desired Discord channel.
3. AWS Lambda for CodePipeline Notifications
3.1 Understanding the Lambda Function Structure
The heart of this solution is an AWS Lambda function written in Node.js. This function responds to CodePipeline state changes triggered by EventBridge, a serverless event bus. The Lambda function fetches details about the pipeline execution and formats a message to be posted in Discord.
3.2 Utilizing CodePipeline Helper Functions
The codepipeline-helper.js
file contains helper functions for interacting with AWS CodePipeline. It establishes a connection to CodePipeline, fetches pipeline execution details, and retrieves the current state of the pipeline.
// codepipeline-helper.js
require('dotenv').config();
const AWS = require('aws-sdk');
AWS.config.update({ region: process.env.REGION });
const codepipeline = new AWS.CodePipeline(process.env.REGION);
const PIPELINE_HISTORY = 'https://###REGION###.console.aws.amazon.com/codepipeline/home?region=###REGION####/view/###PIPELINE###/history';
module.exports.getPipelineExecutionDetails = (executionId, pipeline) => {
const promises = [];
promises.push(getPipelineExecution(executionId, pipeline));
promises.push(getPipelineState(pipeline));
return Promise.all(promises).then(([execution, state]) => {
return {
execution: execution,
state: state,
executionHistoryUrl: PIPELINE_HISTORY.replace(new RegExp('###REGION###', 'g'), process.env.REGION).replace(
new RegExp('###PIPELINE###', 'g'),
pipeline
),
};
});
};
// ... other functions like getPipelineExecution and getPipelineState
// The GitHub link to the complete code can be found at the end of the blog.
3.3 Crafting Discord Messages with Discord Helper
The discord-helper.js
file is responsible for creating Discord messages based on the CodePipeline execution details. It extracts relevant information such as commit details, and stage statuses, and creates a formatted message ready for Discord.
// discord-helper.js
const CodePipelineHelper = require('./codepipeline-helper');
const Constants = require('./constants');
const AppName = process.env.DISCORD_CHANNEL ? process.env.DISCORD_CHANNEL : 'Github';
module.exports.createDiscordMessage = (codepipelineEventDetails) => {
return CodePipelineHelper.getPipelineExecutionDetails(
codepipelineEventDetails['execution-id'],
codepipelineEventDetails.pipeline
).then((pipelineDetails) => {
// get git info from github directly??? this might require authorization
const gitCommitInfo = pipelineDetails.execution.pipelineExecution.artifactRevisions[0];
// ... other code for creating Discord message
});
};
// ... getColorByState function and other related functions
// The GitHub link to the complete code can be found at the end of the blog.
4. Terraform Infrastructure as Code (IaC)
Terraform is employed to manage AWS resources in a declarative manner. The infrastructure is defined in the main.tf
file, specifying IAM roles, policies, Lambda functions, and EventBridge rules.
4.1 IAM Role and Policies
IAM roles and policies are defined to grant necessary permissions to the Lambda function, allowing it to interact with CloudWatch Logs and CodePipeline.
# main.tf
resource "aws_iam_role" "lambda_role" {
name = "${var.LAMBDA_APP_NAME}-codepipeline-discord-lambda-role"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_iam_role_policy" "lambda_role_policy" {
name = "${var.LAMBDA_APP_NAME}-discord-codepipeline-lambda-role-policy"
role = aws_iam_role.lambda_role.id
policy = <<EOF
{
"Version" : "2012-10-17",
"Statement" : [{
"Sid": "WriteLogsToCloudWatch",
"Effect" : "Allow",
"Action" : [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource" : "arn:aws:logs:*:*:*"
}, {
"Sid": "AllowAccesstoPipeline",
"Effect" : "Allow",
"Action" : [
"codepipeline:GetPipeline",
"codepipeline:GetPipelineState",
"codepipeline:GetPipelineExecution",
"codepipeline:ListPipelineExecutions",
"codepipeline:ListActionTypes",
"codepipeline:ListPipelines"
],
"Resource" : "*"
}
]
}
EOF
}
# The GitHub link to the complete code can be found at the end of the blog.
4.2 Deploying Lambda Function with Terraform
The Lambda function, defined in the aws_lambda_function
resource block is deployed with the associated IAM role and policies.
# main.tf
resource "aws_lambda_function" "lambda" {
filename = data.archive_file.lambda_zip.output_path
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
description = "Posts a message to Discord channel '${var.DISCORD_CHANNEL}' every time there is an update to codepipeline execution."
function_name = "${var.LAMBDA_APP_NAME}-discord-codepipeline-lambda"
role = aws_iam_role.lambda_role.arn
handler = "handler.handle"
runtime = "nodejs14.x"
timeout = var.LAMBDA_TIMEOUT
memory_size = var.LAMBDA_MEMORY_SIZE
environment {
variables = {
"DISCORD_WEBHOOK_URL" = var.DISCORD_WEBHOOK_URL
"DISCORD_CHANNEL" = var.DISCORD_CHANNEL
"RELEVANT_STAGES" = var.RELEVANT_STAGES
"REGION" = var.REGION
}
}
}
# ... other resources like aws_lambda_alias, aws_cloudwatch_event_rule, etc.
# The GitHub link to the complete code can be found at the end of the blog.
4.3 EventBridge Rule for CodePipeline State Changes
An EventBridge rule is established to capture state changes in all CodePipelines. This rule triggers the Lambda function whenever a relevant event occurs.
# main.tf
resource "aws_cloudwatch_event_rule" "pipeline_state_update" {
name = "${var.LAMBDA_APP_NAME}-discord-codepipeline-rule"
description = "capture state changes in all CodePipelines"
event_pattern = <<PATTERN
{
"detail-type": [
"CodePipeline Pipeline Execution State Change"
],
"source": [
"aws.codepipeline"
]
}
PATTERN
}
resource "aws_lambda_permission" "allow_cloudwatch" {
statement_id = "AllowExecutionFromCloudWatch"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.lambda.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.pipeline_state_update.arn
qualifier = aws_lambda_alias.lambda_alias.name
}
resource "aws_cloudwatch_event_target" "lambda_trigger" {
rule = aws_cloudwatch_event_rule.pipeline_state_update.name
arn = aws_lambda_alias.lambda_alias.arn
}
# The GitHub link to the complete code can be found at the end of the blog.
5. Customization and Configuration
This solution is designed to be adaptable to specific team needs. You can customize relevant stages to monitor, configure Lambda function parameters, and extend functionality as required.
5.1 Adapting Relevant Stages
In constants.js
, you can adjust the relevant stages based on your pipeline structure.
// constants.js
const relevantStages = process.env.RELEVANT_STAGES || 'BUILD,DEPLOY';
module.exports.ACTION_LEVEL_STATES = {
FAILED: 'FAILED',
CANCELED: 'CANCELED',
STARTED: 'STARTED',
SUCCEEDED: 'SUCCEEDED',
};
module.exports.STAGES = {
SOURCE: 'SOURCE',
BUILD: 'BUILD',
DEPLOY: 'DEPLOY',
};
module.exports.DISCORD_COLORS = {
INFO: 3447003,
WARNING: 15158332,
SUCCESS: 3066993,
ERROR: 10038562,
};
module.exports.RELEVANT_STAGES = relevantStages
.split(',')
.map((stage) => module.exports.STAGES[stage.toUpperCase()])
.filter((stage) => stage != null);
// The GitHub link to the complete code can be found at the end of the blog.
5.2 Configuring Lambda Function Parameters
In variables.tf
, you can configure parameters such as the Discord webhook URL, channel, AWS region, and more.
# variables.tf
variable "LAMBDA_APP_NAME" {
description = "lambda function name."
default = "cicd-channel"
}
variable "DISCORD_WEBHOOK_URL" {
description = "webhook URL provided by Discord."
default = "https://mikaeels.com" // replace with webhook URL provided by Discord
}
variable "DISCORD_CHANNEL" {
description = "discord channel where messages are going to be posted."
default = "#cicd"
}
variable "REGION" {
description = "AWS deployment region."
default = "eu-west-2"
}
variable "RELEVANT_STAGES" {
description = "stages for which you want to get notified (ie. 'SOURCE,BUILD,DEPLOY'). Defaults to all)"
default = "SOURCE,BUILD,DEPLOY"
}
variable "LAMBDA_MEMORY_SIZE" {
default = "128"
}
variable "LAMBDA_TIMEOUT" {
default = "10"
}
# The GitHub link to the complete code can be found at the end of the blog.
5.3 Extending Functionality
The discord-helper.js
file can be extended to include additional details or integrations based on your requirements.
// discord-helper.js
const CodePipelineHelper = require('./codepipeline-helper');
const Constants = require('./constants');
const AppName = process.env.DISCORD_CHANNEL ? process.env.DISCORD_CHANNEL : 'Github';
module.exports.createDiscordMessage = (codepipelineEventDetails) => {
return CodePipelineHelper.getPipelineExecutionDetails(
codepipelineEventDetails['execution-id'],
codepipelineEventDetails.pipeline
).then((pipelineDetails) => {
// get git info from github directly??? this might require authorization
const gitCommitInfo = pipelineDetails.execution.pipelineExecution.artifactRevisions[0];
// create discord fields per each stage
const executionStages = pipelineDetails.state.stageStates.filter(
(x) => x.latestExecution.pipelineExecutionId === codepipelineEventDetails['execution-id']
);
const fields = executionStages.map((stage) => {
const actionState = stage.actionStates[0];
switch (stage.stageName.toUpperCase()) {
case Constants.STAGES.SOURCE:
return {
name: `Commit`,
value: `[\`${gitCommitInfo.revisionId.substring(0, 10)}\`](${gitCommitInfo.revisionUrl}) - ${
gitCommitInfo.revisionSummary
}`,
inline: false,
};
case Constants.STAGES.DEPLOY:
case Constants.STAGES.BUILD:
return {
name: `${actionState.actionName}`,
value: actionState.latestExecution.externalExecutionUrl
? `[${actionState.latestExecution.status}](${actionState.latestExecution.externalExecutionUrl})`
: actionState.latestExecution.status,
inline: true,
};
default:
console.log(`Unknown stage: ${stage.stageName}`);
}
});
const discordMessage = {
username: `${AppName}`,
avatar_url: 'https://gravatar.com/avatar/1fd3410d57f8b729ec89a431054cbf41?s=400&d=robohash&r=x',
content: `Code Pipeline status updated: [${codepipelineEventDetails.pipeline}](${pipelineDetails.executionHistoryUrl})`,
embeds: [
{
color: getColorByState(pipelineDetails.execution.pipelineExecution.status),
fields: fields,
footer: {
text: 'With from CodePipeline π',
},
},
],
timestamp: new Date().toISOString,
};
return discordMessage;
});
};
// states for action events in codepipeline
function getColorByState(state) {
switch (state.toUpperCase()) {
case Constants.ACTION_LEVEL_STATES.FAILED:
return Constants.DISCORD_COLORS.ERROR;
case Constants.ACTION_LEVEL_STATES.SUCCEEDED:
return Constants.DISCORD_COLORS.SUCCESS;
case Constants.ACTION_LEVEL_STATES.CANCELED:
return Constants.DISCORD_COLORS.WARNING;
case Constants.ACTION_LEVEL_STATES.STARTED:
default:
return Constants.DISCORD_COLORS.INFO;
}
}
// The GitHub link to the complete code can be found at the end of the blog.
6. Deployment
Terraform is utilized to deploy the entire infrastructure to AWS.
Initialize the Terraform environment.
terraform init
Plan the deployment.
terraform plan
Apply the configuration.
terraform apply
7. Conclusion
In conclusion, this comprehensive guide empowers you to automate CodePipeline notifications to Discord using AWS Lambda and Terraform. Following the provided references and step-by-step instructions can enhance collaboration, improve visibility, and respond promptly to changes in your CI/CD workflows. The combination of serverless computing, IaC, and effective Discord integration ensures a streamlined and efficient DevOps experience.
Here's the GitHub repo link to the complete code. π
https://github.com/mikaeelkhalid/aws-codepipeline-discord-notifier-terraform
Top comments (0)