Scheduled LabmdaでDaily費用レポート

初日から間に合わない事になってしまって反省してます。一人アドベントカレンダー的更新第1日目行ってみたいと思います。どこかの誰かのネタとかぶっている可能性もありますが、最近やったことを書いていきます。

今日のネタはAWS LambdaのScheduled Eventをつかって日次の費用レポートをSlackにPostするというものです。

まずはLambda Functionを実装します。CloudWatchからBilling NamespaceのEstimatedChargesメトリクスを前日の6:00〜本日の6:00までの費用計とさらに1日前の費用計とで取得して差分を取るという簡易的な日次費用取得となっています。end_time/start_time周りの時刻がアレな感じなのは、一番それっぽい値が取れたというあまり根拠の無いend_timeなどを6時にしているのと、一回で2日分まとめて取得していないのは2日前の0時からから本日の0時までだとうまくいかなかったからというネガティブな理由です。。。あくまでも費用感を知るためのものとしておいてください。

この例ではConsolidatedBillingの親アカウントから複数アカウント分の費用を取得するようにしています。

2015.12.02 9:45 追記: 月初日の費用取得が 0USD になってしまうバグがありました。後で修正します。
2015.12.02 11:22 追記: 月初日の費用取得が 0USD になってしまうバグを修正しました。値の取り方も変えてよりトリッキーな感じに。。。なんだか汚い感じがしますが許してください><

# -*- coding: utf-8 -*-
import sys, traceback
import boto3
import json
import datetime, pytz

import slackweb

def billing_info(cloudwatch, account_no, start_time, end_time):
    billing_info = cloudwatch.get_metric_statistics(
                Namespace = 'AWS/Billing',
                MetricName = 'EstimatedCharges',
                Dimensions = [{'Name': 'LinkedAccount', 'Value': account_no},{'Name': 'Currency', 'Value': 'USD'},],
                StartTime = start_time,
                EndTime = end_time,
                Period = 3600,
                Statistics = ["Maximum"],
                )
    return billing_info

def latest_bill(billing_info):
    bill = 0
    billing_dict = {}
    for info in billing_info['Datapoints']:
        timestamp = int(time.mktime(info['Timestamp'].timetuple()))
        billing_dict[timestamp] = info['Maximum']

    x = 0
    former_bill = 0
    billing_dict = sorted(billing_dict.items(), key=lambda x: x[0])
    for t, b in billing_dict:
        if x != 0:
            bill += b if former_bill > b else (b - former_bill)
        former_bill = b
        x += 1

    return bill

def lambda_handler(event, context):
    hook_url = "https://hooks.slack.com/services/xxxxxxxxx/yyyyyyyyy/zzzzzzzzzzzzzzzzzzzzzzzz"
    username = "billing_bot"
    channel = "#report"
    icon_emoji = ":money_with_wings:"

    try:
        tokyo_tz = pytz.timezone('Asia/Tokyo')
        d = datetime.datetime.now()
        todays_end_time = datetime.datetime(d.year, d.month, d.day, 6, 0, 0, 0, tokyo_tz)
        if d.day > 2:
          todays_start_time = todays_end_time - datetime.timedelta(days=1)
        else:
          todays_start_time = todays_end_time - datetime.timedelta(days=1, hours=3)
        yesterday = yesterdays_end_time.strftime("%Y-%m-%d")

        cloudwatch = boto3.client('cloudwatch', region_name='us-east-1')

        post_message = ""
        for account_name, account_no in {'Account1':'000000000000', 'Account2':'111111111111'}.items():
            todays_billing_info = billing_info(cloudwatch, account_no, todays_start_time, todays_end_time)

            todays_bill = latest_bill(todays_billing_info)

            if post_message != "":
                post_message += "\n"
            post_message += "%s: %f USD" % (account_name, todays_bill)

        post_message = "昨日(%s)1日分のAWS費用は以下のとおりでした。\n```\n%s\n```\n今日もコスト意識を持って行きましょう。" % (yesterday, post_message)

        slack = slackweb.Slack(url=hook_url)
        slack.notify(text=post_message, 
                     channel=channel,
                     username=username,
                     icon_emoji=icon_emoji)
    except:
        print traceback.format_exc()
    else:
        print('finished.')

これをこんな感じでZIPファイルにする。

$mkdir billing_bot
$ vi lambda_function.py
(上のPythonコード)
$ vi requirements.txt
slackweb
pytz
$ pip -r requirements.txt -t ./
$ zip -r ~/myLambdaFunction.zip *

Management ConsoleからLambdaのメニューに進んで、次のように設定していく。(例は朝9:10 JSTにスケジュールしています)

  1. Create a Lambda Dunction
  2. lambda-canary
  3. Configure Event Source
    • Event source type: Scheduled Event
    • Name: billing_bot
    • Description: daily billing report
    • Schedule Expression: cron(10 0 * * ? *)
  4. Configure function
    • Name: billing_bot
    • Runtime: Python2.7
  5. Lambda function code
    • Upload a .ZIP file: 先ほど作成したZIPファイルを選択
  6. Lambda function handler and role
    • lambda_function.lambda_handler (lambda functionのメインファイル名.handler関数名)
    • Role: lambda_billing_bot (※roleのPolicyは後述)
  7. Advanced settings
    • Memory: 128MB
    • Timeout: 1min
  8. Enable nowをチェック
  9. Create function

Lambda functionに設定するRoleのPolicyはこんな感じ。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "cloudwatch:GetMetricStatistics"
            ],
            "Resource": "*"
        }
    ]
}

Lambda Functionが作成できたら、「Test」ボタンを押してテスト実行をしてみます。するとSlackにこんな感じでPostされます。

f:id:matetsu:20151202004539p:plain

はい、うまくいきましたね。

かなりはしょった感は否めませんが、なんとなく雰囲気は伝わったと思います。突っ込みどころは多くあると思いますので、優しく教えていただければと思います。

では、1日坊主にならないことを祈って、初日はこの辺で。