Notify CloudWatchAlarm of Slack with Metrics Graph.

Wed, 24 Oct 2018 10:00:00


Introduction

Hello everyone. You can now get CloudWatch's graph with API.

Until now, even if the alarm flies, it was necessary to access the management console and check the dashboard for the specific data transition situation.

I created a serverless application for notifying Slack of contents of CloudWatchAlarm with graph. Personally, I think that adding this function would make monitoring of AWS a lot easier.

https://github.com/k-masatany/cloudwatch_snapshot_graph_to_slack


Operation flow

  • Pass over the threshold of CloudWatchAlarm
  • SNS messages that has alarm details are published to Topic
  • Invoke Lambda
  • Get alarm details in SNS Topic and get CloudWatchMetrics images since 1 hour ago to now with getMetricsGraphFromCloudWatch API
  • Put the acquired data to S3
  • Create slack webhook payload using SNS information and the URL of the put image
  • Notification to Slack

image


CloudFormation

To deploy this application, use AWS SAM.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  cloudwatch_snapshot_graph_to_slack
  Cloudwatch alarm notification to Slack with snapshot graph.
Parameters:
  SNSTopic:
    Type: String
    Default: 'alarm-notice-topic'
  AssetsBucketName:
    Type: String
    Default: 'image-upload-bucket'
  WebHookURL:
    Type: String
    Default: 'https://example.com'
  Channel:
    Type: String
    Default: 'slack-channel'
  Username:
    Type: String
    Default: 'SlackBot'
  IconEmoji:
    Type: String
    Default: ':slack:'
 
Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      Description: 'post sns messages to slack incoming webhooks'
      CodeUri: cloudwatch_snapshot_graph_to_slack/
      Handler: app.lambdaHandler
      Runtime: nodejs8.10
      Timeout: 30
      MemorySize: 512
      Events:
        SNS:
          Type: SNS
          Properties:
            Topic: !Ref Topic
      Environment:
        Variables:
          SLACK_WEBHOOK_URL: !Ref WebHookURL
          SLACK_CHANNEL: !Ref Channel
          SLACK_USERNAME: !Ref Username
          SLACK_ICONEMOJI: !Ref IconEmoji
          BACKET_NAME: !Ref AssetsBucketName
          TZ: Asia/Tokyo
      Role: !GetAtt IamRole.Arn
 
  AssetsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref AssetsBucketName
      AccessControl: PublicRead
      LifecycleConfiguration:
        Rules:
          - Status: Enabled
            ExpirationInDays: 7
      WebsiteConfiguration:
        IndexDocument: index.html
        ErrorDocument: error.html
  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AssetsBucket
      PolicyDocument:
        Statement:
          - Action:
              - 's3:GetObject'
            Effect: 'Allow'
            Resource: !Sub arn:aws:s3:::${AssetsBucket}/*
            Principal: '*'
 
  Topic:
    Type: 'AWS::SNS::Topic'
    Properties:
      TopicName: !Ref SNSTopic
 
  IamRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: 'sts:AssumeRole'
      Policies:
        - PolicyName: 'cloudwatch_snapshot_graph_to_slack'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'autoscaling:Describe*'
                  - 'cloudwatch:Describe*'
                  - 'cloudwatch:Get*'
                  - 'cloudwatch:List*'
                  - 'logs:Get*'
                  - 'logs:List*'
                  - 'logs:Describe*'
                  - 'logs:TestMetricFilter'
                  - 'logs:FilterLogEvents'
                  - 'sns:List*'
                  - 'sns:Get*'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: 'arn:aws:logs:*:*:*'
              - Effect: 'Allow'
                Action:
                  - s3:PutObject
                Resource: !Sub arn:aws:s3:::${AssetsBucket}/*

Edit Parameter for your own environment and use it. Since Makefile is prepared, it can also be overwritten with environment variable.

In order to deploy SAM, you need S3 bucket ahead of time, so please make it in advance. Once deployed, SNS, Lambda, and S3 buckets for publishing are created, Set CloudWatchAlarm notification destination to created SNS.


Lambda code

The code of Lambda running on this serverless application is as follows. The runtime is Node.js 8.10.

"use strict"
 
const axios = require("axios")
var AWS = require("aws-sdk")
const BASE_URL = process.env.SLACK_WEBHOOK_URL
 
async function getMetricsGraphFromCloudWatch(MessageId, message) {
    const props = {
        width: 320,
        height: 240,
        start: "-PT1H",
        end: "PT0H",
        timezone: "+0900",
        view: "timeSeries",
        stacked: false,
        metrics: [
            [
                message.Trigger.Namespace,
                message.Trigger.MetricName,
                message.Trigger.Dimensions[0].name,
                message.Trigger.Dimensions[0].value
            ]
        ],
        stat:
            message.Trigger.Statistic.charAt(0).toUpperCase() +
            message.Trigger.Statistic.slice(1).toLowerCase(),
        period: message.Trigger.Period
    }
    const widgetDefinition = {
        MetricWidget: JSON.stringify(props)
    }
 
    const cloudwatch = new AWS.CloudWatch()
    const s3 = new AWS.S3()
 
    try {
        const cw_res = await cloudwatch
            .getMetricWidgetImage(widgetDefinition)
            .promise()
        const res = await s3
            .putObject({
                Bucket: process.env.BACKET_NAME,
                Key: `${MessageId}.png`,
                Body: cw_res.MetricWidgetImage,
                ContentType: "image/png"
            })
            .promise()
        return res
    } catch (err) {
        console.error(err)
    }
}
 
async function createPayload(records) {
    let attachments = []
    for (const record of records) {
        const sns = record.Sns
        const message = JSON.parse(sns.Message)
        let color = "good"
        if (message.NewStateValue == "ALARM") {
            color = "danger"
        } else if (message.NewStateValue != "OK") {
            color = "warning"
        }
        await getMetricsGraphFromCloudWatch(sns.MessageId, message)
        let attachment = {
            fallback: sns.Subject,
            title: message.AlarmName,
            pretext: sns.Subject,
            color: color,
            text: message.NewStateReason,
            image_url: `https://s3-ap-northeast-1.amazonaws.com/${
                process.env.BACKET_NAME
            }/${sns.MessageId}.png`
        }
        attachments.push(attachment)
    }
 
    return {
        channel: process.env.SLACK_CHANNEL,
        username: process.env.SLACK_USERNAME,
        icon_emoji: process.env.SLACK_ICONEMOJI,
        attachments: attachments
    }
}
 
function createOptions(payload) {
    let options = {
        method: "post",
        baseURL: BASE_URL,
        headers: {
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
        },
        data: `payload=${JSON.stringify(payload)}`
    }
    return options
}
 
exports.lambdaHandler = async (event, context) => {
    const payload = await createPayload(event.Records)
    const options = createOptions(payload)
 
    try {
        const res = await axios.request(options)
        response = {
            statusCode: 200,
            body: JSON.stringify({
                message: "hello world",
                location: res.data.trim()
            })
        }
    } catch (err) {
        console.log(err)
        return err
    }
 
    return response
}

Result

With this serverless application, Slack will be notified like this.

image

Since it is possible to check the time series data together with the alert, it is very easy because you do not have to check the dashboard every time and check the data transition.


Finally

This time I tried to notify the content of CloudWatchAlarm with Slack with image. Since I feel that this is very convenient by looking at the actual notice content, I would like to refine the application during the course of using.