LambdaでAWSの請求情報をLINE、Slackに送信する

# 何をするか
- AWSの請求金額をLINE、Slackに通知する
- Totalで1~2時間くらいあればできる

# 事前準備
- [予想 AWS 請求額をモニタリングする請求アラートを有効にする](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html#turning_on_billing_metrics)
    - ルートユーザでログインしてチェック入れるだけ
    - これでCloudWatchから請求額を取得することができる
- [LINEのmessagingAPIを有効化](https://developers.line.me/ja/services/messaging-api/)し、アクセストークンとユーザIDを取得
- [SlackのIncoming Webhooksを有効化](https://slack.com/apps/A0F7XDUAZ--web-)し、Webhook URLを取得

# 簡単な説明
- 以下の処理をするLambda関数を作成
    - 請求情報の取得
    - LINEに請求情報を送信
    - Slackに請求情報を送信
- CloudWatch の CloudWatch Events で、通知するLambda関数と通知タイミングを設定

# ハマりポイント（あとで出てきます）
- 請求情報を取得する時のRegion
- Lambda に設定する実行ロール
- LINE messagingAPI へのPOSTデータの型
- CloudWatchのcron式

# 作成したLambda関数（順に説明します）
```python:index.py
import boto3
import json
import datetime
import os
import urllib.request
_env = os.environ

#メイン処理
def lambda_handler(event, context):
    #請求情報取得
    _billing = getBillingMetric()
    _billing["message"] = "今月の請求金額は $" + str(_billing['Datapoints'][0]['Maximum']) + " だよ！"
    #LINEに送信
    sendNoticeToLine(_billing)
    #Slackに送信
    sendNoticeToSlack(_billing)

def getBillingMetric():
    _cw = boto3.client('cloudwatch', region_name=_env["REGION"])
    _billing = _cw.get_metric_statistics(
        Namespace='AWS/Billing',
        MetricName='EstimatedCharges',
        Dimensions=[{'Name': 'Currency', 'Value': 'USD'}],
        StartTime=datetime.datetime.today() - datetime.timedelta(days=1),
        EndTime=datetime.datetime.today(),
        Period=86400,
        Statistics=['Maximum'])

    print(_billing)
    return _billing

def sendNoticeToLine(_bill):
    _url = 'https://api.line.me/v2/bot/message/push'
    _data = json.dumps({
        "to": _env["LINE_USER_ID"]
        ,"messages": [{"type": "text", "text": _bill["message"]}]
    }).encode()
    _header = {
      "Content-type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + _env["LINE_ACCESS_TOKEN"]
    }

    print(_data)
    sendRequest(_url, _data, _header)

def sendNoticeToSlack(_bill):
    _url = 'https://hooks.slack.com/services/' + _env["SLACK_URL"]
    _data = json.dumps({
        "text": _bill["message"]
    }).encode()
    _header = {
      "Content-type": "application/json"
    }
    print(_data)
    sendRequest(_url, _data, _header)

def sendRequest(_url, _data, _header):
    _req = urllib.request.Request(_url, _data, _header)
    try:
        with urllib.request.urlopen(_req) as _res:
            _body = _res.read()
            print(_body)
    except urllib.error.HTTPError as _err:
        print("HTTPError: " + str(_err.code))
        print(_err)
    except urllib.error.URLError as _err:
        print("HTTPError: " + _err.reason)
        print(_err)

```
### 環境変数
| キー     |    値    |
|:---------|:-----------|
|REGION    |"us-east-1"(固定)|
|SLACK_URL |IncommingWebhookのWebhook Url（ドメイン以下）|
|TZ        |"Asia/Tokyo"|
## メイン処理
- getBillingMetric：請求情報の取得
- sendNoticeToLine：LINEに請求情報を送信
- sendNoticeToSlack：Slackに請求情報を送信

※ [testable](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/best-practices.html) にするため、それぞれの関数を別に分けている

```[L8−L16]_index.py
#メイン処理
def lambda_handler(event, context):
    #請求情報取得
    _billing = getBillingMetric()
    _billing["message"] = "今月の請求金額は $" + str(_billing['Datapoints'][0]['Maximum']) + " だよ！"
    #LINEに送信
    sendNoticeToLine(_billing)
    #Slackに送信
    sendNoticeToSlack(_billing)
```

## 請求情報の取得
- boto3でCloudWatch用のClientを作成
    - <font color="red">[ハマりポイント]</font> [Regionは **us-east-1** で固定](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html)（Lambdaの共通関数で指定）
- get_metric_statistics関数で請求情報を取得
- 請求情報を呼び出し元に返却
- 現在日-1 から 現在日までの最大の値（＝1日は昨月〆、2日以降は当日までの請求額となる）

```[L18−L30]_index.py
def getBillingMetric():
    _cw = boto3.client('cloudwatch', region_name=_env["REGION"])
    _billing = _cw.get_metric_statistics(
        Namespace='AWS/Billing',
        MetricName='EstimatedCharges',
        Dimensions=[{'Name': 'Currency', 'Value': 'USD'}],
        StartTime=datetime.datetime.today() - datetime.timedelta(days=1),
        EndTime=datetime.datetime.today(),
        Period=86400,
        Statistics=['Maximum'])

    print(_billing)
    return _billing
```

## LambdaにIAMロールを設定する
- 一旦、ここまでで請求情報が取得できるかを確認するため、Lambdaに実行ロールを設定する
- <font color="red">[ハマりポイント]</font>　請求情報を参照する権限ではなく、[CloudWatchのメトリクスを参照する権限が必要](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/iam-identity-based-access-control-cw.html)
- 上記URLの例のまま、以下をインラインポリシーに追加したロールを指定

```
{
  "Version": "2012-10-17",
  "Statement":[{
      "Effect":"Allow",
      "Action":["cloudwatch:GetMetricStatistics","cloudwatch:ListMetrics"],
      "Resource":"*",
      "Condition":{
         "Bool":{
            "aws:SecureTransport":"true"
            }
         }
      }
   ]
}
```



## リクエスト送信用の共通関数
- LINEのPUSH API、SlackのIncomming Webhooksを呼び出すときに使うリクエスト送信処理を共通化する
- 関数は [urllib.request.Request](https://docs.python.jp/3/library/urllib.request.html#urllib.request.Request) を使う
    - 第一引数には送信先URLを指定
    - 第二引数には送信データを指定
    - 第三引数には送信ヘッダを指定


```[L57−L68]_index.py
def sendRequest(_url, _data, _header):
    _req = urllib.request.Request(_url, _data, _header)
    try:
        with urllib.request.urlopen(_req) as _res:
            _body = _res.read()
            print(_body)
    except urllib.error.HTTPError as _err:
        print("HTTPError: " + str(_err.code))
        print(_err)
    except urllib.error.URLError as _err:
        print("HTTPError: " + _err.reason)
        print(_err)
```



## LINEに請求情報を送信
- [APIのURLはv2のもの](https://developers.line.me/ja/reference/messaging-api/#anchor-0c00cb0f42b970892f7c3382f92620dca5a110fc)
- <font color="red">[ハマりポイント]</font>　POSTデータに送信データを設定する
    - toにLINEのユーザIDを**文字列形式**で指定する（Lambdaの環境変数
で設定）
    - messagesにメッセージを**配列形式**で指定する
- Authorizationヘッダに "Bearer " + LINEのアクセストークンを指定する（Lambdaの環境変数
で設定）
- リクエスト送信処理を呼び出して終了

```[L32−L44]_index.py
def sendNoticeToLine(_bill):
    _url = 'https://api.line.me/v2/bot/message/push'
    _data = json.dumps({
        "to": _env["LINE_USER_ID"]
        ,"messages": [{"type": "text", "text": _bill["message"]}]
    }).encode()
    _header = {
      "Content-type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + _env["LINE_ACCESS_TOKEN"]
    }

    print(_data)
    sendRequest(_url, _data, _header)
```

## Slackに請求情報を送信
- URLはIncomming Webhooks の Webhook URL を指定（Lambdaの環境変数
で設定）
- POSTデータとヘッダはシンプルなもの
- リクエスト送信処理を呼び出して終了

```[L46−L55]_index.py
def sendNoticeToSlack(_bill):
    _url = 'https://hooks.slack.com/services/' + _env["SLACK_URL"]
    _data = json.dumps({
        "text": _bill["message"]
    }).encode()
    _header = {
      "Content-type": "application/json"
    }
    print(_data)
    sendRequest(_url, _data, _header)
```

## Lambda関数をテスト実行し、正常に動作することを確認する
- エラーが出ていたら、エラーメッセージ、エラーコードから原因を切り分けてシューティング
    - IAM権限周り
    - LINEmessagingAPI
    - Slack Incomming Webhooks
- 私はIAM権限と、LINEの400（リクエスト形式エラー）でハマりました

# CloudWatch の CloudWatch Events で通知するLambda関数と通知タイミングを設定
#### CloudWatch の イベント を選択し、「ルールの作成」を選択する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/217144/2b918e82-3ab9-a66e-5c9b-611b263dff16.png)

#### スケジュールイベントを作成し、Cron式で記述
- <font color="red">[ハマりポイント]</font> [CloudWatchで日と曜日を指定するときは**どちらかに?**をつかう](https://www.kabegiwablog.com/entry/2018/05/23/100000)
- 以下のキャプチャは毎日GMT 0時（日本時間では9時）にLambda関数を実行する場合
- Cron式は[分 時 日 月 曜日 年]の6項目を半角空白で区切ったもの（Syntaxに問題がなければ、以後10回のトリガー日が表示される）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/217144/2f539cde-a49a-cdcb-e436-fca207b8dbbe.png)

#### 次のページでイベントに名前と詳細説明を付けて終了

# 振り返り
- リクエスト送信処理は大体の外部APIを呼び出すときに個別で指定する項目なので、共通関数化しようと思う
- IAMロールへの理解が足りないので、慣れるまでチャレンジする