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

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

Clock Icon2023.10.12

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、ゲームソリューション部の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)

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を使って取得します。

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を使って作成します。

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で定期通知してみたことを記事にしました。
どなたかの参考になると幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.