DEV Community

Mikaeel Khalid for AWS Community Builders

Posted on • Originally published at blog.mikaeels.com on

Automating AWS CodePipeline Notifications to Discord Using Lambda and Terraform

Image description

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.

Image description

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.

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

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.

Enter fullscreen mode Exit fullscreen mode

6. Deployment

Terraform is utilized to deploy the entire infrastructure to AWS.

Initialize the Terraform environment.

terraform init

Enter fullscreen mode Exit fullscreen mode

Plan the deployment.

terraform plan

Enter fullscreen mode Exit fullscreen mode

Apply the configuration.

terraform apply

Enter fullscreen mode Exit fullscreen mode

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)