kubell Creator's Note

ビジネスチャット「Chatwork」のエンジニアのブログです。

ビジネスチャット「Chatwork」のエンジニアのブログです。

読者になる

Datadog の SLO を Terraform Module 化した話

こんにちは!SRE グループの木村(@sssle11)です。 今年、SRE グループでは SLO の Terraform Module 化に取り組みました。この記事では、その背景と実装について紹介させていただきます。

この記事は kubell Advent Calendar 2025 の 23 日目の投稿です。


背景: SLO を IaC 管理していなかった課題

kubell では Chatwork サービスの SLO を Datadog 上で手動設定していましたが、以下のような課題がありました。

課題 1: 変更履歴の追跡が困難

設定変更の履歴が追跡しにくく、誰がいつ何を変更したのかが不明確でした。問題発生時の原因調査や、設定の経緯を確認する際に支障が出ていました。

課題 2: 品質担保の仕組みがない

コードレビューのプロセスがないため、設定ミスや意図しない変更が見逃されるリスクがありました。また、複数の SLO を一貫した方法で管理することが難しい状態でした。

課題 3: メンテナンス不足による陳腐化

作成された SLO が継続的にメンテナンスされておらず、サービス構成の変更に伴い定義自体が陳腐化していました。また、それを更新・見直す仕組みも整っていませんでした。

これらの課題を解決するため、SLO の Terraform Module 化に取り組みました。


Terraform Module 化による IaC 管理

SLO とバーンレートアラートを作成できる Terraform Module を作成しました。 このモジュールにより、以下のことが可能になりました。

SLO の定義をコード化

Datadog の SLO 設定を Terraform のリソースとして定義し、バージョン管理下に置くことで、変更履歴の追跡が可能になりました。モジュールでは、以下の 3 種類の SLO タイプに対応しています。

  • monitor: 既存の Datadog モニターを母集団にした SLO
  • count: メトリクスの分子/分母から比率を算出するカウントベース SLO
  • time_slices: 一定間隔ごとの判定で達成率を評価するタイムスライス SLO

SLO の種類に応じた設定を変数として受け取ることで、柔軟に SLO を定義できる設計となっています。 例えば、カウントベースの SLO では、成功リクエスト数と総リクエスト数の比率から可用性を計算できます。

バーンレートアラートの自動生成

SLO に紐づくバーンレートアラートも Module 内で自動的に作成されるため、SLO とアラートの整合性が保たれます。 バーンレートアラートは、エラーバジェットの消費速度を監視し、SLO 違反のリスクを早期に検知するために重要です。

モジュール内では、SLO の ID を参照してバーンレートアラートを自動生成するため、SLO を作成するだけで適切なアラート設定が完了します。

各チームが自身の SLO を管理しやすいディレクトリ構成

各チームが自身の責任範囲の SLO の管理をしやすくするため、プラットフォーム単位でディレクトリを分割する構成を採用しています。これにより、チーム単位で独立して管理・更新できるようになります。

ディレクトリ構成の例

実際のディレクトリ構成は以下のようなイメージです。

slo/
  ├── platform1/
  │   ├── main.tf      # モジュール呼び出し
  │   ├── locals.tf    # SLO 定義
  │   └── variables.tf # 変数定義
  └── platform2/
      ├── main.tf
      ├── locals.tf
      └── variables.tf

各プラットフォームの locals.tf に SLO を定義し、main.tf で Module を呼び出すことで SLO を作成しています。

Module の設計で工夫した点

Module を設計する際に工夫した点をいくつか紹介します。

3 種類の SLO タイプを 1 つの Module で統一

Datadog の SLO には monitor ベース、count ベース、time_slice ベースの 3 種類があります。 これらを別々の Module として作成することも考えましたが、呼び出し側のインターフェースを統一するために 1 つの Module にまとめました。

type 変数で SLO の種類を指定し、内部では for_each を使った条件分岐で適切なリソースのみを作成しています。

resource "datadog_service_level_objective" "monitor" {
  for_each = var.type == "monitor" ? { "this" = true } : {}
  # ...
}

resource "datadog_service_level_objective" "count" {
  for_each = var.type == "count" ? { "this" = true } : {}
  # ...
}

これにより、呼び出し側は type = "count" のように変数を変えるだけで SLO タイプを切り替えられます。

バーンレートアラートの自動生成

SLO を作成する際、バーンレートアラートも一緒に設定するケースがほとんどです。 これを別々に管理すると設定漏れや不整合が発生しやすいため、Module 内で SLO の ID を参照してバーンレートアラートを自動生成する設計にしました。

locals {
  slo_ids = {
    monitor     = try(datadog_service_level_objective.monitor["this"].id, null)
    count       = try(datadog_service_level_objective.count["this"].id, null)
    time_slices = try(datadog_service_level_objective.time_slices["this"].id, null)
  }
  slo_id = local.slo_ids[var.type]
}

resource "datadog_monitor" "this" {
  count = var.burn_rate_alert_config != null ? 1 : 0

  type  = "slo alert"
  query = <<QUERY
burn_rate("${local.slo_id}").over("${var.burn_rate_alert_config.query.timeframe}")...
QUERY
  # ...
}

burn_rate_alert_config 変数を渡せばアラートが作成され、渡さなければ作成されません。 これにより、SLO とアラートの整合性が自動的に保たれ、設定漏れを防ぐことができます。

モジュールの使用例

実際にこのモジュールを使って SLO を定義する例を紹介します。

例えば、メッセージ送信の可用性を測定する SLO を定義する場合、locals.tf に以下のように記述します。

locals {
  message_send_slo = {
    name        = "Message Send Availability"
    type        = "count"
    description = "SLO for message send functionality"
    tags        = ["team:my_team", "service:my_service"]

    thresholds = [
      { timeframe = "7d",  target = 99.9, warning = 99.95 },
      { timeframe = "30d", target = 99.9, warning = 99.95 },
    ]

    count_query = {
      numerator   = "sum:my_service.message.send.success{*}.as_count()"
      denominator = "sum:my_service.message.send.total{*}.as_count()"
    }

    target_threshold  = 99.9
    warning_threshold = 99.95
    timeframe         = "30d"

    burn_rate_alert_config = {
      name     = "Message Send SLO Burn Rate Alert"
      priority = 1
      message  = "Message send SLO burn rate is high"
      query = {
        timeframe    = "30d"
        long_window  = "1h"
        short_window = "5m"
      }
      burn_rate_alert_monitor_thresholds = {
        critical = 14.4
      }
    }
  }
}

そして、main.tf でモジュールを呼び出します。

module "message_send_slo" {
  source = "../../modules/datadog/slo"

  name        = local.message_send_slo.name
  type        = local.message_send_slo.type
  description = local.message_send_slo.description
  tags        = local.message_send_slo.tags

  thresholds = local.message_send_slo.thresholds
  count_query = local.message_send_slo.count_query

  target_threshold  = local.message_send_slo.target_threshold
  warning_threshold = local.message_send_slo.warning_threshold
  timeframe         = local.message_send_slo.timeframe

  burn_rate_alert_config = local.message_send_slo.burn_rate_alert_config
}

この設計により、SLO の定義を宣言的に記述でき、設定の変更もコードレビューを通じて安全に行えるようになりました。 また、モジュール化により、複数のプラットフォームで同じパターンで SLO を定義できるため、一貫性が保たれます。

IaC 化によって得られたメリット

Terraform Module 化により、以下のメリットが得られました。

変更履歴の可視化

Git でバージョン管理されることで、誰がいつ何を変更したかが明確になりました。問題発生時の原因調査や、過去の設定経緯の確認が容易になっています。

コードレビューによる品質担保

SLO の設定変更に対してプルリクエストベースでレビューを行えるようになり、設定ミスや意図しない変更を事前に防止できるようになりました。

一貫性のある SLO 管理

モジュール化により、複数の SLO を同じパターンで定義できるため、設定の一貫性が保たれます。新しい SLO を追加する際も、既存の定義を参考にすることで迷いなく作成できます。

スケーラビリティの向上

宣言的に SLO を定義できるため、機能追加や変更に伴う SLO の追加・更新が容易になりました。各チームが自身の責任範囲の SLO を独立して管理できる構成になっているため、運用の負荷も分散されています。

今後の展望

今回の IaC 化により、SLO の管理基盤が整いました。今後は以下の点に取り組んでいく予定です。

SLO のカバレッジ拡大

現在は一部の機能に対して SLO を定義していますが、今後は他の機能にも展開していく予定です。 Module 化により、新しい SLO の追加が容易になったため、カバレッジの拡大がスムーズに行えると考えています。

各チームへの展開

現在は SRE グループが主体となって SLO の管理を行っていますが、最終的には各開発チームが自身の責任範囲の SLO を自立的に管理できる体制を目指しています。 ディレクトリ構成やモジュールの設計は、そのような展開を見据えたものになっています。