🔶 はじめに
AWSの利用料金を、あまり気にせず使っていたのですが、謎の料金増加を指摘され、確認したら、意図せず有効になったサービスや、試しに使ってそのまま消し忘れたサービス等がいくつかあったようで、さすがに猛省。
そこで、利用料金の増加に気付けるように、毎週Slackに利用料金を投稿する仕組みを作りました。
🔶 使うサービス
🔷 Lambda
- 様々な言語のコードをデプロイできる実行環境。
- コードは、様々なAWSプロダクトで発生したアクションをトリガーに実行する事ができます。
- https://aws.amazon.com/jp/lambda/
🔷 Slack Incoming Webhooks
- 外部サービスからのリクエストを受けて、Slackのワークスペースに投稿してくれるサービス。
- 簡単に言うと、Slackに投稿できる、POSTメソッドのAPIです。
- https://get.slack.help/hc/ja/articles/115005265063
🔶 Slack Incoming Webhooks の設定
以下を参考にしてください。
SlackのIncoming Webhooksを使い倒す
🔶 Lambdaの実装
🔷 Lambda関数作成
Lambda関数の基本的な作り方は、以下を参考にしてください。
API Gateway + LambdaでREST API開発を体験しよう [10分で完成編]
🔷 ポリシーの設定
料金情報の取得には、Cost Explorer Serviceの、GetCostAndUsageを利用します。
そのため、Lambda関数に以下のポリシーを追加します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "ce:GetCostAndUsage",
"Resource": "*"
}
]
}
🔷 実装
今回作る関数は、以下の処理を行うものです。
- 一週間分の料金情報を取得
- 任意のサービスの料金を計算
- SlackのAPIに料金情報を投げる
3ステップを行うだけの、特に難しいソースではありません。
ソースは以下に紹介しますが、コピーしてつなげれば、動きます。
メイン処理
- 最初に、
const moment = require('moment')
でmoment.jsを読み込んでいますが、これは日付けの書式設定が簡単なので使っているだけで、new Date()
を使う場合は不要です。 - moment.jsを使う場合は、モジュールのインポートが必要なので、以下を参考に行ってください。
Lambda関数にNode.jsモジュールを入れるなら、Cloud9が簡単でおすすめ
/**
* 指定範囲の料金の集計サービス
*
*/
const AWS = require('aws-sdk');
const costexplorer = new AWS.CostExplorer();
const moment = require('moment');
const https = require ('https');
const url = require('url');
/**
* メイン処理
*
*/
exports.handler = async (event) => {
let costRaw = await getCost();
let costJson = await costCalc(costRaw);
let result = await requestToAPI(costJson);
return result;
};
一週間分の料金情報を取得
-
PARAMS
の値を、getCostAndUsage
に渡すことで、料金情報を取得しています。 -
"Granularity": 'DAILY'
は、1日の料金を出力する設定です。 -
"TimePeriod"
は、料金を出力する範囲を設定します。今回は1週間分なので、"Start"
8日前から、"End"
1日前の範囲にします。
1点注意したいのが、"End"
の日の料金は含まれないという事です。 -
"Metrics": [ 'UnblendedCost' ]
は、ディスカウント適用前の金額を出力する設定です。値の意味については以下を参照してください。
AWS Cost Explorerに渡す、Metricsの値の意味 - 設定の仕様については、以下を参照してください。
AWS Cost Explorer Service
ただ、1点注意したいのが、AWS Cost Explorer Serviceに短期間で大量のリクエスト(具体的な数値は非公開)を送ると、LimitExceededException
がレスポンスで返るようになります。こうなると、解除されるまで待つしかありません。(上限緩和未対応)
/* yyyy-mm-ddの書式で、集計の開始日と終了日をセット*/
const yesterday = moment().subtract(1, 'days').format('YYYY-MM-DD');
const weekAgo = moment().subtract(8, 'days').format('YYYY-MM-DD');
/* request body */
const PARAMS = {
"Granularity": 'DAILY',
"GroupBy": [
{
"Type": 'DIMENSION',
"Key": 'SERVICE',
}
],
"Metrics": [ 'UnblendedCost' ],
"TimePeriod": {
"Start": weekAgo,
"End": yesterday
},
};
/**
* Cost Management APIsから料金情報の取得処理
*
* @return AWSから取得した料金情報
*/
function getCost() {
console.log(`■□■□ Cost Management APIsから料金情報の取得処理 ■□■□`);
return new Promise(
(resolve) => {
costexplorer.getCostAndUsage(PARAMS, (err, data) => {
if (err) {
console.error(err, err.stack);
return;
}
resolve(data);
});
}
);
}
任意のサービスの料金を計算
-
getCostAndUsage
から返ってくる料金情報は、1日毎に、サービス別に料金が記載されているので、これを見やすいように、サービス別に、1週間分の料金の合計を出します。
今回は、よく使うサービスのみ計算します。
/**
* 使用料金の計算処理
*
* @param data AWSから取得した料金情報
* @return 料金集計結果のJSON
*/
function costCalc(data) {
console.log(`■□■□ 使用料金の計算処理 ■□■□`);
return new Promise(
(resolve) => {
let ec2Cost = 0, rdsCost = 0, ec2OtherCost = 0, elbCost = 0, cwCost = 0, other = 0;
// let totalDays = moment(yesterday).diff(weekAgo, 'day'); // 集計範囲の日数
let services, cost;
/* 期間の料金の集計 */
for(let i=0; i<data.ResultsByTime.length; i++) { // 日毎チェック
services = data.ResultsByTime[i].Groups;
for(let j=0; j<services.length; j++) { // サービス毎チェック
cost = services[j].Metrics.UnblendedCost.Amount; // ディスカウント適用前(EDP割引等)のコスト
switch(services[j].Keys[0]) {
case "Amazon Elastic Compute Cloud - Compute":
ec2Cost = ec2Cost + Number(cost);
break;
case "EC2 - Other":
ec2OtherCost = ec2OtherCost + Number(cost);
break;
case "Amazon Relational Database Service":
rdsCost = rdsCost + Number(cost);
break;
case "Amazon Elastic Load Balancing":
elbCost = elbCost + Number(cost);
break;
case "AmazonCloudWatch":
cwCost = cwCost + Number(cost);
break;
default:
other = other + Number(cost);
break;
}
}
}
/* 料金の集計結果 */
let result = {
"range": {
"startDate": weekAgo,
"endDate": yesterday
},
"services": {
"ec2": ec2Cost,
"ec2other": ec2OtherCost,
"rds": rdsCost,
"elb": elbCost,
"cw": cwCost,
"other" : other
},
"total" : ec2Cost + ec2OtherCost + rdsCost + elbCost + cwCost + other
};
resolve(result);
}
);
}
SlackのAPIに料金情報を投げる
- Slack Incoming Webhooksのエンドポイントに、投稿したいメッセージをセットして、POSTリクエストを投げます。
-
PAYLOAD
の値がリクエストbodyになります。-
channel
: 投稿先のチャンネル -
username
: 投稿時の投稿者名 -
text
: 投稿内容 -
icon_url
: 投稿時のアイコンのURL
-
- ちなみに、
process.env.○○
はLambdaの環境変数です。Slackに渡すパラメータは、環境変数にした方が管理が楽ですが、必要無ければ直接書き込んでもOKです。
const ENDPOINT = process.env.Endpoint_url;
const PAYLOAD = {
"channel": process.env.channel,
"username": process.env.username,
"text": "",
"icon_url": process.env.Icon_url
};
/**
* Slackへ料金情報を投稿処理
*
* @param data 料金集計結果のJSON
* @return Slackへ投稿リクエストを投げた結果のレスポンス
*/
function requestToAPI(data) {
console.log(`■□■□ Slackへ料金情報を投稿処理 ■□■□`);
return new Promise(
(resolve, reject) => {
PAYLOAD.text = `先週(${data.range.startDate}〜${data.range.endDate} 00:00まで)のAWSの使用料は以下の通りです。\n\n`
+ "```"
+ `・EC2 : ${Math.round((data.services.ec2 + data.services.ec2other) * 10) / 10}$\n`
+ `・RDS : ${Math.round(data.services.rds * 10) / 10}$\n`
+ `・ELB : ${Math.round(data.services.elb * 10) / 10}$\n`
+ `・CloudWatch : ${Math.round(data.services.cw * 10) / 10}$\n`
+ `・other : ${Math.round(data.services.other * 10) / 10}$\n`
+ `--------------------------------\n`
+ `・total : ${Math.round(data.total * 10) / 10}$\n\n`
+ `※ 金額は、ディスカウント適用前で、端数切捨てです。\n`
+ "```";
console.log('■□■□■□ SlackへPOSTリクエスト:\n' + JSON.stringify(data));
const parser = url.parse(ENDPOINT);
let req, body = '';
let options = {
host : parser.host,
port : 443,
path : parser.path,
method : 'POST',
headers : {
'Content-Type' : 'application/json'
}
};
/* APIへのリクエスト */
req = https.request(options, (res) => {
res.setEncoding('utf8');
/* レスポンスボディのデータ読み込み処理(dataイベント発生)*/
res.on('data', (chunk) => {
body += chunk;
});
/* 読み込むデータが無くなった時の処理(endイベント発生) */
res.on('end', () => {
if(res.statusCode == 200) {
console.log('■□■□ レスポンス情報 ');
console.log(body);
resolve(body);
} else {
console.log('■□■□ レスポンス情報なし ');
}
});
});
req.write(JSON.stringify(PAYLOAD));
req.on('error', (e) => {
console.error(`problem with request: ${e.message}`);
reject(e);
});
req.end();
});
}
🔷 テスト
ここまで上手く設定できていたら、Lambda関数を実行すると、Slackに以下のように投稿されます。
これを、CloudWatch Eventで、毎週Lambda関数を実行するように設定したら、金額の変動に気付けるようになります。
ただ1点注意として、getCostAndUsage
に料金情報を要求するのも料金が発生します。やり過ぎには注意してください。
🔶 まとめ
このように料金計算が面倒なだけで、内容は難しく無いので、ぜひ試してみてください。
🔶 おまけ
こちらにも挑戦して、Slackに投稿する内容に一手間加えてみてください。
LambdaでTrusted Advisorの節約可能な月額料金を取得する(Node.js)