LoginSignup
2
1

More than 5 years have passed since last update.

Node.jsでAWSの請求金額をLINEで通知する

Posted at

LINEDevelopersの設定をする

  • LINEDevelopersに登録とチャンネル(通知が送られてくるアカウント)を作成する。
    LINE Developers公式

LINE.png

チャンネルを作ると、上のような設定状況を見ることができる。名前は「AWS料金通知」とした。
ここでAPIを使うためのアクセストークンとメッセージを送信する先のユーザコードを確認しておく。

LINEAPIのプッシュメッセージでメッセージを送る

まず自分のLINEアカウントにメッセージを送信する。

const https = require('https');

const LINEMessage = (() => {
  let _postData = '';
  let _message = '';
  let _requestBody = new Object();

  const setMessage = (data) => {
    _message = String(`今月のAWS概算請求金額は「${data}$」です。`);
    _requestBody.to = process.env.LINE_USER_ID;
    _requestBody.messages = [{'type': 'text','text': _message}];
    _postData = JSON.stringify(_requestBody);
  }

  const pushMessage = () => {
    return new Promise(resolve => {
      //setMessageの使用を強制させる
      if(_postData === ''){
        console.log('setMessageの使用が必要です');
        return;
      }
      const headers= {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': `Bearer ${process.env.LINE_ACCESS_TOKEN}`,
        'Content-Length':Buffer.byteLength(_postData)
      };

      const _options = {
        hostname: 'api.line.me',
        port: 443,
        path: '/v2/bot/message/push',
        method: 'POST',
        headers: headers
      };

      const req = https.request(_options, (res) => {
        console.log('statusCode:', res.statusCode);
        res.on('end', () => {
          resolve(true);
        });
      });

      req.on('error', (e) => {
        console.error(e);
      });

      req.write(_postData);
      req.end();
    });
  }
  return {
    pushMessage: pushMessage,
    setMessage: setMessage
  }
})();

LINEから提供されているbot-sdkは使用せず、Node.js標準のhttpsモジュールを使用してエンドポイントにPOSTする方式でメッセージを送信する。外部ライブラリを使用した場合AWS Lambda側でパッケージを使用する用の設定が別途必要。

  • 私がハマった部分

メッセージボディが下記の場合は、400エラーを返す。

const requestBody = {
  'to':'XXXXXXXXXXX',
  'messages':{
    'type': 'text',
    'text': 'Hello, world'
  }
};

公式ドキュメントを見ながら、何が間違っているのかわからず2,3時間これに消費した。
メッセージボディが下記の場合は、正しい。(配列形式になっているか)

const requestBody = {
  'to':'XXXXXXXXXXX',
  'messages': [{
    'type': 'text',
    'text': 'Hello, world'
    }]
};

参考:
LINEAPIへPOSTしてプッシュメッセージを送信する
テキストメッセージのJSONオブジェクト

AWSのAPI権限周りの設定をする

  • IAMユーザのポリシーの設定でCloudWatchReadOnlyAccessを付与する。
  • 認証情報をローカルに保存する

参考:
共有認証情報ファイルから Node.js に認証情報をロードする
~/.aws/credentialsファイルでプロファイルを切り替える

AWSのCloudWatchから概算請求金額を取得する

CloudWatchのgetMetricStatisticsメソッドを使って請求金額を取得する。

const AWS = require('aws-sdk');

const FechAWSBilling = (() => {
  const cw = new AWS.CloudWatch({region: 'us-east-1'});
  const date = new Date
  const endTime = date.toISOString();
  date.setDate(date.getDate() -1);
  const startTime = date.toISOString();

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

  const getMetricStatistics = async () => {
    return new Promise(resolve => {
      cw.getMetricStatistics(params, (err, data) => {
        if (err){
          console.log(err, err.stack);
        }
        resolve(data);
      });
    })
  }
  return{
    getMetricStatistics: getMetricStatistics
  }
})();

ハマりやすい部分はパラメータの部分だと思われる。
初見では各プロパティが何を意味しているのか全く分からなかった。(今もよく分かっていない)

const params = {
  StartTime:'2019-06-10T12:21:45.351Z',
  EndTime:'2019-06-12T12:21:45.351Z',
  MetricName:'EstimatedCharges',
  Namespace:'AWS/Billing',
  Period:86400,
  Dimensions:[
    {
      Name:'Currency',
      Value:'USD'
    }
  ],
  Statistics:['Maximum']
};

MetricName:アクセスするリソース名
CloudWatchメトリクス名

Namespace:アクセスするサービス名
CloudWatch名前空間

Period:86400:データの取得幅
1=1秒なので
60(秒) * 60(分) * 24(時間) = 86400(秒) = 1日

参考:
CloudWatchの概念
CloudWatchAPIドキュメント

Amazon Lambdaの登録を行う

毎日定時にCloudWatchのイベントを発生させ、それをトリガーにしてLambdaを起動させる。lambda.png

CloudWatchのルールの登録を行う
ru-ru.png

毎日18時にイベントを発火する。(単位はGMT,日本時間との時差は9時間)
しかしこの公式通りやってもうまく設定できなかった。なぜ?
Rate または Cron を使用したスケジュール式

  • Lambdaにソースコードを登録をする
const https = require('https');
const AWS = require('aws-sdk');

/** 
 * LINEでPushメッセージを送信する
*/
const LINEMessage = (() => {
  let _postData = '';
  let _message = '';
  let _requestBody = new Object();

  const setMessage = (data) => {
    _message = String(`今月のAWS概算請求金額は「${data}$」です。`);
    _requestBody.to = process.env.LINE_USER_ID;
    _requestBody.messages = [{'type': 'text','text': _message}];
    _postData = JSON.stringify(_requestBody);
  }

  const pushMessage = () => {
    return new Promise(resolve => {
      //setMessageの使用を強制させる
      if(_postData === ''){
        console.log('setMessageの使用が必要です');
        return;
      }
      const headers= {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': `Bearer ${process.env.LINE_ACCESS_TOKEN}`,
        'Content-Length':Buffer.byteLength(_postData)
      };

      const _options = {
        hostname: 'api.line.me',
        port: 443,
        path: '/v2/bot/message/push',
        method: 'POST',
        headers: headers
      };

      const req = https.request(_options, (res) => {
        console.log('statusCode:', res.statusCode);
        res.on('end', () => {
          resolve(true);
        });
      });

      req.on('error', (e) => {
        console.error(e);
      });

      req.write(_postData);
      req.end();
    });
  }
  return {
    pushMessage: pushMessage,
    setMessage: setMessage
  }
})();

/** 
 * AWSから請求金額を取得する
*/
const FechAWSBilling = (() => {
  const cw = new AWS.CloudWatch({region: 'us-east-1'});
  const date = new Date
  const endTime = date.toISOString();
  date.setDate(date.getDate() -1);
  const startTime = date.toISOString();

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

  const getMetricStatistics = async () => {
    return new Promise(resolve => {
      cw.getMetricStatistics(params, (err, data) => {
        if (err){
          console.log(err, err.stack);
        }
        resolve(data);
      });
    })
  }
  return{
    getMetricStatistics: getMetricStatistics
  }
})();


exports.handler = async () => {
  const bill = await FechAWSBilling.getMetricStatistics();
  LINEMessage.setMessage(bill.Datapoints[0].Maximum);
  const pushComplete = await LINEMessage.pushMessage();
  if(pushComplete){
    console.log('関数は正常に実行されました。');
    return;
  }
};
  • 私がハマった部分
    Nodeの非同期処理に注意する。 下記のコードは、Pushメッセージ送信が完了する前にLambda側での処理が終了してECONNRESETエラーが発生する場合がある。Lambda外でのデバック時はエラーが発生しない分、原因調査に数時間消費した。lambda側でのテストでメッセージの受信ができること確認する。
  const bill = await FechAWSBilling.getMetricStatistics();
  LINEMessage.setMessage(bill.Datapoints[0].Maximum);
  LINEMessage.pushMessage();
}

下記のコードのようにPushメッセージが完了するようにする。

  const bill = await FechAWSBilling.getMetricStatistics();
  LINEMessage.setMessage(bill.Datapoints[0].Maximum);
  const pushComplete = await LINEMessage.pushMessage();
  if(pushComplete){
    console.log('関数は正常に実行されました。');
    return;
  }
}

ソースコードの登録が完了したら、LINEで請求金額が通知されることを確認する。

LINE通知.png

以上です、ありがとうございました。

※AWSの公式ドキュメントの膨大さに気絶しそうになる。

2
1
0

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
2
1