Help us understand the problem. What is going on with this article?

CloudWatchアラームとグラフを一緒にSlack通知する

2019/7/25 追記

AWS Chatbotというサービスで簡単にCloudWatchアラームをSlackに通知できるようになりました。

個別に通知内容をカスタマイズしたい場合は変わらずLambda関数を作る必要がありますので
そのような場合に本記事の内容が参考になれば幸いです。

はじめに

Amazon CloudWatch に、AWS コンソール以外でカスタムダッシュボードを作成できる機能が追加されました。
https://aws.amazon.com/jp/about-aws/whats-new/2018/09/amazon-cloudwatch-adds-ability-to-build-custom-dashboards-outside-the-aws-console/

実装例として、GetMetricWidgetImage APIとSESを使用してCloudWatchアラームと一緒に
グラフ画像をメール通知する方法が公式Blogで紹介されています。

Reduce Time to Resolution with Amazon CloudWatch Snapshot Graphs and Alerts | Amazon Web Services
https://aws.amazon.com/jp/blogs/devops/reduce-time-to-resolution-with-amazon-cloudwatch-snapshot-graphs-and-alerts/

Slackで画像も通知できたらな。。。と思い、上記を参考にして作ってみました。

ざっくり構成

image.png

Lambdaは python3.6 で実装していますが、基本的な構成はメール通知版とほとんど変わりません。
CloudWatchでアラームを検知したら、SNSに通知します。
Lambda関数がSNSトリガーで起動します。GetMetricWidgetImage APIから対象アラームの
グラフを取得し、Slack APIのfiles.uploadメソッドでアラーム情報とグラフをPostします。

結果イメージ

アラームが発生すると以下のように対象メトリクスのグラフがアップロードされます。
image.png

注釈を挿入できるので、しきい値を超えた状態が簡単に確認できると思います。
Slack Botを使用してるため、後述のLambda関数の設定にはBotユーザのTokenと
通知先のチャンネルIDが必要です。
プライベートチャンネルの場合は、Botユーザをinviteしてください。

OK状態のアラームを定義していれば、しきい値を下回った場合も通知されます。
以下が GetMetricWidgetImage API で取得した画像です。
image.png

Lambda関数

ひよコードかもしれませんが、ご容赦ください:hatched_chick:
環境変数 CHANNEL にチャンネルIDを、TOKENにBotユーザのTokenを設定する必要があります。

lambda_function.py
import base64
import boto3
import json
import logging
import os
import requests

from botocore.exceptions import ClientError

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def get_params(title, message):
    token = os.environ['TOKEN']
    channel = os.environ['CHANNEL']
    reason = message['NewStateReason']
    slack_params = {
        'token': token, 
        'channels': channel,
        'initial_comment': reason,
        'title': title
    }
    return slack_params

def get_properties(alarm_state):
    if alarm_state == "ALARM":
        properties = { 'color': '#ff6961', 'label': 'Trouble threshold start' }
    elif alarm_state == "OK":
        properties = { 'color': '#009933', 'label': 'Trouble threshold end' }
    return properties

def get_image(title, message):
    threshold = message['Trigger']['Threshold']
    metrics = [ message['Trigger']['Namespace'], message['Trigger']['MetricName'],
        message['Trigger']['Dimensions'][0]['name'], message['Trigger']['Dimensions'][0]['value'] ]
    alarm_state = message['NewStateValue']

    annotation_properties = get_properties(alarm_state)

    widget_definition = json.dumps(
        {
            "width": 600,
            "height": 400,
            "start": '-PT2H',
            "end": "PT0H",
            "timezone": '+0900',
            "view": "timeSeries",
            "stacked": False,
            "stat": "Average",
            "title": title,
            "metrics": [ metrics ],
            "period": 300,
            "annotations": {
                "horizontal": [
                {
                    "color": annotation_properties['color'],
                    "label": annotation_properties['label'],
                    "value": threshold
                }]
            }
        }
    )

    cloudwatch = boto3.client('cloudwatch')

    try:
        response = cloudwatch.get_metric_widget_image(
            MetricWidget = widget_definition
        )
    except ClientError as e:
        print(e.response['Error']['Message'])
    else:
        image = {'file': response['MetricWidgetImage']}
        print("Get Image succeeded:")
        return image

def upload_slack(upload_url, params, files):
    req = requests.post(upload_url, params=params, files=files)
    try:
        req.raise_for_status()
        logger.info("Message posted.")
        return req.text
    except requests.RequestException as e:
        logger.error("Request failed: %s", e)

def lambda_handler(event, context):
    title = event['Records'][0]['Sns']['Subject']
    message = json.loads(event['Records'][0]['Sns']['Message'])
    upload_url = 'https://slack.com/api/files.upload'
    params = get_params(title, message)
    files = get_image(title, message)
    response = upload_slack(upload_url, params, files)
    return response

widget_definition でAPIの入力パラメータを定義しています。
全体の構造については以下のドキュメントを参照してください。

GetMetricWidgetImage: Metric Widget Structure and Syntax
https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/CloudWatch-Metric-Widget-Structure.html

上記を元に boto3 の get_metric_widget_image メソッドでグラフを取得します。

Boto 3 Documentation CloudWatch
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudwatch.html#CloudWatch.Client.get_metric_widget_image

2018/9/26 時点で、東京リージョンのLambda実行環境に含まれるboto3では
get_metric_widget_image を使用できませんでした。いずれアップグレードされるとは思いますが、
現時点ではデプロイパッケージ(zip)に最新版のboto3を含めるようにしてください。
また通信ライブラリにrequestsを使用しているのでこちらもデプロイパッケージに含めます。

設定補足

SNS

空のTopicを作成しておきます。

Lambda

トリガーにSNSを追加します。

image.png

トリガーの設定で対象のSNS Topicを指定します。

image.png

繰り返しになりますが、環境変数にCHANNEL(通知対象チャンネルID)と
TOKEN(BotユーザのToken)を指定します。

image.png

実行ロールはメトリクスデータを取得できるようポリシーを設定してください。
タイムアウトはデフォルトの3秒ではタイムアウトする可能性があるので5秒にしておきました。

image.png

CloudWatch

対象のCloudWatchアラームの通知アクションに作成したSNS Topic を指定します。
image.png

参考URL

https://github.com/aws-samples/aws-cloudwatch-snapshot-graphs-alert-context
https://dev.classmethod.jp/cloud/aws/lambda-python-tips-all-events-are-not-dict/

以上です。
参考になれば幸いです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away