LoginSignup
2
5

More than 5 years have passed since last update.

LambdaでAWSの請求情報をLINE、Slackに送信する

Last updated at Posted at 2018-08-19

何をするか

  • AWSの請求金額をLINE、Slackに通知する
  • Totalで1~2時間くらいあればできる

事前準備

簡単な説明

  • 以下の処理をするLambda関数を作成
    • 請求情報の取得
    • LINEに請求情報を送信
    • Slackに請求情報を送信
  • CloudWatch の CloudWatch Events で、通知するLambda関数と通知タイミングを設定

ハマりポイント(あとで出てきます)

  • 請求情報を取得する時のRegion
  • Lambda に設定する実行ロール
  • LINE messagingAPI へのPOSTデータの型
  • CloudWatchのcron式

作成したLambda関数(順に説明します)

index.py
import boto3
import json
import datetime
import os
import urllib.request
_env = os.environ

#メイン処理
def lambda_handler(event, context):
    #請求情報取得
    _billing = getBillingMetric()
    _billing["message"] = "今月の請求金額は $" + str(_billing['Datapoints'][0]['Maximum']) + " だよ!"
    #LINEに送信
    sendNoticeToLine(_billing)
    #Slackに送信
    sendNoticeToSlack(_billing)

def getBillingMetric():
    _cw = boto3.client('cloudwatch', region_name=_env["REGION"])
    _billing = _cw.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'])

    print(_billing)
    return _billing

def sendNoticeToLine(_bill):
    _url = 'https://api.line.me/v2/bot/message/push'
    _data = json.dumps({
        "to": _env["LINE_USER_ID"]
        ,"messages": [{"type": "text", "text": _bill["message"]}]
    }).encode()
    _header = {
      "Content-type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + _env["LINE_ACCESS_TOKEN"]
    }

    print(_data)
    sendRequest(_url, _data, _header)

def sendNoticeToSlack(_bill):
    _url = 'https://hooks.slack.com/services/' + _env["SLACK_URL"]
    _data = json.dumps({
        "text": _bill["message"]
    }).encode()
    _header = {
      "Content-type": "application/json"
    }
    print(_data)
    sendRequest(_url, _data, _header)

def sendRequest(_url, _data, _header):
    _req = urllib.request.Request(_url, _data, _header)
    try:
        with urllib.request.urlopen(_req) as _res:
            _body = _res.read()
            print(_body)
    except urllib.error.HTTPError as _err:
        print("HTTPError: " + str(_err.code))
        print(_err)
    except urllib.error.URLError as _err:
        print("HTTPError: " + _err.reason)
        print(_err)

環境変数

キー
REGION "us-east-1"(固定)
SLACK_URL IncommingWebhookのWebhook Url(ドメイン以下)
TZ "Asia/Tokyo"

メイン処理

  • getBillingMetric:請求情報の取得
  • sendNoticeToLine:LINEに請求情報を送信
  • sendNoticeToSlack:Slackに請求情報を送信

testable にするため、それぞれの関数を別に分けている

[L8−L16]_index.py
#メイン処理
def lambda_handler(event, context):
    #請求情報取得
    _billing = getBillingMetric()
    _billing["message"] = "今月の請求金額は $" + str(_billing['Datapoints'][0]['Maximum']) + " だよ!"
    #LINEに送信
    sendNoticeToLine(_billing)
    #Slackに送信
    sendNoticeToSlack(_billing)

請求情報の取得

  • boto3でCloudWatch用のClientを作成
  • get_metric_statistics関数で請求情報を取得
  • 請求情報を呼び出し元に返却
  • 現在日-1 から 現在日までの最大の値(=1日は昨月〆、2日以降は当日までの請求額となる)
[L18−L30]_index.py
def getBillingMetric():
    _cw = boto3.client('cloudwatch', region_name=_env["REGION"])
    _billing = _cw.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'])

    print(_billing)
    return _billing

LambdaにIAMロールを設定する

  • 一旦、ここまでで請求情報が取得できるかを確認するため、Lambdaに実行ロールを設定する
  • [ハマりポイント] 請求情報を参照する権限ではなく、CloudWatchのメトリクスを参照する権限が必要
  • 上記URLの例のまま、以下をインラインポリシーに追加したロールを指定
{
  "Version": "2012-10-17",
  "Statement":[{
      "Effect":"Allow",
      "Action":["cloudwatch:GetMetricStatistics","cloudwatch:ListMetrics"],
      "Resource":"*",
      "Condition":{
         "Bool":{
            "aws:SecureTransport":"true"
            }
         }
      }
   ]
}

リクエスト送信用の共通関数

  • LINEのPUSH API、SlackのIncomming Webhooksを呼び出すときに使うリクエスト送信処理を共通化する
  • 関数は urllib.request.Request を使う
    • 第一引数には送信先URLを指定
    • 第二引数には送信データを指定
    • 第三引数には送信ヘッダを指定
[L57−L68]_index.py
def sendRequest(_url, _data, _header):
    _req = urllib.request.Request(_url, _data, _header)
    try:
        with urllib.request.urlopen(_req) as _res:
            _body = _res.read()
            print(_body)
    except urllib.error.HTTPError as _err:
        print("HTTPError: " + str(_err.code))
        print(_err)
    except urllib.error.URLError as _err:
        print("HTTPError: " + _err.reason)
        print(_err)

LINEに請求情報を送信

  • APIのURLはv2のもの
  • [ハマりポイント] POSTデータに送信データを設定する
    • toにLINEのユーザIDを文字列形式で指定する(Lambdaの環境変数 で設定)
    • messagesにメッセージを配列形式で指定する
  • Authorizationヘッダに "Bearer " + LINEのアクセストークンを指定する(Lambdaの環境変数 で設定)
  • リクエスト送信処理を呼び出して終了
[L32−L44]_index.py
def sendNoticeToLine(_bill):
    _url = 'https://api.line.me/v2/bot/message/push'
    _data = json.dumps({
        "to": _env["LINE_USER_ID"]
        ,"messages": [{"type": "text", "text": _bill["message"]}]
    }).encode()
    _header = {
      "Content-type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + _env["LINE_ACCESS_TOKEN"]
    }

    print(_data)
    sendRequest(_url, _data, _header)

Slackに請求情報を送信

  • URLはIncomming Webhooks の Webhook URL を指定(Lambdaの環境変数 で設定)
  • POSTデータとヘッダはシンプルなもの
  • リクエスト送信処理を呼び出して終了
[L46−L55]_index.py
def sendNoticeToSlack(_bill):
    _url = 'https://hooks.slack.com/services/' + _env["SLACK_URL"]
    _data = json.dumps({
        "text": _bill["message"]
    }).encode()
    _header = {
      "Content-type": "application/json"
    }
    print(_data)
    sendRequest(_url, _data, _header)

Lambda関数をテスト実行し、正常に動作することを確認する

  • エラーが出ていたら、エラーメッセージ、エラーコードから原因を切り分けてシューティング
    • IAM権限周り
    • LINEmessagingAPI
    • Slack Incomming Webhooks
  • 私はIAM権限と、LINEの400(リクエスト形式エラー)でハマりました

CloudWatch の CloudWatch Events で通知するLambda関数と通知タイミングを設定

CloudWatch の イベント を選択し、「ルールの作成」を選択する

image.png

スケジュールイベントを作成し、Cron式で記述

次のページでイベントに名前と詳細説明を付けて終了

振り返り

  • リクエスト送信処理は大体の外部APIを呼び出すときに個別で指定する項目なので、共通関数化しようと思う
  • IAMロールへの理解が足りないので、慣れるまでチャレンジする
2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5