コンテンツにスキップするには Enter キーを押してください

CloudWatchAlarmをグラフ付きでSlackに通知する

最近趣味でトランペットを始めた政谷です。

少し前に、CloudWatchのグラフをAPIで取得できるようになりましたね
これまでは、アラームが飛んで来ても、具体的なデータの推移状況はマネジメントコンソールにアクセスして、ダッシュボードを確認する必要がありました。

個人的に、この機能追加によって、AWSの運用監視がグッと楽になるのではないかと思い、
CloudWatchAlarmの内容をグラフ付きでSlack通知するためのサーバーレスアプリケーションを作りました。

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

動作フロー

  • CloudWatchAlarmが発火
  • SNSにTopicが追加される
  • Lambdaが発火
  • SNS Topicに記載されたTrigger情報を取得して、getMetricsGraphFromCloudWatchAPIで1時間前までのCloudWatchMetricsの画像を取得
  • 取得したデータをS3にPut
  • SNSの情報と、Putした画像のURLを使って、Slackのwebhookのペイロードを作成
  • Slack通知

CloudFormation

このアプリケーションのデプロイには、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}/*

Parameterを自分の環境向けに編集して使用します。
Makefileを用意しているので、環境変数で上書きすることもできます。

SAMのデプロイには、先んじてS3バケットが必要になりますので、事前に作成しておいてください。
デプロイしたら、SNSとLambdaと公開用のS3バケットが作成されるので、
CloudWatchAlarmの通知先をSNSにしてLambdaが発火するようにします。

Lambdaコード

このサーバレスアプリケーションで動作するLambdaのコードは下記になります。
ランタイムはNode.js8.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
}

結果

このサーバーレスアプリケーションを使うと、Slackにこんな感じで通知されます

これならアラートと一緒に時系列データを確認できるので、一々ダッシュボードを確認してデータの遷移状況を確認する必要がないのでとても楽ですね。

まとめ

今回はCloudWatchAlarmの内容を、画像付きでSlack通知してみました。
実際の通知内容を見て、これは非常に便利だと感じているので、業務でもバンバン使ってアプリケーションを洗練させていきたいと思います。

現場からは以上です。

インターネットの海で泳ぐときは、だいたいペンギンの姿をしています。

コメントする

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です