GPUインスタンスの料金が怖いので、サービス毎の請求額を毎日LINEに通知してみた


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62413/576ae14d-a890-0ed8-f63f-7d1318b5e3c9.png)


## 概要

先日、AWSの利用料金をSlackで飛ばす記事を書いた。

Slackだと気づかないこともあるので、Lineで送ることにした.

### 参考文献

前回記事 - [GPUインスタンスの料金が怖いので、サービス毎の請求額を毎日Slackに通知してみた - Qiita](https://qiita.com/peace098beat/items/6b6db403a0cc822ea58f)

ソースコードの元記事 - [AWSサービス毎の請求額を毎日Slackに通知してみた ｜ DevelopersIO](https://dev.classmethod.jp/cloud/aws/notify-slack-aws-billing/)


## 実装結果

LineにてAWSの使用料金が送られてきていることを確認した。便利だ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62413/2c7994c4-8701-fa73-2316-91dd6aa0cc33.png)


## コード

`LINE Notify` へのPostを追加したコードは以下

```py
import json
import os
import boto3
import urllib.request
from datetime import datetime, timedelta, date
 
 
SLACK_WEBHOOK_URL = os.environ['SLACK_WEBHOOK_URL']
LINE_TOKEN = os.environ['LINE_TOKEN']
 

def lambda_handler(event, context):
    client = boto3.client('ce', region_name='us-east-1')
 
    # 合計とサービス毎の請求額を取得する
    total_billing = get_total_billing(client)
    service_billings = get_service_billings(client)
 
    # Slack用のメッセージを作成
    (title, detail) = get_message(total_billing, service_billings)

    # Slackへ通知
    post_slack(title, detail)
    # Lineへ通知
    post_line(title, detail)
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }


def get_total_billing(client) -> dict:
    (start_date, end_date) = get_total_cost_date_range()
 
    # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ce.html#CostExplorer.Client.get_cost_and_usage
    response = client.get_cost_and_usage(
        TimePeriod={
            'Start': start_date,
            'End': end_date
        },
        Granularity='MONTHLY',
        Metrics=[
            'AmortizedCost'
        ]
    )
    return {
        'start': response['ResultsByTime'][0]['TimePeriod']['Start'],
        'end': response['ResultsByTime'][0]['TimePeriod']['End'],
        'billing': response['ResultsByTime'][0]['Total']['AmortizedCost']['Amount'],
    }
 
 
def get_service_billings(client) -> list:
    (start_date, end_date) = get_total_cost_date_range()
 
    # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ce.html#CostExplorer.Client.get_cost_and_usage
    response = client.get_cost_and_usage(
        TimePeriod={
            'Start': start_date,
            'End': end_date
        },
        Granularity='MONTHLY',
        Metrics=[
            'AmortizedCost'
        ],
        GroupBy=[
            {
                'Type': 'DIMENSION',
                'Key': 'SERVICE'
            }
        ]
    )
 
    billings = []
 
    for item in response['ResultsByTime'][0]['Groups']:
        billings.append({
            'service_name': item['Keys'][0],
            'billing': item['Metrics']['AmortizedCost']['Amount']
        })
    return billings
 
 
def get_message(total_billing: dict, service_billings: list) -> (str, str):
    start = datetime.strptime(total_billing['start'], '%Y-%m-%d').strftime('%m/%d')
 
    # Endの日付は結果に含まないため、表示上は前日にしておく
    end_today = datetime.strptime(total_billing['end'], '%Y-%m-%d')
    end_yesterday = (end_today - timedelta(days=1)).strftime('%m/%d')
 
    total = round(float(total_billing['billing']), 2)
 
    title = f'{start}～{end_yesterday}の請求額は、{total:.2f} USDです。'
 
    details = []
    for item in service_billings:
        service_name = item['service_name']
        billing = round(float(item['billing']), 2)
 
        if billing == 0.0:
            # 請求無し（0.0 USD）の場合は、内訳を表示しない
            continue
        details.append(f'　・{service_name}: {billing:.2f} USD')
 
    return title, '\n'.join(details)
 
 
 
def get_total_cost_date_range() -> (str, str):
    start_date = get_begin_of_month()
    end_date = get_today()
 
    # get_cost_and_usage()のstartとendに同じ日付は指定不可のため、
    # 「今日が1日」なら、「先月1日から今月1日（今日）」までの範囲にする
    if start_date == end_date:
        end_of_month = datetime.strptime(start_date, '%Y-%m-%d') + timedelta(days=-1)
        begin_of_month = end_of_month.replace(day=1)
        return begin_of_month.date().isoformat(), end_date
    return start_date, end_date
 
 
def get_begin_of_month() -> str:
    return date.today().replace(day=1).isoformat()
 
 
def get_prev_day(prev: int) -> str:
    return (date.today() - timedelta(days=prev)).isoformat()
 
 
def get_today() -> str:
    return date.today().isoformat()

def post_slack(title, detail):
    print(SLACK_WEBHOOK_URL)
    url = SLACK_WEBHOOK_URL
    set_fileds = [{"title":title, "value":detail, "short":False}]
    data = {"attachments":[{'color':'danger', 'fields':set_fileds}]}
    request_headers = {'Content-Type': 'application/json; charset=utf-8'}
    body = json.dumps(data).encode("utf-8")
    request = urllib.request.Request(
        url=url, data=body, method='POST', headers=request_headers)
    urllib.request.urlopen(request)
    pass


def post_line(title, detail):
    
    url = 'https://notify-api.line.me/api/notify'
    token = LINE_TOKEN
    
    message = "\nおはようございます。\n\n" + title + "\n\n" + detail
    
    payload = {'message': message}
    
    payload = urllib.parse.urlencode(payload).encode("utf-8")
    
    request_headers = {'Authorization': 'Bearer ' + token}
    
    request = urllib.request.Request(
        url=url, 
        data=payload,
        method='POST',
        headers=request_headers)
        
    urllib.request.urlopen(request)
    
    
```

以上