APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

AWSの使用料金を毎日LINEに通知する方法

はじめに

こんにちは!クラウド事業部の中根です。

みなさんは、個人で使用しているAWSアカウントの請求料金をどうやって確認していますか?
私は、Budgetsやコストと使用状況レポートのメール配信などを試していましたが、プライベートであまりEメールを使わないので、気づかないということがしばしばありました。
そこで、プライベートでよく使うLINEに通知するようにしました。
本記事では、やり方をご紹介します。

全体像

以下のような仕組みで通知を行います。

構築方法

1. Cost Explorerを有効化

まだ有効化されていない方は、有効にしてください。
https://docs.aws.amazon.com/ja_jp/cost-management/latest/userguide/ce-enable.html

2. Messaging APIの設定

以下のリンクを参考に、公式アカウントの作成とMessaging APIの設定を行ってください。
https://developers.line.biz/ja/docs/messaging-api/getting-started/#create-oa-line-business-id
LINE Developersのコンソール画面で、チャネルができていることが確認できればOKです。

3. パラメータの取得

後で使用するため、「ユーザーID」と「チャネルアクセストークン」を取得します。

ユーザーID

作成したチャネルの詳細画面に遷移します。
「チャネル基本設定」の「あなたのユーザーID」を取得します。

チャネルアクセストークン

作成したチャネルの詳細画面に遷移します。
「Messaging API設定」の「チャネルアクセストークン(長期)」を発行して取得します。

4. IAMロールの作成

Lambda関数で利用するIAMロールを作成します。

まずは以下のポリシーを持つIAMポリシーを作成してください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "NotifyLineToBillingLambdaPolicy",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "ce:GetCostAndUsage"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

続いて、IAMロールを作成します。
ユースケースはLambdaにして、作成したポリシーを追加してください。

5. Lambda関数の作成

AWSコンソール画面から、Lambda関数を作成します。

ランタイムは「Python」、IAMロールは4.で作成したものを指定してください。

6. Lambda関数の設定

環境変数

関数の「設定」タブ > 「環境変数」から、3.で取得したパラメータを設定します。
LINE_USER_ID: ユーザーID
LINE_CHANNEL_ACCESS_TOKEN: チャネルアクセストークン

タイムアウト

関数の「設定」タブ > 「一般設定」からタイムアウトを10秒に設定します。

コード

作成した関数のコードを、以下のコードに変更してください。

import os
import boto3
from datetime import datetime, timedelta, date
import json
import urllib.request

# 環境変数からLINE BotのチャネルアクセストークンとユーザーIDを取得
LINE_CHANNEL_ACCESS_TOKEN = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
LINE_USER_ID = os.environ['LINE_USER_ID']

def lambda_handler(event, context) -> None:
    client = boto3.client('ce', region_name='us-east-1')

    # 合計の請求額を取得する
    total_billing = get_total_billing(client)

    # Line用のメッセージを作成して投げる
    message = get_message(total_billing)
    post_line(message)


def get_total_billing(client) -> dict:
    (start_date, end_date) = get_cost_date_range()

    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_message(total_billing: dict) -> (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です。'

    return title

def post_line(title: str) -> None:
    url = 'https://api.line.me/v2/bot/message/push'
    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN
    }
    body = {
        'to': LINE_USER_ID,
        'messages': [
            {
                "type": "text",
                "text": title,
            }
        ]
    }

    try:
        req = urllib.request.Request(url, data=json.dumps(body).encode(), method='POST', headers=headers)
        urllib.request.urlopen(req)
            
        return {
            'statusCode': 200,
        }
    except urllib.request.exceptions.RequestException as e:
        print(e)
    else:
        print(response.status_code)


def get_cost_date_range() -> (str, str):
    start_date = get_first_day_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_first_day_of_month() -> str:
    return date.today().replace(day=1).isoformat()

def get_today() -> str:
    return date.today().isoformat()

テストで関数実行すると、LINEに通知が来ているはずです。

7. スケジュール実行

EventBridgeの画面へ遷移し、スケジュールを作成します。

ご自身の好きなタイミングで実行されるようにしてください。
以下は、毎日9時に実行される場合の例です。

ターゲットには、作成したLambda関数を指定してください。

あとはデフォルトのままで作成して良いです。
これで、毎日LINEに通知が来るようになるはずです!

料金について

残念ながら、完全無料とはなりません。
Cost Explorer APIを実行する際に、0.01 USD/リクエストの料金が発生します。
したがって、毎月0.3USD(≒47円)の料金が発生することになります。

また、Lambdaの無料利用枠を超えてしまった場合はその料金も発生します。
本記事の仕組み単体では超えることはないです。

しかし、例えばt3.smallのEC2インスタンスだと約0.48 USD/日 の料金が発生します。
消し忘れによる無駄なコストを抑えることができると思えば安いと個人的には思ってます!

参考にさせていただいた記事

Lambda関数のコードは、こちらの記事を参考に作成しました。
こちらの記事で使用されているLINE Notifyは2025年3月でサービス終了ということで、Messaging APIを使用していたり、料金を抑えるためにCost Explorer APIの実行回数を減らすように変えています。
各サービスごとの料金まで見られるようにしたい場合は、こちらの記事のコードが非常に参考になります!

dev.classmethod.jp

関連記事

コスト通知だけだと、翌日以降の対応が必要ですが、その場でコスト発生を防ぎたい場面がありませんか?
そんな方は、こちらも参考にしていただけると嬉しいです。

techblog.ap-com.co.jp

終わりに

APCはAWS Advanced Tier Services(アドバンストティアサービスパートナー)認定を受けております。

その中で私達クラウド事業部はAWSなどのクラウド技術を活用したSI/SESのご支援をしております。
www.ap-com.co.jp

https://www.ap-com.co.jp/service/utilize-aws/

また、一緒に働いていただける仲間も募集中です!
今年もまだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。

www.ap-com.co.jp