AWSの利用料金をPythonからSlackに投稿する

Pythonのお勉強のため、AWSの利用料金をSlackに投稿するようにしたメモ。

## 参考リンク

以下の先人の記事を参考に作成。

- [LambdaでAWSの料金を毎日Slackに通知する（Python3）](https://qiita.com/isobecky74/items/88e8e0dcb0ee224a31e4)

## 動作イメージ

<img width="349" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/233c7fae-bdeb-526e-9141-b206680e3f5a.png">

## Slackの準備

何かと使えそうだし個人用Slackを作ることにする。

- [個人 Slack のススメ](https://qiita.com/saitotak/items/ac0eb7ddc0d8d83cbe91)

### Webhook URLの取得

以下を参考にWebhook URLを取得する。

- [SlackのWebhook URL取得手順](https://qiita.com/vmmhypervisor/items/18c99624a84df8b31008)

### curlでテスト

curlで投稿をテストする。

```shell
CHANNEL="#sandbox"
WEBHOOK_URL="https://hooks.slack.com/services/hogehoge"
MESSAGE="テストメッセージ"
curl -X POST ${WEBHOOK_URL} \
  --data-urlencode "payload=
  {\"channel\": \"${CHANNEL}\",
   \"text\": \"${MESSAGE}\"}"
```

以下のように投稿されればOK。

<img width="265" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/03e4e880-3eb4-c608-af59-d8b2b9b64024.png">

## AWS CLI

Pythonの前にAWS CLIで料金を取得してみる。

AWS CLIをセットアップする。

```shell
pip3 install awscli
aws configure
```

料金を確認する。

```shell:コマンド
aws cloudwatch get-metric-statistics \
  --region us-east-1 \
  --namespace "AWS/Billing" \
  --metric-name "EstimatedCharges" \
  --start-time "2019-01-01T00:00:00Z" \
  --end-time "2019-01-02T00:00:00Z" \
  --period 86400 \
  --statistics Maximum \
  --dimensions Name=Currency,Value=USD
```

```shell-session:実行例
$ aws cloudwatch get-metric-statistics \
>   --region us-east-1 \
>   --namespace "AWS/Billing" \
>   --metric-name "EstimatedCharges" \
>   --start-time "2019-01-01T00:00:00Z" \
>   --end-time "2019-01-02T00:00:00Z" \
>   --period 86400 \
>   --statistics Maximum \
>   --dimensions Name=Currency,Value=USD
EstimatedCharges
DATAPOINTS	0.45	2019-01-01T00:00:00Z	None
$
```

## Python + AWS Lambda

AWS Lambda上のPythonからSlackに投稿する。

### IAM Roleの作成

[IAMコンソール](https://console.aws.amazon.com/iam/home#/roles)でRoleを作成し、`AWSLambdaBasicExecutionRole`と`CloudWatchReadOnlyAccess`のポリシーをアタッチする。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/3c979139-9fa4-f0b0-4ace-5509afe56274.png)

作成したRoleのARNを確認しておく。

### コード作成

作成するファイルは以下の3つ。→[GitHub](https://github.com/sotoiwa/aws_cost_checker)

```
.
├── lambda.json
├── lambda_function.py
└── requirements.txt
```

`lambda.json`はlambda-uploderが使用するJSON形式のLambda関数の定義ファイル。先ほど作成したRoleのARN、Slack Webhook URL、Slackチャンネル名を指定する。lambda-uploaderの使い方と、jsonの書き方は以下を参照。

- [lambda-uploader](https://github.com/rackerlabs/lambda-uploader)

```json:lambda.json
{
  "name": "aws_cost_checker",
  "description": "My AWS cost checker.",
  "region": "ap-northeast-1",
  "runtime": "python3.7",
  "handler": "lambda_function.lambda_handler",
  "role": "arn:aws:iam::hogehoge",
  "timeout": 300,
  "memory": 128,
  "variables":
    {
      "SLACK_WEBHOOK_URL": "https://hooks.slack.com/services/hogehoge",
      "SLACK_CHANNEL": "#hogehoge"
    }
}
```

`lambda_function.py`が関数本体。以下を参考に作成。Low-Level ClientではなくResourceクラスを使ってみる。

- [Boto3に再入門してみた](https://qiita.com/shiimaxx/items/e436c1e0019f0497b5c4)
- [CloudWatch - Boto 3 Docs](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudwatch.html)
- [Incoming Webhooks - Slack API](https://api.slack.com/incoming-webhooks)

```python:lambda_function.py
import datetime
import json
import os

import boto3
import pytz
import requests

SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']
SLACK_CHANNEL = os.environ['SLACK_CHANNEL']


def get_cost():
    """boto3を使って直近の1日の最大の予想請求額を取得する

    :return: response
    """

    now = datetime.datetime.now()

    # Low-Level Clientの場合
    #
    # client = boto3.client('cloudwatch', region_name='us-east-1')
    # response = client.get_metric_statistics(
    #     Namespace='AWS/Billing',
    #     MetricName='EstimatedCharges',
    #     Dimensions=[
    #         {
    #             'Name': 'Currency',
    #             'Value': 'USD'
    #         }
    #     ],
    #     StartTime = now - datetime.timedelta(days=1),
    #     EndTime = now,
    #     Period=86400,
    #     Statistics=['Maximum']
    # )

    cloudwatch = boto3.resource('cloudwatch', region_name='us-east-1')
    metric = cloudwatch.Metric('AWS/Billing', 'EstimatedCharges')
    response = metric.get_statistics(
        Dimensions=[
            {
                'Name': 'Currency',
                'Value': 'USD'
            },
        ],
        StartTime=now - datetime.timedelta(days=1),
        EndTime=now,
        Period=86400,
        Statistics=['Maximum']
    )

    return response


def build_message(response):
    """SlackにPOSTするメッセージボディを作成する

    :param response:
    :return: message
    """

    cost = response['Datapoints'][0]['Maximum']
    timestamp = (response['Datapoints'][0]['Timestamp'] + datetime.timedelta(days=1)).astimezone(
        pytz.timezone('Asia/Tokyo')).strftime(
        '%Y年%m月%d日%H時%M分')

    text = '{}までのAWSの料金'.format(timestamp)
    attachment_text = '${}'.format(cost)

    if float(cost) >= 100.0:
        color = 'danger'   # red
    elif float(cost) >= 10.0:
        color = 'warning'  # yellow
    else:
        color = 'good'     # green

    atachements = {'text': attachment_text, 'color': color}

    message = {
        'text': text,
        'channel': SLACK_CHANNEL,
        'attachments': [atachements],
    }

    return message


def post_message(message):
    """SlackにPOSTする

    :param message:
    :return:
    """

    response = requests.post(SLACK_WEBHOOK_URL, data=json.dumps(message))
    response.raise_for_status()


def lambda_handler(event, context):

    response = get_cost()

    message = build_message(response)

    post_message(message)
```

`requirements.txt`は使用するモジュールの定義。

```:requirements.txt
boto3
pytz
requests
```

### アップロード

lambda-uploaderをインストールする。

```
pip3 install lambda-uploader 
```

アップロードする。

```
$ lambda-uploader
Î» Building Package
Î» Uploading Package
Î» Fin
$
```

### スケジュール実行

lambda-uploaderによってLambda関数が作成されるので、[Lambdaコンソール](https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1)からトリガーとしてCloudWatch Eventsを追加する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/7a20f182-2aa7-d47e-3fc9-438470934843.png)

スケジュールを設定する。とりあえず`cron(* * * * ? *)`毎分実行するようにする。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/9c97a30b-efe4-7f0d-63e5-4cade4ff1c24.png)

Slackに投稿されることを確認する。

<img width="349" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/2a69d4d4-7a30-e6d3-0ce9-4c430d162d1e.png">

上手く投稿できなかったら、CloudWatch Logsでログを確認する。上手く投稿が確認できたら、スケジュールを`cron(0 15 * * ? *)`（日本時間の0時）に変更する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/86449/3331c118-253d-eb37-0085-5687a5d414e8.png)