日々のAWS請求額をグラフ付きでSlackに通知する

日々のAWS請求額をグラフ付きでSlackに通知する:


はじめに

以前、CloudWatch の GetMetricWidgetImage API を利用して

CloudWatchアラームと対応するグラフを一緒にSlackへ通知する記事を投稿しました。

CloudWatchアラームとグラフを一緒にSlack通知する
https://qiita.com/hayao_k/items/026e704b5fad3037aea0

上記の応用で、Slackに日々通知しているAWSの利用金額について

前日との差分をグラフ付きでPostすれば1日あたりどれくらい増えているか把握しやすいのでは

と考えて作ってみました。


結果イメージ

CloudWatchから指定した条件のグラフを取得してSlackに通知します。

グラフには注釈として当日と前日の金額をプロットします。


image.png


画像のみ拡大。

グラフタイトルに概算合計請求額と、増分を記載しています。
image.png



ざっくり構成



image.png


CloudWatch EventsでLambdaを毎日指定時刻に起動します。

Lambda関数はCloudWatchのGetMetricStatisticsでBillingから当日と前日の概算合計請求額

を取得します。更にGetMetricWidgetImageで請求額情報をプロットしたグラフを取得します。

取得したイメージは、Slack APIのfiles.uploadメソッドで指定したチャンネルにPostします。


Lambda関数

ひよコードかもしれませんが、ご容赦ください:hatched_chick:

ランタイムはpython 3.6です。

lambda_function.py
import base64 
import boto3 
import datetime 
import json 
import logging 
import os 
import requests 
from botocore.exceptions import ClientError 
 
logger = logging.getLogger() 
logger.setLevel(logging.INFO) 
 
client = None 
 
def get_cloudwatch_client(): 
    global client 
    client = boto3.client('cloudwatch', region_name='us-east-1') 
    return client 
 
def get_params(): 
    token = os.environ['TOKEN'] 
    channel = os.environ['CHANNEL'] 
    title = 'AWS EstimatedCharges' 
    message = '本日の利用金額は...' 
    slack_params = { 
        'token': token,  
        'channels': channel, 
        'initial_comment': message, 
        'title': title 
    } 
    return slack_params 
 
def get_metrics(client=None): 
    if client is None: 
        client = get_cloudwatch_client() 
 
    try:  
        response = client.get_metric_statistics( 
            Namespace = 'AWS/Billing', 
            MetricName = 'EstimatedCharges', 
            Dimensions =[ 
                { 
                    'Name': 'Currency', 
                    'Value': 'USD' 
                } 
            ], 
            StartTime = datetime.datetime.now() - datetime.timedelta(days=1.5), 
            EndTime = datetime.datetime.now(), 
            Period = 21600, 
            Statistics = ['Maximum'] 
        ) 
    except ClientError as e: 
        logger.error("Request failed: %s", e.response['Error']['Message']) 
    else: 
        return response 
 
def get_image(metrics, client=None): 
    if client is None: 
        client = get_cloudwatch_client() 
 
    sorted_data = sorted(metrics['Datapoints'], key=lambda x: x['Timestamp']) 
    logger.info("Sorted Data: %s", sorted_data) 
    today = sorted_data[-1]['Maximum'] 
    yesterday = sorted_data[0]['Maximum'] 
    diff = round(today - yesterday, 2)  
    if diff > 0: 
        title = ('Estimated Chages: $' + str(today) + 
                 ' / Increased $' + str(diff) + ' from Yesterday') 
    else: 
        title = ('Estimated Chages: $' + str(today)) 
 
    widget_definition = json.dumps( 
        { 
            "width": 600, 
            "height": 400, 
            "start": "-PT72H", 
            "end": "PT0H", 
            "timezone": "+0900", 
            "view": "timeSeries", 
            "stacked": True, 
            "stat": "Maximum", 
            "title": title, 
            "metrics": [  
                ["AWS/Billing", "EstimatedCharges", "Currency", "USD" ] 
            ], 
            "period": 21600, 
            "annotations": { 
                "horizontal": [ 
                    { 
                        "label": "Today", 
                        "value": today 
                    }, 
                    { 
                        "label": "Yesterday", 
                        "value": yesterday 
                    } 
                ] 
            }, 
            "yAxis": { 
                "left": { 
                    "label": "$", 
                    "min": (yesterday * 0.75), 
                    "max": (today * 1.05) 
                } 
            }, 
        } 
    ) 
 
    try: 
        response = client.get_metric_widget_image( 
            MetricWidget = widget_definition 
        ) 
    except ClientError as e: 
        logger.error("Request failed: %s", e.response['Error']['Message']) 
    else: 
        image = {'file': response['MetricWidgetImage']} 
        logger.info("Get Image succeeded.") 
        return image  
 
def lambda_handler(event, context): 
    params = get_params() 
    metrics = get_metrics() 
    files = get_image(metrics) 
    upload_url = 'https://slack.com/api/files.upload' 
 
    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) 
    return req 
グラフの注釈に金額をプロットするために、get_metric_statisticsメソッドで

概算合計請求額を取得しています。

get_metric_statistics
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudwatch.html#CloudWatch.Client.get_metric_statistics

datetimeで実行時刻から、StartTimeとEndTimeを指定します。

days=1だと、6時間毎のDatapointが3つしか取得できなかった(最新のデータポイントから

18時間前の金額しか取得できなかった)ため、1.5としています。

StartTime=datetime.datetime.now() - datetime.timedelta(days=1.5), 
EndTime=datetime.datetime.now(), 


widget_definition でGetMetricWidgetImageの入力パラメータを定義しています。

"annotations" が注釈です。get_metric_statisticsで取得した金額を指定します。

軸の指定(yAxis)で、グラフの表示幅を調整しています。

"yAxis": { 
                "left": { 
                    "label": "$", 
                    "min": (yesterday * 0.75), 
                    "max": (today * 1.05) 
                } 
            }, 
その他全体の構造については以下のドキュメントを参照してください。

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

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

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

2018/11/12 時点で、東京リージョンのLambda実行環境に含まれるboto3では

get_metric_widget_image を使用できませんでした。いずれアップグレードされると思いますが、

現時点ではデプロイパッケージ(zip)に最新版のboto3を含めるようにしてください。

また通信ライブラリにrequestsを使用しているのでこちらもデプロイパッケージに含めます。

デプロイパッケージの作成 (Python)
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html


設定補足


Slack

Slack Botを使用してるため、Lambda関数の設定には通知先のチャンネルIDと

BotユーザのTokenが必要です。

プライベートチャンネルに通知する場合は、Botユーザをinviteしてください。

チャンネルIDはURLに含まれる以下の部分です。

https://<your_team_name>.slack.com/messages/<channel_id> 
Token取得については以下の記事が参考になります。
https://qiita.com/ykhirao/items/3b19ee6a1458cfb4ba21


Lambda

環境変数 CHANNEL にチャンネルIDを、TOKENにBotユーザのTokenを設定します。


image.png


実行ロールはメトリクスデータを取得できるようポリシーを設定してください。

(CloudWatchReadOnlyAccessなど)

タイムアウトはデフォルトの3秒ではタイムアウトする可能性があるので10秒に設定します。
image.png



CloudWatch Events

ルールの作成でイベントソースをスケジュールとし、Cron式を入力します。

UTC で設定しますので、日本標準時との時差9時間を考慮する必要があります。

以下の例は平日の17時に起動する場合の設定です。

0 0 ? * MON-FRI * 
ターゲットに作成したLambda関数を指定して、ルールを保存します。


image.png


以上です。

参考になれば幸いです。

コメント

このブログの人気の投稿

Discordのボイスチャンネルの入退室を通知させる

Googleスプレッドシートで郵便番号から住所を表示する関数 その2

Javascriptで全角スペースと半角スペースを正規表現で指定する