Datadogのアラート設定をterraform化して複数オーガニゼーションで使い回す

# はじめに
本記事はDatadog Advent Calendar 2019の12/23(月)分の記事です。
別アドベントカレンダーで、[Datadogを全社的に導入した時にやったこと](https://qiita.com/syogun/private/f0209d61587c77773886)という記事も書いたので、見ていただけると幸いです。

# 導入背景
VISITSでは、GCPを使ったプロダクトが増えており、基本的になアーキテクチャは同じ場合が多いです。
各プロダクトは、Datadogの[Managing Multiple-Organization](https://docs.datadoghq.com/ja/account_management/multi_organization/)機能を使用して、
オーガニゼーションを分けています。

マルチオーガニゼーションの例）
<img width="286" alt="スクリーンショット 2019-10-24 14.38.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95324/9f7cf146-6cb8-2374-e796-edbd3d575e42.png">

この場合、各オーガニゼーションで設定するMonitors(以下、アラート)は、基本的に同じである場合が多く、
であればコード化して汎用性のあるものにしようと考え、terraform化しました。
Monitoring as Codeという言うらしいです。

# 結果
まず最初にterraform化して感じたメリデメをご紹介。

## メリット
* 使い回しができる。オーガニゼーションが増えても簡単にアラート設定が可能。（もちろん作りによります）
* アラート再現性がある。（仮に私が退職しても誰でもアラート設定できる）
* アラートの文言を統一できる。（仮に1人がアラート設定してても文言はバラバラになることが多い）
* 各プロダクトで得られた知見を取り込むことで、横展開が可能となる。（このアラート設定は他のオーガニゼーションでも使えるね！等)
* コード管理しているという満足感。（自己満）

## デメリット
* 1つのアラートを修正したら全オーガニゼーションに適用し直す必要がある。（差分が出てしまうので）
* terraformを使える人があまりいないので、terraformのアップロードが属人化してしまう。（適用は簡単だが、更新はできない等）

# 設計思想
* tfファイルは共通化して `variables` で各オーガニゼーションごとのパラメーターを調整する。
* チームで運用することを想定して、tfstateファイルはGCSで管理。（事前にGCSバケットを作る必要があります）
* 一旦 `terraform plan`と`terraform apply`のみにしぼる(Importとかできない)

# ファイル構成

```shell

├── README.md
├── <プロダクト名>
│   ├── datadog.tfvars # プロダクトの毎の変数(パラメーター)を設定
│   ├── monitor_service_list.txt #アラート設定したいテンプレートを指定
│   ├── secrets.tfvars #Datadogの機密情報(API key等)を設定。gitignoreでGit管理対象外とする。
│   └── tmp #作業ディレクトリ。ここにtfファイルをコピーして適用
├── templates #各アラートのテンプレート
│   ├── apm.tf
│   ├── cloudfunction.tf
│   ├── cloudsql.tf
│   ├── gae.tf
│   ├── gae_log.tf
│   ├── gce.tf
│   ├── gce_docker_containers.tf
│   ├── gce_proces_nginx.tf
│   ├── lb_log.tf
│   ├── provider.tf
│   ├── synthetics.tf
│   ├── synthetics_vi.tf
│   ├── tfstate.tf
│   └── variables.tf
├── terraform.sh #terraformのラッパースクリプト

```

以下に各ファイル、ディレクトリの補足説明をします。

## datadog.tfvars
プロダクトの毎のパラメーターを設定しています。
例えば、Slackのチャンネル名、GCPプロジェクトID、アラートの閾値、外形監視するURL等。

例)

```shell
## Slack Channel Name
slacke_channel = "<your slack channel>"

## GCP Project ID
gcp_project_id = "<your gcp project id>"

## Production ENV Name
env_name = "<your env>"
```

## monitor_service_list.txt
templatesの中からアラート設定を適用するファイル名(.tfより前)を指定します。

```
gce
gce_proces_nginx
gae
gae_log
cloudsql
lb_log
synthetics
apm_ruby
```


## secrets.tfvars
DatadogのAPI KeyやAPP Keyといった秘匿性のある情報を管理します。
このファイルは、`.gitignore`でignoreしましょう。

```
datadog_api_key = "*************"
datadog_app_key = "*************"
```

## tmp(ディレクトリ)
後述のterraformラッパースクリプトがこのディレクトリにtfファイルやvariablesをコピーしてapplyします。

## 各アラートのテンプレート
各サービスのごとのアラート設定ファイルをおきます。
以下、Datadog APM監視のSampleです。

```shell
# apm.tf

##########################################################
# 説明
# APMを監視するtfファイルです。
#
# 動作要件
#  以下のvariableを各プロダクトの `datadog.tfvars` に設定してください。
##########################################################

##############################
## variable
##############################
variable "apm_service_web" {}
variable "apm_service_db" {}
variable "apm_service_web_error_rate_critical" {}
variable "apm_service_db_error_rate_critical" {}
variable "apm_service_web_latency_critical" {}
variable "apm_service_db_latency_critical" {}
variable "apm_exclude_condition" {}
variable "env_name" {}



##############################
## APM web Error Rate 監視
##############################
resource "datadog_monitor" "apm_error_rate_web" {
  name               = "[APM] Service ${var.apm_service_web} has a high error rate"
  type               = "query alert"
  message            = <<EOF
 Service ${var.apm_service_web}のError RateがSLOを超過しています。
詳細を[Datadog Logs](https://app.datadoghq.com/logs)から確認してください。
@${var.slack_channel}

  EOF
  query              = "avg(last_10m):( avg:trace.rack.request.errors{service:${var.apm_service_web},env:${var.env_name}} / avg:trace.rack.request.hits{service:${var.apm_service_web},env:${var.env_name}} ) > ${var.apm_service_web_error_rate_critical}"
  escalation_message = ""
  thresholds = {
    critical = "${var.apm_service_web_error_rate_critical}"
  }
  notify_no_data      = false
  new_host_delay      = 300
  renotify_interval   = 0
  timeout_h           = 0
  include_tags        = true
  require_full_window = true
  notify_audit        = false
  tags                = []
}

##############################
## APM db Error Rate 監視
##############################
resource "datadog_monitor" "apm_error_rate_db" {
  name               = "[APM] Service ${var.apm_service_db} has a high error rate"
  type               = "query alert"
  message            = <<EOF
 Service ${var.apm_service_db}のError RateがSLOを超過しています。
詳細を[Datadog Logs](https://app.datadoghq.com/logs)から確認してください。
@${var.slack_channel}

  EOF
  query              = "avg(last_10m):( avg:trace.mysql2.query.errors{service:${var.apm_service_db},env:${var.env_name}} / avg:trace.mysql2.query.hits{service:${var.apm_service_db},env:${var.env_name}} ) > ${var.apm_service_db_error_rate_critical}"
  escalation_message = ""
  thresholds = {
    critical = "${var.apm_service_db_error_rate_critical}"
  }
  notify_no_data      = false
  new_host_delay      = 300
  renotify_interval   = 0
  timeout_h           = 0
  include_tags        = true
  require_full_window = false
  notify_audit        = false
  tags                = []
}

##############################
## APM web latency 監視
##############################
resource "datadog_monitor" "apm_request_latency_web" {
  name               = "[APM] Service ${var.apm_service_web} has a high p90 latency"
  type               = "metric alert"
  message            = <<EOF
 Service ${var.apm_service_web}のRequest LatencyがSLOを超過しています。
 詳細を[Datadog APM](https://app.datadoghq.com/apm/search)から確認してください。
@${var.slack_channel}

  EOF
  query              = "avg(last_10m):avg:trace.rack.request.duration.by.service.90p{service:${var.apm_service_web},env:${var.env_name}${var.apm_exclude_condition}} > ${var.apm_service_web_latency_critical}"
  escalation_message = ""
  thresholds = {
    critical = "${var.apm_service_web_latency_critical}"
  }
  notify_no_data      = false
  new_host_delay      = 300
  renotify_interval   = 0
  timeout_h           = 0
  include_tags        = true
  require_full_window = true
  notify_audit        = false
  tags                = []
}

##############################
## APM db latency 監視
##############################
resource "datadog_monitor" "apm_request_latency_db" {
  name               = "[APM] Service ${var.apm_service_db} has a high p90 latency"
  type               = "metric alert"
  message            = <<EOF
 Service ${var.apm_service_db}のRequest LatencyがSLOを超過しています。
 詳細を[Datadog APM](https://app.datadoghq.com/apm/search)から確認してください。
@${var.slack_channel}

  EOF
  query              = "avg(last_10m):avg:trace.mysql2.query.duration.by.service.90p{service:${var.apm_service_db},env:${var.env_name}} > ${var.apm_service_db_latency_critical}"
  escalation_message = ""
  thresholds = {
    critical = "${var.apm_service_db_latency_critical}"
  }
  notify_no_data      = false
  new_host_delay      = 300
  renotify_interval   = 0
  timeout_h           = 0
  include_tags        = true
  require_full_window = false
  notify_audit        = false
  tags                = []
}
```

## terraform.sh
terraformのラッパースクリプトのサンプルです。
`bash terraform <プロダクト名>`といった感じで実行し、terraformをapplyします。（雑ですいません）

```shell
#!/bin/bash

set -u

# Check argument
if [ $# -ne 1 ]; then
  echo "実行するには1つの引数が必要です。" 1>&2
  echo "引数には各プロダクト名を指定してください。" 1>&2
  echo "例) bash terraform.sh <プロダクト名>" 1>&2
  exit 1
fi

PRODUCT_NAME=$1

ls -d ${PRODUCT_NAME} 1>/dev/null 2>/dev/null 
if [ $? -ne 0 ]; then
    echo "${PRODUCT_NAME}のディレクトリが存在しません。terraformを適用するプロダクト毎にディレクトリを作成してください。"
    exit 1
fi

# prepare
gsutil 1>/dev/null 2>/dev/null
if [ $? -ne 0 ]; then
  echo "gsutilがインストールされていません。"
  exit 1
fi

gcloud version 1>/dev/null 2>/dev/null
if [ $? -ne 0 ]; then
  echo "gcloudがインストールされていません。"
  exit 1
fi

ls -d ${PRODUCT_NAME}/tmp 1>/dev/null 2>/dev/null
if [ $? -ne 0 ]; then
  mkdir ${PRODUCT_NAME}/tmp
else
  rm -rf ${PRODUCT_NAME}/tmp/*.tf
  rm -rf ${PRODUCT_NAME}/tmp/*.tfvars
fi

# setup gcp project
gcloud config set project visits-technologies-share
if [ $? -ne 0 ]; then
  echo "GCP <Your GCP Project Name> プロジェクトが設定出来ませんした。GCPで権限を確認してください。"
  exit 1
fi

# copy tf files
if [ -e ${PRODUCT_NAME}/monitor_service_list.txt ]; then
  while read line
  do
    echo "cp -pr templates/${line}.tf ${PRODUCT_NAME}/tmp/"
    cp -pr templates/${line}.tf ${PRODUCT_NAME}/tmp/
  done < ${PRODUCT_NAME}/monitor_service_list.txt
else
  echo "${PRODUCT_NAME}/monitor_service_list.txtが見つかりません。"
fi

## copy PRODUCT tfvars
cp -p ${PRODUCT_NAME}/*.tfvars ${PRODUCT_NAME}/tmp/
## copy common tf files
cp -p templates/provider.tf ${PRODUCT_NAME}/tmp/
cp -p templates/tfstate.tf ${PRODUCT_NAME}/tmp/
cp -p templates/variables.tf ${PRODUCT_NAME}/tmp/

# setup tfstate
gsutil ls gs://visits-${PRODUCT_NAME}-terraform-state
if [ $? -ne 0 ]; then
  echo "tfstate用のGCSバケットが作成されていません。tfstate.tfファイルを確認してください。"
  exit 1
fi

if [ -e ${PRODUCT_NAME}/tmp/tfstate.tf ]; then
  sed -i '' "s/__PRODUCT_NAME__/${PRODUCT_NAME}/g" ${PRODUCT_NAME}/tmp/tfstate.tf
fi

# init
cd ${PRODUCT_NAME}/tmp
terraform init

# plan
terraform plan -var-file=datadog.tfvars -var-file=secrets.tfvars
if [ $? -ne 0 ]; then
  echo "terraform planでエラーが発生しました。詳細を確認してください。"
  exit 1
fi

# apply
terraform apply -var-file=datadog.tfvars -var-file=secrets.tfvars

```

# まとめ
まだまだ改善の余地しかないですが、一応最低限は運用できています。
再現性のあるものができたとは思いつつ、監視を変更するたびにyamlを書き換える必要があるので、なかなか運用が面倒だなーというのが正直な感想です。
このあたりは、もう少しPDCA回して最適化していきたいです。