Terraformで監視のコード化 ~ 基盤構築からチームへの展開 Part2 ~

はじめに

​ 前回はこちらでAWSの監視コード化の話をしました。 blog.engineer.adways.net

今回はGCP Stackdriverの監視設定をコード化するにあたり、前回と同様ディレクトリ構造やCI周りについてお話しします。 対象読者は次の条件のうちどれか一つを充たす方を想定しています。

  • Terraformを触ったことがある
  • GCP上で監視設定を作ったことがある

概要

今回は、GKE上で動作しているプロジェクトが対象です。
主な機能はapiでfluentd, nginxが各コンテナで動作しています

  • pod

    • api
    • fluentd
    • nginx
  • アラートの発火条件

    • ex) backend_latencies*1 最大レスポンスタイムが3分間1秒を超えた
    • ex) backend_latencies 平均レスポンスタイムが3分間500msを超えた
    • ex) apiのheath check
  • 今回、tfstateの保存先はGCSにしました。

ディレクトリ構成

前回と同じようにWorkspaceを使った構成を試したのですが、いざ蓋を開けたらそれほど単純ではなかったので そこについては後述します。

それぞれ環境ごとにディレクトリを作成し、そこで各々terraform initをするようにしました。
terraform/stackdriverはmoduleで、用途は前回と大体同じでアラートポリシーの名前に環境名を入れるため変数で渡したりしています。*2

terraform
├── プロジェクト名
│   └── environments
│       ├── production
│       │   ├── stackdriver
│       │   │   ├── backend.tf
│       │   │   ├── main.tf
│       │   │   ├── provider.tf
│       │   │   └── variables.tf
│       ├── sandbox
│       │   └── stackdriver
│       │       ├── backend.tf
│       │       ├── main.tf
│       │       ├── provider.tf
│       │       └── variables.tf
│       └── test
│           └── stackdriver
│               ├── backend.tf
│               ├── main.tf
│               ├── provider.tf
│               └── variables.tf
└── stackdriver // module
    ├── README.md
    └── alert
        ├── lb_alert_policy.tf
        ├── monitoring_group.tf
        ├── variables.tf
        └── versions.tf

stackdriver/alert/lb_alert_policy.tfの一部

  • var.env: 環境名
  • var.service_name: プロジェクト名

などを渡しています、今回変数として渡せるものは最小限にしています、閾値なども考えましたが、 現状固定で必要性がでてきたタイミングで対応するようにします。

resource "google_monitoring_alert_policy" "lb" {
  combiner     = "OR"
  display_name = "[${var.env}] ${var.service_name} lb"
  conditions {
    display_name = "backend_latencies 最大レスポンスタイムが3分間1秒を超えた"
    condition_threshold {
      aggregations {
        alignment_period     = "60s"
        cross_series_reducer = "REDUCE_MAX"
        per_series_aligner   = "ALIGN_PERCENTILE_99"
      }
      comparison      = "COMPARISON_GT"
      duration        = "180s"
      filter          = "metric.type=\"loadbalancing.googleapis.com/https/backend_latencies\" resource.type=\"https_lb_rule\" resource.label.\"url_map_name\"=monitoring.regex.full_match(\".*.${var.ingress_name}.*\")"
      threshold_value = 1000.0
      trigger {
        count = 1
      }
    }
  }
  conditions {
    display_name = "backend_latencies 平均レスポンスタイムが3分間500msを超えた"
    condition_threshold {
      aggregations {
        alignment_period     = "60s"
        cross_series_reducer = "REDUCE_MEAN"
        per_series_aligner   = "ALIGN_PERCENTILE_99"
      }
      comparison      = "COMPARISON_GT"
      duration        = "180s"
      filter          = "metric.type=\"loadbalancing.googleapis.com/https/backend_latencies\" resource.type=\"https_lb_rule\" resource.label.\"url_map_name\"=monitoring.regex.full_match(\".*.${var.ingress_name}.*\")"
      threshold_value = 500.0
      trigger {
        count = 1
      }
    }
  }
  conditions {
    display_name = "${var.service_name} health check"
    condition_threshold {
      aggregations {
        alignment_period     = "1200s"
        cross_series_reducer = "REDUCE_COUNT_FALSE"
        per_series_aligner   = "ALIGN_NEXT_OLDER"
      }
      comparison      = "COMPARISON_GT"
      duration        = "300s"
      filter          = "metric.type=\"monitoring.googleapis.com/uptime_check/check_passed\" resource.type=\"uptime_url\" metric.label.\"check_id\"=\"${var.env}-health-check\""
      threshold_value = 1.0
      trigger {
        count = 1
      }
    }
  }
  notification_channels = ["projects/${var.project}/notificationChannels/${var.project_warning_channel_id}"]
}

Workspaceを使わなかった経緯

複数のcredentialを上手く切り替えれなかった。

まずこのプロジェクトの方針として、testとsandbox,productionは別々のGCPプロジェクトとして管理しているため、credentialも当然それごとに管理しています。

init実行時にローカルにもtfstateが作成されると思いますが、そこにcredentialの指定も記載され、 次回別のcredentialに切り替えようとしても、このファイルの中身を見に行くのでアクセス権限がないとエラーになります。Workspaceの機能や回避策などを調べましたが、良い方法にたどり着かなかったので、上記の構成にしました。*3 *4

またtfvarsを使わない理由としては、環境ごとにディレクトリを作成したので、実行時のオペミスを懸念してtfvarsではなくvariablesに定義する方を選びました。*5

デプロイ

やることや工夫した点は前回と同じ。 ただCIツールがCircleCIではなく、GitLab CIだったので、 GitLabCI用に一時的なファイルはcacheではなくartifactとして出力するようにしました。*6

運用面でのリスク

コード化はしましたが、それでも一部で監視だけになっているのと、いつでもGUIから変更できるようになっています。
監視設定がGUIで書き換えられて、放置されるなんてことが起きてしまうリスクがあるため、監視の設定を定期的に監視するようにしました。

.gitlab-ci.yml

dryrun-terraform:
  stage: check
  script:
    - cd terraform/プロジェクト名/environments/$CI_ENVIRONMENT_NAME/stackdriver
    - terraform plan -input=false -out=terraform.plan -detailed-exitcode > terraform.plan.result || echo $? > failed_exitcode
    - cat terraform.plan.result
  artifacts:
    paths:
      - terraform/*/*/*/*/terraform.plan
      - terraform/*/*/*/*/terraform.plan.result
      - terraform/*/*/*/*/failed_exitcode
    expire_in: 3 days

# 監視設定が手動で変更されていないかどうか
run-terraform-monitoring-schedule-alarm:
  stage: run
  script:
    - cd executer/terraform/プロジェクト名/environments/$CI_ENVIRONMENT_NAME/stackdriver
    - cat terraform.plan.result
    - >
      if [[ -f failed_exitcode ]]; then
        echo 'Any changes'
        exit $(cat failed_exitcode)
      else
        echo 'No changes'
        exit 0
      fi
  only:
    variables:
      - $TERRAFORM_MONITORING

terraform planコマンドにはexitcodeを吐き出すオプションがあり、
exitcodeを記載したファイルをartifactとして出力し、監視のJobでチェックしています。

terraform plan -input=false -out=terraform.plan -detailed-exitcode > terraform.plan.result || echo $? > failed_exitcode

-detailed-exitcode*7

0 = Succeeded with empty diff (no changes)
1 = Error
2 = Succeeded with non-empty diff (changes present)

チームへの展開

今回元々の基盤は他のメンバーが作成してくれていて、それに対して構成やWorkspace、moduleなどレビューや共同でブラッシュアップを行いました。また監視設定の追加などの改善案件を他のメンバーが主導するなど、チーム内で協力して展開することができ、大変助かりました。

おわりに

ディレクトリ構成は、前回のAWSのプロジェクトと同じ構成にしたかったのですが、
実現できなかったのが残念です。プロジェクトによっては現状ディレクトリ構成を変えざるを得ない状況もあると思うのと、無理にWorkspaceを使うこともないなと思いました。

今回で僕のTerraformのお話はおしまいです。
個人的にTerraformは、state管理とplanで変更箇所が明確にでてくれるのが一番よかったなと思います。あとは学習コストがそれほど高くなかったのもGoodポイントでした。導入コストはコード化する部分次第で変わってくるので、最初は監視から手をつけていくのが妥当だと感じました。

次回書く機会があったら、また別のお話を書いてると思います。

*1:プロキシがバックエンドにリクエストを送信してから、レスポンスを受け取るのにかかった時間

*2:modulesを作らなかった経緯はディレクトリ掘りたくなかったんでしょうね

*3:参考: Terraformにおけるディレクトリ構造のベストプラクティス

*4:毎回initするなどの方法もあるかと思いました

*5:tfvarsを使う念頭でオペミスを防止する策を、別途決めても良かったかも。

*6:jobの流れをCircleCIみたいに明確に書けないのがネックでした。。

*7:公式