Datadog の監視設定を Terraform で管理する

この記事は [弁護士ドットコム Advent Calendar 2019 - Qiita](https://qiita.com/advent-calendar/2019/bengo4com) の6日目の記事です。

# TL;DR

- Bits :dog2: 
- Datadog の設定を terraform 管理した
- Datadog の設定をリポジトリで管理して CI が実施できるようにした
- コード化されるのは嬉しいが import は大変

# はじめに

こんにちは。弁護士ドットコムの SRE チームの中村といいます。

いきなりですが Datadog のロゴの犬の名前知ってますか？
こないだの Datadog Summit Tokyo で知ったのですが [Bits](https://www.datadoghq.com/ja/about/press/resources/) らしいです。覚えてあげてくださいね。

さて Bits くんもとい Datadog 便利ですよね。
弁護士ドットコムでは全社的に Datadog を導入してサービスの監視を行っています。
リソースメトリクスだけではなくワークメトリクスをベースにしてサービス状態を監視でき大変重宝しております。
（メトリクスの分類は[こちら](https://www.datadoghq.com/ja/blog/%E3%83%A2%E3%83%8B%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%AE%E3%83%99%E3%82%B9%E3%83%88%E3%83%97%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%82%B9/)等をご参考にどうぞ）

この賢いお犬様は integration や agent を設定するだけでそれなりにメトリクスを収集してくれます。
それだけでも大変便利なのですが、運用するためにアラートや各種設定をする上では次のような点が悩みどころです。

- 各環境やアプリで同じようなアラートを GUI 上でポチポチ設定する必要があってツラい
- 運用する中で調整したアラートの設定の変更履歴を残しておきたい

今回 Datadog の設定をコード管理することで課題感を解決できないか試してみようと思います。
Monitoring as a Code ですね。


# 監視設定のコード化

Terraform には [Datadog provider](https://www.terraform.io/docs/providers/datadog/index.html) が用意されています。
こちらを元に既存のアラートと外形監視の設定を import してコード化してみます。

### 余談

先日発表された CloudFormation の 3rd Party サポートで [Datadog も対象](https://www.datadoghq.com/blog/monitoring-as-code-with-datadog-and-cloudformation/)であることが発表されました。
Datadog Resource を使って以下の操作が可能なようです。
> - enable Datadog’s AWS integration
> - create, update, and delete monitors for your services
> - schedule downtime for monitors
> - manage users for your Datadog account

integration の有効化とか便利そうですが、ひとまずは対応しているリソースが少なさそうなので今回はおとなしく Terraform で試してみます。


## 1. 前提

以下の環境で実施します

Terraform = v0.12.17
Datadog Provider = 2.5 

## 2. import 対象

Datadog の Web 画面から手動で作成した以下のテストとモニター（Datadog でのアラート）を import してみます。

- web アプリケーションの URL の synthetics test
- web アプリケーションのアクセスログから 5xx 系のステータスコードを集計する monitor

## 3. 監視設定の import 

まずは1ファイルで作業してみます。次のようなファイルを作成します。

```main.tf
provider "datadog" {
  api_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  app_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

resource "datadog_synthetics_test" "access_check" {

}

resource "datadog_monitor" "log_5xx" {

}
```

`terrafom init` を実施し、各リソースの ID は Datadog の画面 URL から確認し import していきます。

```sh
# synthetics
terraform import datadog_synthetics_test.access_check abc-123-xyz

# monitor
terraform import datadog_monitor.log_5xx 0123456
```

それぞれ `Import successful!` と表示されれば完了です。
`terraform state show` コマンド等を参考にしながら地道に tf ファイルを埋めていきます。

書けました。

```main.tf
provider "datadog" {
  api_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  app_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

resource "datadog_synthetics_test" "access_check" {
  type   = "api"
  name   = "webapp-synthetics"
  status = "live"
  request = {
    "method"  = "GET"
    "timeout" = "30"
    "url"     = "https://example.com/health"
  }

  assertions = [
    {
      "operator" = "lessThan"
      "target"   = "100"
      "type"     = "responseTime"
    },
    {
      "operator" = "is"
      "target"   = "200"
      "type"     = "statusCode"
    },
  ]

  locations = [
    "aws:ap-northeast-1",
  ]

  options = {
    "min_failure_duration" = "900"
    "min_location_failed"  = "1"
    "tick_every"           = "300"
  }

  tags = [
    "role:webapp",
    "env:staging",
  ]

  message = <<EOM
webapp のアクセスに問題がありそうです
[ダッシュボード](https://app.datadoghq.com/dashboard/123-456-789/hogehoge) からサーバの負荷やリクエスト数を確認しましょう
@slack-alert_channel_name
EOM
}

resource "datadog_monitor" "log_5xx" {
  type  = "log alert"
  name  = "HTTP Status Count Alert 5xx staging"
  query = "logs(\"env:staging role:webapp @http.status_code:[500 TO 599]\").index(\"main\").rollup(\"count\").last(\"5m\") >= 10"
  thresholds = {
    "critical" = "10"
    "warning"  = "1"
  }

  enable_logs_sample  = true
  require_full_window = false

  tags = [
    "staging",
  ]

  message = <<EOM
{{#is_alert}}
5xx 系が連続して検出されています :eyes:
[ログはこちら](https://app.datadoghq.com/logs?query=env:staging+AND+role:webapp+AND+@http.status_code:[500 TO 599])
@slack-alert_channel_name <!here>
{{/is_alert}}

{{#is_warning}}
5xx 系が検出されています :eyes:
[ログはこちら](https://app.datadoghq.com/logs?query=env:staging+AND+role:webapp+AND+@http.status_code:[500 TO 599])
@slack-alert_channel_name
{{/is_warning}}
EOM
}
```

`message` 部分がツラい気がしますが可視化されるのは良さそうです。

## 4. リファクタリング

### 1. state をローカルからリモート管理に移行

設定はコード化されたのですが、リソースの状態を表す state ファイルはローカルマシン上に存在します。
リモートリポジトリで管理するためには state ファイルがリモートにある方が都合が良さそうです。

s3 に配置することとし remote_state 用のファイルを追加します。

```backend.tf
terraform {
  backend "s3" {
    bucket = "BUCKET_NAME"
    key    = "PASS/TO/terraform.tfstate"
    region = "ap-northeast-1"
  }
}
```

direnv 等で backend に指定した S3 にアクセスできるクレデンシャルが設定されている状態で `terraform init` を実行すると次のようなメッセージが表示され state をコピーしてくれます。

```
Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "s3" backend. No existing state was found in the newly
  configured "s3" backend. Do you want to copy this state to the new "s3"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes


Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
```

`terraform state push` でやるつもりだったのですがリモートに存在しない場合はコピーしてくれるのですね。えらい。


### 2. tf ファイルの分割とクレデンシャルの変数化

ついでなので tf ファイルを分割して変数化も行います。
以下の構成に分割しました。

```
backend.tf
main.tf
provider.tf
variables.tf
```

provider.tf と variables.tf は以下のとおりです。

```provider.tf
provider "datadog" {
  api_key = var.dd_api_key
  app_key = var.dd_app_key
}
```

```variables.tf
variable "dd_api_key" {}
variable "dd_app_key" {}
```

これで Datadog のクレデンシャルを環境変数を利用して `TF_VAR_dd_api_key=xxxxxxxx` のような形で渡せるようになります。

### 3. モジュール化

作成した tf ファイルを利用して、同じような設定で api のテストやアラートも設定したいところです。
モジュール化して利用できるように分割します。

以下の構成に分割しました。

```
modules/
  synthetics.tf
  monitor.tf
backend.tf
main.tf    #resource は modules 配下に移行
provider.tf
variables.tf
```

main.tf にあった synthetics と monitor の設定をそれぞれ modules 配下のファイルに移行して 

```synthetics.tf
resource "datadog_synthetics_test" "access_check" {
  type   = "api"
  name   = "webapp-synthetics"
...(略)
```

```monitor.tf
resource "datadog_monitor" "log_5xx" {
  type  = "log alert"
  name  = "HTTP Status Count Alert 5xx staging"
...(略)
```

main.tf では modules の呼び出しだけ行います。

```main.tf
module "webapp" {
  source = "./modules"
}
```

この状態で plan を実行すると `†破壊と創造†` が行われてしまうので state を移動して対応します。

```sh
# 確認
$ terraform state list
datadog_monitor.log_5xx
datadog_synthetics_test.access_check

# 移動
$ terraform state mv datadog_monitor.log_5xx module.webapp.datadog_monitor.log_5xx
$ terraform state mv datadog_synthetics_test.access_check module.webapp.datadog_synthetics_test.access_check
```

これで必要に応じて API のリソースも作成できるような構成になりました。
実際は argument を変数化して DRY にしてやる必要がありますがここでは一旦割愛します。


# 監視設定のリポジトリ管理

コード化できたので引き続きリポジトリ管理を行います。
弊社は　gitlab と gitlab CI を利用しているため、そちらを利用して実施します。

## 1. リポジトリ管理

作成されたソースコードを push し、リポジトリ管理を行います。

## 2. CI 設定

`.gitlab-ci.yml` を作成して以下のように定義します。

```.gitlab-ci.yml
image: hashicorp/terraform:0.12.17

stages:
  - plan
  - apply

plan:
  stage: plan
  script:
    - terraform init
    - terraform plan
  artifacts:
    name: "${CI_COMMIT_REF_NAME}"
    paths:
      - .terraform

apply:
  stage: apply
  script:
    - pwd
    - terraform apply -auto-approve
  when: manual
```

このような yml を書いてやることで push の度に plan が実行される環境が完成しました。


まだまだ既存の設定の import やディレクトリ構成の見直し、plan 内容の通知も必要そうですが、ひとまずこれで監視設定を管理することができました。
Bits もしっぽブンブンなのではないでしょうか。

# まとめ

terraform を使ってコード化することで監視設定が可視化されました。
課題として感じていた内容が若干和らいだかな…

> - 各環境やアプリで同じようなアラートを GUI 上でポチポチ設定する必要があってツラい
> - 運用する中で調整したアラートの設定の変更履歴を残しておきたい

ダッシュボードやログ周りの設定もコード化できればもう少し便利かもしれません。
引き続き試してみてブラッシュアップしてみたいところです。

またコード化することでアプリケーションエンジニアも監視設定を追加しやすくなります（はず）。
どうしてもサービスの新機能追加時には監視設定をインフラエンジニアにお願いする。というような運用が一般的だと思いますが、既存の監視内容を参考にアラートを追加する部分までアプリケーションエンジニアが作業できるような体制が作れそうな気がします。

CI の仕組みにも乗っけることができたのでスケジュールジョブを設定して日次で plan を実行し差分がないか確認すること等もできそうです。

Monitoring as a Code をサービスの監視を民主化するためのとっかかりにしていけたらよいなと思っています。