AWS
CloudWatch
lambda

AWSの料金をLambdaのcronで定期チェックしてSlackに通知する

More than 1 year has passed since last update.

はじめに

皆様、新年あけましておめでとうございます
今年もまた思いつきで記事をあげていくのでよろしくお願いいたしますm(_ _)m

去年まではもくもくiOS勉強会というのを約2年やっていたのですが、今年からもくもくAWS勉強会をはじめることになったのでそれにあたり何か書くネタを考えていました。

個人でAWSを使うと気になるのが、青天井な従量課金。(もちろんある程度コントローラブルですがw)
うっかり作って放置したインスタンスなどでやばい金額が発生しては困るので毎日の課金状況を知れるための仕組みづくりをしようと思います。

もし勉強会にご興味あればこちらで情報が見れます ^ω^

公式Facebookグループ
https://www.facebook.com/groups/1710936869140187/

公式Slackグループ
https://moku2aws.herokuapp.com/

公式Connpassページ
http://moku2aws.connpass.com/

手順

AWSアカウントの作成

まずはAWSのアカウントを作りましょう。
トップページ右上の「サインアップ」のボタンから登録することができます。
https://aws.amazon.com/jp/

AWS料金のモニタリング設定

AWSのアカウントを作ったらまずは料金のモニタリングのための設定をします。
こちらのページでわかりやすく説明されているため、参考にどうぞ。

Amazon Web Services(AWS) で無料枠を超えた時にアラートを飛ばす(日本語メニュー)
http://qiita.com/murachi1208/items/118f016983e26d3338b2

Lambda Function 用の IAM ロールの作成

今回使う Lambda では Function に対してロールというものを割り当てる必要があります。
ロールというのはユーザとは違い EC2 インスタンスに対して付与できる権限ラベルのようなもので、インスタンスがロールを持つことで特定の AWS サービスに対してのアクセス権を持つことができるようになります。
適当に名前をつけて(今回は billing_lambda_role としました)、ロールタイプに「AWS Lambda」を選択、ポリシーに「CloudWatchReadOnlyAccess」を選択して作成します。

ロールタイプの選択
roletype.png

ポリシーの選択
policy.png

ロール作成の確認画面
role.png

Lambda Function の作成

では、ここからが本記事の本丸。
Lambda Function の作成をします。
AWS の料金情報(billing)についてはバージニア北部(us-east-1)のリージョンでしか取得できないのですが、Lambda Function は S3 と違い全リージョンをまたいだ Function のリスト表示ができないため、東京リージョン(ap-northeast-1)であえて作成します。

東京リージョンで作成した Function であっても Lambda コード内でリージョン指定を指定してあげれば特に問題なくデータを取得できます。

さて、最終的に必要なコードは以下のとおりです。
hookUrl と slackChannel を指定して Lambda の Test ボタンを押すと通知がきます。
ちなみに slackChannel と言ってますが、 @username とすることでユーザに対して Slackbot からの DM を送ることができるようになります。
一般的なチャンネルに送りたい場合は #channelName と書く必要があります。(プライベートチャンネルも指定可)

var aws = require('aws-sdk');
var url = require('url');
var https = require('https');
var hookUrl, slackChannel;
var cloudwatch = new aws.CloudWatch({region: 'us-east-1', endpoint: 'http://monitoring.us-east-1.amazonaws.com'});

hookUrl = '<Slack incomming webhook url>';
slackChannel = '<Channel name>';

var postMessage = function(message, callback) {
    var body = JSON.stringify(message);
    var options = url.parse(hookUrl);
    options.method = 'POST';
    options.headers = {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
    };

    var postReq = https.request(options, function(res) {
        var chunks = [];
        res.setEncoding('utf8');
        res.on('data', function(chunk) {
            return chunks.push(chunk);
        });
        res.on('end', function() {
            var body = chunks.join('');
            if (callback) {
                callback({
                    body: body,
                    statusCode: res.statusCode,
                    statusMessage: res.statusMessage
                });
            }
        });
        return res;
    });

    postReq.write(body);
    postReq.end();
};

exports.handler = function(event, context) {
    console.log('Start checking cloudwatch billing info.');

    var startDate = new Date();
    startDate.setDate(startDate.getDate() - 1); // get yesterday.

    var params = {
        MetricName: 'EstimatedCharges',
        Namespace: 'AWS/Billing',
        Period: 86400, /* 1 day */
        StartTime: startDate,
        EndTime: new Date(),
        Statistics: ['Maximum'],
        Dimensions: [
            {
                Name: 'Currency',
                Value: 'USD'
            }
        ]
    };
    cloudwatch.getMetricStatistics(params, function(err, data) {
        if (err) {
            console.error(err, err.stack);
            context.fail("Failed to get billing metrics.");
        } else {
            console.log(data);
            var datapoints = data['Datapoints'];
            if (datapoints.length < 1) {
                console.error("There is no billing info.");
                context.fail("There is no billing info.");
            }

            var latestData = datapoints[datapoints.length - 1];
            console.log(latestData);

            var dateString = [startDate.getFullYear(), startDate.getMonth() + 1, startDate.getDate()].join("/");
            var statusColor = "good";
            if (latestData['Maximum'] > 0) statusColor = "danger";

            var slackMessage = {
                channel: slackChannel,
                attachments: [
                    {
                        color:statusColor,
                        text:dateString + "'s billing is " + latestData['Maximum'] + "$"
                    }
                ]
            };

            postMessage(slackMessage, function(response) {
                if (response.statusCode < 400) {
                    console.info('Message posted successfully');
                    context.succeed();
                } else if (response.statusCode < 500) {
                    console.error("Error posting message to Slack API: " + response.statusCode + " - " + response.statusMessage);
                    context.succeed();
                } else {
                    // Let Lambda retry
                    context.fail("Server error when processing message: " + response.statusCode + " - " + response.statusMessage);
                }
            });
        }
    });
};

Lambda cron の設定

re:Invent での発表がありましたが、Lambda が cron のように実行できるようになりました!
これによりわざわざ EC2 インスタンスをたてなくても簡単な処理であれば Lambda で定期実行することができるようになりました。

今回は1日1回取得できればいいので下記のように設定します。
cron の指定は UTC になるので日本時間の朝9時に毎日通知して欲しい場合には cron(0 0 * * ? *) と指定してあげればOKです。

スクリーンショット 2016-01-01 22.22.06.png

2016/02/26 追記

せっかくコメントもいただいたのでもうちょっとメッセージを見やすくするためにAttachmentを使って色で直感的にわかるようにしてみました。

具体的にはslackMessageの構造の部分が下記のように変わっています。
グリーンなら安心、レッドならなんかやばいみたいな感じにしていますw

Attachmentを使うとさらに色々メッセージを工夫できて楽しいので是非色々お試しあれ!
https://api.slack.com/docs/attachments

            var slackMessage = {
                channel: slackChannel,
                attachments: [
                    {
                        color:statusColor,
                        text:dateString + "'s billing is " + latestData['Maximum'] + "$"
                    }
                ]
            };

おわりに

今回は最近好きな Lambda によってAWSの課金状態を Slack で毎日見れる方法を紹介しました。
最近思うのはやはりメールは便利ではあるものの見逃しがちなので、それに変わる通知手段が最近は多様化してきているな、という印象です。

今回の内容を応用すれば Twillio を使って無料枠を超えた時に電話でその旨を通知してもらえるものも簡単に作ることができそうなので、気が向いたらそういったら記事も書いてみようかと思います。