MNTSQ Techブログ

「MNTSQ(モンテスキュー)」のTechブログです。

見たいものは全て見よ!ーAIエージェントで変わった監視モニタリングIaC ー

MNTSQ Tech Blog TOP > 記事一覧 > 見たいものは全て見よ!ーAIエージェントで変わった監視モニタリングIaC ー

はじめに

「監視モニタリングのIaCとか机上の空論だろ。労力とリターンが見合わんわ」

…と思っていた時期が私にもありました(慣用句)

前回の記事でも少し触れましたが、AIエージェントの登場によってDatadog × Terraformのような監視モニタリングのIaCの実践が劇的に楽になり、気づけば手動でポチポチとモニタリングの設定をする運用の方が限りなく非効率になってしまいました。

AIエージェントをどう利用するかという部分は、まだまだ過渡期であり皆さま試行錯誤中ではあると思いますが、弊社SREチームではAIエージェントを活用しモニタリング対象の飛躍的な拡充モニタリングコード品質の大幅な改善運用負荷の劇的な軽減を実現できました。そこで、どのような取り組みを行い、これを実現したかを紹介したいと思います。

※ 本記事で扱うのはDatadog × Terraform での実践内容となります

前回の記事はこちら tech.mntsq.co.jp

これまで: 監視モニタリングのIaCは重い課題だった

一般的にSaaSを運営している開発組織では、本番環境のみではなくステージング環境、開発環境など複数の環境を管理しています。モニタリングを真面目にやろうとすると、「環境数 × 対象」でモニタやダッシュボードが乗算的に増えていく ため、手動で管理はほぼ不可能に近いです。(頑張って作ったとしても、細かな変更を全体に反映できず、結局保守はできない)

弊社の場合、ダッシュボードは本当に重要な対象に絞って本番環境だけで整備、モニタはマルチアラートを利用して数を減らすなどの工夫で凌いでいましたが、やはり保守の手間はなかなか重いものでした。

「じゃあコード管理すれば良いのでは?」と思うかもしれませんが......

Fargate用ダッシュボード CPU、メモリ、エフェメラルストレージのウィジェットを記載するコードの一部

# ── Fargate サービスのメトリクスウィジェット ──
  fargate_widgets = {
    for svc in var.services :
    svc.ecs_service => [
      # CPU使用率
      {
        definition = {
          title          = "CPU使用率(%, コンテナ単位)"
          title_size     = "16"
          title_align    = "left"
          show_legend    = true
          legend_layout  = "auto"
          legend_columns = ["avg", "min", "max", "value", "sum"]
          type           = "timeseries"
          requests = [{
            formulas = [{
              formula = "query1 / query2 * 100"
            }]
            queries = [
              {
                name        = "query1"
                data_source = "metrics"
                query       = "avg:container.cpu.usage{${local.service_tag[svc.ecs_service]}:${svc.ecs_service},${local.container_filter[svc.ecs_service]}} by {task_arn}"
              },
              {
                name        = "query2"
                data_source = "metrics"
                query       = "avg:container.cpu.limit{${local.service_tag[svc.ecs_service]}:${svc.ecs_service},${local.container_filter[svc.ecs_service]}} by {task_arn}"
              }
            ]
            response_format = "timeseries"
            style = {
              palette    = "dog_classic"
              order_by   = "values"
              line_type  = "solid"
              line_width = "normal"
            }
            display_type = "line"
          }]
        }
        layout = { x = 0, y = 2, width = local.widget_width, height = 3 }
      },
      # メモリ使用率
      {
        definition = {
          title          = "メモリ使用率(%, コンテナ単位)"
          title_size     = "16"
          title_align    = "left"
          show_legend    = true
          legend_layout  = "auto"
          legend_columns = ["avg", "min", "max", "value", "sum"]
          type           = "timeseries"
          requests = [{
            formulas = [{
              formula = "query1 / query2 * 100"
              number_format = {
                unit = {
                  type      = "canonical_unit"
                  unit_name = "percent"
                }
              }
            }]
            queries = [
              {
                name        = "query1"
                data_source = "metrics"
                query       = "max:container.memory.usage{${local.service_tag[svc.ecs_service]}:${svc.ecs_service},${local.container_filter[svc.ecs_service]}} by {task_arn}"
              },
              {
                name        = "query2"
                data_source = "metrics"
                query       = "max:container.memory.limit{${local.service_tag[svc.ecs_service]}:${svc.ecs_service},${local.container_filter[svc.ecs_service]}} by {task_arn}"
              }
            ]
            response_format = "timeseries"
            style = {
              palette    = "dog_classic"
              order_by   = "values"
              line_type  = "solid"
              line_width = "normal"
            }
            display_type = "line"
          }]
        }
        layout = { x = 0, y = 5, width = local.widget_width, height = 3 }
      },
      # エフェメラルストレージ
      {
        definition = {
          title          = "エフェメラルストレージ空き領域(%)"
          title_size     = "16"
          title_align    = "left"
          show_legend    = true
          legend_layout  = "auto"
          legend_columns = ["avg", "min", "max", "value", "sum"]
          type           = "timeseries"
          requests = [{
            formulas = [{
              formula = "(query2 - query1) / query2 * 100"
            }]
            queries = [
              {
                name        = "query2"
                data_source = "metrics"
                query       = "max:ecs.fargate.ephemeral_storage.reserved{${local.service_tag[svc.ecs_service]}:${svc.ecs_service}} by {task_arn}"
              },
              {
                name        = "query1"
                data_source = "metrics"
                query       = "max:ecs.fargate.ephemeral_storage.utilized{${local.service_tag[svc.ecs_service]}:${svc.ecs_service}} by {task_arn}"
              }
            ]
            response_format = "timeseries"
            style = {
              palette    = "dog_classic"
              order_by   = "values"
              line_type  = "solid"
              line_width = "normal"
            }
            display_type = "line"
          }]
        }
        layout = { x = 0, y = 8, width = local.widget_width, height = 3 }
      }
    ]
    if !svc.is_ec2
  }

これは流石に無理では......?何かを追加したくなる度に、どのように宣言すれば良いかを調べ、↑のようなコードを書かなければいけないわけです。少なくとも私はダッシュボードを1つ作成する前にPCを叩き割る自信があります。

そもそもIaCは宣言的な記述が求められる故に常に一定の学習コストがかかり、自分が得意とする領域か、やらないことが許されない状況(プロダクトのインフラとか)でもない限りなかなか実践できないというのが現状だったのではないでしょうか。少なくとも、監視モニタリング領域でIaCを実践するのは、確保できる工数と照らし合わせると、不可能に近いというのが、弊社の実態でした。

AIエージェントの登場で何が変わったか

ここに大きな転機が来ました。冗長なコードを人間が理解する必要がなくなった のです。

実装はブラックボックスで良い

これまでなら、新しい監視をひとつ足すたびに次のような作業が必要でした。

  • Datadog provider の最新仕様を確認する
  • 似たような既存モニタの実装を探してコピーし、差分を埋めていく
  • メトリクスのタグ表記(env: / service: / container_name: など、起動形態で違う)を調べる
  • メッセージのテンプレート構文({{#is_alert}} 等)を思い出す・調べる

これらを、エージェントが規約と既存コードを参照しながら、ものの数十秒でこなします。人間は「ECS タスクの CPU が 100% に張り付いたら検知したい」「RDS Serverless v2 の ACU 使用率が高騰したらアラートを送ってほしい」と指示を出すだけでよく、そのまま PR にできるレベルの成果物が出てきます。

ここで重要なのは、実装をブラックボックスのまま受け入れて構わないということです。関心があるのは見たいデータが正しく取れているかのみです。それを確認できれば、生成されたコードは読む必要がないし、覚えない。それぞれが独立している故に、挙動がおかしければ捨てて作り直せばよく、それでも GUI でポチポチ作るより圧倒的に速い。「書く頻度が低くて、毎回仕様を忘れる」性質を持つ監視モニタリングコードと、この使い方は非常に相性がよいです。

これまで「IaC 化したいが、書く労力に見合うリターンが見えない」という理由で諦めていた領域に、はじめて手を出せるようになりました。

規約によって品質を揃える

とはいえ、ブラックボックスを丸ごと信用すると品質がブレるリスクは当然あります。命名がバラつき、通知先がバラつき、タグ付けがバラつくと、運用負荷はむしろ増えてしまう。

弊社ではClaudeCodeを使用しているので、これを避けるためにコーディングの規約を .claude/rules/ 配下に書き溜めています。Datadog 関連では、たとえば以下のようなルールを明文化してあり、エージェントは生成時にこれらを参照します。

  • モニタ・ダッシュボード名は 🤖 プレフィックスで Terraform 管理であることを示す
  • Slack チャンネル・メンションは locals.tf で集中管理し、モジュール側は変数経由で参照のみ
  • メトリクスのタグには env:<env> を必ず入れ、全環境平均を見てしまうミスを防ぐ
  • 新しい AWS メトリクスを使うときは、CloudWatch Metric Stream の送信対象フィルタも更新する

これに加えて、.claude/rules/datadog-monitors.md には「シンプルアラートとマルチアラートの違い」「renotify_interval の標準値」「環境フィルタ漏れの典型ミス」など、具体的なコードパターンまで載せてあります。

結果として、誰が、あるいはどのエージェントが書いても、ほぼ同じ形のコードが出てきます。レビュアは「規約からの逸脱がないか」だけを確認すればよく、レビュー労力もかなり下がりました。コードはブラックボックスにしつつ、規約は人間が育てる、という分担です。

規約の一部抜粋 規約の一部です。全体ではサンプルコードを含む記述を400行くらい書いてます

# .claude/rules/datadog-monitors.md

---
paths:
  - "terraform/aws/services/datadog/monitors_*.tf"
  - "terraform/aws/services/datadog/modules/monitors/**"
  - "terraform/aws/services/datadog/*.tf"
---

# Datadog Monitor 作成ガイド

## アーキテクチャ概要

モニターは以下の3層構造で実装する:

1. **呼び出し層**: `terraform/aws/services/datadog/monitors_<監視対象>.tf`
   - 監視対象リソースの一覧を `locals` で定義
   - `for_each` で環境 × リソースの組み合わせごとにモジュールを呼び出す
2. **モジュール層**: `terraform/aws/services/datadog/modules/monitors/<monitor_name>/`
   - `main.tf` にモニターの実体(`datadog_monitor` リソース)を定義
   - `variables.tf` にモジュールの入力変数を定義
3. **共通設定**: `terraform/aws/services/datadog/locals.tf`
   - `slack_channel`, `error_channel`, `mention` 等

## ファイル配置

```
terraform/aws/services/datadog/
├── monitors_<監視対象>.tf                    # 呼び出し層
├── modules/monitors/<monitor_name>/
│   ├── main.tf                              # モニター実体
│   └── variables.tf                         # 入力変数
├── locals.tf                                # 共通設定(slack_channel, mention等)
├── variables.tf                             # サービスモジュールの入力変数
└── provider.tf                              # プロバイダ設定
```

## モニター名の規約

- 必ず `🤖` プレフィックスを付ける(Terraform管理であることを示す)
- 環境名は **Terraform変数 `var.env`** で埋め込む: `🤖 【${var.env}】...`
  - 旧: `【{{env.name}}】`(Datadogテンプレート変数)→ 廃止
  - 新: `【${var.env}】`(Terraform変数、for_eachで環境ごとに生成するため)
- **Datadogテンプレート変数(`{{xxx.name}}`)をモニター名・メッセージに使わない**
  - 環境名、リソース名等はすべてTerraform変数(`var.env`, `var.display_name` 等)で埋め込む
  - Datadogテンプレート変数はモニター一覧画面では未展開のまま表示されるため視認性が悪い
  - 例外: `{{value}}`(アラート発火時の値)や `{{#is_alert}}` 等の条件分岐は引き続き使用する

## Slackチャンネル / メンションの使い方

`locals.tf` で定義された変数をモジュールに渡して使う:

```hcl
# Slackチャンネル(重要度別)
local.slack_channel.info      # 情報レベル
local.slack_channel.warning   # 警告レベル
local.slack_channel.alert     # 緊急レベル

# エラー通知チャンネル(環境別)
local.error_channel[env]      # 環境ごとのエラー通知先

# メンション(チーム別)
local.mention.sre        # SREチーム
local.mention.swe        # SWEチーム
local.mention.eng        # エンジニア全体
local.mention.cre        # CREチーム
local.mention.algo       # アルゴチーム
local.mention.ai_agent   # AI Agentチーム
```

--- <以下省略> ---

監視モニタリングIaCの実践例

具体的に弊社がどのようなコード構成をとっているかも軽く紹介しておきます。

コードを最小に保つ多層構成

Datadog 関連リソースは、以下の 3 層で管理しています。

envs/<env>/datadog/datadog.tf       ← ① 環境呼び出し層
        │
        ▼
services/datadog/                   ← ② サービスラッパー層(module)
        ├─ monitors_<対象>.tf         (locals + for_each で展開)
        └─ dashboard_<対象>.tf
        │
        ▼
services/datadog/modules/           ← ③ モジュール層(sub module)
        ├─ monitors/<name>          (datadog_monitor の実体)
        └─ dashboards/<name>       (datadog_dashboard_json の実体)
役割
① 環境呼び出し層 環境リストや、プロダクトコードのoutputなどの依存関係を渡してサービスを呼ぶだけ
② サービスラッパー層 監視対象や閾値などの設定差分を列挙しモジュール層に渡す
③ モジュール層 datadog_monitor / datadog_dashboard の実体。「SQSの滞留モニタ」「ECSサービスのダッシュボード」といった、環境やプロダクト毎によらない抽象的なコードを配置する

弊社の場合、Datadogのアカウントは本番用と開発用の2アカウントで運用しているため、①の環境呼び出し層でproduction, stagingなどの複数の環境をまとめてサービスラッパー層に渡しています。

ポイントは ② のラッパー層で、setproduct関数を使って「環境 × 監視対象リソース」や「監視対象 × 閾値」などの組み合わせを for_each で展開している点です。こうすることで、例えば新しい環境を足すときは環境リストに 1 行、新しい監視対象を足すときも locals に 1 行といった具合に、 追加コストが "リスト 1 行" にまで圧縮されている のがこの構成の効きどころです。AIエージェントによる生成とも非常に噛み合います。

実装サンプル

③モジュール層、②サービスラッパー層の実装サンプルはこんな感じです。

③SQSのDLQにメッセージが落ちてきたことを知らせる抽象モニタ

# ~/modules/monitors/sqs_dlq/main.tf

locals {
  doc_link_line = var.doc_link != "" ? "\n[こちらの手順](${var.doc_link})を参考に対処してください。" : ""
}

resource "datadog_monitor" "main" {
  name = "🤖 【${var.env}】${var.service_display_name} SQS DLQ ${var.display_name} にメッセージが見つかりました"
  type = "query alert"

  query = "min(last_5m):min:aws.sqs.approximate_number_of_messages_visible{queuename:${var.env}-${var.queue_name},env:${var.env}} > 0"

  message = <<EOT
${var.slack_channel.alert}

${var.mention.sre} ${var.mention_team}

{{#is_alert}}
${var.env} 環境の ${var.service_display_name} SQS DLQ ${var.display_name} にメッセージが入りました。
ワーカーの処理に失敗したメッセージが存在している可能性があります。${local.doc_link_line}

メッセージ数: {{value}} メッセージ
{{/is_alert}}

{{#is_recovery}}
${var.env} 環境の ${var.service_display_name} SQS DLQ ${var.display_name} からメッセージが削除されました。
DLQへの失敗メッセージへの対応が完了しました。
{{/is_recovery}}
EOT

  monitor_thresholds {
    critical = 0
  }

  on_missing_data     = "show_no_data"
  require_full_window = false
  renotify_interval   = 120
  renotify_statuses   = ["alert"]

  tags = [
    "service:${var.service_tag}",
    "env:${var.env}",
    "managed_by:terraform",
  ]
}

②DLQモニタに「CLM」というプロダクトのSQSを設定を渡すラッパー層

# ~/services/datadog/monitor_clm_sqs_dlq.tf
# CLM SQS DLQモニター
# DLQにメッセージが落ちてきた時にアラートを発する
# 環境 × キューごとにモニターを生成する
# キュー定義は locals_clm_sqs.tf の local.clm_sqs_queues から導出

module "monitor_clm_sqs_dlq" {
  for_each = {
    for pair in setproduct(var.dashboard_envs, local.clm_sqs_queues) :
    "${pair[0]}-${pair[1].display_name}" => {
      env          = pair[0]
      queue_name   = "${pair[1].queue_name}-dlq"
      display_name = pair[1].display_name
    }
  }

  source = "./modules/monitors/sqs_dlq"

  env                  = each.value.env
  queue_name           = each.value.queue_name
  display_name         = each.value.display_name
  service_display_name = "CLM"
  service_tag          = "clm"
  mention_team         = local.mention.clm
  doc_link             = "<対応手順書のURL>"
  slack_channel        = local.slack_channel
  mention              = local.mention
}
# ~/services/datadog/locals_clm_sqs.tf

locals {
  # CLM SQSキュー定義(環境プレフィックスなし)
  # listを使用して定義順序を保持
  clm_sqs_queues = [
    {
      queue_name   = "clm-default-app-job-worker-sqs-critical"
      display_name = "critical"
    },
    # 取り扱うキューを列挙する。ここでは省略
  ]
}

②のコードはあくまで「CLM」という特定のプロダクトのDLQモニタを定義するものです。別のプロダクトの監視を行いたいときは、②のコードを別途作成します。③のコードは再利用可能です。

このように抽象化を行うことによって、コードを最小にして多数のモニタを管理することが可能になります。そして実装部分はAIエージェントに丸投げしてしまえば、少ない運用工数で、多数のモニタリング対象を、高品質なコードで管理できるわけです。

弊社で運用している監視モニタリングの規模

2026/05/20現在の規模感は以下のとおりです。

項目
対象環境 8 環境
モニター種別 / 生成されるモニター数 30 種類 / 約 800 個
ダッシュボード種別 / 生成されるダッシュボード数 7 種類 / 約 80 個
ラッパー+モジュール層のコード行数 約 1 万行

おおざっぱに 1 万行で 800 のモニタと 80 のダッシュボードを支えている 計算です。

先に述べたように、実装規約を整備したおかげで誰でも気軽に対象の追加ができるようになり、現在でも日々監視モニタリングは充実していっています。

おわりに

本記事執筆のきっかけは、AIエージェントの登場による監視モニタリングIaCの変化は、単に"運用が楽になった" だけの話ではない と感じたためです。

これまでは「重要なものだけ厳選して監視するのが限界」と言わざるを得ませんでした。リソースを増やすほど管理コストが増えるため、観測対象は常に「これは本当に必要か」というフィルタを通って絞り込まれていたわけです。

それが、追加コストがほぼ無視できるほど軽くなった瞬間、「観測したいものは全部観測する」というスタンスに振り切れる ようになりました。新しいワーカーを足したら CPU・メモリ・レイテンシのアラートを同時に足し、新しい SQS キューを切ったら滞留と DLQ のモニタも足し、新しい RDS クラスタを建てたらスロークエリやコネクション数のダッシュボードも足す ── これらが開発フローの中で容易に実現できるようになります。

開発組織全体への良い影響もあります。新機能リリース時に「とりあえずダッシュボードはある」状態が標準になり、初動の異常検知が早くなります。モニタを足すコストも軽いため、開発者が「この指標を見たい」と SRE に相談する敷居も下がる、あるいは開発者自身がダッシュボードやモニタを追加することも今後可能となっていくはずです。

モニタリングは「保険」のように扱われがちで、潤沢な工数を割きづらい領域です。だからこそ、コストを劇的に下げてくれる手段が出てきたなら、観測の "深さ" そのものを変えにいくべきでしょう。

MNTSQ株式会社 SRE 西室