本記事記述および実装内容は、多くの部分をAIで処理しており、人間チェックは甘いです
結論
Azure AI Foundry / Azure AI Services の model deployment は、2026-01-15-preview の Pause API を Logic Apps Consumption から自動キックできました。
検知レイヤーには Cognitive Services の Platform Metric TokenTransaction を対象にした Metric Alert を使い、Logic App 側で Log Analytics の Query API を叩いて当月累計を再計算 してからしきい値を超えた deployment だけ Pause する 2 段階構成にしました。検証では burn 完了から Pause 完了まで end-to-end 約 6 分 18 秒 を実測しています。
TL;DR
- 検知: Metric Alert (
Microsoft.Insights/metricAlerts,TokenTransactionSum > 0 overPT5M,evaluationFrequency PT1M, dimensionModelDeploymentName) で deployment 別に発火させる - 処理: Logic App Consumption が Action Group 経由で起動し、当月累計 token を Log Analytics Query API で再計算 → しきい値超過なら ARM の Pause API (
2026-01-15-preview) を Managed Identity で叩く - Logic App は
condition.allOf[0].dimensions[0].valueからdeploymentNameを抽出(空文字 fallback あり、coalesceで 2 段)し、accountResourceIdは Bicep parameter への static fallback を持たせる設計にした - 実測: burn 完了 → Alert Fired まで 約 6 分 15 秒、Logic App run 約 1.49 秒、Pause API 200 +
deploymentState=Paused - Pause 後の推論は
HTTP 423 PausedDeploymentで拒否され、Resume API で再開できた - セキュリティ補足: Action Group → Logic App は SAS callback URL (Managed Identity 非対応) なので production では Custom Webhook (
webhookReceivers+useAadAuth: true) への置換を推奨
はじめに / 背景
Azure AI Foundry や Azure OpenAI 系の model deployment は、検証中でも意図せず token を消費し続けることがあります。開発環境では「一定量を超えたら人間に通知する」だけでなく、「一旦止める」仕組みがあると安心です。
この記事では、月間 token 上限を超えたら Logic Apps から Foundry deployment を Pause する構成を検証したときの実装メモと、Logic App workflow の中身、実測で見えた Azure Monitor / preview API の挙動をまとめます。
対象読者は、Azure AI Foundry / Azure OpenAI のコストガードレールを IaC で作りたい Azure エンジニアです。
全体アーキテクチャ
構成要素は次の通りです。
| レイヤー | 役割 |
|---|---|
| Foundry / AI Services |
gpt-5-mini などの deployment が推論 token を消費する |
| Diagnostic Settings | AI Services の metrics / logs を Log Analytics Workspace へ送る(Logic App の月次再クエリ用) |
| Metric Alert | Cognitive Services Platform metric TokenTransaction を Sum > 0 で監視(PT5M window, PT1M 評価、ModelDeploymentName で dimension splitting) |
| Action Group | Common Alert Schema webhook で Logic App を起動し、通知メールも送る |
| Logic Apps Consumption | 当月累計 token を再計算し、しきい値超過時だけ Pause API を呼ぶ |
| Managed Identity | Logic App から Log Analytics Query API と ARM Pause API を呼ぶ |
2 段階アラート設計の意図
Metric Alert と Logic App で 役割をきっぱり分離 しています。
| 段階 | やること | 担当 |
|---|---|---|
| ① 短窓検知 | 「最近 token が動いた」を 1 分粒度で検知 | Metric Alert (TokenTransaction Sum > 0 over PT5M) |
| ② 累計判定 | 当月累計が threshold を超えたら Pause | Logic App から LAW Query API を呼んで再計算 |
Metric Alert 単独で「当月累計」を判定しないのは、Platform metric は 直近 N 分 の窓しか持てない(Metric Alert の windowSize は最大 PT1D、それを超える集計は不可)からです。逆に Logic App 単独で 1 分粒度ポーリングするとコスト・運用コストが見合いません。
そこで Metric Alert は "動きがあったら Logic App を叩け" という gate、Logic App は "本当に超過しているか確かめてから Pause" という 2 役にしています。
実装ハイライト
Metric Alert(Bicep)
modules/alert-rule.bicep 抜粋。scope を AI Services account に向け、ModelDeploymentName dimension で deployment 別に発火させます。
metricName: 'TokenTransaction'にしているが、これはOpenAI専用。TotalTokensにするとAnthoropicなどでも使えるので、そちらにすべき
resource alert 'Microsoft.Insights/metricAlerts@2018-03-01' = {
name: alertName
location: 'global'
properties: {
severity: 2
enabled: true
scopes: [accountResourceId]
targetResourceType: 'Microsoft.CognitiveServices/accounts'
targetResourceRegion: location
evaluationFrequency: 'PT1M'
windowSize: 'PT5M'
criteria: {
'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
allOf: [
{
name: 'TokenBurnDetected'
criterionType: 'StaticThresholdCriterion'
metricName: 'TokenTransaction'
metricNamespace: 'Microsoft.CognitiveServices/accounts'
timeAggregation: 'Total'
operator: 'GreaterThan'
threshold: 0
dimensions: [
{
name: 'ModelDeploymentName'
operator: 'Include'
values: [deploymentName]
}
]
}
]
}
actions: [{ actionGroupId: actionGroupId }]
}
}
ポイントは次の 3 点です。
-
metricNamespace: 'Microsoft.CognitiveServices/accounts'を明示しないと、TokenTransactionの解決でハマることがあります -
dimensions[].operator: 'Include'+values: [deploymentName]で 1 deployment に絞れますが、['*']にすると全 deployment の splitting alert になります -
windowSizeをPT5M未満にすると、Platform metric の集計遅延に負けて false negative が増えます
Foundry - 警告
Foundry 画面。右の「ユーザーの応答」で完了などステータスを変更可能。

アラートルール
条件
アクション
詳細
アクショングループ
Logic App ワークフロー全体の流れ
Logic App (Consumption) は次の順で動きます。coalesce で payload schema の差分を吸収しつつ、最終的に accountResourceId は Bicep parameter にハードコードしたものへ fallback できる作りです。
各 action でやっていることを 1 つずつ説明します。
Trigger: HTTP Request
Action Group から Common Alert Schema を POST で受け取ります。schema は最小限 (schemaId と data のみ) を宣言しています。trigger 自体は callback URL (SAS) ベースなので、Action Group には Logic App の listCallbackURL() の結果を渡しています(この方式のセキュリティ上の注意は後述の「セキュリティ」セクション参照)。
Init_deploymentName
alert payload から deploymentName を取り出します。Metric Alert (Common Alert Schema) では condition.allOf[0].dimensions[0].value に dimension splitting 結果の deployment 名が必ず入る ので、そこを 1 段目に置き、payload 異常時に Logic App 全体を落とさないための空文字を 2 段目に置く、というシンプルな 2 段 coalesce にしています。
coalesce(
first(first(triggerBody()?.data?.alertContext?.condition?.allOf)?.dimensions)?.value,
''
)
空文字 fallback を入れているのは、後段 Check_threshold で長さチェックして安全に skip するためです(パース失敗で Pause API が malformed URL を叩くのを防ぐ目的)。
補足: 検証当初は
alertContext.DimensionsとalertContext.SearchResults.tables[0].rows[0][1]も coalesce 経路に含めた 3 段構成にしていましたが、Metric Alert の Common Alert Schema では実際にヒットするのはcondition.allOf[0].dimensionsだけだったため、最終版では 2 段に整理しています。他の alert schema(Log Alerts V2 など)と相互運用したい場合は、その候補を 1 段足してください。
Init_accountResourceId
Pause URL を組み立てるためには AI Services account の Resource ID が必要です。Log Alerts V2 の SearchResults 行があれば 1 列目から取れますが、Metric Alert payload には SearchResults がありません。そのため Bicep parameter として渡した accountResourceId (defaultValue でハードコード) に必ず fallback できる作りにしています。
coalesce(
triggerBody()?.data?.alertContext?.SearchResults?.tables[0]?.rows[0][0],
parameters('accountResourceId')
)
Query_monthly_tokens
Log Analytics Query API を Managed Identity (audience: https://api.loganalytics.io) で POST します。3 回 / 30 秒間隔の retry policy 付き。
{
"method": "POST",
"uri": "https://api.loganalytics.io/v1/workspaces/<WORKSPACE_CUSTOMER_ID>/query",
"headers": { "Content-Type": "application/json" },
"body": {
"query": "AzureMetrics | where ResourceProvider =~ \"MICROSOFT.COGNITIVESERVICES\" | where MetricName == \"TokenTransaction\" | where TimeGenerated >= startofmonth(now()) | where _ResourceId =~ \"<ACCOUNT_RESOURCE_ID>\" | summarize MonthlyTokens = sum(Total)"
},
"authentication": { "type": "ManagedServiceIdentity", "audience": "https://api.loganalytics.io" },
"retryPolicy": { "type": "fixed", "count": 3, "interval": "PT30S" }
}
実測では AzureMetricsV2 ではなく AzureMetrics 側に Cognitive Services の token 行が入りました(後述)。
Parse_monthly_tokens
LAW のレスポンスは tables[0].rows[0][0] に集計値 1 つだけが入ります。coalesce(..., 0) で空のときも 0 に倒します。
coalesce(body('Query_monthly_tokens')?.tables[0]?.rows[0][0], 0)
Check_threshold
3 条件の AND を取って、本当に Pause すべきか判定します。
| 条件 | 意味 |
|---|---|
int(outputs('Parse_monthly_tokens')) > parameters('monthlyTokenThreshold') |
当月累計がしきい値を超えている |
length(variables('deploymentName')) > 0 |
deployment 名が解決できている |
length(variables('accountResourceId')) > 0 |
account の Resource ID が解決できている |
deploymentName と accountResourceId の長さチェックを入れているのは、payload 解析が失敗したまま誤って空文字で Pause URL を組み立てるのを防ぐためです。
Compose_pauseUri(true 分岐)
accountResourceId の末尾スラッシュを除去してから Pause URL を組み立てます。
concat(
'https://management.azure.com',
if(endsWith(variables('accountResourceId'), '/'),
substring(variables('accountResourceId'), 0, sub(length(variables('accountResourceId')), 1)),
variables('accountResourceId')),
'/deployments/', encodeUriComponent(variables('deploymentName')),
'/pause?api-version=', parameters('pauseApiVersion')
)
Pause_deployment(true 分岐)
組み立てた URL に Managed Identity (audience: https://management.azure.com) で空ボディ POST します。こちらも 3 回 / 30 秒 retry 付き。
{
"method": "POST",
"uri": "<computed pause uri>",
"headers": { "Content-Type": "application/json" },
"body": {},
"authentication": { "type": "ManagedServiceIdentity", "audience": "https://management.azure.com" },
"retryPolicy": { "type": "fixed", "count": 3, "interval": "PT30S" }
}
Log_below_threshold(false 分岐)
しきい値を超えていない場合は Pause せず、Compose に診断ログだけ残します。
concat('Below threshold; not pausing. MonthlyTokens=',
string(outputs('Parse_monthly_tokens')),
' Deployment=', variables('deploymentName'))
run history の Compose を見ればなぜ Pause しなかったかが追えるので、検証フェーズで便利でした。
Response
最後に HTTP 200 を返します。body には monitoring 用に主要な変数を入れています。Action Group の webhook receiver は 200 を期待するので、Pause したかどうかに関わらず必ず Response を返します。
{
"statusCode": 200,
"body": {
"monthlyTokens": "@outputs('Parse_monthly_tokens')",
"deploymentName": "@variables('deploymentName')",
"accountResourceId": "@variables('accountResourceId')",
"threshold": "@parameters('monthlyTokenThreshold')"
}
}
Logic App の Managed Identity に必要な RBAC
# Log Analytics Reader on the workspace
az role assignment create \
--role "Log Analytics Reader" \
--assignee <logic-app-principal-id> \
--scope <workspace-resource-id>
# Cognitive Services Contributor on the AI Services account (Pause/Resume を含む)
az role assignment create \
--role "Cognitive Services Contributor" \
--assignee <logic-app-principal-id> \
--scope <ai-services-account-resource-id>
Pause/Resume は dataAction ではなく ARM control plane の action なので、Cognitive Services Contributor で動きました。Contributor の他、最小権限を狙うなら Microsoft.CognitiveServices/accounts/deployments/pause/action と .../resume/action を含むカスタムロールでも代用可能なはずです。
実測してわかったこと
ここが今回いちばん重要な学びです。
1. AzureMetricsV2 ではなく AzureMetrics に出た
当初は AzureMetricsV2 を見に行きましたが、Cognitive Services の token 行は取得できませんでした。また、手元の workspace では AzureMetricsV2 schema に MetricNamespace カラムがありませんでした。
実際に token metrics が見えたのは AzureMetrics で、観測できた MetricName は次の 4 種です。
| MetricName | 意味合い | 採用有無 |
|---|---|---|
TokenTransaction |
request ごとの token transaction | ✅ Metric Alert / monthly recalc に採用 |
TotalTokens |
total tokens 相当 | 参考値 |
InputTokens |
prompt tokens 相当 | 参考値 |
OutputTokens |
completion tokens 相当 | 参考値 |
MetricNamespace や ModelDeploymentName のような列が常にある前提で KQL を書くと、workspace / table 差分で semantic error になります。検証開始時に getschema と sample query を残すのがおすすめです。
2. Metric Alert payload の dimensions 位置
Common Alert Schema (Metric Alert) の場合、deployment 名は次の path に来ました。
data.alertContext.condition.allOf[0].dimensions[0].value
Logic App の Init_deploymentName で coalesce の 2 番目の候補 を引いている path がこれです。
Metric Alert payload には SearchResults 概念が無いので、accountResourceId は payload からは取れません。Logic App 側で Bicep parameter accountResourceId (defaultValue でハードコード) に fallback できる作りにしておく必要があります。
payload の最小イメージ
{
"schemaId": "AzureMonitorCommonAlertSchema",
"data": {
"alertContext": {
"condition": {
"allOf": [
{
"dimensions": [
{
"name": "ModelDeploymentName",
"value": "gpt-5-mini"
}
]
}
]
}
}
}
}
3. deploymentState は preview REST GET で確認する
stable な az cognitiveservices account deployment show では、deploymentState が null になることがありました。
Pause / Resume の状態確認は、当面 2026-01-15-preview の REST GET を正とするのが安全です。今回も preview REST GET で Paused / Running を確認しました。
az rest \
--method get \
--url "https://management.azure.com/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/rg-<env>-japaneast/providers/Microsoft.CognitiveServices/accounts/aiservices-<env>-<suffix>/deployments/gpt-5-mini?api-version=2026-01-15-preview" \
--query "properties.deploymentState" \
--output tsv
4. triggeredAlerts endpoint は対応外
triggeredAlerts REST endpoint は、今回構成した Metric Alert に対して期待した結果が返らなかったため、alert 発火の evidence は Logic App run history と action details を正としました。
検証結果サマリ
| ID | シナリオ | 結果 | 確認内容 |
|---|---|---|---|
| S1 | Alert firing and Logic App run succeeded | ✅ PASS | token 消費後に Metric Alert が発火し、Logic App run が Succeeded
|
| S2 | Pause API 200 and deploymentState Paused | ✅ PASS | monthly tokens がしきい値を超え、Pause API が HTTP 200。preview GET で Paused
|
| S3 | Paused deployment rejects inference | ✅ PASS | Pause 後の推論が HTTP 423 PausedDeployment で拒否 |
| S4 | Resume API restores inference | ✅ PASS | Resume API が HTTP 200。preview GET で Running、推論も再開 |
実測値としては、Metric Alert 発火から Logic App 実行、Pause API 呼び出しまで一連の流れを確認できました。コストは dev-safe 範囲内 (詳細は省略) です。
検知 latency の内訳
burn 完了から Pause 完了まで 約 6 分 18 秒 でした。内訳は次のとおりです。
| 区間 | 実測値 | 備考 |
|---|---|---|
| ① Cognitive Services → Platform metric への ingestion | 約 1〜3 分 | Platform metric の標準遅延 |
| ② Metric Alert の次回評価サイクル待ち | 最悪 +1 分 | evaluationFrequency: 'PT1M' |
| ③ Alert Fired → Action Group → Logic App webhook | 約 1.16 秒 | callback URL POST |
| ④ Logic App run 全体 (LAW Query + Pause API) | 約 1.49 秒 | Logic App run 履歴 |
| 合計 (今回実測) | 約 6 分 18 秒 | burn 完了 → Pause 完了 |
evaluationFrequency は alert 課金に影響しません(Metric Alert は Alerts Metric Monitored の signal/month 単位で課金されるため、評価頻度を変えても料金は変わらない)。PT5M に緩めると検知 latency の上振れが ±4 分になります。dev 環境のコストガードレールとしては PT1M を推奨します。
Resume REST API は POST body が空でも Content-Length: 0 ヘッダが必須 です。curl で -d "" を付けないと HTTP 411 Length Required を返します。az rest は自動で付けるので問題ありません。
セキュリティ: Action Group → Logic App は SAS callback URL で叩く
検証完了後に「Action Group が Logic App を起動するときの認証はどうなっているのか?」と気になって調べたところ、Managed Identity ではなく signed callback URL (SAS) を使う方式 だと分かりました。dev 環境用なので即修正はしませんでしたが、本番運用する場合に意識すべき点なので書き残します。
仕組み
Microsoft.Insights/actionGroups の logicAppReceivers は、Logic App workflow の trigger に対する sig= SAS token 付きの callback URL を ARM に保存し、alert 発火時にこの URL に HTTPS POST します。
logicAppReceivers: [
{
name: 'pause-deployment'
resourceId: logicAppResourceId
callbackUrl: logicAppCallbackUrl
useCommonAlertSchema: true
}
]
callbackUrl は Bicep の listCallbackURL('${logicApp.id}/triggers/manual', '2019-05-01').value で取得します。Logic App receiver の schema には useAadAuth 相当のプロパティが存在しない ため、AAD / Managed Identity 認証はできません(webhook receiver にはあります)。
つまり「URL を知っている人 = Logic App を実行できる人」という状態になります。
懸念点
| # | 懸念 | 影響度 |
|---|---|---|
| 1 | callback URL の漏洩 = 実行権限の漏洩 | 中〜高 |
| 2 | URL の保存場所が多い (Action Group 定義、ARM deployment history、azd .env、Bicep output) |
中 |
| 3 | Bicep module の output で SAS を返却 (outputs-should-not-contain-secrets warning) |
中 |
| 4 | 実行される処理が強力(ARM Pause API を叩く)。URL 漏洩 + 連打で deployment を強制 Pause させ続ける DoS 的攻撃が成立しうる | 中 |
| 5 | Logic App access key の rotation が運用上重く、放置になりやすい | 中 |
Bicep linter が outputs-should-not-contain-secrets warning を出していたのを「output しないと action-group module に渡せないし…」と放置していました。後述の対策で、main.bicep 内で listCallbackURL() を直接呼ぶ ことで module output から secret を消せます。Linter warning を放置しない、というのが今回の教訓のひとつです。
緩和要素(現実的なリスクは低めの理由)
- callback URL の hostname (
prod-XX.<region>.logic.azure.com) は subscription/region を知らないと外部から推測困難 (security-through-obscurity) - リポジトリに
.envは commit していない(.gitignore済み) - URL を読める Azure principal は既に Logic App
Contributor相当の権限を持つことが多く、追加リスクが小さい
改善案(費用対効果順)
| 優先度 | 改善 | 実装難度 |
|---|---|---|
| HIGH | Bicep module の output から logicAppCallbackUrl を削除し、main.bicep 内で listCallbackURL() を直接呼んで action-group に渡す |
低 |
| HIGH | Logic App 側で payload の alertTargetIDs / accountResourceId を validate し、想定外の deployment は no-op で 200 OK 返却 |
低 |
| MED | Logic App の accessControl.triggers.allowedCallerIpAddresses で外部 IP 制限 |
中 |
| MED | Logic App access key の定期 rotation を自動化 | 中 |
| LOW |
logicAppReceivers を捨てて webhookReceivers + Function App (useAadAuth: true) に置き換える |
高 |
Custom Webhook (webhookReceivers) との比較
| 観点 |
logicAppReceivers (今回採用) |
webhookReceivers (custom webhook) |
|---|---|---|
| 認証方式 |
sig= SAS callback URL のみ |
AAD Bearer token (useAadAuth: true) または無認証 |
| Managed Identity | ❌ 不可 | ✅ Action Group の System-assigned Identity で取得 |
| 呼び先 | Logic App (Consumption) 限定 | 任意の HTTPS endpoint (Function App, App Service, APIM, SaaS など) |
| 実装コスト | 低 (Logic App workflow 1 個) | 中 (Function App + audience 検証コード + Managed Identity) |
production 相当の環境で再利用する場合は、Function App + webhook receiver + AAD 認証 への置き換えを推奨します。dev 環境のコストガードレールとしては、上記 HIGH の 2 件 (Bicep output 削除、payload validation) で十分実用に耐えると判断しました。
参考: Configure authentication for a secure webhook
コスト構成・変動要素・算出例
Azure Retail Prices API から japaneast の公開リテール価格を取得して整理しました
(USD、EA / CSP 割引未考慮、2026-05 時点)。
主要メーターと無料枠
| リソース | meter | 無料枠 | 超過後 |
|---|---|---|---|
| Metric Alert | Alerts Metric Monitored |
10 signals / 月 | $0.10 / signal / 月 |
| Action Group: Email | Email Notification | 1,000 通 / 月 | $0.000002 / email |
| Action Group: Webhook (= Logic App) | Webhook | 100,000 / 月 | $0.0000006 / call |
| Logic Apps Consumption — Built-in | Built-in Actions | 4,000 actions / 月 | $0.000028 / action |
| Log Analytics (PerGB2018) | Data Ingestion | 5 GB / 日(≒ 150 GB / 月) | $3.34 / GB |
| LAW Retention | Retention | 31 日 | $0.15 / GB / 月 |
(参考)gpt-5-mini Global Standard |
Foundry Models | n/a | Input $0.25 / 1M、Output $2.00 / 1M |
本検証構成(1 alert / 1 Logic App / Email + Webhook receiver / Metrics ingest のみ)は 全項目が無料枠に収まる ため、月額コストは実質 $0 です。Logic Apps を Consumption(Standard ではない)にしたことが効いています。
コスト変動要素
| パラメータ | 直接影響する課金項目 | 効果 |
|---|---|---|
| alert rule の数(= deployment 数 N) | Metric Alert signal | 線形 N。先頭 10 個まで無料、11 個目から $0.10 / 月 |
| alert 発火頻度 | Logic Apps built-in / LAW Query | run 数 × ~9 actions/run。月 4,000 actions(≒ 444 run)まで無料 |
evaluationFrequency (PT1M vs PT5M) |
(signal/month 単位なので)直接の影響は無し | 発火タイミングの早さが変わるだけ |
| Diagnostic Settings の対象(AllMetrics / AllLogs / category 限定) | LAW Ingestion | metrics のみなら数 MB / 月。AllLogs を有効化すると GB 単位に増加 |
| LAW retention 日数 | LAW Retention | 31 日超で線形 |
| 通知チャネル | Action Group | Email / Webhook はほぼ無料、SMS / Voice / Push は per-message 課金 |
| Pause 完了までの latency | (= gpt-5-mini の token 課金削減量) |
真の節約効果はここ。Pause が早いほど無駄 token を抑えられる |
算出例 3 パターン(dev 検証 / 小規模 prod / 中規模 prod)
ケース A: 本検証構成(1 deployment、月 10 alert 発火、軽量 ingest)
| 項目 | 計算 | 月額 USD |
|---|---|---|
| Metric Alert signal | 1 → 無料枠 (≤ 10) | $0.00 |
| Action Group: Email | 10 → 無料枠 | $0.00 |
| Action Group: Webhook | 10 → 無料枠 | $0.00 |
| Logic Apps built-in | 10 run × 9 = 90 → 無料枠 (≤ 4,000) | $0.00 |
| LAW Ingestion | ~10 MB → 無料枠 | $0.00 |
| 小計(アラート基盤) | $0.00 / 月 | |
(参考)gpt-5-mini 1,100 token |
1,100 × $0.25 / 1M | $0.0003 |
ケース B: 小規模 prod(5 deployment、月 1,000 alert、Metrics ingest のみ)
| 項目 | 計算 | 月額 USD |
|---|---|---|
| Metric Alert signal | 5 → 無料枠 | $0.00 |
| Action Group: Email | 1,000 → 無料枠 | $0.00 |
| Action Group: Webhook | 1,000 → 無料枠 | $0.00 |
| Logic Apps built-in | 9,000 → 5,000 課金 × $0.000028 | $0.14 |
| LAW Ingestion | ~100 MB → 無料枠 | $0.00 |
| 小計 | ~$0.14 / 月 |
ケース C: 中規模 prod(20 deployment、月 10,000 alert、AllLogs 1 TB ingest、retention 90 日)
| 項目 | 計算 | 月額 USD |
|---|---|---|
| Metric Alert signal | 10 課金 × $0.10 | $1.00 |
| Action Group: Email | 9,000 課金 × $0.000002 | $0.02 |
| Action Group: Webhook | 10,000 → 無料枠 | $0.00 |
| Logic Apps built-in | 86,000 課金 × $0.000028 | $2.41 |
| LAW Ingestion (AllLogs) | 850 GB × $3.34 | $2,839 |
| LAW Retention (90 日) | ~1,967 GB·月 × $0.15 | ~$295 |
| 小計 | ~$3,138 / 月 |
ケース C では LAW Ingestion (AllLogs) が支配的で、アラート基盤そのもの (~$3.43) は誤差レベルです。コスト削減ポイントは Diagnostic Settings の category 絞り込み(AllMetrics のみ ON、AllLogs OFF)。
ボトムライン
- 本検証構成は実質 $0 / 月(無料枠に完全収納)
- deployment 数が 10 個を超えた瞬間に Metric Alert が課金開始 → 設計判断のしきい値
- コスト爆発の真の犯人は LAW の AllLogs 有効化 と SMS / Voice 通知 であり、アラート pipeline 自体は安い
- 真の節約効果は token 消費の抑止。アラート基盤のランニングコスト ($0〜$3) と引き換えに桁違いの token コスト爆発を防ぐ構造
複数 Deployment への拡張
検証は 1 deployment (gpt-5-mini) で完結しましたが、複数の deployment(例: gpt-5-mini + gpt-4o + text-embedding-3-large)を 1 つの AI Services account 上で運用する場合の構成判断を整理します。
2 つの構成オプション
| 観点 | Option A: alert rule × N 本 | Option B: 1 alert rule + dimension splitting |
|---|---|---|
| alert rule | deployment 数 N | 1 本 |
| Action Group | 1 本(共通) | 1 本(共通) |
| Logic App | 1 本(共通) | 1 本(共通) |
| threshold | rule ごとに個別設定可能 | 全 deployment で共通 1 値 |
| Azure Monitor Alerts 画面 | deployment ごとに Fired / Resolved | run history を掘る必要がある |
| Metric Alert 課金 | N signals(11 個目から $0.10 / 月) | 1 signal(無料枠内) |
| IaC 行数 | rule 定義が N 倍 | 一定 |
Logic App 側のループは不要
Metric Alert の dimension splitting を有効化すると、各 dimension instance(= 各 deployment)は 独立した alert として fire し、Action Group webhook も 1 fire = 1 call で Logic App に飛びます。よって:
- 1 Logic App run = 1 deployment(並列に複数 run が走るだけ)
- 本記事の 2 段
coalesceロジックがそのまま使える - For ループは不要
For ループが必要になるのは「1 alert rule に複数の criteria を束ねた構成」「Log Search Alert で結果が複数行」「Azure Functions などで集約後に Logic App を呼ぶ構成」のいずれかに限られます。今回はどれにも該当しません。
Per-deployment 別 threshold が必要な場合
Metric Alert の threshold は rule 単位で 1 値しか持てません。splitting を有効化しても全 dimension で共通 threshold が適用されます。
- 厳密に deployment ごとに別 threshold が要件 → Option A 推奨(rule 数 = N)
- どうしても rule 数を抑えたい → Option B + Logic App 側で 再判定(Switch アクションで
{deploymentName → threshold}の lookup map を持つ)。ただし alert 側 threshold は「最も緩い deployment の閾値」まで下げる必要があり、発火頻度が増える副作用があります
判断早見表
| 要件 | 推奨構成 | Logic App の中身 |
|---|---|---|
| 全 deployment で同じ threshold で十分 | Option B(1 alert + splitting / 1 AG / 1 LA) | 現状の 2 段 coalesce のまま(for ループ無し) |
| deployment ごとに別 threshold(厳密値) | Option A + 1 AG + 1 LA | 現状ロジック(for ループ無し) |
| 別 threshold + rule 数を抑制したい | Option B + LA 内 lookup map | Switch で分岐(for ループ不要) |
| 1 alert rule に複数 criterion を相乗り | Option B 拡張版 |
allOf[] を For each で走査 |
将来複数 deployment に拡張する際は Option A を第一候補 とすることを推奨します。Azure Monitor Alerts 画面で deployment ごとの fire / resolve 状態が可視化される運用メリットが、rule 数増加の認知負荷を上回るためです。
既知の課題 / 未確認事項
- Pause / Resume API は
2026-01-15-previewのため、GA 時の API version、response schema、RBAC 要件は再確認が必要です。 -
AzureMetrics/AzureMetricsV2の table 名、schema、dimension 出力は、リージョンやテナント、診断設定の状態で差分が出る可能性があります。 - deployment 別の metrics dimension が出ない場合、複数 deployment を 1 account に載せる構成では別設計が必要です。
- Logic App の payload schema 解析は Metric Alert に特化した 2 段
coalesce(condition.allOf[0].dimensions+ 空文字 fallback) に整理してありますが、将来の alert schema 変更や他種 alert (Log Alerts V2 等) との相互運用が必要になったら候補を増やしてください。 - alert 発火確認に
triggeredAlertsendpoint を使えるとは限らないため、Logic App run history などの代替 evidence を用意する必要があります。
まとめ
Foundry deployment の token 使用量を監視し、月間上限を超えたら自動 Pause する構成は、Metric Alert + Logic Apps Consumption + Managed Identity で実装できました。
実装上の勘所は次の 3 つです。
-
検知は Metric Alert (
TokenTransactionSum > 0, dimensionModelDeploymentName)、累計判定は Logic App で LAW 再クエリ という 2 段階構成にする。Metric Alert 単独で月次累計はできない (windowSize最大PT1D) ため。 - Logic App は Metric Alert の
condition.allOf[0].dimensions[0].valueから deployment 名を取り出す 2 段coalesce(空文字 fallback 付き)と、Bicep parameteraccountResourceIdへの static fallback を必ず入れる。これで payload 異常時にも Pause API へ malformed URL を投げず安全に skip できる。 - Pause / Resume /
deploymentStateの確認は2026-01-15-previewの REST API を正とする。stable CLI ではdeploymentStateが null になることがある。
セキュリティ面では、Action Group → Logic App が SAS callback URL を使う方式で Managed Identity が使えない ことを検証後に認識しました。dev 環境としては許容しつつ、production 相当では Bicep output から SAS を消す + payload validation + 将来的に Custom Webhook (useAadAuth: true) への置き換え を推奨します(詳細は「セキュリティ」セクション)。
Preview API 依存ではありますが、開発環境のコスト暴走対策としては実用的なガードレールになりそうです。
補足: 当初は Scheduled Query Rule で実装していた
実は最初の実装では Scheduled Query Rule (SQR) で検知レイヤーを組んでいました(AzureMetrics | where MetricName == "TokenTransaction" を PT5M 評価、PT1H window で監視)。検証中に「2 段階構成(短窓 trigger + Logic App での月次再計算)を採るのであれば、SQR の必然性はなく Metric Alert で十分」と気付き、本記事の Metric Alert 構成に差し替えました。
差し替えによる変化は次の通りです。
| 観点 | v1 (Scheduled Query Rule) | v2 (Metric Alert・本記事) |
|---|---|---|
| 発火条件 | KQL | 静的 threshold + dimension splitting |
| dimension splitting | KQL の extend DeploymentName で投影 |
metric dimension ModelDeploymentName で native splitting |
| Diagnostic Settings → LAW の AllMetrics 送信 | 必須 | 不要(Logic App 月次再クエリでは継続使用) |
| ingestion latency | 約 3〜5 分 | 約 1〜3 分 |
| end-to-end latency 実測 | 約 18.2 分 | 約 6 分 18 秒 |
| Logic App workflow 定義 | — |
v1→v2 切替時は変更不要(coalesce 第 2 候補 = Metric Alert payload の condition.allOf[0].dimensions[0].value と一致したため)。本記事の最終版では Metric Alert 専用に 2 段 coalesce に整理 |
差し替えた結果、end-to-end latency が 18.2 分 → 6 分 18 秒 (約 66% 短縮) されました。Logic App の workflow は v1→v2 切替時は無修正で動いた(coalesce 第 2 候補 = condition.allOf[0].dimensions[0].value が Metric Alert payload と完全一致したため)ので、当時は「coalesce で複数 schema 候補を並べる defensive parse の価値を再確認した経験」になりました。Metric Alert 専用に絞った最終版(本記事掲載分)では、その学びを踏まえて使われない候補は削除し 2 段 coalesce に整理しています。
「短窓 trigger + 長窓で再計算」を採るときは最初から Metric Alert を選ぶ、というのが今回の最大の学びです。
参考リンク
- Azure Monitor common alert schema
- Metric alerts overview (Azure Monitor)
- Supported metrics with Azure Monitor (Cognitive Services)
- Log Analytics API overview
- Logic Apps - Managed identity for HTTP action
- Action group - Webhook receiver
- Configure authentication for a secure webhook
- Bicep linter rule - outputs-should-not-contain-secrets





