47
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-09-27

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/

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

47
38
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?