LoginSignup
90
87

More than 5 years have passed since last update.

AWSの請求を毎日Slackに通知させる

Last updated at Posted at 2016-12-11
この記事は、ファーストサーバ Advent Calendar 2016の12日目の記事です。

はじめまして、ishikunと申します。
Qiita初記事投稿ということで少々緊張しています。

Advent Calendarに何を書こうかなー、こんなこともあんなこともやってみたいしなぁ…と思っていたのですが、そんなことを考えているうちにあっという間に自分の番ですね。

今回は、最近「AWSの請求を毎日Slackに通知させる」ことをやってみたのでその方法を書いてみたいと思います。

なぜやったのか

AWSとは、Amazonが提供しているクラウドコンピューティングサービスです。

クラウドコンピューティングサービスの多くは、転送量稼働時間ストレージの使用量などさまざまな要素で料金が決まります。従量課金制ということです。

Simple Monthly Calculatorを使えば事前に見積は可能ですが、従量課金制のため、実際に使用した量や時間は日々変動し、もし払えない請求が来てしまったら怖いです。

ということで、毎日、おおよその請求額が監視できていると安心ですね:smiley:

実現方法を調べてみる

AWSの請求を毎日Slackに通知する方法は、やはり実現している先人の方がいらっしゃいました。
今回は以下の記事を参考にしています。

  1. 『AWSのCloudWatchで取得できるBillingの情報を毎日Slackに通知させて費用を常に把握する』 x 『AWS CLIの処理をAWS Data Pipelineで自動化する』
  2. AWSの料金をLambdaのcronで定期チェックしてSlackに通知する

1.の方法で実現するとこうなる

使用するサービス

  • CloudWatch
  • Data Pipeline
  • S3

Slack通知の見た目(想定)

2.の方法で実現するとこうなる

使用するサービス

  • CloudWatch
  • Lambda

Slack通知の見た目(想定)

実現する

今回は、1.のようにサービスごとの明細も表示させて、2.のようにCloudWatchとLambdaのみで実現したいと思います。

Lambdaは、Node.js、Python、Java、C#のみの対応のようです。
残念ながら普段の業務で触っているRubyは含まれていません。:cry:
Node.jsは触ったことはありませんし、2.のスクリプトを参考にして実現したいと思います。

手順

1. AWSマネージメントコンソールを開く

AWSマネージメントコンソールを開き、サービスからLambdaを開きます。
※CloudWatchのリージョンはスクリプトから指定しているので、Lambdaのリージョンはアジア・パシフィック(東京)で構いません。

AWS マネジメントコンソール.png

2. Lambdaにスクリプトを登録する

(1) Create a Lambda functionをクリックする

Lambda Management Console

(2) Blank Functionをクリックする

1.png

(3) トリガーを設定する

CloudWatch Events - Scheduleを選択し、Rule nameは適当に設定します。
Schedule expressionは、cron(分 時 ? * * *)と入力してください。
これで毎日、設定した時間に通知されます。

【注意】時間はUTCで設定します。JST(日本標準時)から9時間分、減算して設定してください。

2.png

(4) スクリプトを設定する

Nameは適当な値を設定してください。RuntimeはNode.jsです。
Codeは、以下のなんじゃこらスクリプトを参照してください。Handlerはexports.handlerを設定してください。
Roleは、事前にCloudWatchのReadOnlyがついた権限を作成しておき、それを指定してください。

3.png

(5) Create functionをクリックする

これで完了です。

4.png

なんじゃこらスクリプト

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

/* ここから設定 */

// Slackのチャンネル名を指定。#generalや@ishikunなど。
var channel_name = '';

// Slack Incoming Webhook URLを指定。 
var channel_url  = '';

// サービス名を配列で指定。
var serviceNames = ['AmazonEC2', 'AmazonRDS', 'AmazonRoute53', 'AmazonS3', 'AmazonSNS', 'AWSDataTransfer', 'AWSLambda', 'AWSQueueService'];

/* ここまで設定 */

var floatFormat = function(number, n) {
    var _pow = Math.pow(10 , n) ;
    return Math.round(number * _pow)  / _pow;
}

var postBillingToSlack = function(billings, context) {
    var fields = [];
    for (var serviceName in billings) {
        fields.push({
            title: serviceName,
            value: floatFormat(billings[serviceName], 2) + " USD",
            short: true
        });
    }
    var message = {
        channel: channel_name,
        attachments: [{
            fallback: '今月のAWSの利用費は、' + floatFormat(billings['Total'], 2) + ' USDです。',
            pretext: '今月のAWSの利用費は…',
            color: 'good',
            fields: fields
        }]
    };
    var body = JSON.stringify(message);
    var options = url.parse(channel_url);
    options.method = 'POST';
    options.header = {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body)
    };
    var statusCode;
    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('');
            statusCode = res.statusCode;
        });
        return res;
    });
    postReq.write(body);
    postReq.end();
    if (statusCode < 400) {
      context.succeed();
    }
}

var getBilling = function(context) {
    var now = new Date();
    var startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1,  0,  0,  0);
    var endTime   = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 23, 59, 59);

    var billings = {};

    var total_params = {
        MetricName: 'EstimatedCharges',
        Namespace: 'AWS/Billing',
        Period: 86400,
        StartTime: startTime,
        EndTime: endTime,
        Statistics: ['Average'],
        Dimensions: [
            {
                Name: 'Currency',
                Value: 'USD'
            }
        ]
    };

    cw.getMetricStatistics(total_params, function(err, data) {
        if (err) {
            console.error(err, err.stack);
        } else {
            var datapoints = data['Datapoints'];
            if (datapoints.length < 1) {
                billings['Total'] = 0;
            } else {
                billings['Total'] = datapoints[datapoints.length - 1]['Average']
            }
            if (serviceNames.length > 0) {
                serviceName = serviceNames.shift();
                getEachServiceBilling(serviceName);
            }
        }
    });

    var getEachServiceBilling = function(serviceName) {
        var params = {
            MetricName: 'EstimatedCharges',
            Namespace: 'AWS/Billing',
            Period: 86400,
            StartTime: startTime,
            EndTime: endTime,
            Statistics: ['Average'],
            Dimensions: [
                {
                    Name: 'Currency',
                    Value: 'USD'
                },
                {
                    Name: 'ServiceName',
                    Value: serviceName
                }
            ]
        };
        cw.getMetricStatistics(params, function(err, data) {
            if (err) {
                console.error(err, err.stack);
            } else {
                var datapoints = data['Datapoints'];
                if (datapoints.length < 1) {
                    billings[serviceName] = 0;
                } else {
                    billings[serviceName] = datapoints[datapoints.length - 1]['Average']
                }
                if (serviceNames.length > 0) {
                    serviceName = serviceNames.shift();
                    getEachServiceBilling(serviceName);
                } else {
                    postBillingToSlack(billings, context)
                }
            }
        });
    }
}

exports.handler = function(event, context) {
    getBilling(context);
}

実現した

見事Slackに通知がきました!
これで安心して、クリスマス:christmas_tree:と正月:bamboo:が迎えられそうですね!

Slack通知結果

90
87
3

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
90
87