リソースの消し忘れを簡単にチェックしてコストを節約する方法を教えて下さい(Lambda/Node.js 版)

2022.12.02

困っていた内容

検証したリソースを消し忘れてしまい思わぬコストがかかってしまいました。簡単にチェックする方法を教えて下さい。

どう対応すればいいの?

自動でチェックしてSlackに通知してもらいましょう!そこで Lambda/Node.js で毎日のコストと EC2 数を調べるスクリプトを作ってみました。

  • 毎日の利用コスト
  • EC2 インスタンス数

上記を CloudWatch Events で定期的に実行して Slack に通知します。

やってみた

実行環境

Lambda 作成リージョン:バージニア北部(us-east-1)
ランタイム:Node.js 16.x
デフォルトの実行ロールの変更:ReadOnlyAccess ポリシーを持った IAM ロール

コスト

function getCostAndUsage(){
    var endDate = new Date();
    var startDate = new Date();
    startDate.setDate(startDate.getDate() - 1);
    var strEndM = String("0" + String(endDate.getMonth() + 1)).slice( -2 );
    var strEndD = String("0" + String(endDate.getDate())).slice( -2 );
    var strStrtM = String("0" + String(startDate.getMonth() + 1)).slice( -2 );
    var strStrtD = String("0" + String(startDate.getDate())).slice( -2 );
    var endDateStr = String(endDate.getFullYear()) + "-" + strEndM + "-" + strEndD;
    var startDateStr = String(startDate.getFullYear()) + "-" + strStrtM + "-" + strStrtD;
    var params = {
        Granularity: "DAILY",
        Metrics: [ 'BlendedCost'],
        TimePeriod: { Start: startDateStr, End: endDateStr }
    };
    var ce = new AWS.CostExplorer();
    ce.getCostAndUsage(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
        } else {
            var cost = Object(data.ResultsByTime[0])["Total"]["BlendedCost"]["Amount"];
            msg += 'コスト(USD):' + cost + LF + LF;
            // NEXT
            ec2describeInstances();
        }
    });
}

前日から当日のコストを取得しています。 startDate と endDate という変数に今日と昨日の日付を入れています。このスクリプトが 10/9 に実行された場合は START は 10/8 で END は 10/9 になります。 結果は msg という変数に入れて後で Slack に送るメッセージの一部として使用します。

台数

function ec2describeInstances(){
    var ec2 = new AWS.EC2();
    var params = {};
    ec2.describeInstances(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
        } else {
            var cnt = '0';
            if(data.Reservations) {
                cnt = String(data.Reservations.length);
            }
            msg += "EC2: " + String(cnt) + "台起動中です!" + LF;
            // NEXT
            describeVolumes();
        }
    });
}

まず、対象リージョンをリソースを確認したいリージョンに変更します。 describeInstances のレスポンスである Reservations の長さがインスタンス数になります。 コストと同様に台数を msg という変数に入れます。 同じ考え方で例えば RDS のインスタンス数なども簡単にとれます。

Slack 送信

function sendslack (message) {
    const SLACK_TOKEN = process.env.SLACK_TOKEN;
    const SLACK_CHANNEL = process.env.SLACK_CHANNEL;
    const SLACK_TO = process.env.SLACK_TO;
    var finalMsg = SLACK_TO + LF + message;
    let formData = {
        token   : SLACK_TOKEN, 
        channel : SLACK_CHANNEL, 
        text    : finalMsg, 
        as_user : true
    };
    let options = {
        hostname    : SLACK_HOST, 
        path        : SLACK_PATH, 
        method      : 'POST', 
        headers : {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    };
    let req = HTTPS.request (options, function(res) {
        res.on('data', function(d) {
            console.log(d + "\n"); 
        });
    });
    req.write(QUERYSTRING.stringify (formData));
    req.on('error', function(e) {
        console.log("Slackl 送信失敗");
    });
    req.end();
}

受け取った msg という引数をSlackに送信します。 トークンなどの機密情報は環境変数に入れてコード上ではprocess.env.【環境変数名】で参照します。 こちらは下記のブログを参考に作成させていただきました。

Slackチャンネルにメッセージを投稿できるSlackAppを作成する
LambdaからSlackへ通知

EventBridge (CloudWatch Events) 設定

例えば下記のようにEventBridge (CloudWatch Events) を登録することで定期的にこのLambda 関数を実行することができます。

下記のケースでは毎日 16:10 JST に実行するように設定しています。


実行結果

Slack はこんな感じになります

全コード

const HTTPS = require('https');
const QUERYSTRING = require('querystring');
const SLACK_HOST = 'slack.com';
const SLACK_PATH = '/api/chat.postMessage';
const LF = '\n';
var msg = '';
var AWS = require('aws-sdk'); 

exports.handler = (event, context, callback) => {
    getCostAndUsage(); 
};

function getCostAndUsage(){
    var endDate = new Date();
    var startDate = new Date();
    startDate.setDate( startDate.getDate() - 1 );
    var endDateStr = String(endDate.getFullYear()) + "-" + String(endDate.getMonth() + 1) + "-" + String(endDate.getDate());
    var startDateStr = String(startDate.getFullYear()) + "-" + String(startDate.getMonth() + 1) + "-" + String(startDate.getDate());
    var params = {
        Granularity: "DAILY",
        Metrics: [ 'BlendedCost'],
        TimePeriod: { Start: startDateStr, End: endDateStr }
    };
    var ce = new AWS.CostExplorer();
    ce.getCostAndUsage(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
        } else {
            var cost = Object(data.ResultsByTime[0])["Total"]["BlendedCost"]["Amount"];
            msg += 'コスト(USD):' + cost + LF + LF;
            // NEXT
            ec2describeInstances();
        }
    });
}

function ec2describeInstances(){
    var ec2 = new AWS.EC2();
    var params = {};
    ec2.describeInstances(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
        } else {
            var cnt = '0';
            if(data.Reservations) {
                cnt = String(data.Reservations.length);
            }
            msg += "EC2: " + String(cnt) + "台起動中です!" + LF;
            // NEXT
            sendslack();
        }
    });
}

function sendslack () {
    const SLACK_TOKEN = process.env.SLACK_TOKEN;
    const SLACK_CHANNEL = process.env.SLACK_CHANNEL;
    const SLACK_TO = process.env.SLACK_TO;
    var finalMsg = SLACK_TO + LF + msg;
    let formData = {
        token   : SLACK_TOKEN, 
        channel : SLACK_CHANNEL, 
        text    : finalMsg, 
        as_user : true
    };
    let options = {
        hostname    : SLACK_HOST, 
        path        : SLACK_PATH, 
        method      : 'POST', 
        headers : {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    };
    let req = HTTPS.request (options, function(res) {
        res.on('data', function(d) {
            console.log(d + "\n"); 
        });
    });
    req.write(QUERYSTRING.stringify (formData));
    req.on('error', function(e) {
        console.log("Slackl 送信失敗");
    });
    req.end();
}

費用

コストエクスプローラは、1 回の API 呼出に対して、0.01 USD の料金が発生します。 毎日通知すると月額で約45円(1 USD =150 円換算)の料金が発生します。
Cost Explorerによるコストの分析 | AWS

関連情報

AWS Budgets で費用が設定額を超えた場合にアラート通知できますのでこちらの設定も必須ですね。

AWS Budgets で 1-Click テンプレートが使えるようになりました

最後に

クラウドのいいところは低コストで簡単に検証できるところ。でも、検証後にリソースを消し忘れて思わぬ出費になってしまうことも。 毎回確認すれば良いのですが意外と面倒でついつい怠けがち。毎日コストと消し忘れたリソースがないか簡単に確認したかったので作ってみました。
次回は同じ機能を Lambda (Python) で作成予定です。
このブログがどなたかのお役にたてば幸いです。