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

<div class="alert alert-success text-center">
この記事は、<a href="http://qiita.com/advent-calendar/2016/firstserver">ファーストサーバ Advent Calendar 2016</a>の12日目の記事です。
</div>
---
はじめまして、ishikunと申します。
Qiita初記事投稿ということで少々緊張しています。

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

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

## なぜやったのか
AWSとは、Amazonが提供しているクラウドコンピューティングサービスです。

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

[Simple Monthly Calculator](http://calculator.s3.amazonaws.com/calc5.html?lng=ja_JP)を使えば事前に見積は可能ですが、従量課金制のため、実際に使用した量や時間は日々変動し、もし払えない請求が来てしまったら怖いです。

ということで、毎日、おおよその請求額が監視できていると安心ですね:smiley: 
 
## 実現方法を調べてみる

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

1. [『AWSのCloudWatchで取得できるBillingの情報を毎日Slackに通知させて費用を常に把握する』 x 『AWS CLIの処理をAWS Data Pipelineで自動化する』](http://qiita.com/sawanoboly/items/56d6f191d18d7f207873)
2. [AWSの料金をLambdaのcronで定期チェックしてSlackに通知する](http://qiita.com/saku/items/fc6b70a420a5c510de2b)


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

#### 使用するサービス
* CloudWatch
* Data Pipeline
* S3

#### Slack通知の見た目（想定）
![](https://qiita-image-store.s3.amazonaws.com/0/11652/53f3872b-455b-6fcd-754e-a5e95f50de89.png)


### 2.の方法で実現するとこうなる
#### 使用するサービス
* CloudWatch
* Lambda

#### Slack通知の見た目（想定）

![](https://qiita-image-store.s3.amazonaws.com/0/11652/aeab21b0-aca3-7982-7fcd-f4ee9f26ece4.png)

## 実現する

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

Lambdaは、Node.js、Python、Java、C#のみの対応のようです。
残念ながら普段の業務で触っているRubyは含まれていません。:cry: 
Node.jsは触ったことはありませんし、[2.のスクリプト](http://qiita.com/saku/items/fc6b70a420a5c510de2b#lambda-function-%E3%81%AE%E4%BD%9C%E6%88%90)を参考にして実現したいと思います。

### 手順

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

![AWS マネジメントコンソール.png](https://qiita-image-store.s3.amazonaws.com/0/11652/b663bd97-f2ba-9025-8077-4de14d2ad613.png)

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

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

![Lambda Management Console](https://qiita-image-store.s3.amazonaws.com/0/11652/17b9b951-7ef0-0832-59bf-bb3e30dfb447.png)

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

![1.png](https://qiita-image-store.s3.amazonaws.com/0/11652/177bdd54-39b2-7ca2-5785-55b88a99115f.png)

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

<div class="alert alert-warning">
【注意】時間は<strong>UTC</strong>で設定します。JST（日本標準時）から9時間分、減算して設定してください。
</div>

![2.png](https://qiita-image-store.s3.amazonaws.com/0/11652/39c31896-80f6-ac7b-072c-a34432273832.png)

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

Nameは適当な値を設定してください。Runtimeは`Node.js`です。
Codeは、以下の[~~なんじゃこら~~スクリプト](#%E3%81%AA%E3%82%93%E3%81%98%E3%82%83%E3%81%93%E3%82%89%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88)を参照してください。Handlerは`exports.handler`を設定してください。
Roleは、事前にCloudWatchのReadOnlyがついた権限を作成しておき、それを指定してください。

![3.png](https://qiita-image-store.s3.amazonaws.com/0/11652/27482ad2-27a9-4fd2-6b40-307a0b32af2e.png)

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

これで完了です。

![4.png](https://qiita-image-store.s3.amazonaws.com/0/11652/f5f32d34-0c9b-1cde-b40d-9b8e75cbe836.png)


#### ~~なんじゃこら~~スクリプト

```js
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通知結果](https://qiita-image-store.s3.amazonaws.com/0/11652/2a1222cf-351e-6f79-eb2e-813dac37564d.png)