Send Amazon CloudWatch Alarms to Slack

Send Amazon CloudWatch Alarms to Slack

The following article provides a practical example of Running AWS Lambda Functions Locally Using LocalStack. The purpose of this AWS Lambda function is to forward AWS CloudWatch alarms to Slack by utilizing an SNS topic as an intermediary. Please ensure you have completed all the prerequisites mentioned in the referenced article before continuing. In addition, we will use Nuke to automate various helpful tasks. You can learn more about Nuke here. So, let's begin.

Lambda Function

Run the following commands to set up our project:

dotnet new lambda.EmptyFunction -n SNS2Slack -o .
dotnet add src/SNS2Slack package Slack.Webhooks
dotnet add src/SNS2Slack package Amazon.Lambda.SNSEvents
dotnet new sln -n SNS2Slack
dotnet sln add --in-root src/SNS2Slack

Open the solution, navigate to the SNS2Slack project, and create an Alarm.cs file containing the following content:

namespace SNS2Slack;

public class Alarm
{
    public string? AlarmName { get; set; }
    public string? AlarmDescription { get; set; }
    public string? AWSAccountId { get; set; }
    public string? NewStateValue { get; set; }
    public string? NewStateReason { get; set; }
    public string? OldStateValue { get; set; }
    public Trigger? Trigger { get; set; }
}

public class Trigger
{
    public string? MetricName { get; set; }
    public string? Namespace { get; set; }
    public string? Statistic { get; set; }
    public string? GreaterThanOrEqualToThreshold { get; set; }
    public decimal Period { get; set; }
    public decimal Threshold { get; set; }
    public decimal EvaluationPeriods { get; set; }
}

Open the Function.cs file and update the content as follows:

using Amazon.Lambda.Core;
using Amazon.Lambda.SNSEvents;
using Slack.Webhooks.Elements;
using Slack.Webhooks;
using System.Text.Json;
using Slack.Webhooks.Blocks;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace SNS2Slack;
public class Function
{
    private readonly SlackClient _slackClient;
    public Function()
    {
        var webHook = Environment.GetEnvironmentVariable("SlackWebHook");
        _slackClient = new SlackClient(webHook);
    }

    public async Task FunctionHandler(SNSEvent evnt, ILambdaContext context)
    {
        foreach (var record in evnt.Records)
        {
            await ProcessRecord(record, context);
        }
    }

    private async Task ProcessRecord(SNSEvent.SNSRecord record, ILambdaContext context)
    {
        context.Logger.LogInformation($"Processed record {record.Sns.Message}");
        if (record.Sns.Message == null)
        {
            return;
        }
        var alarm = JsonSerializer.Deserialize<Alarm>(record.Sns.Message);
        if (string.IsNullOrEmpty(alarm?.AlarmName))
        {
            return;
        }
        var slackMessage = new SlackMessage
        {
            Markdown = true
        };
        slackMessage.Blocks = new List<Block>
        {
            new Section
            {
                Text = new TextObject($"{alarm.AlarmDescription}\n")
                {
                    Type = TextObject.TextType.Markdown
                }
            }
        };
        await _slackClient.PostAsync(slackMessage);
    }
}

Essentially, we receive the alarm within an SNS record and subsequently send its description to Slack. Create a template.yml file as follows:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  SNS

Globals:
  Function:
    Timeout: 15
    MemorySize: 512
    Runtime: dotnet6
    Architectures:
      - x86_64

Parameters:
  SlackWebHook:
    Type: String

Resources:
  SNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: "SlackTopic"

  SlackForwarderFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: SNS2Slack::SNS2Slack.Function::FunctionHandler
      CodeUri: ./src/SNS2Slack/
      Environment:
        Variables:
          SlackWebHook: !Ref SlackWebHook
      Events:
        SNSEvent:
          Type: SNS
          Properties:
            Topic: !Ref SNSTopic

Outputs:
  SNSArn:
    Description: SNS ARN
    Value: !Ref SNSTopic

The SAM script creates an SNS topic to which CloudWatch Alarms will be sent and a Lambda function triggered by the mentioned SNS topic.

Local Stack

Add a docker-compose.yml file at the solution level with the following content:

version: "3.8"

services:
  localstack:
    container_name: "my-localstack"
    image: localstack/localstack
    ports:
      - "127.0.0.1:4566:4566"
      - "127.0.0.1:4510-4559:4510-4559"
    environment:
      - DEBUG=1
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - ".volume/tmp/localstack:/tmp/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

Slack

Go to https://api.slack.com/apps and create a new application (from scratch):

Activate the Incoming WebHooks feature:

Finally, add a new Webhook to Workspace:

Once completed, copy the Webhook URL for future use.

Nuke

Run the nuke :setup command and follow the instructions (we recommend using all the default values) to set up the project. Navigate to the _build the project and update the content as follows:

using Nuke.Common;
using Nuke.Common.Tooling;

class Build : NukeBuild
{
    [PathExecutable(name: "docker-compose")]
    public readonly Tool DockerCompose;
    [PathExecutable(name: "samlocal")]
    public readonly Tool SamLocal;
    public static int Main () => Execute<Build>(x => x.SamLocalBuild);
    [Parameter()]
    public string SlackWebHook;
    public bool IsRunning()
    {
        var output = DockerCompose("ps --status running --quiet");
        return output.Count > 0;
    }

    Target StartEnv => _ => _
    .OnlyWhenDynamic(() => !IsRunning())
    .Executes(() =>
    {
        DockerCompose("up -d");
    });

    Target StopEnv => _ => _
        .Executes(() =>
        {
            DockerCompose("down");
        });

    Target SamLocalBuild => _ => _
        .Executes(() =>
        {
            SamLocal("build");
        });

    Target SamLocalDeploy => _ => _
        .Requires(() => SlackWebHook)
        .DependsOn(SamLocalBuild)
        .DependsOn(StartEnv)
        .Executes(() =>
        {
            SamLocal($"deploy --no-confirm-changeset --disable-rollback --resolve-s3 --s3-prefix sns2slack --stack-name sns2slack --region us-east-1  --capabilities CAPABILITY_IAM --parameter-overrides SlackWebHook={SlackWebHook}");
        });
}

In the class above, we are automating the following tasks:

  • StartEnv: Start the LocalStack environment by running the docker-compose file.

  • SamLocalBuild: Build the Lambda function using samlocal CLI.

  • SamLocalDeploy: Deploy the Lambda function to the LocalStack environment, requesting the Slack Webhook URL.

  • StopEnv: Stop and remove the LocalStack environment.

Run the following command to deploy the Lambda function locally:

nuke SamLocalDeploy --slack-web-hook <MY_WEBHOOK_URL>

We can use aws lambda list-functions --endpoint-url=http://localhost:4566 to verify the Lambda function deployment.

Cloud Watch Alarm

Register a CloudWatch alarm by using the created SNS topic as an action:

aws cloudwatch put-metric-alarm --endpoint-url=http://localhost:4566 --alarm-name my-alarm --alarm-description 'This is my slack message' --comparison-operator GreaterThanThreshold --evaluation-periods 1 --alarm-actions arn:aws:sns:us-east-1:000000000000:SlackTopic

Next, change the state of the alarm with:

aws cloudwatch set-alarm-state --endpoint-url=http://localhost:4566 --alarm-name my-alarm --state-reason "Testing" --state-value ALARM

In conclusion, this article demonstrates how to send Amazon CloudWatch alarms to Slack using AWS Lambda functions. By following the provided steps, we'll be able to set up and deploy a Lambda function locally **(**thanks to LocalStack), integrate it with Slack, and automate useful tasks with Nuke. All the code is available here. Thanks, and happy coding.