Datadogのモニター設定をTerraformのImport機能を使ってコード化する方法

こちらは Voicy Advent Calendar 2021 15日目の記事です。

今年はAdvent Calendarの2本目を書くことになりました。前回は、エンジニアリングマネージャーからテックリードに戻ってみて役に立った3つの考え方を書きました。 この話の中で取り上げた中でTechnlogyManagementの仕事の中心で、最近の仕事ではDatadogの監視設定をTerraform化する作業をもくもくとしていたので、ちょっと便利だった機能をご紹介します。

弊社はサービスの監視にDatadogを使っています。このサービスはSaaS型の総合監視ツールで、ログを収集したサーバーのCPUやメモリーの収集して閾値を超えたらアラートを通知することができます。 Terraformは、AWS上に構築したシステムなどをコード化するためのツールです。コードはHCL(HashiCorp Configuration Language)で記述します。

最近は、Terraformを使ってインフラの管理をすることは多いと思いますが、監視設定までもコード化する会社はまだまだ少ないのではと思っています。 また、システム構築をする場合、先にTerraformで設定を記述したあとに、terraform applyAWSに設定を反映することが多いと思いますが、監視設定の場合は画面を見ながら閾値を調整することが多いのでなかなかコード化するのが難しいです。 そこで今回は、TerraformのImport機能を使って、Datadogの監視設定もコード化していきます。

流れはこちらです。

  1. Datadogでモニターを作成する
  2. TerraformのImport機能を使って設定情報をコード化する
  3. おすすめしないImport方法

Datadogでモニターを作成する

まず、Datadogでモニターを作成します。取得したいメトリクスの検索や閾値の設定が視覚的にわかりやすく先にコンソールから作るのが楽です。 5つのステップで設定します。

  1. Choose the detection method(検出方法を選択してください)
  2. Define the metric(メトリックを定義する)
  3. Set alert conditions(アラート条件を設定する)
  4. Notify your team(チームに通知する)
  5. Say what's happening(何が起こっているかを言う)

今回は、CPUのLoadを監視する設定を書きます。これはデフォルトの検出方法の項目です。

f:id:smikami:20211214233710p:plain
datadog_monitor1
f:id:smikami:20211214233743p:plain
datadog_monitor2
f:id:smikami:20211214233826p:plain
datadog_monitor3

こんな感じで設定してみました。これをTerraformのImport機能を使って取り込んでいきます。

TerraformのImport機能を使って設定情報をコード化する

次にDatadogからTerraformのImport機能を利用し、設定情報を取得します。

まずは、Importするために必要な最低限のmain.tfファイルを作成します。

variable "datadog_api_key" {}
variable "datadog_app_key" {}

provider "datadog" {
  api_key = var.datadog_api_key
  app_key = var.datadog_app_key
}

resource "datadog_monitor" "system_cpu_load" {
}

この部分は、providerにdatadogを指定します。api_keyとapp_keyを指定します。 鍵の情報をterraformコマンド実行時に渡すのが良いためvariableを使って渡します。

variable "datadog_api_key" {}
variable "datadog_app_key" {}

provider "datadog" {
  api_key = var.datadog_api_key
  app_key = var.datadog_app_key
}

importするために、何も書かれていないresourceを定義します。

resource "datadog_monitor" "system_cpu_load" {
}

datadog_monitorがリソース名です。設定方法はこちらに記載されています。 system_cpu_loadは管理するための名前です。適当な名前で良いですが、同じtfstate内でユニークである必要があります。

Init

terraform initで初期設定をおこないます。

$ terraform init

下記のメッセージが表示されていれば成功です。

Terraform has been successfully initialized!

ローカルに.terraformフォルダが作成されます。

tfvarsの作成

ローカルにdatadog.tfvarsファイルを作成します。内容は下記の通りです。 空になっているところにAPI KeyとApp Keyを指定します。 取得方法については公式ドキュメントのAPI キーとアプリケーションキーを読んでください。

datadog_api_key = ""
datadog_app_key = ""

TerraformのImport機能を使って設定情報をコード化する

これで、Importする準備が整いました。下記のコマンドを実行します。

$ terraform import -var-file=datadog.tfvars datadog_monitor.system_cpu_load 57801778

-var-fileには、tfvarsの作成のところで指定したファイルを指定します。 datadog_monitor.system_cpu_loadは、リソース名.名前です。リソース名はdatadog_monitorで固定です。57801778 は、モニターのIDです。作成したモニターのURLから取得するのがわかりやすいです。

https://app.datadoghq.com/monitors/57801778

成功すると下記の出力結果が表示されます。

datadog_monitor.system_cpu_load: Importing from ID "57801778"...
datadog_monitor.system_cpu_load: Import prepared!
  Prepared datadog_monitor for import
datadog_monitor.system_cpu_load: Refreshing state... [id=57801778]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.

Importが成功すると、ローカルにterraform.tfstateファイルが作成されます。 中身を見てみましょう

{
  "version": 4,
  "terraform_version": "0.12.31",
  "serial": 1,
  "lineage": "",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "datadog_monitor",
      "name": "system_cpu_load",
      "provider": "provider.datadog",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "enable_logs_sample": null,
            "escalation_message": "",
            "evaluation_delay": 0,
            "force_delete": null,
            "groupby_simple_monitor": null,
            "id": "57801778",
            "include_tags": true,
            "locked": false,
            "message": "{{#is_alert}}\n@slack-err_alert エラーがでたよ。\n{{/is_alert}} }}",
            "monitor_threshold_windows": [],
            "monitor_thresholds": [
              {
                "critical": "0.8",
                "critical_recovery": "0.7",
                "ok": "",
                "unknown": "",
                "warning": "0.6",
                "warning_recovery": "0.5"
              }
            ],
            "name": "[TEST] system.local.1 Alert",
            "new_group_delay": 0,
            "new_host_delay": 300,
            "no_data_timeframe": 0,
            "notify_audit": false,
            "notify_no_data": false,
            "priority": 0,
            "query": "avg(last_5m):avg:system.load.1{*} \u003e 0.8",
            "renotify_interval": 0,
            "renotify_occurrences": 0,
            "renotify_statuses": [],
            "require_full_window": false,
            "restricted_roles": [],
            "tags": [],
            "timeout_h": 0,
            "type": "query alert",
            "validate": null
          },
          "private": ""
        }
      ]
    }
  ]
}

注: lineageとprivateは、空にしています。

差分を見る

この状態でterraform planをしてみましょう。

terraform plan -var-file=datadog.tfvars

resource "datadog_monitor" "system_cpu_load" {}に何も指定していないため下記のエラーがでます。

Error: Missing required argument

  on main.tf line 9, in resource "datadog_monitor" "system_cpu_load":
   9: resource "datadog_monitor" "system_cpu_load" {

The argument "message" is required, but no definition was found.


Error: Missing required argument

  on main.tf line 9, in resource "datadog_monitor" "system_cpu_load":
   9: resource "datadog_monitor" "system_cpu_load" {

The argument "name" is required, but no definition was found.


Error: Missing required argument

  on main.tf line 9, in resource "datadog_monitor" "system_cpu_load":
   9: resource "datadog_monitor" "system_cpu_load" {

The argument "query" is required, but no definition was found.


Error: Missing required argument

  on main.tf line 9, in resource "datadog_monitor" "system_cpu_load":
   9: resource "datadog_monitor" "system_cpu_load" {

The argument "type" is required, but no definition was found.

これは、terraform.tfstateファイルと、tfファイルの中で定義している内容に差分があるためです。これから、terraform.tfstateの内容に合わせるため、tfstateの内容を見ながら、tfファイルに追記していきます。

こんな感じで比較しながら、設定すると楽です。

f:id:smikami:20211214234336p:plain
diff

tfファイルの修正

それでは、Errorになった項目を追加していきましょう。

  • message
  • name
  • query
  • type

設定する値は、terraform.tfstateの中にあるresources:instancesの値を使います。

resource "datadog_monitor" "system_cpu_load" {
  message = "{{#is_alert}}\n@slack-err_alert エラーがでたよ。\n{{/is_alert}} }}"
  name = "[TEST] system.local.1 Alert"
  query = "avg(last_5m):avg:system.load.1{*} \u003e 0.8"
  type = "query alert"
}

terraform planを実行すると、下記の出力結果がでました。

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # datadog_monitor.system_cpu_load will be updated in-place
  ~ resource "datadog_monitor" "system_cpu_load" {
        evaluation_delay     = 0
        id                   = "57801778"
        include_tags         = true
        locked               = false
        message              = <<~EOT
            {{#is_alert}}
            @slack-err_alert エラーがでたよ。
            {{/is_alert}} }}
        EOT
        name                 = "[TEST] system.local.1 Alert"
        new_group_delay      = 0
        new_host_delay       = 300
        no_data_timeframe    = 0
        notify_audit         = false
        notify_no_data       = false
        priority             = 0
        query                = "avg(last_5m):avg:system.load.1{*} > 0.8"
        renotify_interval    = 0
        renotify_occurrences = 0
        renotify_statuses    = []
      ~ require_full_window  = false -> true
        restricted_roles     = []
        tags                 = []
        timeout_h            = 0
        type                 = "query alert"

      - monitor_thresholds {
          - critical          = "0.8" -> null
          - critical_recovery = "0.7" -> null
          - warning           = "0.6" -> null
          - warning_recovery  = "0.5" -> null
        }
    }

Plan: 0 to add, 1 to change, 0 to destroy.

エラーはなくなりましたが、require_full_windowmonitor_thresholdsに差分があります。さらに追加していきます。

resource "datadog_monitor" "system_cpu_load" {
  message = "{{#is_alert}}\n@slack-err_alert エラーがでたよ。\n{{/is_alert}} }}"
  name = "[TEST] system.local.1 Alert"
  query = "avg(last_5m):avg:system.load.1{*} \u003e 0.8"
  type = "query alert"
  require_full_window = false
  monitor_thresholds {
    critical = "0.8"
    critical_recovery = "0.7"
    warning = "0.6"
    warning_recovery = "0.5"
  }
}

terraform planを実行してみましょう。 下記が出力されていればDatadogとTerraformとの差分がありません。

No changes. Infrastructure is up-to-date.

最後にtfファイルをGitなどにマージして終了になります。

おすすめしないImport方法

毎回、tfstateの情報からtfファイルに記述するのは面倒です。 そこで、JSONで指定できる、datadog_monitor_jsonを使ってみました。

Terraform Import

$ terraform import -var-file=datadog.tfvars datadog_monitor_json.system_cpu_load 57801778

terraform.tfstateの結果は下記の通りです。

{
  "version": 4,
  "terraform_version": "0.12.31",
  "serial": 1,
  "lineage": ",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "datadog_monitor_json",
      "name": "system_cpu_load",
      "provider": "provider.datadog",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "id": "57801778",
            "monitor": "{\"message\":\"{{#is_alert}}\\n@slack-err_alert エラーがでたよ。\\n{{/is_alert}} }}\",\"name\":\"[TEST] system.local.1 Alert\",\"options\":{\"escalation_message\":\"\",\"include_tags\":true,\"locked\":false,\"new_host_delay\":300,\"notify_audit\":false,\"notify_no_data\":false,\"renotify_interval\":0,\"require_full_window\":false,\"thresholds\":{\"critical\":0.8,\"critical_recovery\":0.7,\"warning\":0.6,\"warning_recovery\":0.5}},\"priority\":null,\"query\":\"avg(last_5m):avg:system.load.1{*} \\u003e 0.8\",\"tags\":[],\"type\":\"query alert\"}",
            "url": null
          },
          "private": ""
        }
      ]
    }
  ]
}

monitorにJSONが文字列で格納されています。monitorだけを指定すればよいだけなので下記の通り指定するだけで完了します。

resource "datadog_monitor_json" "system_cpu_load" {
  monitor = "{\"message\":\"{{#is_alert}}\\n@slack-err_alert エラーがでたよ。\\n{{/is_alert}} }}\",\"name\":\"[TEST] system.local.1 Alert\",\"options\":{\"escalation_message\":\"\",\"include_tags\":true,\"locked\":false,\"new_host_delay\":300,\"notify_audit\":false,\"notify_no_data\":false,\"renotify_interval\":0,\"require_full_window\":false,\"thresholds\":{\"critical\":0.8,\"critical_recovery\":0.7,\"warning\":0.6,\"warning_recovery\":0.5}},\"priority\":null,\"query\":\"avg(last_5m):avg:system.load.1{*} \\u003e 0.8\",\"tags\":[],\"type\":\"query alert\"}"
}

これはmonitorだけを指定すればよいので一見楽にできそうですが、ただのJSONの文字列が入っているだけなので非常に変更しにくいですし、可読性も悪いです。planは一発で通るので楽ですが、正直メンテしたくないなと思いました。

他にも、TerrafomのModuleを使う手もあります。パターンがある程度決まってくればモジュール化も検討しても良いと思います。 ただし、モジュール化のメリットを生かして複数の監視対象に同じ設定を反映する場合は、たくさんのダッシュボードを作りがちです。そこでDatadogには[マルチアラートモニター](https://docs.datadoghq.com/ja/monitors/monitor_types/composite/]という機能があり、1つのモニターをグルーピングして同じ監視設定で一括監視ができます。例えば、全てのDyanmoDBのテーブルに対してReadCapacityUnitsの監視設定をおこない、CapacityUnitsを超えたらアラートするということも簡単に書けます。

query

avg(last_10m):sum:aws.dynamodb.consumed_read_capacity_units{aws_account:xxxxxxx} by {tablename} / sum:aws.dynamodb.provisioned_read_capacity_units{aws_account:xxxxxxx} by {tablename} > 0.8

aws_accountはxxxxxxxに指定します。env:prodなどとしても良いでしょう。タグが設定し忘れている場合にはaccountをフィルターにするとアカウントすべてが対象になります。

最後に

これで、Datadogの監視の設定も、コード化することができましたね。 DatadogとTerraformの機能を活用し、サービスの監視設定を効率よくして楽していきましょう!