AWS の請求金額をモニタリングする

前回の記事、[AWS EC2 インスタンスが何台起動しているかをモニタリングする](https://qiita.com/aokad/items/eb58d9f503254a7bdf54) では AWS で起動しているインスタンスの数を定期的にカウントできるようにしました。
そうしたところ、やっぱり実際の請求金額も見たいというご要望がありましたので、今回はサービスごとの請求金額を次のような積み上げグラフとしてモニタリングできるようにします。

![aws-billing1.PNG](https://qiita-image-store.s3.amazonaws.com/0/226829/5dace2dd-077b-8382-1a1d-df3a47a7d2ce.png)

 - AWS からの請求金額を定期的に集計します。
 - 集計は請求金額の大半を占める EC2 と S3、油断したときに発生するデータ転送量、それ以外で分けるようにします。

※ しきい値を決めて通知するための設定は今回は省略します。[前回](https://qiita.com/aokad/items/eb58d9f503254a7bdf54)と同様ですので、lambda関数を差し替えればよいです。

## 1. AWS Lambda 関数を作成する

### 1.1 設定

以下設定で新しくlambda 関数を作成します。
※ 詳細な手順は、前回記事 [AWS Lambda 関数の実装](https://qiita.com/aokad/items/eb58d9f503254a7bdf54#1-aws-lambda-%E9%96%A2%E6%95%B0%E3%81%AE%E5%AE%9F%E8%A3%85) を参考にしてください。

 - 名前：monitoring_billing
 - ランタイム：Python 2.7
 - ロール：「カスタムロールの作成」を選択して以下の権限を与えてください
     * "ce:GetDimensionValues"
     * "ce:GetCostAndUsage"
     * "cloudwatch:PutMetricData"

今回は AWS Cost Explorer (ce) サービスから請求情報を取得するための権限と、Cloudwatch メトリクスにデータを登録する権限が必要です。

### 1.2 コードの編集

このコードで lambda_function を置き換えます。

<details><summary>lambda_function (クリックして表示してください)</summary><div>

```Python:lambda_function
# -*- coding: utf-8 -*-
"""
Created on Tue Sep 11 13:22:20 2018

@author: Okada
"""

import json
import boto3
import datetime

def get_amount(response):
    return float(response["ResultsByTime"][0]["Total"]["UnblendedCost"]["Amount"])

def get_services(start_day, end_day):
    response = boto3.client('ce').get_dimension_values(
        TimePeriod = {"Start": start_day, "End": end_day}, 
        Dimension = 'USAGE_TYPE_GROUP',
        Context = 'COST_AND_USAGE'
    )
    
    data_transfer = []
    ec2 = []
    s3 = []
    for item in response["DimensionValues"]:
        service = item["Value"].split(":")
        if "Data Transfer - " in service[1]:
            data_transfer.append(item["Value"])
            
        elif service[0].lower() == "ec2":
            ec2.append(item["Value"])
            
        elif service[0].lower() == "s3":
            s3.append(item["Value"])

    return [data_transfer, ec2, s3]

def get_cost_and_usage (start_day, end_day, services=[]):
    
    if len(services) == 0:
        return (boto3.client('ce').get_cost_and_usage(
            TimePeriod = {"Start": start_day, "End": end_day}, 
            Granularity = "MONTHLY", 
            Metrics = ["UnblendedCost"]
         ))
        
    return (boto3.client('ce').get_cost_and_usage(
        TimePeriod = {"Start": start_day, "End": end_day}, 
        Granularity = "MONTHLY", 
        Metrics = ["UnblendedCost"],
        Filter = {"Dimensions": {"Key": "USAGE_TYPE_GROUP", "Values": services}}
    ))

def put_metric_data(service, value):
    return (boto3.client("cloudwatch").put_metric_data(
        Namespace="COST_MONITOR", 
        MetricData=[{
            "MetricName": "service-billing", 
            "Dimensions": [{"Name":"service", "Value": service}], 
            "Value": value, "Unit": "None"
        }]
    ))

def lambda_handler(event, context):
    
    now = datetime.datetime.utcnow()
    
    start_dt = now - datetime.timedelta(days = 1)
    start_day = "%d-%02d-%02d" % (start_dt.year, start_dt.month, 1)
    end_day = now.strftime("%Y-%m-%d")
    
    [data_transfer, ec2, s3] = get_services(start_day, end_day)
    status = {"Start": start_day, "End": end_day, "Unit": "USD", "Values": {}}
    target = {"Data Transfer": data_transfer, "Elastic Compute Cloud": ec2, "Simple Storage Service": s3}
    
    known_amount = 0.0
    for label in target:
        response = get_cost_and_usage (start_day, end_day, target[label])
        amount = get_amount(response)
        put_metric_data(label, amount)
        status["Values"][label] = amount
        known_amount += amount
        
    response = get_cost_and_usage (start_day, end_day)
    amount = get_amount(response)
    other_amount = amount - known_amount
    put_metric_data("other", other_amount)
    status["Values"]["other"] = other_amount
    status["Values"]["total"] = sum(status["Values"].values())
    status["Total"] = amount
    
    return {
        "statusCode": 200,
        "body": json.dumps(status)
    }

```
</div></details>

### 1.3 コードの解説

この項目は設定には影響しませんが、興味があれば読んでみてください。

今回作成したコードは以下の流れになっています。

 - 1. 請求の集計期間を月初めから今日までと定義する
 - 2. EC2、S3、データ転送の金額を個別に集計する
   * 2.1. サービスの請求金額を取得
   * 2.2. サービスの請求金額をメトリクスに登録
   * 2.3. メトリクス登録済み請求金額を覚えておく
 - 3. AWS 全体の請求金額を取得
 - 4. (サービスの請求金額 - 登録済み請求金額) を Other としてメトリクスに登録

AWS Cost Explorer サービスを利用してサービスごとの請求金額を取得するには以下の順に行います。
まず、請求が発生しているサービスを取得します。
集計期間は月初めから今日までとします。
このとき、サービスの区分は請求書と同じにしたいので、'USAGE_TYPE_GROUP' を指定します。

```Python:get_services関数
import boto3

start_day = "2018-10-01"
end_day = "2018-10-12"
response_service = boto3.client('ce').get_dimension_values(
    TimePeriod = {"Start": start_day, "End": end_day}, 
    Dimension = 'USAGE_TYPE_GROUP',
    Context = 'COST_AND_USAGE'
)
```

問い合わせの結果を見てみます。
`u'Value': ` の項目がサービス名です。

```Python
>>> import pprint
>>> pprint.pprint(response_service)
{u'DimensionValues': [{u'Attributes': {u'unit': u'GB'},
                       u'Value': u'EC2: Data Transfer - CloudFront (Out)'},
                      {u'Attributes': {u'unit': u'GB'},
                       u'Value': u'EC2: Data Transfer - Inter AZ'},
                      {u'Attributes': {u'unit': u'GB'},
                       u'Value': u'EC2: Data Transfer - Internet (In)'},
...
                      {u'Attributes': {u'unit': u'GB'},
                       u'Value': u'S3: Data Transfer - Region to Region (In)'},
                      {u'Attributes': {u'unit': u'GB'},
                       u'Value': u'S3: Data Transfer - Region to Region (Out)'},
                      {u'Attributes': {u'unit': u'GB-Month'},
                       u'Value': u'S3: Storage - Standard'}],
 'ResponseMetadata': {...},
 u'ReturnSize': 19,
 u'TotalSize': 19}
```

今回はデータ転送、EC2、 S3 で集計を分けたいので、それぞれのサービス名をリストアップします。
まず、サービス名に `Data Transfer -` と入っているものをデータ転送とします。
残りのうち、`S3: ` で始まるものを S3、`EC2: ` で始まるものを EC2 として分けます。
それ以外のサービスは最後にまとめて数えるため、ここではリストにいれません。

※この分類は請求書と照らし合わせながら筆者が独断で決めました。AWSの仕様変更によっては今後変わる可能性が大いにあります。

```Python:get_services関数
data_transfer = []
ec2 = []
s3 = []
for item in response["DimensionValues"]:
    service = item["Value"].split(":")
    if "Data Transfer - " in service[1]:
        data_transfer.append(item["Value"])
        
    elif service[0].lower() == "ec2":
        ec2.append(item["Value"])
        
    elif service[0].lower() == "s3":
        s3.append(item["Value"])
```

次にサービスを指定して請求金額を取得します。
`Granularity = "MONTHLY"` オプションで月ごとの集計になります。`"DAILY"` とすれば日ごとの集計になります。
サービスを指定する場合は `Filter` オプションに前の関数で作成したサービスのリストを渡します。 `Filter` オプションを指定しない場合は全サービスの請求金額が取得できます。

```Python:get_cost_and_usage関数
services = ['S3: API Requests - Standard',
    'S3: Data Transfer - Region to Region (In)',
    'S3: Data Transfer - Region to Region (Out)',
    'S3: Storage - Standard']

response_cost = boto3.client('ce').get_cost_and_usage(
    TimePeriod = {"Start": start_day, "End": end_day}, 
    Granularity = "MONTHLY", 
    Metrics = ["UnblendedCost"],
    Filter = {"Dimensions": {"Key": "USAGE_TYPE_GROUP", "Values": services}}
)
```

このように返ってきます。

```Python
>>> pprint.pprint(response_cost)
{'ResponseMetadata': {...},
 u'ResultsByTime': [{u'Estimated': True,
                     u'Groups': [],
                     u'TimePeriod': {u'End': u'2018-10-12',
                                     u'Start': u'2018-10-01'},
                     u'Total': {u'UnblendedCost': {u'Amount': u'175.0786809984',
                                                   u'Unit': u'USD'}}}]}
```

最後に取得した金額をメトリクスに登録します。

```Python:put_metric_data関数

service = "Simple Storage Service"
value = float(response_cost["ResultsByTime"][0]["Total"]["UnblendedCost"]["Amount"])

boto3.client("cloudwatch").put_metric_data(
    Namespace="COST_MONITOR", 
    MetricData=[{
       "MetricName": "service-billing", 
       "Dimensions": [{"Name":"service", "Value": service}], 
       "Value": value, "Unit": "None"
    }]
))
```

## 2. モニタリング設定

作成した AWS Lambda 関数を定期的に呼び出すようにします。

### 2.1 CloudWatch ルール

 - イベントソース：スケジュール 6 時間 ごと
 - ターゲット：Lambda 関数 monitoring_billing （今回作成したlambda関数）

AWS 請求金額は即時反映されないようですので日に数回更新すれば十分だと思います。

ここで登録したメトリクスは CloudWatchメトリクス → COST_MONITOR → service と移動し、すべてにチェックを入れるとグラフ表示することができます。
積み上げ表示にしたいので右上のメニューから「折れ線グラフ」を「スタックエリア」に変更します。

## 3. まとめ

### 3.1 グラフを確認する

作成したグラフと実際の請求書を見比べてみます。

まず、グラフにカーソルを当てて詳細を確認します。
![aws-billing-zoom.png](https://qiita-image-store.s3.amazonaws.com/0/226829/11e440a2-d843-dc1f-add6-352ee1618b35.png)

次に実際の請求書を確認します。
![aws-billing-table.png](https://qiita-image-store.s3.amazonaws.com/0/226829/88d7739d-c9db-3bb7-873d-89b0c1c7929f.png)

テーブルで金額を表してみます。ほぼ一致していることが確認できます。

|項目|グラフ|請求書|
|:--|-----:|----:|
|Data Transfer|17.2|17.19|
|Elastic Compute Cloud|80.4|80.39|
|Simple Storage Service|8.05|8.05|
|Other|90.7|90.68(*)|

(*) 196.31(総額) - (17.19+80.39+8.05)

### 3.2 インスタンス起動数と並べて表示する

CloudWatch ダッシュボードを作成して、前回作成したインスタンス起動数のグラフと並べてみます。
当然ながら、インスタンス起動数と連動して Elastic Compute Cloud の金額も上昇しているように見えます。
![aws-billing.PNG](https://qiita-image-store.s3.amazonaws.com/0/226829/2bafed6b-b074-99ea-5b05-e0305cf0211c.png)

以上です。