【Terraformコード公開(Datadog編)】ECS冗長化構成でAPM・ログ・メトリクス・SLOを実装する

2026 年 5 月 14 日 | AWS / Observability

【技術相談】本件の内容に関して 30 分間の無料相談承ります →

【Terraformコード公開(Datadog編)】ECS冗長化構成でAPM・ログ・メトリクス・SLOを実装する

この記事をシェア

こんにちは、練馬区旭丘で個人事業主として AI コンサル兼 AWS 構築をやっている野口です。

基礎編、New Relic 編に続く、実践編シリーズの 2 本目 Datadog 編です。前回と同じく、現実の高トラフィック ECS 環境に Datadog を入れていく Terraform の書き方をまとめます。

最初に大事なお断りです。この記事の Terraform コードは「環境に合わせて調整する雛形」であり、コピー& apply で本番投入できる完成品ではありません。VPC、IAM ポリシー、タグ規約、Terraform ディレクトリ構成は会社ごとに違うので、必ず自社環境に書き直してから使ってください。

とはいえ、「ゼロから書き始めて、どこから手をつけたらいいか分からない」段階の人が、設計判断の足場として読めるレベルまでは書きました。Datadog は機能が多すぎて、最初の構築で何から手をつけるかが本当に分かりにくいです。この記事を起点に、自社で必要な部分を組み立ててください。


この記事で扱うもの

  • Datadog AWS Integration(IAM Role 連携、Metric / Logs 自動収集の有効化)
  • ECS / Fargate タスク定義への Datadog Agent サイドカー組み込み
  • FireLens + Fluent Bit による Datadog Logs への直接転送
  • APM / トレースの環境変数設計
  • ALB / ECS / RDS / ElastiCache を見る Monitor の Terraform 化
  • 可用性 SLO と レイテンシ SLO の Terraform 化
  • 高ボリューム ECS 環境でのコスト制御(ホスト課金、Logs Index、Custom Metrics、High Cardinality)
  • 現場で本当に踏む Datadog 特有の落とし穴

想定アーキテクチャ

実践編シリーズ共通で、現実的な高トラフィック ECS 構成を想定します。

  • ALB(複数 AZ、HTTPS 終端、WAF 連携あり)
  • ECS on Fargate(複数 AZ、Service Auto Scaling 有効、タスク数 50〜500 の振れ幅)
  • RDS Aurora MySQL Multi-AZ(Writer + Reader 2 本)
  • ElastiCache for Redis(クラスタモード有効、複数シャード)
  • VPC エンドポイント(S3, ECR, CloudWatch Logs, Secrets Manager 向け)
  • Datadog AWS Integration(IAM Role + 自動収集)
  • アプリコンテナ + Datadog Agent サイドカー + FireLens サイドカー

ポイントは、Datadog の場合は 「AWS 統合(CloudWatch ベース)」と「Agent 直送」の二段構えで組むのが定石ということ。

  • AWS Integration: ALB / RDS / ElastiCache / ECS Service-level メトリクスを CloudWatch 経由で収集
  • Agent サイドカー: ECS タスク内部からプロセス・コンテナ・APM・DogStatsD を直接送信
  • FireLens: アプリログを直接 Datadog Logs エンドポイントへ転送

なぜ二段構えにするかというと、AWS Integration だけだとコンテナ内部の粒度が見えず、Agent だけだと ALB や RDS の AWS サービスメトリクスが取り切れないからです。両方走らせるのが Datadog の現実解。


実装方針:何を Terraform で管理するか

Terraform で管理するスコープを最初に切ります。

  • AWS provider と Datadog provider
  • DD_API_KEY / DD_APP_KEY は Secrets Manager または SSM Parameter Store に格納し、ARN 参照でのみ渡す
  • Datadog AWS Integration リソース(datadog_integration_aws_account
  • ECS Task Definition(アプリ + Datadog Agent + FireLens の 3 コンテナ構成)
  • Datadog Monitor(datadog_monitor
  • Datadog SLO(datadog_service_level_objective

CI/CD(PR ごとに plan、main マージで apply)は別記事で扱います。Datadog Dashboard を JSON で書く話も尺の都合で次回以降。


variables.tf 相当:環境差分を集約する

variable "aws_region" {
  description = "AWSリージョン"
  type        = string
  default     = "ap-northeast-1"
}

variable "environment" {
  description = "環境名(prod/staging/dev)"
  type        = string
}

variable "service_name" {
  description = "サービス名(タグ・ARNの一部に使用)"
  type        = string
}

variable "team" {
  description = "オーナーチーム名(タグに付与)"
  type        = string
}

variable "cluster_name" {
  description = "ECS クラスタ名"
  type        = string
}

variable "datadog_site" {
  description = "Datadog サイト(datadoghq.com / datadoghq.eu / us3.datadoghq.com など)"
  type        = string
  default     = "datadoghq.com"
}

# API Key は Terraform 変数に平文で渡さない
# Secrets Manager に事前格納し、ARN 参照で受け取る
variable "datadog_api_key_secret_arn" {
  description = "Datadog API Key を格納した Secrets Manager の ARN"
  type        = string
}

variable "datadog_app_key_secret_arn" {
  description = "Datadog Application Key を格納した Secrets Manager の ARN(Terraform provider 認証用)"
  type        = string
}

variable "common_tags" {
  description = "全リソース共通タグ"
  type        = map(string)
  default = {
    ManagedBy = "Terraform"
  }
}

この設計のポイントは「DD_API_KEY / DD_APP_KEY は Secrets Manager に事前に置いて、ARN だけ Terraform に渡す」こと。Terraform 変数に平文で渡すのはやめてください。terraform.tfstate に API Key が平文で残るのは事故のもとです(リモートステートが暗号化されていても、CI/CD のログや plan 出力に流れる経路は多い)。

SSM Parameter Store(SecureString)でも同じ設計で動きます。社内のシークレット管理基盤に合わせてください。

自社環境で変えるポイント

  • common_tags は社内のタグ規約に合わせる(CostCenter、Owner、Compliance、Version など)
  • datadog_site は契約しているリージョンに合わせる(US1 と EU は別 SaaS なので間違えると永遠にデータが上がらない
  • 環境別の値は terraform.tfvars または workspace で切り替え

Provider 設定

terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    datadog = {
      source  = "DataDog/datadog"
      version = "~> 3.40"
    }
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = merge(var.common_tags, {
      Environment = var.environment
      Service     = var.service_name
      Team        = var.team
    })
  }
}

# Datadog provider は API Key / APP Key を Secrets Manager から動的取得
data "aws_secretsmanager_secret_version" "datadog_api_key" {
  secret_id = var.datadog_api_key_secret_arn
}

data "aws_secretsmanager_secret_version" "datadog_app_key" {
  secret_id = var.datadog_app_key_secret_arn
}

provider "datadog" {
  api_key = data.aws_secretsmanager_secret_version.datadog_api_key.secret_string
  app_key = data.aws_secretsmanager_secret_version.datadog_app_key.secret_string
  api_url = "https://api.${var.datadog_site}/"
}

api_url必ず明示してください。これを忘れると、契約が EU や US3 なのに datadoghq.com(US1)に向いて、provider が認証エラーで死にます。

CI で動かす場合は、Datadog の API Key / APP Key を直接環境変数 DD_API_KEY / DD_APP_KEY で渡してもよいですが、ローカル / CI / プロダクション apply で経路を統一するために Secrets Manager 経由を推奨します。


Datadog AWS Integration

Datadog 側で「この AWS アカウントを連携する」設定を Terraform で書きます。これで CloudWatch メトリクスや AWS リソースのメタデータが自動収集されるようになります。

# Datadog 側で External ID を発行(連携準備)
resource "datadog_integration_aws_account" "this" {
  account_tags = [
    "env:${var.environment}",
    "team:${var.team}"
  ]

  aws_account_id = data.aws_caller_identity.current.account_id
  aws_partition  = "aws"

  auth_config {
    aws_auth_config_role {
      role_name = "DatadogAWSIntegrationRole-${var.service_name}-${var.environment}"
    }
  }

  metrics_config {
    automute_enabled               = true
    collect_cloudwatch_alarms      = false
    collect_custom_metrics         = true
    enabled                        = true
    namespace_filters {
      # 必要な namespace だけ含める
      include_only = [
        "AWS/ApplicationELB",
        "AWS/ECS",
        "AWS/RDS",
        "AWS/ElastiCache",
        "AWS/Lambda",
        "AWS/NATGateway",
      ]
    }
  }

  logs_config {
    # CloudWatch Logs を Datadog に取り込むなら true。Logs は FireLens 直送が主なら false 推奨
    lambda_forwarder {
      lambdas = []
    }
  }

  resources_config {
    cloud_security_posture_management_collection = false
    extended_collection                          = true
  }

  traces_config {
    xray_services {
      # X-Ray を取り込まないなら空
    }
  }
}

data "aws_caller_identity" "current" {}

# Datadog 用の IAM Role(Datadog アカウントから AssumeRole される)
data "aws_iam_policy_document" "datadog_assume_role" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::464622532012:root"] # Datadog 側 AWS アカウント
    }

    condition {
      test     = "StringEquals"
      variable = "sts:ExternalId"
      values   = [datadog_integration_aws_account.this.id]
    }
  }
}

resource "aws_iam_role" "datadog_integration" {
  name               = "DatadogAWSIntegrationRole-${var.service_name}-${var.environment}"
  assume_role_policy = data.aws_iam_policy_document.datadog_assume_role.json
}

# Datadog が推奨する権限セット
# 細かく絞るなら Datadog の公式ドキュメントの最小権限ポリシーを使う
resource "aws_iam_role_policy_attachment" "datadog_readonly" {
  role       = aws_iam_role.datadog_integration.name
  policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}

resource "aws_iam_role_policy" "datadog_extras" {
  role = aws_iam_role.datadog_integration.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "tag:GetResources",
          "tag:GetTagKeys",
          "tag:GetTagValues",
          "cloudwatch:Describe*",
          "cloudwatch:Get*",
          "cloudwatch:List*",
          "ec2:Describe*",
          "ecs:Describe*",
          "ecs:List*",
          "rds:Describe*",
          "elasticache:Describe*",
          "elasticloadbalancing:Describe*"
        ]
        Resource = "*"
      }
    ]
  })
}

ここが一番の落とし穴

namespace_filters.include_only書かずに連携すると、すべての AWS サービスのメトリクスが Datadog に流れます。Custom Metrics 課金が発生する namespace もあるので、月末に「Custom Metrics が異常に多い」と気付いて遡って絞り込みする羽目になります。最初から必要な namespace だけ列挙すること。

External ID は datadog_integration_aws_account.this.id を参照するのが定石。Datadog 側が発行する識別子で、これを Trust Policy の Condition に入れないと他テナント混入リスクがあります(Datadog のサポート画面でも警告が出ます)。

自社環境で変えるポイント

  • namespace_filters の中身は自社で本当に必要な AWS サービスだけに絞る
  • collect_cloudwatch_alarms は CloudWatch Alarm を Datadog に上げるかどうか。Datadog 側で Monitor を作るならむしろ false 推奨(二重通知が起きる)
  • cloud_security_posture_management_collection を有効にすると CSPM 課金が乗るので、必要時のみ

provider バージョン注意: datadog_integration_aws_account は Datadog provider v3.40 系から導入された比較的新しいリソースです。古い構築では datadog_integration_aws(廃止予定の旧名)が使われていることがあります。terraform plan で差分を必ず確認してください。


ECS Task Definition:アプリ + Datadog Agent + FireLens の 3 コンテナ構成

Datadog の流儀では、ECS Fargate タスクに Datadog Agent をサイドカーとして同居させるのが基本パターンです。Agent がトレースを受けて Datadog に転送し、DogStatsD でカスタムメトリクスも受け付けます。

# FireLens 自身のログ用 CloudWatch Logs Group
resource "aws_cloudwatch_log_group" "firelens" {
  name              = "/ecs/${var.service_name}/${var.environment}/firelens"
  retention_in_days = 7
}

resource "aws_ecs_task_definition" "app" {
  family                   = "${var.service_name}-${var.environment}"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "1024"
  memory                   = "2048"
  execution_role_arn       = aws_iam_role.ecs_task_execution.arn
  task_role_arn            = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    # ========== 1) アプリコンテナ ==========
    {
      name      = "app"
      image     = "${var.ecr_repo_url}:${var.app_image_tag}"
      essential = true
      portMappings = [{ containerPort = 3000, protocol = "tcp" }]

      environment = [
        { name = "NODE_ENV",             value = var.environment },
        # APM / トレース
        { name = "DD_SERVICE",           value = var.service_name },
        { name = "DD_ENV",               value = var.environment },
        { name = "DD_VERSION",           value = var.app_image_tag },
        { name = "DD_AGENT_HOST",        value = "127.0.0.1" }, # awsvpc なのでサイドカーは localhost で到達
        { name = "DD_TRACE_AGENT_PORT",  value = "8126" },
        { name = "DD_DOGSTATSD_PORT",    value = "8125" },
        { name = "DD_LOGS_INJECTION",    value = "true" },      # trace_id をログに注入
        { name = "DD_PROFILING_ENABLED", value = "true" },
        { name = "DD_TRACE_SAMPLE_RATE", value = "0.1" },       # 高トラフィック前提で10%
        # Unified Service Tagging
        { name = "DD_TAGS", value = "env:${var.environment},service:${var.service_name},team:${var.team},cluster:${var.cluster_name}" }
      ]

      # アプリログは FireLens 経由で Datadog Logs へ直送
      logConfiguration = {
        logDriver = "awsfirelens"
        options = {
          Name           = "datadog"
          Host           = "http-intake.logs.${var.datadog_site}"
          TLS            = "on"
          dd_service     = var.service_name
          dd_source      = "nodejs"
          dd_tags        = "env:${var.environment},team:${var.team},cluster:${var.cluster_name}"
          provider       = "ecs"
        }
        secretOptions = [
          {
            name      = "apikey"
            valueFrom = var.datadog_api_key_secret_arn
          }
        ]
      }
    },

    # ========== 2) Datadog Agent サイドカー ==========
    {
      name      = "datadog-agent"
      image     = "public.ecr.aws/datadog/agent:latest"
      essential = true

      environment = [
        { name = "DD_SITE",                        value = var.datadog_site },
        { name = "DD_APM_ENABLED",                 value = "true" },
        { name = "DD_APM_NON_LOCAL_TRAFFIC",       value = "true" },
        { name = "DD_DOGSTATSD_NON_LOCAL_TRAFFIC", value = "true" },
        { name = "ECS_FARGATE",                    value = "true" },
        { name = "DD_ENV",                         value = var.environment },
        { name = "DD_SERVICE",                     value = var.service_name },
        { name = "DD_TAGS",                        value = "env:${var.environment},service:${var.service_name},team:${var.team},cluster:${var.cluster_name}" },
        # 高カーディナリティ抑止のため process / container メトリクスを制限したい場合は以下を調整
        { name = "DD_PROCESS_AGENT_ENABLED",       value = "false" }
      ]

      secrets = [
        {
          name      = "DD_API_KEY"
          valueFrom = var.datadog_api_key_secret_arn
        }
      ]

      portMappings = [
        { containerPort = 8126, protocol = "tcp" }, # APM
        { containerPort = 8125, protocol = "udp" }  # DogStatsD
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-group         = aws_cloudwatch_log_group.firelens.name
          awslogs-region        = var.aws_region
          awslogs-stream-prefix = "datadog-agent"
        }
      }

      healthCheck = {
        command  = ["CMD-SHELL", "agent health"]
        interval = 30
        timeout  = 5
        retries  = 3
      }
    },

    # ========== 3) FireLens (Fluent Bit) サイドカー ==========
    {
      name      = "log_router"
      image     = "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable"
      essential = true

      firelensConfiguration = {
        type = "fluentbit"
        options = {
          enable-ecs-log-metadata = "true"
        }
      }

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-group         = aws_cloudwatch_log_group.firelens.name
          awslogs-region        = var.aws_region
          awslogs-stream-prefix = "firelens"
        }
      }

      memoryReservation = 50
    }
  ])
}

この設計の肝

  • DD_SERVICE / DD_ENV / DD_VERSION の 3 点セット(Unified Service Tagging)を必ず設定。Datadog が APM / ログ / メトリクスを横断で結びつける鍵
  • DD_AGENT_HOST = 127.0.0.1 で同タスク内の Agent サイドカーへ到達(awsvpc モードなのでコンテナ間は localhost)
  • DD_API_KEY は Secrets Manager から ECS Secrets 経由で注入。環境変数に平文で書かない
  • アプリログは FireLens で Datadog 直送。CloudWatch Logs を経由しない経路にすることで、CloudWatch 課金と遅延を回避
  • DD_TRACE_SAMPLE_RATE = 0.1(10%)は高トラフィック前提の妥当な開始値。低トラフィックなら 1.0 で全件取ってよい
  • DD_LOGS_INJECTION = truetrace_id がログに自動注入されるので、ログとトレースの相関がクリック 1 つで取れる

自社環境で変えるポイント

  • dd_source はアプリのランタイムに合わせる(nodejs / python / java / golang / dotnet)
  • DD_VERSION は ECR タグや CI のコミットハッシュなど デプロイ単位で一意になる値にする(パフォーマンス劣化のリリース起因を見つけるために重要)
  • CPU / Memory は実測ベースで調整。Agent サイドカーは最低 256 CPU / 512MB は確保しないと、高負荷時に Agent が詰まってトレースを落とします

高ボリューム環境での注意点

  • DD_PROCESS_AGENT_ENABLED を有効にすると Process メトリクスが大量に上がる。タスク数が多い環境では false 推奨
  • DD_PROFILING_ENABLED は強力だが、Profiling 課金が発生する。本番の主要サービスだけ有効化する運用が現実的
  • Fluent Bit の memoryReservation は最低 50MB。500 タスク並列で動く環境では 100MB 推奨

FireLens のカスタム Filter(ログ取り込みコスト制御)

タスク定義に Name = datadog を書くだけで最低限動きますが、Datadog Logs は取り込み課金が支配的なので、Fluent Bit 段階での絞り込みが必須です。

resource "aws_s3_bucket" "fluentbit_config" {
  bucket = "${var.service_name}-fluentbit-config-${var.environment}"
}

resource "aws_s3_object" "fluentbit_config" {
  bucket  = aws_s3_bucket.fluentbit_config.id
  key     = "fluentbit.conf"
  content = <<-EOT
    [SERVICE]
        Flush               1
        Log_Level           info
        Parsers_File        parsers.conf

    # DEBUG ログを破棄
    [FILTER]
        Name                grep
        Match               *
        Exclude             level debug

    # ヘルスチェックのアクセスログを破棄
    [FILTER]
        Name                grep
        Match               *
        Exclude             path ^/healthz$

    # 共通タグを付与
    [FILTER]
        Name                modify
        Match               *
        Add                 env     ${var.environment}
        Add                 service ${var.service_name}
        Add                 team    ${var.team}
        Add                 cluster ${var.cluster_name}

    [OUTPUT]
        Name                datadog
        Match               *
        Host                http-intake.logs.${var.datadog_site}
        TLS                 on
        apikey              $${DD_API_KEY}
        dd_service          ${var.service_name}
        dd_source           nodejs
        dd_tags             env:${var.environment},team:${var.team},cluster:${var.cluster_name}
  EOT
}

この設定の意味

  • DEBUG ログを Fluent Bit 段階で破棄。Datadog 取り込み課金を最大要因の手前で削る
  • /healthz のアクセスログを破棄。ALB ヘルスチェックが秒単位で叩くので、これだけで全ログの 30〜50% を占めることがある
  • 共通タグを付与しておくと、Datadog の Logs Explorer や Pipeline で絞り込みやすい

現場の落とし穴

  • Log_Level debug のまま本番投入する事故は Datadog でも頻発。月初に通知が来てから気付くやつ
  • ヘルスチェックログを残しがちだが、本番ではほぼ全件捨ててよい。残すなら 100:1 程度のサンプリング
  • S3 設定ファイル経由は反映に ECS タスク入れ替え(デプロイ)が必要。Terraform apply 即反映ではない

Datadog Monitor の Terraform 化

「Monitor は UI でポチポチ作成」だと、レビューも履歴管理もできずインフラ as Code が成立しません。Terraform で書いて PR レビューに乗せる、が鉄則です。

ALB の 5xx 率

resource "datadog_monitor" "alb_5xx_rate" {
  name    = "[${var.environment}] ALB 5xx Rate High - ${var.service_name}"
  type    = "metric alert"
  message = <<-EOT
    ALB 5xx の発生率が閾値を超えました。
    アプリ / DB / 上流サービスのいずれかが劣化している可能性があります。
    @slack-${var.team}-oncall
  EOT

  query = <<-QUERY
    sum(last_5m):(
      sum:aws.applicationelb.httpcode_target_5xx{service:${var.service_name},env:${var.environment}}.as_count() /
      sum:aws.applicationelb.request_count{service:${var.service_name},env:${var.environment}}.as_count()
    ) * 100 > 1
  QUERY

  monitor_thresholds {
    critical          = 1
    warning           = 0.5
    critical_recovery = 0.5
    warning_recovery  = 0.2
  }

  notify_no_data      = false
  evaluation_delay    = 60 # AWS Integration の取り込み遅延を吸収
  require_full_window = false

  tags = ["env:${var.environment}", "service:${var.service_name}", "team:${var.team}"]
}

p95 レイテンシ

resource "datadog_monitor" "p95_latency" {
  name    = "[${var.environment}] p95 Latency High - ${var.service_name}"
  type    = "metric alert"
  message = "p95 レスポンスタイムが閾値を超えました。 @slack-${var.team}-oncall"

  query = <<-QUERY
    avg(last_5m):p95:trace.express.request{service:${var.service_name},env:${var.environment}} > 1
  QUERY

  monitor_thresholds {
    critical = 1    # 1秒
    warning  = 0.7
  }

  evaluation_delay = 30
  tags             = ["env:${var.environment}", "service:${var.service_name}", "team:${var.team}"]
}

ECS タスクの CPU / メモリ

resource "datadog_monitor" "ecs_cpu" {
  name    = "[${var.environment}] ECS CPU High - ${var.service_name}"
  type    = "metric alert"
  message = "ECS タスクの CPU 使用率が閾値を超えました。スケールアウト発生中か確認してください。"

  query = <<-QUERY
    avg(last_10m):avg:aws.ecs.cpuutilization{servicename:${var.service_name},env:${var.environment}} > 80
  QUERY

  monitor_thresholds {
    critical = 80
    warning  = 70
  }

  evaluation_delay = 60
  tags             = ["env:${var.environment}", "service:${var.service_name}", "team:${var.team}"]
}

resource "datadog_monitor" "ecs_memory" {
  name    = "[${var.environment}] ECS Memory High - ${var.service_name}"
  type    = "metric alert"
  message = "ECS タスクのメモリ使用率が閾値を超えました。リーク or サイジング再考のサイン。"

  query = <<-QUERY
    avg(last_10m):avg:aws.ecs.memoryutilization{servicename:${var.service_name},env:${var.environment}} > 80
  QUERY

  monitor_thresholds {
    critical = 80
    warning  = 70
  }

  evaluation_delay = 60
  tags             = ["env:${var.environment}", "service:${var.service_name}", "team:${var.team}"]
}

ECS タスクの再起動検知

resource "datadog_monitor" "ecs_task_restart" {
  name    = "[${var.environment}] ECS Task Restart Burst - ${var.service_name}"
  type    = "metric alert"
  message = "ECS タスクが短時間に複数回再起動しています。 OOM / Health check / デプロイ起因を確認してください。"

  # 期待タスク数と実行タスク数の乖離が一定以上続いたら検知
  query = <<-QUERY
    avg(last_15m):(
      avg:aws.ecs.service.desired{service:${var.service_name},env:${var.environment}} -
      avg:aws.ecs.service.running{service:${var.service_name},env:${var.environment}}
    ) > 2
  QUERY

  monitor_thresholds {
    critical = 2
  }

  evaluation_delay = 60
  tags             = ["env:${var.environment}", "service:${var.service_name}", "team:${var.team}"]
}

RDS の CPU / コネクション数

resource "datadog_monitor" "rds_cpu" {
  name    = "[${var.environment}] RDS CPU High - ${var.service_name}"
  type    = "metric alert"
  message = "RDS の CPU 使用率が閾値を超えました。重いクエリ or コネクション急増を確認してください。"

  query = <<-QUERY
    avg(last_10m):avg:aws.rds.cpuutilization{dbinstanceidentifier:${var.service_name}-${var.environment}-*} > 80
  QUERY

  monitor_thresholds {
    critical = 80
    warning  = 70
  }

  evaluation_delay = 120 # RDS メトリクスは到達遅延が大きめ
  tags             = ["env:${var.environment}", "service:${var.service_name}", "team:${var.team}"]
}

resource "datadog_monitor" "rds_connections" {
  name    = "[${var.environment}] RDS Connections High - ${var.service_name}"
  type    = "metric alert"
  message = "RDS のコネクション数が閾値を超えました。 max_connections に近付いていないか確認。"

  query = <<-QUERY
    avg(last_5m):avg:aws.rds.database_connections{dbinstanceidentifier:${var.service_name}-${var.environment}-*} > 200
  QUERY

  monitor_thresholds {
    critical = 200
    warning  = 150
  }

  evaluation_delay = 120
  tags             = ["env:${var.environment}", "service:${var.service_name}", "team:${var.team}"]
}

ElastiCache の Evictions

resource "datadog_monitor" "elasticache_evictions" {
  name    = "[${var.environment}] ElastiCache Evictions Detected - ${var.service_name}"
  type    = "metric alert"
  message = "ElastiCache でエビクションが発生しています。キャッシュサイズ or TTL の見直しが必要です。"

  query = <<-QUERY
    sum(last_15m):sum:aws.elasticache.evictions{cache_cluster_id:${var.service_name}-${var.environment}-*}.as_count() > 0
  QUERY

  monitor_thresholds {
    critical = 0
  }

  evaluation_delay = 120
  tags             = ["env:${var.environment}", "service:${var.service_name}", "team:${var.team}"]
}

Monitor 設計のポイント

  • evaluation_delay を必ず設定。AWS Integration 経由のメトリクスは 1〜2 分の取り込み遅延がある。これを無視すると、遅延データで誤発火・未発火する
  • @slack-xxx@pagerduty-xxx の通知記法message に書くと、そのまま通知ルーティングできる
  • 閾値はサービスの SLO から逆算する。「とりあえず 80%」で組まない
  • tags を必ず付けるteam タグがあると、Datadog の Monitor 一覧でチーム別フィルタが効く

自社環境で変えるポイント

  • trace.express.request のメトリクス名はランタイムで変わる(trace.django.request / trace.rails.request など)
  • 閾値は実環境のベースラインを 1〜2 週間観測してから決める
  • 通知ルーティング先は社内のオンコールハンドルに置き換え

SLO の Terraform 化

Datadog の強みのひとつが SLO 機能です。「障害が起きたか」ではなく「サービス品質を担保できているか」を追えるようにすると、運用全体の景色が変わります。

可用性 SLO(5xx 率ベース)

resource "datadog_service_level_objective" "availability" {
  name        = "${var.service_name} ${var.environment} availability"
  type        = "metric"
  description = "ALB の 2xx/3xx を成功、5xx を失敗として 30日 99.9% を狙う"

  query {
    numerator   = "sum:aws.applicationelb.request_count{service:${var.service_name},env:${var.environment}}.as_count() - sum:aws.applicationelb.httpcode_target_5xx{service:${var.service_name},env:${var.environment}}.as_count()"
    denominator = "sum:aws.applicationelb.request_count{service:${var.service_name},env:${var.environment}}.as_count()"
  }

  thresholds {
    timeframe = "30d"
    target    = 99.9
    warning   = 99.95
  }

  tags = ["env:${var.environment}", "service:${var.service_name}", "team:${var.team}"]
}

レイテンシ SLO(p95 < 500ms ベース)

resource "datadog_service_level_objective" "latency" {
  name        = "${var.service_name} ${var.environment} latency"
  type        = "metric"
  description = "p95 が 500ms 未満で推移する時間の割合を 30日 99% で担保"

  query {
    # 「500ms未満で済んだリクエスト数」/「全リクエスト数」
    numerator   = "sum:trace.express.request.hits{service:${var.service_name},env:${var.environment},http.status_code:2xx}.as_count()"
    denominator = "sum:trace.express.request.hits{service:${var.service_name},env:${var.environment}}.as_count()"
  }

  thresholds {
    timeframe = "30d"
    target    = 99.0
    warning   = 99.5
  }

  tags = ["env:${var.environment}", "service:${var.service_name}", "team:${var.team}"]
}

SLO 設計のポイント

  • timeframe は 7d / 30d / 90d から選ぶ。30d がエラーバジェット運用の基本
  • target は事業上の許容失敗率から決める。99.9% は「月あたり 43 分のダウンが許容範囲」の意味。これを安易に 99.99% にすると運用が回らない
  • 可用性とレイテンシは別 SLO に分けること。混ぜると「どっちで予算消費したのか」が見えなくなる
  • SLO は Monitor の代わりではない。バーンレート Monitor を別途作って、エラーバジェット消費速度が速い時にだけアラートを飛ばすのが定石(次のセクションで触れる)

バーンレート Monitor(SLO と組み合わせる)

resource "datadog_monitor" "slo_burn_rate" {
  name    = "[${var.environment}] SLO Burn Rate High - ${var.service_name}"
  type    = "slo alert"
  message = "30日 SLO のエラーバジェットを急速に消費しています。エンジニアリング作業を一時停止して原因調査を。"

  query = <<-QUERY
    burn_rate("${datadog_service_level_objective.availability.id}").over("1h").long_window("1h").short_window("5m") > 14.4
  QUERY

  monitor_thresholds {
    critical = 14.4 # 1時間で30日予算の2%を消費するペース
  }

  tags = ["env:${var.environment}", "service:${var.service_name}", "team:${var.team}"]
}

エラーバジェット消費が速いときだけ起こす、というのが SRE のマルチウィンドウ・バーンレート手法の核心です。「毎月 99.9% は達成できているのに、月初に一気に予算を食って月末まで張り詰めている」みたいな状態を可視化できます。


タグ設計:Unified Service Tagging を破らない

Datadog 運用で一番重要なのがタグ設計です。これが破綻すると、Monitor / SLO / ダッシュボードの「同じサービスを示すフィルタ」が一致しなくなって地獄を見ます。

最低限揃えるべきタグ:

タグ用途
envprod / staging / dev環境フィルタ
servicepayment-apiサービス単位の集約
teamplatform / paymentオンコール・予算配分
versionv1.2.3 / abc1234(コミット SHA)デプロイ起因のリグレッション検出
clusterprod-tokyo-1ECS クラスタ・リージョン区別

これを APM Agent / Logs / Metrics の全経路で同じ値にすることで、Datadog の Unified Service Tagging が機能します。今回のサンプルコードは全箇所でこの 5 タグを揃えてあるので、そのまま雛形にしてもらえれば最低限のタグ設計はクリアできます。

やってはいけないこと

  • アプリ側で service:payment と書いて、Monitor 側で service:payment-api と書く(無言で何も検知しない Monitor が完成する)
  • AWS Integration 側のタグと FireLens 側のタグが食い違う
  • env の値が production だったり prod だったり混在する

高カーディナリティ対策:これだけは絶対に守る

Datadog のコストと検索パフォーマンスを爆死させる最大の原因が タグの高カーディナリティです。

絶対にタグにしてはいけない値

  • user_id(百万単位のユニーク値)
  • order_id / transaction_id(リクエストごとに新規)
  • request_id / trace_id(リクエストごとに新規)
  • email(個人情報の観点でも禁止)
  • session_id
  • 完全な URL(クエリパラメータ込み)

どこに入れるべきか

  • ログ本文に書く: 検索したい識別子はログのメッセージや構造化フィールドに入れる。Logs Explorer の Full-Text 検索 / Facet で引ける
  • Span のタグに入れる: APM の Span(トレース)のカスタムタグなら、サンプリング後のデータにしか乗らないのでカーディナリティ爆発が起きにくい
  • メトリクスのタグには絶対に入れない: メトリクスは「タグの組み合わせ数 × ホスト数」で課金される。user_id をメトリクスタグにしたら、Custom Metrics 課金が即破綻

判断基準:「この属性のユニーク値が 1 万を超えるか?」。超えるならタグではなくログ本文や Span 属性に。これだけ守れば 9 割の事故は防げます。


高ボリューム環境でのコスト制御

Datadog の請求は ホスト課金 + APM ホスト課金 + Logs Ingestion + Logs Indexed + Custom Metrics + Profiling + ... と機能別に積み上がります。コスト制御のレバーは以下に集約されます。

1. ホスト課金(Infrastructure)

  • Fargate タスクは「タスク = 1 ホスト」相当として課金される。タスク数 500 で常時動くサービスは、それだけで月数十万円規模
  • 対策: スケールイン設計を真面目にやる。Service Auto Scaling のターゲット使用率を厳しめに。アイドルタスクを残さない
  • 対策: 開発 / staging 環境のタスク数を本番並みに維持しない。staging は CI 走るときだけ起動する設計もアリ

2. APM ホスト課金

  • APM 課金もホスト単位。Agent が動いているタスク数で決まる
  • 対策: 本番だけ APM を有効化、staging は無効化 or サンプリング率を極端に下げる
  • 対策: 重要サービスにのみ APM を入れる。バッチ処理など細かいワーカーまで全部入れない

3. Logs Ingestion(取り込み GB)

  • 取り込んだ全ログに課金される(Indexed か否かに関わらず)
  • 対策: Fluent Bit 段階で DEBUG ログとヘルスチェックを破棄(前述)
  • 対策: アクセスログを 100:1 などでサンプリング
  • 対策: 構造化ログ化して、不要フィールドをドロップ

4. Logs Indexed(インデックス保持 GB)

  • 検索可能な状態で保持するログに別途課金(保持期間も効く)
  • 対策: Datadog 側で Index Filter を設計し、デバッグログや低重要度ログは「取り込むけどインデックスしない」運用に
  • 対策: 30 日 / 15 日 / 7 日 など、重要度別に保持期間を分ける
  • 対策: 90 日以上の長期保持は Logs Archive(S3 送り) に切り替え。検索性を犠牲にしてコストを大幅圧縮

5. Custom Metrics

  • Datadog のカスタムメトリクスは「メトリクス名 × タグ組み合わせ数」で課金。これが一番事故りやすい
  • 対策: メトリクス 1 本あたりのカーディナリティを 1000 以下に抑える設計
  • 対策: user_id などをタグにしない(前述)
  • 対策: Datadog の Metrics Without Limits 機能で、ダッシュボードで使うタグだけを限定的に有効化

6. Profiling

  • Profiling は別課金。Continuous Profiler を全タスクで有効化すると地味に効く
  • 対策: 主要サービスだけ有効化。バッチや低トラフィックサービスはオフ

7. コンテナ数(Live Containers)

  • Live Containers の課金単位はコンテナ数。1 タスク 3 コンテナ構成 × 500 タスク = 1500 コンテナ
  • 対策: サイドカーをむやみに増やさない。今回の 3 コンテナ構成は妥当ライン

「いつから絞り始めるか?」は New Relic 編と同じく、最初の 1 ヶ月は緩めに送って実態を見て、2 ヶ月目から絞り込むのが現実解。最初から絞りすぎると「何が見えていないのか」が分からなくなって、別の事故が起きます。


Datadog 特有の落とし穴 — 現場で本当に踏むやつ

落とし穴 1: datadog_site を間違えて永遠にデータが上がらない

EU 契約なのに datadoghq.com を使い続けて「データが入らない」と数日溶かす。Provider と Agent と FireLens の 3 箇所すべてで datadoghq.eu に揃える必要があります。

落とし穴 2: AWS Integration の namespace_filters を書かず全部流す

何も指定しないとほぼ全 AWS namespace が流れて、Custom Metrics 課金が爆発。必ず明示列挙

落とし穴 3: Unified Service Tagging が食い違う

Agent では service:payment、FireLens では service:payment-api、Monitor では service:payment_api。これで Datadog 上では「全部別サービス」として扱われ、APM とログとメトリクスが結びつかない。今回のサンプルで全箇所同じ変数を参照しているのは意図的です。

落とし穴 4: CloudWatch Alarm を Datadog に流して二重通知

collect_cloudwatch_alarms = true のまま運用すると、CloudWatch Alarm と Datadog Monitor の両方から同じ事象で通知が飛ぶ。Datadog 側で Monitor を作るなら false

落とし穴 5: evaluation_delay 未設定で誤発火

AWS Integration のメトリクスは 1〜2 分の取り込み遅延がある。これを無視して evaluation_delay = 0 で組むと、データ未到達で「閾値以下」と判定されて深夜にアラートが飛ぶ。60〜120 秒は入れること。

落とし穴 6: user_id を APM のカスタム属性に入れる

APM の Span にカスタムタグとして user_id を入れると、Search & Analytics のカーディナリティ警告が出ます。ログ本文に書くか、サンプリング後のトレースの属性として明示的に保持期間を区切るように設計してください。

落とし穴 7: ECS Fargate で Agent が EC2 モードで起動する

ECS_FARGATE = true を環境変数で設定しないと、Agent が EC2 モードで動き、ホスト情報を取得できずにメトリクスがズレます。Fargate では必須

落とし穴 8: DD_API_KEY を環境変数に平文で書く

Terraform で environment = [{ name = "DD_API_KEY", value = "xxxxx" }] と書いてしまう事故。state に平文で残るうえ、CI のログにも漏れます。必ず ECS Secrets 経由で Secrets Manager から注入


まとめ:Datadog 編が向いている現場

Datadog + Terraform でこの構成を組むのが向いているのは、

  • ECS / EKS など コンテナ主体の本番環境で、ホスト・コンテナ・APM・ログを一元的に見たい
  • SRE / Platform チームが独立して存在し、SLO ベースで運用設計したい
  • マルチクラウドや AWS 外の対象も将来的に含む可能性がある
  • Monitor / SLO / ダッシュボードを GitOps で管理してチームでレビューしたい

逆に 「アプリのコード深部を本気で追いたい」 なら New Relic、「100% AWS で外部 SaaS にデータを出したくない」 なら CloudWatch(次回扱う)の方が合う場面が多いです。

Datadog の強みは 機能の網羅性と運用面の成熟度ですが、それと裏腹にコスト制御の難しさが常について回ります。この記事のテンプレートは「最初から絞り気味の出発点」として書いてあるので、ここから自社の要件に合わせて緩めたり厳しくしたりしてください。

繰り返しになりますが、このコードは雛形です。VPC、IAM、Terraform リポジトリ規約は会社ごとに違うので、必ず自社環境に書き直してから使ってください。とはいえ、設計判断のたたき台としては十分なところまでは書いたつもりです。

次回は CloudWatch 編。AWS ネイティブで完結させたい現場向けに、Metric Streams / Logs Insights / Application Signals / Composite Alarm を Terraform でどう組むかを扱います。


関連キーワード

  • Datadog / Datadog Agent / Datadog Logs / Datadog APM
  • Terraform / DataDog provider / AWS provider
  • ECS / Fargate / awsvpc / Service Auto Scaling
  • Unified Service Tagging / DD_SERVICE / DD_ENV / DD_VERSION
  • FireLens / Fluent Bit / Datadog Logs Intake
  • Monitor / SLO / バーンレート / エラーバジェット
  • Custom Metrics / High Cardinality / Metrics Without Limits
  • Logs Ingestion / Logs Indexed / Logs Archive
  • Secrets Manager / ECS Secrets
  • オブザーバビリティ / SRE

この記事が役に立ったらシェアしてください

Datadog で本番監視を組もうとしている仲間に共有してみてください。

カテゴリ

AWS / Observability

公開日

2026 年 5 月 14 日

💬 無料技術相談のご案内

この記事でご紹介した技術について、導入や活用のご相談を30 分間無料承っております。

  • 「自社でも導入できる?」といった技術的な疑問
  • 既存システムとの連携・移行に関するご相談
  • コスト感や導入スケジュールの目安

30 年以上の IT 経験をもとに、率直にお答えします。強引なセールスや勧誘は一切ありません。

野口真一 野口真一

お気軽にご相談ください

記事に関するご質問や、AI・IT 技術導入のご相談など、お気軽にお問い合わせください。