Advent Calendarに何を書こうかなー、こんなこともあんなこともやってみたいしなぁ…と思っていたのですが、そんなことを考えているうちにあっという間に自分の番ですね。
今回は、最近「AWSの請求を毎日Slackに通知させる」ことをやってみたのでその方法を書いてみたいと思います。
なぜやったのか
AWSとは、Amazonが提供しているクラウドコンピューティングサービスです。
クラウドコンピューティングサービスの多くは、転送量や稼働時間、ストレージの使用量などさまざまな要素で料金が決まります。従量課金制ということです。
Simple Monthly Calculatorを使えば事前に見積は可能ですが、従量課金制のため、実際に使用した量や時間は日々変動し、もし払えない請求が来てしまったら怖いです。
ということで、毎日、おおよその請求額が監視できていると安心ですね
実現方法を調べてみる
AWSの請求を毎日Slackに通知する方法は、やはり実現している先人の方がいらっしゃいました。
今回は以下の記事を参考にしています。
- 『AWSのCloudWatchで取得できるBillingの情報を毎日Slackに通知させて費用を常に把握する』 x 『AWS CLIの処理をAWS Data Pipelineで自動化する』
- 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は含まれていません。
Node.jsは触ったことはありませんし、2.のスクリプトを参考にして実現したいと思います。
手順
1. AWSマネージメントコンソールを開く
AWSマネージメントコンソールを開き、サービスからLambdaを開きます。
※CloudWatchのリージョンはスクリプトから指定しているので、Lambdaのリージョンはアジア・パシフィック(東京)で構いません。
2. Lambdaにスクリプトを登録する
(1) Create a Lambda functionをクリックする
(2) Blank Functionをクリックする
(3) トリガーを設定する
CloudWatch Events - Scheduleを選択し、Rule nameは適当に設定します。
Schedule expressionは、cron(分 時 ? * * *)
と入力してください。
これで毎日、設定した時間に通知されます。
(4) スクリプトを設定する
Nameは適当な値を設定してください。RuntimeはNode.js
です。
Codeは、以下のなんじゃこらスクリプトを参照してください。Handlerはexports.handler
を設定してください。
Roleは、事前にCloudWatchのReadOnlyがついた権限を作成しておき、それを指定してください。
(5) Create functionをクリックする
これで完了です。
なんじゃこらスクリプト
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に通知がきました!
これで安心して、クリスマスと正月が迎えられそうですね!