エムスリーエンジニアリンググループ AI・機械学習チームでソフトウェアエンジニアをしている中村(po3rin) です。検索とGoが好きです。 今回はElastic Cloudの料金監視BotをRustでサクッと作ったのでそのお話をします。
なぜ料金監視が必要だったか
Elastic Cloudの料金は使用するインスタンス費用とデータ転送費用に加えスナップショットに関する料金が発生します。
インスタンス費用はインスタンスを変えなければ費用は固定で予測しやすいのですが、データ転送費用やスナップショット料金は日々の運用の中で動的であり、ユーザーからのリクエストの増加や運用中の要請(一時的にリソースをあげる、スナップショットの利用)によっては予期しない料金が発生し、予算をオーバーする可能性があります。
Elastic Cloudの料金はコンソールから確認できますが、毎日コンソールで費用を確認するのは酷です。そこで、今回予算をオーバーしそうなコスト増加を見つけたらSlackに通知するBotを作りました。
EC Cost Alert Bot
作ったものはOSSとして公開しています。あまりカスタマイズ性がないので、PR募集中です。
EC Cost Alert BotはElastic Cloudの1時間にかかる費用($/h)を監視して、それがあらかじめ決めておいた閾値 HOURLY_RATE_THRESHOLD
を超えたらSlackに通知します。
cargo でインストールして下記のように実行できます。オプションは環境変数とフラグによる指定をサポートしています。
$ cargo install ec_cost_slack_bot # set env EC_API_KEY, EC_ORGANIZATION_ID, SLACK_WEBHOOK_URL, HOURLY_RATE_THRESHOLD $ ec_cost_slack_bot
これを実行すると下記のようにSlackに通知が飛んできます(スクショは個人持ちのElastic Cloudアカウントの結果です)。今月トータルと、コスト種別をざっと確認できます。
Rustによる実装
RustでのBot実装は個人的にお気に入りのcrateの組み合わせであるsurf+claspで実装しています。surfはasync/await に対応した HTTP クライアントライブラリです。非同期処理ランタイムとしてはasync_std を利用しています。
例えば、下記のようにElastic Cloud のbilling APIを叩けます。シンプルでいいですね。
#[derive(Serialize, Deserialize)] struct Res { costs: Costs, hourly_rate: f64, } #[derive(Serialize, Deserialize, Debug)] struct Dimension { #[serde(rename = "type")] typ: String, cost: f64, } #[derive(Serialize, Deserialize)] struct Costs { total: f64, dimensions: Vec<Dimension>, } async fn es_cost(ec_api_key: &str, organization_id: &str) -> Result<Res, surf::Error> { let header_val = "ApiKey ".to_string() + ec_api_key; let Res { costs, hourly_rate } = surf::get( "https://api.elastic-cloud.com/api/v1/billing/costs/".to_string() + organization_id, ) .header("Authorization", header_val) .recv_json() .await?; Ok(Res { costs, hourly_rate }) }
clapはRustのためのコマンドラインパーサーです。いろんな書き方ができますが僕は下記のBuilder patternを使った設定記述が好きです。
let app = App::new("ec_cost_slack_bot") .version("0.1.0") .about("Output Elastic Cloud cost report") .arg( Arg::new("key") .short('k') .long("ec-api-key") .env("EC_API_KEY") .takes_value(true) .help("Elastic Cloud api key"), ) // ... .arg( Arg::new("threshold") .short('t') .long("hourly-rate-threshold") .env("HOURLY_RATE_THRESHOLD") .takes_value(true) .help("Slack webhook url"), );
これくらいシンプルな仕様のツールなら、この二つのcrateを使うと100行ちょっとでAPIを叩くCLIが作れるので便利です。
デプロイ
弊社ではこのツールをGitLab CI schedule機能で運用しています。下記のJobを.gitlab-ci.ymlに定義するだけで、GitLabのUIから定義できるcron機能で実行できます。環境変数はGitLabのシークレット機能で渡しています。
stages: - run ec_cost_deployments_schedule: image: rust:1.63.0 stage: run variables: HOURLY_RATE_THRESHOLD: "0.1" before_script: # https://github.com/po3rin/ec_cost_slack_bot - cargo install ec_cost_slack_bot script: - /usr/local/cargo/bin/ec_cost_slack_bot rules: - if: $CI_PIPELINE_SOURCE == "schedule"
上記の閾値 HOURLY_RATE_THRESHOLD
を超えたらSlackに通知します。
Gitlab CI schedule機能のリファレンスはこちら https://docs.gitlab.com/ee/ci/pipelines/schedules.html
まとめ
Elastic Cloudのコスト監視BotをRustで作ったお話をしました。デプロイもGitLab CIのschedule機能ですぐにリリースできました。
We are Hiring!
エムスリーでは日本の医療を前進させるために、日々の運用改善をしていくメンバーを募集しています。