TiDB Cloud APIを使って、請求情報をEventBridge Scheduler+Lambda+SNSで定期通知してみた

2023.10.12

こんにちは、ゲームソリューション部のsoraです。
今回は、TiDB Cloud APIを使って、請求情報をEventBridge Scheduler+Lambda+SNSで定期通知してみたことについて書いていきます。

構成

EventBridge Schedulerで指定した間隔でLambdaを呼び出して、TiDB Cloud APIを使っての請求情報を取得して、SNS経由でSlack通知する構成です。

Lambda(Python)コード

今回の構成では、TiDB Cloud APIから情報取得する用とSlack通知用の2つがあります。
ディレクトリ構成は以下です。

.
├── lambda
│   ├── slack_notification.py
│   └── tidb-api
│       ├── dist
│       │   └── tidb_api.py
│       └── requirements.txt
├── main.tf
└── terraform.tfvars


TiDB Cloud APIから情報取得するLambdaコードは以下です。
月初と月中で情報を取得することを想定しています。
※テストのためTiDBのAPIキーをLambdaに書くようにしていますが、本来だと良くないためSecrets Managerなどで管理するようにしてください。

TiDB Cloud APIについては、以下をご参照ください。
Billing System OPENAPI (v1beta1)

tidb_api.py

import boto3
import json
import os
import requests
from requests.auth import HTTPDigestAuth
import datetime
from dateutil.relativedelta import relativedelta
from zoneinfo import ZoneInfo

SNSTopicArn = os.environ['SNSTopicArn']
TiDBAPIPublicKey = os.environ['TiDBPublicKey']
TiDBAPIPrivateKey = os.environ['TiDBPrivateKey']

def lambda_handler(event, context):
    public_key = TiDBAPIPublicKey
    private_key = TiDBAPIPrivateKey
    # 実行する年月を取得
    date_source = datetime.datetime.now(ZoneInfo("Asia/Tokyo")).date()
    if date_source.day == 1:
        date_source = date_source - relativedelta(months=1)
    date = str(date_source)
    year_month = date[0:7]
    r_get = requests.get('https://billing.tidbapi.com/v1beta1/bills/' + year_month, auth=HTTPDigestAuth(public_key, private_key))
    billing_source = r_get.json()
    print(billing_source)

    message = []
    message.append("TiDB Cloudの利用料を通知します。")
    message.append(f"利用年月: {billing_source['overview']['billedMonth']} ,利用料: {billing_source['overview']['runningTotal']}")
    if billing_source['overview']['runningTotal'] != "0":
        for project in billing_source['summaryByProject']['projects']:
            message.append(f"プロジェクト名: {project['projectName']} ,プロジェクト利用料: {project['runningTotal']}")
    print(message)

    # メッセージをSNSに送信
    # 通知を送信するSNSトピックのARNを指定
    sns_client = boto3.client('sns')
    sns_topic_arn = SNSTopicArn
    sns_client.publish(
        TopicArn=sns_topic_arn,
        Message=json.dumps(message, indent=3, ensure_ascii=False)
    )


Pythonのライブラリはrequirements.txtを使って取得します。

requirements.txt

requests==2.31.0
datetime==5.2
python-dateutil==2.8.2
urllib3==1.26.17

boto3を使うときに、urlib3 2.xを使っていると以下エラーが出たため、urlib3 1.xを指定しています。
cannot import name 'DEFAULT_CIPHERS' from 'urllib3.util.ssl_
参考:"cannot import name 'DEFAULT_CIPHERS' from 'urllib3.util.ssl_'"の原因

Slack通知用のLambdaは、色々なところで紹介されていると思いますので割愛します。

Terraformコード

Terraformを使って作成します。

main.tf

variable tidb_public_key {}
variable tidb_private_key {}
variable slack_webhook_url {}
variable slack_channel_name {}

terraform {
    #AWSプロバイダーのバージョン指定
    required_providers {
        aws = {
            source  = "hashicorp/aws"
            version = "~> 5.19.0"
        }
    }
    #tfstateファイルをS3に配置する(配置先のS3は事前に作成済み)
    backend s3 {
        bucket = "<S3 bucket名>"
        region = "ap-northeast-1"
        key    = "tidb-api.tfstate"
    }
}
#AWSプロバイダーの定義
provider aws {
    region = "ap-northeast-1"
}

#============ IAMロール作成 start ============
##EventBridge Sheduler 
data aws_iam_policy_document events_assume_role {
    statement {
        effect = "Allow"
        principals {
            type = "Service"
            identifiers = ["scheduler.amazonaws.com"]
        }
        actions = ["sts:AssumeRole"]
    }
}
resource aws_iam_role iam_for_events {
    name               = "EventBridge_TiDB_API_Role"
    assume_role_policy = data.aws_iam_policy_document.events_assume_role.json
    managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AWSLambdaRole"]
}

##Lambda(TiDB APIの情報取得用)
data aws_iam_policy_document lambda_assume_role {
    statement {
        effect = "Allow"
        principals {
            type = "Service"
            identifiers = ["lambda.amazonaws.com"]
        }
        actions = ["sts:AssumeRole"]
    }
}
resource aws_iam_role iam_for_lambda {
    name               = "Lambda_TiDB_API_Role"
    assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
    inline_policy {
        name = "my_inline_policy"
        policy = jsonencode({
            Version = "2012-10-17"
            Statement = [
                {
                    Action   = ["sns:Publish"]
                    Effect   = "Allow"
                    Resource = "*"
                },
            ]
        })
    }
}
#============ IAMロール作成 end ============


#============ リソース作成 start ============
##SNS(トピック)
###トピックの作成
resource aws_sns_topic tidb_api_topic {
    name = "TiDB-API-topic"
}
data aws_iam_policy_document sns_assume_role {
    statement {
        effect = "Allow"
        principals {
            type = "Service"
            identifiers = ["lambda.amazonaws.com"]
        }
        actions = ["sns:Publish"]
        resources = [aws_sns_topic.tidb_api_topic.arn]
    }
}
resource aws_sns_topic_policy sns_topic_policy {
    arn    = aws_sns_topic.tidb_api_topic.arn
    policy = data.aws_iam_policy_document.sns_assume_role.json
}

##Lambda(TiDB APIの情報取得用)
data archive_file lambda_tidbapi {
    type        = "zip"
    source_dir = "lambda/tidb-api/dist"
    output_path = "tidb_api.zip"
}
resource aws_lambda_function lambda_tidbapi {
    filename      = "tidb_api.zip"
    function_name = "tidb-api-execution"
    role          = aws_iam_role.iam_for_lambda.arn
    handler       = "tidb_api.lambda_handler"
    source_code_hash = data.archive_file.lambda_tidbapi.output_base64sha256
    runtime = "python3.10"
    timeout = 30
    environment {
        variables = {
            SNSTopicArn = aws_sns_topic.tidb_api_topic.arn
            TiDBPublicKey = var.tidb_public_key
            TiDBPrivateKey = var.tidb_private_key
        }
    }
}

##Lambda(Slack通知用)
data archive_file lambda_slack {
    type        = "zip"
    source_file = "lambda/slack_notification.py"
    output_path = "slack_notification.zip"
}
resource aws_lambda_function lambda_slack {
    filename      = "slack_notification.zip"
    function_name = "tidb-api-to-slack"
    role          = aws_iam_role.iam_for_lambda.arn
    handler       = "slack_notification.lambda_handler"
    source_code_hash = data.archive_file.lambda_slack.output_base64sha256
    runtime = "python3.9"
    #slack通知先
    environment {
        variables = {
            HookURL = var.slack_webhook_url
            ChannelName = var.slack_channel_name
        }
    }
}
resource aws_lambda_permission lambda_slack_permission {
    action        = "lambda:InvokeFunction"
    function_name = aws_lambda_function.lambda_slack.function_name
    principal     = "sns.amazonaws.com"
    source_arn    = aws_sns_topic.tidb_api_topic.arn
}

##EventBridge Sheduler
resource aws_scheduler_schedule eventbridge {
    name = "tidb-api-schedule"
    description = "TiDB API Notification"
    flexible_time_window {
        mode = "OFF"
    }
    schedule_expression = "cron(0 8 1,15 * ? *)"
    schedule_expression_timezone = "Asia/Tokyo"
    target {
        arn = aws_lambda_function.lambda_tidbapi.arn
        role_arn = aws_iam_role.iam_for_events.arn
    }
}

##SNS(サブスクリプション)
resource aws_sns_topic_subscription slack_subscription {
    topic_arn = aws_sns_topic.tidb_api_topic.arn
    protocol  = "lambda"
    endpoint  = aws_lambda_function.lambda_slack.arn
}
#============ リソース作成 end ============

デプロイ

Pythonのライブラリを取得した後に、Terraformを実行します。

$ pwd
/xxxx/xxxx/tidb-api-lambda/lambda/tidb-api
$ pip install -r requirements.txt --target \dist

$ pwd
/xxxx/xxxx/tidb-api-lambda
$ terraform init
$ terraform apply

動作確認

テストを実行するとSlackに通知が来ることが確認できました。

最後に

今回は、TiDB Cloud APIを使って、請求情報をEventBridge Scheduler+Lambda+SNSで定期通知してみたことを記事にしました。
どなたかの参考になると幸いです。