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

# LINEDevelopersの設定をする  


* LINEDevelopersに登録とチャンネル（通知が送られてくるアカウント）を作成する。  
[LINE Developers公式](https://developers.line.biz/ja/)


<img width="675" alt="LINE.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261316/3859e0b3-3bfe-6bdb-95cb-df5a3b9d798f.png">

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


# LINEAPIのプッシュメッセージでメッセージを送る  
まず自分のLINEアカウントにメッセージを送信する。

```javascript
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エラーを返す。

```javascript
const requestBody = {
  'to':'XXXXXXXXXXX',
  'messages':{
    'type': 'text',
    'text': 'Hello, world'
  }
};
```
公式ドキュメントを見ながら、何が間違っているのかわからず2,3時間これに消費した。
メッセージボディが下記の場合は、正しい。（配列形式になっているか)

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

参考： 
[LINEAPIへPOSTしてプッシュメッセージを送信する](https://developers.line.biz/ja/reference/messaging-api/#send-push-message)
[テキストメッセージのJSONオブジェクト](https://developers.line.biz/ja/reference/messaging-api/#text-message)  

# AWSのAPI権限周りの設定をする  
* IAMユーザのポリシーの設定で`CloudWatchReadOnlyAccess`を付与する。
* 認証情報をローカルに保存する  

参考：
[共有認証情報ファイルから Node.js に認証情報をロードする](https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html)
[~/.aws/credentialsファイルでプロファイルを切り替える
](https://qiita.com/NewGyu/items/c183ff8c8d9406292967)


# AWSのCloudWatchから概算請求金額を取得する  
CloudWatchのgetMetricStatisticsメソッドを使って請求金額を取得する。  

```javascript
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
  }
})();
```

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

```javascript
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メトリクス名](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/viewing_metrics_with_cloudwatch.html)  
`Namespace`：アクセスするサービス名
[CloudWatch名前空間]
(https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/aws-services-cloudwatch-metrics.html)  
`Period:86400`：データの取得幅
1=1秒なので
`60(秒) * 60(分) * 24(時間) = 86400(秒) = 1日  `

参考:
[CloudWatchの概念]
(https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html)
[CloudWatchAPIドキュメント](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudWatch.html#getMetricStatistics-property)
<br>

# Amazon Lambdaの登録を行う  
毎日定時にCloudWatchのイベントを発生させ、それをトリガーにしてLambdaを起動させる。<img width="835" alt="lambda.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261316/54220164-9a95-91f3-3268-2941952ab6c0.png">
  

CloudWatchのルールの登録を行う
<img width="487" alt="ru-ru.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261316/d2e42d8d-9475-81ca-895f-bac75cf3cddc.png">

毎日18時にイベントを発火する。(単位はGMT,日本時間との時差は9時間)
しかしこの公式通りやってもうまく設定できなかった。なぜ？
[Rate または Cron を使用したスケジュール式]
(https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html)

* Lambdaにソースコードを登録をする  

```javascript
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側でのテストでメッセージの受信ができること確認する。

```javascript
  const bill = await FechAWSBilling.getMetricStatistics();
  LINEMessage.setMessage(bill.Datapoints[0].Maximum);
  LINEMessage.pushMessage();
}
```  

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

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

ソースコードの登録が完了したら、LINEで請求金額が通知されることを確認する。  
![LINE通知.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261316/3059ec0f-91d4-150c-0da3-362342f3a986.png)  
以上です、ありがとうございました。  
※AWSの公式ドキュメントの膨大さに気絶しそうになる。