LambdaでAWSの料金を毎日Slackに通知する（Python3）

#はじめに
個人アカウントは基本的に無料枠で運用しているので、少しでも請求がある場合はいち早く気づきたいです。
先日、とあるハンズオンイベントで使ったリソースを消し忘れて、最終的に$30ぐらい請求が来てしまいました。。。

[CloudWatchで請求アラートは設定していますが](http://docs.aws.amazon.com/ja_jp/awsaccountbilling/latest/aboutv2/monitor-charges.html)、閾値超えが想定の場合、当然見逃すことになり、最終的な請求額に驚くハメになります。

これを防ぐためにLambdaで毎日SlackにAWS料金を通知することにします。

先日LambdaがPython3に対応したので、せっかくだし勉強がてらPython3で実装したい。
ネット上にはNode.jsでの実装例が多いようで、今回は[こちら](http://qiita.com/saku/items/fc6b70a420a5c510de2b)を参考にPython3で実装してみます。

#必要なもの
- Slack
    - incoming-webhooks URL
        - 取得については[こちら](http://qiita.com/vmmhypervisor/items/18c99624a84df8b31008)を参考に
    - 適当なchannel
- lambda-uploader
    - requestsモジュールをLambda上でimportするために利用
        - カレントディレクトリにモジュールをインストールして、モジュールごとZipに固めてアップロードでもいけるはずですが、私の環境だとうまくいかなかったので
- aws cli
    - lambda-uploaderで必要
- AWS
    - Lambda関数用IAM Role
        - `CloudWatchReadOnlyAccess`ポリシーをアタッチ
    - [請求アラートの有効化](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html#turning_on_billing_metrics)
       - これを行っておかないと請求情報のCloudWatchのメトリクスが生成されません
       - <img width="1197" alt="スクリーンショット 2018-03-28 0.25.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/135986/01680754-2991-0b73-ce73-f1badc570a40.png">



#事前準備
## ローカル
### lambda-uploaderをインストール
```
$ pip install lambda-uploader 
```
[こちら](http://dev.classmethod.jp/cloud/deploy-aws-lambda-python-with-lambda-uploader/)を参考にさせていただきました。

###aws cliをインストール
```bash:
$ pip install awscli
```
###credential、リージョン設定
```bash:
$ aws configure

確認
$ aws configure list
```

## AWS(Lambda)
コードを`lambda-uploader`でデプロイするためには、あらかじめ空の関数を用意する必要があります。

###関数を作成
<img width="970" alt="スクリーンショット 2018-03-28 10.35.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/135986/c5f14b48-6368-ba5f-363f-8f4d02e3c10d.png">

###IAMロール修正
関数定義の際にポリシーテンプレートから自動作成されたIAMロールに`CloudWatchReadOnlyAccess`ポリシーをアタッチします。コスト情報をLambdaから読み込むのに必要です。
ロールARNはあとで必要になるのでメモしておきます。

<img width="584" alt="スクリーンショット 2018-03-28 10.42.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/135986/3a4e48d3-7142-70f5-5eb9-9258e8bf499d.png">
![スクリーンショット 2018-03-28 10.42.25.png](https://qiita-image-store.s3.amazonaws.com/0/135986/3a597b65-9583-2c44-6918-af814b192b02.png)


#コード
##ディレクトリ構成
ディレクトリ名は任意です。関数名とは無関係です。

```bash:ディレクトリ構成
awscost_to_slack/
|--lambda_function.py
|--requirements.txt
|--lambda.json
```

##lambda_function.py
超過金額に応じて色をつけるようにしています。
\$0.0なら緑、超過したら黄色、\$10超えで赤になります。

```Python:lambda_function.py
#!/usr/bin/env python
# encoding: utf-8

import json
import datetime
import requests
import boto3
import os
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Slack の設定
SLACK_POST_URL = os.environ['slackPostURL']
SLACK_CHANNEL = os.environ['slackChannel']

response = boto3.client('cloudwatch', region_name='us-east-1')

get_metric_statistics = response.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'])

cost = get_metric_statistics['Datapoints'][0]['Maximum']
date = get_metric_statistics['Datapoints'][0]['Timestamp'].strftime('%Y年%m月%d日')

def build_message(cost):
    if float(cost) >= 10.0:
        color = "#ff0000" #red
    elif float(cost) > 0.0:
        color = "warning" #yellow
    else:
        color = "good"    #green

    text = "%sまでのAWSの料金は、$%sです。" % (date, cost)

    atachements = {"text":text,"color":color}
    return atachements

def lambda_handler(event, context):
    content = build_message(cost)

    # SlackにPOSTする内容をセット
    slack_message = {
        'channel': SLACK_CHANNEL,
        "attachments": [content],
    }

    # SlackにPOST
    try:
        req = requests.post(SLACK_POST_URL, data=json.dumps(slack_message))
        logger.info("Message posted to %s", slack_message['channel'])
    except requests.exceptions.RequestException as e:
        logger.error("Request failed: %s", e)
```
##requirements.txt
pip installしたいモジュール名を書きます。

```requirements.txt
requests
```
##lambda.json
Lambda関数名、IAMロールのARNなどは環境に合わせてください。
用意したSlackの`incoming-webhooks URL`と通知するchannelを関数に渡す環境変数として`variables`に定義します。
また、スクリプト本体のファイル名とハンドラの前半(サンプルコードではデフォルト`lambda_function`)を一致させないと動きません。地味にハマるので注意！

```JSON:lambda.json
{
  "name": "LAMBDA_FUNCTION_NAME",
  "description": "DESCRIPTION",
  "region": "ap-northeast-1",
  "handler": "lambda_function.lambda_handler",
  "role": "arn:aws:iam::XXXXXXX:role/ROLE_NAME_FOR_LUMBDA",
  "timeout": 300,
  "memory": 128,
  "variables":
    {
      "slackPostURL":"https://hooks.slack.com/services/XXX",
      "slackChannel":"#XXX"
    }
}
```
##デプロイ
上記ファイルを配置したディレクトリに移動して、lambda-uploaderを実行します。

```bash:
$ cd awscost_to_slack
$ lambda-uploader
Î» Building Package
Î» Uploading Package
Î» Fin
```


##Lambda定期実行設定
CloudWatchのスケジュールイベントを定義して、lambda関数をターゲットに指定します。
時刻はUTCなので注意しましょう。
毎日UTC0:00に実行されるよう設定しました。

<img width="818" alt="スクリーンショット 2018-03-28 10.38.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/135986/e3516ca7-1760-74f4-c3d4-0d291efabfbe.png">


##実行イメージ
![スクリーンショット 2017-06-23 14.15.54.png](https://qiita-image-store.s3.amazonaws.com/0/135986/859b7706-1074-ca7b-31d8-17f886d753cf.png)
こんな感じで毎朝9:00に通知がきます。
今日も無料！

#まとめ
Lambdaはほぼ無料でプログラムが動かせるので楽しいですね！
Python初心者なのでコードが見苦しい点はご容赦ください。