コストのガードレール¶
ひとことで
従量課金(PAYG)のアカウントには費用の上限がなく、予算(Budget)もメールを送るだけでサービスは止めません。そのため 3 層で守ります: ① 予防:コードを書く段階で課金リソースが作れないようにする(validation、直書き)。② 検知:費用が発生したらすぐ気づく(予算アラート、API で実際の費用を確認)。③ 損失の抑止:異常時に自動で止める(アラーム / 予算 → Function がサイトを非公開にする)。
このページは Phase 0、Phase 8、Phase 10 と、以前の「Always Free ガードレール」のページをまとめたものです。
1. まず構成要素を知る¶
| OCI | AWS | ひとことで |
|---|---|---|
| Always Free | 無料利用枠(一部は 12 か月限定) | OCI の無料枠は期限なし。ただし超えた分は通常どおり課金される |
| PAYG(Pay As You Go) | 同じ | 従量課金のアカウント。クレジットカードが紐づいていて、上限はない |
| コンパートメント(Compartment) | (近い)リソース・グループ / アカウントの分割 | 費用もこの単位で集計される。演習リソースはすべて tf-lab に置き、単独で予算を設定しやすくする |
| 予算(Budget)+ アラート・ルール | AWS Budgets | 金額 + ルール(実際 / 予測の費用がいくらを超えたら通知)。通知するだけで、サービスは止めない |
| コスト分析 / Usage API | Cost Explorer | 実際にいくら使ったかを見る(数時間〜約 1 日の遅れがある) |
| モニタリングのアラーム(Monitoring Alarm) | CloudWatch Alarm | メトリック(例:リクエスト数)がしきい値を超えたら発火 |
| 通知(Notifications、トピック) | SNS | アラームをメール、Function などに送る |
| イベント(Events) | EventBridge | サービスが出すイベント(例:予算アラート)→ トピックなどに転送 |
| ファンクション(Functions) | Lambda | 呼ばれたときだけ動くプログラム。実行時間で課金 |
| OCIR | ECR | コンテナ・レジストリ。Function のプログラムはイメージにしてここに置く |
2. しくみ:なぜ「予算を設定した」だけでは足りないのか¶
| 事実 | 結果 |
|---|---|
| PAYG には費用の上限がない | シェイプを 1 つ書き間違えたり、大量のアクセスを受けたりすると、費用は増え続ける |
| 予算はメールを送るだけで、サービスは止めない | 夜中にメールが来ても対応が間に合わない |
| 予算の評価はリアルタイムではない(このリポジトリの記録では数時間〜約 1 日) | 気づいたときにはすでに使われている |
| オブジェクト・ストレージにはレート制限がない | 公開サイトに大量のリクエストが来ると、リクエスト料 / 転送料もそれに比例して増える(Denial of Wallet) |
そのため 3 層のどれも欠かせません:
flowchart LR
A["① 予防<br/>無料の構成だけ使う<br/>コードに書き込む"] --> B["② 検知<br/>予算アラート<br/>実際の費用を確認"]
B --> C["③ 損失の抑止<br/>サイトを自動停止"]
静的サイトのリスクは主にお金
静的サイトにはサーバー側のプログラム、DB、フォームがないため、SQL インジェクションや XSS などは成り立ちません。本当のリスクは大量のリクエストを受けたときの費用です:オブジェクト・ストレージの API リクエストは月 5 万回まで無料で、超えると 1 万回あたり約 $0.0034。アウトバウンド転送量は月 10TB まで無料で、超えると GB 単位で課金されます。
3. 第 ① 層:予防¶
3.1 Always Free のルール¶
| リソース | ルール | 守らなかった場合 |
|---|---|---|
| リージョン(Region) | ホーム・リージョン(Home Region、ap-osaka-1)だけを使う |
Always Free の Compute はホーム・リージョンにしか作れない |
| Compute | A1.Flex は合計 4 OCPU / 24GB、または E2.1.Micro を最大 2 台 | 超えた分は従量課金 |
| ブロック・ボリューム(Block Volume) | ブート・ボリュームも含めて合計 200GB | ブート・ボリュームは最小 50GB → 実質 VM は最大 4 台 |
| ロード・バランサ(Load Balancer) | flexible、帯域 min = max = 10Mbps、1 つだけ |
デフォルト値やその他の帯域は課金対象 |
| Autonomous DB | is_free_tier = true |
漏れると有償の ADB になる |
| OKE | type = "BASIC_CLUSTER" |
ENHANCED はコントロール・プレーンが時間課金 |
| オブジェクト・ストレージ(Object Storage) | 20GB、月 5 万リクエスト | 超えると課金 |
計算リソースの詳細は コンピュートとロード・バランサ を参照してください。
3.2 ルールをコードに書き込む¶
記憶に頼るのは危険です:
# 無料のシェイプだけを許可:書き間違えたら plan の段階でエラー(02-compute/variables.tf、抜粋)
variable "instance_shape" {
type = string
default = "VM.Standard.A1.Flex"
validation {
condition = contains(["VM.Standard.A1.Flex", "VM.Standard.E2.1.Micro"], var.instance_shape)
error_message = "Always Free のシェイプのみ指定できます:VM.Standard.A1.Flex または VM.Standard.E2.1.Micro。"
}
}
- validation にできるものは validation にする:シェイプ、LB の帯域
- 「間違えると課金される」値は変数にせず、直書きする:ADB の
is_free_tier = true、OKE のtype = "BASIC_CLUSTER"。変数にすると変更される可能性が生まれる - 演習が終わったらすぐ
terraform destroy。次の Phase で枠が必要なときは、先に前の Phase を削除する
4. 第 ② 層:検知¶
4.1 予算アラート(実際の設定)¶
| クラウド | 対象 | 金額 | アラート・ルール |
|---|---|---|---|
| OCI | コンパートメント tf-lab |
1,000 円 / 月 | 予測が 80 円を超えたとき。実際が 300 円を超えたら → サイトの自動停止も発動 |
| OCI | テナンシ全体 | 1,000 円 / 月 | 実際が 80% を超えたとき |
| AWS | アカウント全体 | 20 USD / 月 | 実際 50% / 100%、予測 80% / 100% |
- 2 段階で設定する:
tf-labは演習リソースを見張り、テナンシ全体は最後の砦(演習以外の場所で費用が出ても気づける) - OCI の予算はコンパートメントごとに 1 つだけ
4.2 API で実際の費用を確認する¶
- データには数時間〜約 1 日の遅れがあり、当日の数字は不完全
- AWS:
aws ce get-cost-and-usage --granularity MONTHLY --group-by Type=DIMENSION,Key=SERVICE
実際に確認して見つかった意外なこと 2 つ。どちらも「無料だと思っていた / 計算済みだと思っていた」ものです:
| 見つかったこと | 教訓 |
|---|---|
OCI DevOps のビルド・ランナーは従量課金だった(デフォルト 2 OCPU / 8GB、SKU Standard - E4 (DevOps service Build Run))。最初は個別の課金項目はないと思っていたが、あとで請求に出てきた(1 日 0.61 円) |
「無料に見える」サービスにも課金 SKU があることがある → 実際の請求で確認する |
| AWS のドメインは $16 / 年だが、実際の請求は $17.60 = 2,807 円 | 予算を立てるときに消費税(10%)と為替を計算に入れていなかった |
5. 第 ③ 層:損失の抑止(サイトの自動停止)¶
Phase 8 の設計:サイトが大量にアクセスされたとき、または tf-lab の実際の費用が 300 円を超えたとき、Function が自動でサイトのバケットを非公開にします。
flowchart TB
req["大量のリクエスト"] --> bucket["サイトのバケット"]
bucket -->|"リクエスト数のメトリック"| alarm["モニタリングのアラーム<br/>1 時間 > 10,000 回"]
budget["予算<br/>実際 > 300 円"]
alarm --> topic["通知トピック"]
budget -->|"イベント"| topic
topic --> mail["メール"]
topic --> fn["Function"]
fn -.->|"NoPublicAccess に変更"| bucket
| 層 | 発動条件 | 速さ | 役割 |
|---|---|---|---|
| リクエスト数 | サイトのバケットへのリクエストが 1 時間に 10,000 回を超える(通常は 20〜50 回) | 約 3.5 分(実測) | メイン |
| 予算 | tf-lab の当月の実際の費用が 300 円を超える |
遅い(予算自体に遅延がある) | 最後の砦:リクエスト数では見えない費用(転送料、ほかのリソース) |
| 手動 | 07-site で terraform apply "-var=site_public=false" |
即時 | 緊急時 |
復旧は 07-site で terraform apply するだけです。Terraform が NoPublicAccess をドリフトとして扱い、公開に戻します。
# 第 1 層のアラーム(08-guard/alarm.tf、抜粋)
# ディメンション名 resourceDisplayName は list_metrics で調べたもので、推測ではない
resource "oci_monitoring_alarm" "site_requests" {
namespace = "oci_objectstorage"
query = "AllRequests[1h]{resourceDisplayName = \"${var.site_bucket_name}\"}.sum() > ${var.request_threshold}"
severity = "CRITICAL"
pending_duration = "PT1M"
message_format = "ONS_OPTIMIZED" # メールは人が読む形式に。Function が受け取るのは JSON のまま
destinations = [oci_ons_notification_topic.guard.id]
}
Function の判断:
| 受け取ったメッセージ | 動作 | 理由 |
|---|---|---|
アラーム発火(OK_TO_FIRING) |
公開を停止 | |
アラーム回復(FIRING_TO_OK / RESET) |
スキップ | スキップしないと、手動でサイトを復旧したあとにまた止められる |
| 予算のイベント | イベントの内容は見ず、予算の実際の費用を自分で確認し、300 円以上のときだけ停止 | ほかの予算や予測費用のルールのイベントも届く。イベントの JSON 構造は公開されたサンプルが見つからない |
| 空のメッセージ(手動の呼び出し) | 公開を停止 | 緊急時 |
設計のポイント:
- Function の権限は最小:サイトのバケットの設定の変更(
use buckets+ バケット名で限定)と、予算の読み取りだけ(セキュリティ を参照) - 既存の予算はデータ・ソースで読み、import しない:import すると、08-guard を
destroyしたときに Phase 0 で作った予算まで削除される。アラート・ルールを追加するだけにする - しきい値を下げたテストは
-varだけで行い、コードは変えない:-var=request_threshold=50
6. AWS 側:定額料金プラン¶
Phase 10 の AWS 版サイトは、CloudFront の定額料金プランの Freeを使い、そもそも上限を設けています:
| 従量課金 | Free($0 / 月) | |
|---|---|---|
| 月の枠 | 1TB / 1,000 万リクエスト(期限なしの無料枠) | 100GB / 100 万リクエスト |
| 超えたとき | 通常どおり課金。上限なし | 課金されない。超過が続くと配信性能が下がる |
| 攻撃を受けたとき | 攻撃のトラフィックにも課金される | WAF / DDoS 対策でブロックしたリクエストは使用量に含まれない |
| WAF | 別料金 | 含まれる(ルール最大 5 個) |
このリポジトリでは WAF ルールを追加しています:同じ IP から 5 分間に 500 回以上でブロック、さらにマネージド・ルール・グループを 3 つ。
OCI と AWS は同じ問題を、違う考え方で解いています:OCI は「検知したら自動で止める」、AWS は「超えてもお金がかからない」。
7. 設計判断の一覧¶
| 判断 | 採用 | 不採用 | 理由 |
|---|---|---|---|
| 課金リソースを作らせない | validation + 直書き | 記憶に頼る / 変数にする | plan の段階でエラーになる。直書きした値は誰にも変えられない |
| アラートの範囲 | tf-lab + テナンシの 2 段階の予算 |
1 つだけ設定 | 演習以外の場所で費用が出ても気づける |
| 第 1 層の発動 | モニタリングのリクエスト数(数分) | 予算だけに頼る | 予算には数時間〜1 日の遅れがある |
| 損失の抑止の動作 | バケットを非公開にする | バケット / リソースを削除する | 復旧は terraform apply 1 回だけで、データも失わない |
| 予算イベントの扱い | Function が実際の費用を自分で確認 | イベントの内容で判断 | イベントの構造が不確か。ほかのルールのイベントが来ても安全 |
| 既存の予算 | データ・ソースで読み + アラート・ルールを追加 | import | import すると destroy で削除される |
| AWS の転送料 | 定額料金プランの Free + WAF | 従量課金 + アラーム | 超えても課金されないので、損失の抑止を自分で作る必要がない |
8. 検証¶
| 証明したいこと | 方法 | 結果 |
|---|---|---|
| エンドツーエンド:リクエストが増えると自動で止まる | しきい値を一時的に 50 にし、curl で 100 回アクセス | 約 3.5 分後に Function が呼ばれる → サイトが 404 |
| 復旧後にまた止められない(逆方向) | サイトを復旧 → しきい値を戻す → アラームが回復 | Function はスキップ(FIRING_TO_OK)、サイトは 200 のまま |
| 復旧は apply だけで済む | 07-site で terraform apply |
NoPublicAccess -> ObjectReadWithoutList → 200 |
| 予算の層:枠を超えていなければ止めない(逆方向) | Function を直接呼び出し、模擬の予算イベントを渡す | スキップ(実際の費用 0 < 300)→ 予算を読めており、権限もネットワークも通っている |
| 実際の費用 | tools/cost.py |
Phase 0〜6 はすべて 0 円。その後、DevOps のビルド・ランナーの費用が出てきた(1 日 0.61 円) |
テストできていない部分
本物の予算アラートのイベントは、実際の費用が 300 円を超えないと届きません。そのため予算の層のエンドツーエンドはテストしていません。Function は最初に受け取ったイベントの原文をログに書くので、そのときに確認します。
9. ハマりどころ¶
| 現象 | 原因 | 対策 |
|---|---|---|
| 復旧したサイトがまた止まる | アラーム回復(FIRING_TO_OK)のメッセージでも Function が呼ばれる |
Function で FIRING_TO_OK / RESET をスキップ |
| ほかの予算のアラートでもサイトが止まる | イベントの JSON 構造が調べられず、イベント・ルールはイベント・タイプだけで絞り込んでいる → すべての予算・すべてのルールのイベントが届く | Function はイベントの内容を見ず、実際の費用を自分で確認 |
予算を新規作成 → 400-LimitExceeded |
1 つのコンパートメントに予算は 1 つだけ | データ・ソースで既存のものを読み、アラート・ルールだけを追加 |
oci_budget_budgets を display_name で検索したら 2 つ返ってきた |
この引数では絞り込まれない | filter ブロックを使う |
アラート・ルールで毎回 plan に freeform_tags の差分が出る |
API がアラート・ルールのタグを保存しない | アラート・ルールにはタグを書かない。apply のあとは必ずもう一度 plan |
| アラームのメールが長い JSON | message_format = "PRETTY_JSON" |
ONS_OPTIMIZED(変わるのはメールだけで、Function が受け取るのは JSON のまま) |
| DevOps のビルドは無料だと思っていた | ビルド・ランナーは従量課金 | tools/cost.py で実際の SKU を確認 |
| ドメインの実際の費用が予算より多い | 消費税と為替を計算していなかった | 予算を立てるときに含める |
10. 確認問題¶
1. 予算アラートを設定すれば、使いすぎることはありませんか?
いいえ。予算はメールを送るだけでサービスを止めず、評価にも数時間〜1 日の遅れがあります。「予防」(validation、直書き)と「損失の抑止」(自動停止)を組み合わせる必要があります。
2. ADB の is_free_tier を変数にせず直書きするのはなぜですか?
間違えると課金される値を変数にすると、変更される可能性が生まれます(tfvars の書き間違い、ほかの人による上書き)。コードに直書きすれば、変更するにはコードを変えてレビューを通す必要があります。
3. 第 1 層に予算ではなく、リクエスト数のアラームを使うのはなぜですか?
予算には数時間〜1 日の遅れがあり、大量のアクセスを受けたときには間に合いません。リクエスト数のメトリックは数分で反映され、実測では約 3.5 分で停止できました。
4. 手動でサイトを復旧したあと、アラーム回復のメッセージで Function がまた呼ばれると、どうなりますか?どう防ぎますか?
何もしなければ、Function がまたサイトを止めてしまいます。そのため Function は FIRING_TO_OK / RESET を受け取ったらスキップします。
5. すでに予算が 1 つあり、Terraform でアラート・ルールを追加したい。なぜ import しないのですか?
import するとその予算は Terraform の管理下に入り、そのディレクトリを destroy したときに一緒に削除されます。データ・ソースで読み、新しく追加するアラート・ルールだけを管理します。
6. 損失の抑止の動作が、バケットの削除ではなく「非公開にする」なのはなぜですか?
非公開にすればすぐにアクセスを止められ、データも失わず、復旧は terraform apply 1 回だけで済みます。削除すると再アップロードが必要で、誤って削除するおそれもあります。
7. AWS の定額料金プランと OCI の自動停止は、考え方がどう違いますか?
OCI:従量課金なので、異常を検知したら自動でサービスを止める。AWS の定額料金プラン:枠を超えても課金されず(配信性能が下がる)、WAF でブロックしたリクエストも使用量に含まれないので、損失の抑止を自分で作る必要がありません。
コード全文¶
08-guard/alarm.tf(トピック / サブスクリプション / アラーム)
# ------------------------------------------------------------
# Phase 8:サイトのバケットのリクエスト数を監視 → しきい値を超えたら通知 + Function を呼び出す
# 予算(Budget、費用)は約 1 日遅れる → モニタリング(Monitoring)のオブジェクト・ストレージのメトリック(数分以内)を使う
# ------------------------------------------------------------
resource "oci_ons_notification_topic" "guard" {
compartment_id = var.compartment_ocid
name = "${var.project_name}-guard"
description = "Site request alarm -> email + site-guard function"
freeform_tags = local.common_tags
}
# メール:apply 後に確認メールが届き、リンクをクリックすると有効になる(それまでは PENDING)
resource "oci_ons_subscription" "email" {
compartment_id = var.compartment_ocid
topic_id = oci_ons_notification_topic.guard.id
protocol = "EMAIL"
endpoint = var.alert_email
freeform_tags = local.common_tags
}
resource "oci_ons_subscription" "function" {
count = var.function_image == "" ? 0 : 1
compartment_id = var.compartment_ocid
topic_id = oci_ons_notification_topic.guard.id
protocol = "ORACLE_FUNCTIONS"
endpoint = oci_functions_function.site_guard[0].id
freeform_tags = local.common_tags
}
# AllRequests:バケットが受けたすべての HTTP リクエスト(匿名のサイト・アクセス + アップロードなど)
# ディメンション resourceDisplayName = バケット名(2026-10-28 に list_metrics で確認)
# [1h]...sum():直近 1 時間の合計
resource "oci_monitoring_alarm" "site_requests" {
compartment_id = var.compartment_ocid
metric_compartment_id = var.compartment_ocid
display_name = "${var.project_name}-site-requests"
namespace = "oci_objectstorage"
query = "AllRequests[1h]{resourceDisplayName = \"${var.site_bucket_name}\"}.sum() > ${var.request_threshold}"
severity = "CRITICAL"
is_enabled = true
pending_duration = "PT1M"
message_format = "ONS_OPTIMIZED" # メールは人が読みやすい形式(EMAIL サブスクリプションにだけ適用)。Function が受け取るのは JSON のまま
destinations = [oci_ons_notification_topic.guard.id]
body = "Requests to ${var.site_bucket_name} exceeded ${var.request_threshold}/hour. site-guard sets the bucket to NoPublicAccess. Restore: terraform apply in 07-site."
freeform_tags = local.common_tags
}
08-guard/budget.tf(既存の予算 + アラート・ルール + イベント)
# ------------------------------------------------------------
# Phase 8 第 2 層:予算(実際の費用)→ イベント(Events)→ トピック → 同じ Function
# 第 1 層(リクエスト数のアラーム)では見えない費用(転送料、tf-lab 内のほかのリソースなど)の最後の砦
# 予算には遅延がある(ドキュメント:約 24 時間ごとに評価)→ あくまで保険。速いのは第 1 層
#
# 対象コンパートメントごとに予算は 1 つだけ(新規作成 → 400-LimitExceeded)
# → Phase 0 で手動作成した TF-1000en(対象 = tf-lab)を使い、ACTUAL のアラート・ルールを 1 つ追加するだけ
# → import しない:import すると 08-guard の destroy で TF-1000en も削除される。データ・ソースは読み取りのみ
#
# イベントの JSON 構造が調べられない(alertRuleId があるか不明)→ Function はイベントの内容を見ず、
# 予算のイベントを受け取ったら自分で予算 API を呼ぶ:actual_spend >= GUARD_THRESHOLD のときだけ停止
# (TF-1000en の既存の FORECAST ルールやルートの予算のイベントが来ても、1 回確認してスキップするだけ)
# ------------------------------------------------------------
# 予算はルート・コンパートメント(テナンシ)にある
data "oci_budget_budgets" "guard" {
count = var.enable_budget_guard ? 1 : 0
compartment_id = data.oci_identity_compartment.lab.compartment_id
target_type = "COMPARTMENT"
# display_name 引数はサーバー側で絞り込まれない(実測で 2 つ返ってきた)→ filter ブロックでプロバイダー側で絞り込む
filter {
name = "display_name"
values = [var.guard_budget_name]
}
}
locals {
# one():2 つ以上ならエラー。0 個(名前の間違い)→ null → アラート・ルールに budget_id がなく、plan の時点でエラー
guard_budget_id = var.enable_budget_guard ? one(data.oci_budget_budgets.guard[0].budgets).id : null
}
# 実際の費用(ACTUAL)が金額(ABSOLUTE)を超えた → アラート(メール + イベント)
resource "oci_budget_alert_rule" "site_guard" {
count = var.enable_budget_guard ? 1 : 0
budget_id = local.guard_budget_id
display_name = "${var.project_name}-site-guard-actual"
type = "ACTUAL"
threshold_type = "ABSOLUTE"
threshold = var.guard_budget_threshold
recipients = var.alert_email
message = "tf-lab actual spend exceeded the guard threshold. site-guard sets ${var.site_bucket_name} to NoPublicAccess."
# freeform_tags は書かない:アラート・ルールの API はタグを保存しない(作成後に読むと {})→ 書くと毎回 plan に差分が出る
}
# 予算アラートのイベント → アラート用トピック(→ メール + Function)
# eventType:docs.oracle.com「Services that Produce Events」の Budgets(com.oraclecloud.budgets.createtriggeredalert)
# イベントは予算のあるルート・コンパートメントで発生する → ルールもルートに作る
resource "oci_events_rule" "budget_alert" {
count = var.enable_budget_guard ? 1 : 0
compartment_id = data.oci_identity_compartment.lab.compartment_id
display_name = "${var.project_name}-site-guard-budget"
description = "Budget alert -> ${oci_ons_notification_topic.guard.name}"
is_enabled = true
condition = jsonencode({
eventType = ["com.oraclecloud.budgets.createtriggeredalert"]
})
# 内側のブロック:旧名の actions は非推奨 → action(プロバイダーのスキーマで確認)
actions {
action {
action_type = "ONS"
is_enabled = true
topic_id = oci_ons_notification_topic.guard.id
}
}
freeform_tags = local.common_tags
}
# Function が予算の actual_spend を読む
# 予算はルート・コンパートメントにある → in tenancy で許可し、ポリシーもルートに作る(読み取りのみ)
resource "oci_identity_policy" "guard_budget" {
count = var.enable_budget_guard ? 1 : 0
compartment_id = data.oci_identity_compartment.lab.compartment_id
name = "${var.project_name}-guard-budget"
description = "site-guard function reads budgets (actual spend)"
statements = [
"Allow dynamic-group ${oci_identity_dynamic_group.guard_fn.name} to read usage-budgets in tenancy",
]
freeform_tags = local.common_tags
}
08-guard/function/func.py(サイトを止める Function)
"""
site-guard:サイトのバケットを NoPublicAccess にする(公開を停止)
呼び出し元:通知トピック ← ① モニタリングのアラーム(リクエスト数) ② イベント(予算アラート、第 2 層)/ 手動 invoke
認証:リソース・プリンシパル(動的グループ tflab-guard-fn:tflab-site バケットを use できるだけ + read usage-budgets)
復旧:07-site で terraform apply(Terraform がドリフトとして ObjectReadWithoutList に戻す)
"""
import io
import json
import logging
import os
import oci
from fdk import response
# アラームの回復(FIRING_TO_OK)/ リセット(RESET)でもメッセージが来る → 何もしない
# スキップしないと:サイトを手動で復旧したあと、アラーム回復のメッセージでまたサイトが止まる
IGNORE_TYPES = {"FIRING_TO_OK", "RESET"}
# 予算アラートのイベント(イベント → トピック → ここ)
# テナンシ内のすべての予算・すべてのアラート・ルール(FORECAST など)のイベントが来る。イベントの JSON 構造も未検証
# → イベントの内容は見ず、GUARD_BUDGET_ID の actual_spend を自分で確認し、GUARD_THRESHOLD 以上のときだけ停止
BUDGET_EVENT_PREFIX = "com.oraclecloud.budgets."
def handler(ctx, data: io.BytesIO = None):
log = logging.getLogger()
bucket = os.environ["BUCKET_NAME"] # アプリケーションの config
# アラームのメッセージは JSON(ONS_OPTIMIZED が変えるのはメールの形式だけ)。予算のイベントも JSON。手動 invoke なら空でもよい
body = {}
try:
body = json.loads(data.getvalue() or b"{}")
except (ValueError, AttributeError):
pass
alarm_type = body.get("type")
event_type = body.get("eventType")
if alarm_type in IGNORE_TYPES:
log.info("skip: alarm type %s", alarm_type)
return _json(ctx, {"action": "skip", "alarm_type": alarm_type})
signer = oci.auth.signers.get_resource_principals_signer()
if event_type:
# 本物のイベントが初めて来たとき、ログを見れば JSON 構造がわかる
log.info("event payload: %s", json.dumps(body)[:2000])
if not event_type.startswith(BUDGET_EVENT_PREFIX):
log.info("skip: not a budget event")
return _json(ctx, {"action": "skip", "event_type": event_type})
budget_id = os.environ.get("GUARD_BUDGET_ID")
threshold = os.environ.get("GUARD_THRESHOLD")
if not budget_id or not threshold: # 第 2 層がオフ(未設定)→ 止めない
log.info("skip: budget guard not configured")
return _json(ctx, {"action": "skip", "event_type": event_type})
budget = oci.budget.BudgetClient({"region": signer.region}, signer=signer).get_budget(budget_id).data
actual = budget.actual_spend or 0
log.info("budget %s: actual_spend=%s threshold=%s (computed %s)",
budget.display_name, actual, threshold, budget.time_spend_computed)
if actual < float(threshold):
return _json(ctx, {"action": "skip", "event_type": event_type,
"actual_spend": actual, "threshold": float(threshold)})
client = oci.object_storage.ObjectStorageClient({"region": signer.region}, signer=signer)
ns = client.get_namespace().data
before = client.get_bucket(ns, bucket).data.public_access_type
if before != "NoPublicAccess":
client.update_bucket(
ns, bucket,
oci.object_storage.models.UpdateBucketDetails(public_access_type="NoPublicAccess"),
)
after = client.get_bucket(ns, bucket).data.public_access_type
log.info("bucket %s: %s -> %s (alarm type %s, event %s)", bucket, before, after, alarm_type, event_type)
return _json(ctx, {"action": "close", "bucket": bucket, "before": before, "after": after,
"alarm_type": alarm_type, "event_type": event_type})
def _json(ctx, obj):
return response.Response(ctx, response_data=json.dumps(obj),
headers={"Content-Type": "application/json"})
tools/cost.py(Usage API で費用を確認)
"""
Usage API で費用を確認する(≒ コンソールのコスト分析)
認証:~/.oci/config の [DEFAULT](Terraform と同じ)
範囲:テナンシ全体(tf-lab だけでなく、どこで費用が出ても把握する)
注意:データには遅延がある(数時間~1 日)。当日の数字は不完全
使い方:
python tools/cost.py 直近 7 日(今日を含む)
python tools/cost.py --days 30 直近 30 日
"""
import argparse
import collections
import datetime as dt
import sys
import oci
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # Windows の cp932 対策
config = oci.config.from_file(profile_name="DEFAULT")
config["region"] = "ap-osaka-1" # Usage API はホーム・リージョンで呼ぶ
usage = oci.usage_api.UsageapiClient(config)
p = argparse.ArgumentParser(description="OCI cost summary (Usage API)")
p.add_argument("--days", type=int, default=7)
args = p.parse_args()
# DAILY 粒度では開始 / 終了とも UTC 0 時が必須。終了は含まれない → 明日の 0 時
end = dt.datetime.now(dt.timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + dt.timedelta(days=1)
start = end - dt.timedelta(days=args.days)
items = usage.request_summarized_usages(oci.usage_api.models.RequestSummarizedUsagesDetails(
tenant_id=config["tenancy"],
time_usage_started=start,
time_usage_ended=end,
granularity="DAILY",
query_type="COST",
group_by=["service", "skuName"],
)).data.items
print(f"period (UTC): {start:%Y-%m-%d} ~ {end - dt.timedelta(days=1):%Y-%m-%d}")
currency = next((i.currency for i in items if i.currency), "")
# 日ごとの合計
daily = collections.defaultdict(float)
for i in items:
daily[i.time_usage_started.date()] += i.computed_amount or 0
print(f"\n--- daily total ({currency}) ---")
for d in sorted(daily):
print(f" {d} {daily[d]:10.4f}")
# サービス / SKU ごとに集計(金額 0 も表示 → Always Free の使用量も見える)
total = collections.defaultdict(lambda: [0.0, 0.0, ""])
for i in items:
t = total[(i.service or "-", i.sku_name or "-")]
t[0] += i.computed_amount or 0
t[1] += i.computed_quantity or 0
t[2] = i.unit or t[2]
print(f"\n--- by service / SKU ({currency}) ---")
for (svc, sku), (amt, qty, unit) in sorted(total.items(), key=lambda kv: (-kv[1][0], kv[0])):
mark = "!!" if amt > 0 else " "
print(f"{mark} {amt:10.4f} {svc:22.22} {sku:55.55} {qty:12.2f} {unit}")
grand = sum(daily.values())
print(f"\ngrand total: {grand:.4f} {currency}" + (" <-- 費用あり!" if grand > 0 else " (0)"))