【Terraform】依存するリソースに合わせて自動でリソースを作り直す（replace_triggered_by）

## 概要

Terraform の `lifecycle` ブロックで定義できる `replace_triggered_by` について記載します。

## TL;DR

リソースに明示されていない依存関係があり、片方のリソースを作り直し(replace)しようとするとエラーになる場合がある（本記事では、Cloud Monitoringの稼働時間チェックとアラートポリシーについて例示）。

この場合、依存元のリソースの `lifecycle` ブロックで `replace_triggered_by` に依存先リソースを設定すると、一緒に作り直し(replece)をしてくれるためエラーにならない。

`replace_triggered_by` の書き方は以下のように定義する。

``` 
resource "google_monitoring_alert_policy" "https_check_alert" {
  ...略

  lifecycle {
    # ここで指定したリソースが作り直される際に、一緒に作り直されるようになる。
    replace_triggered_by = [
      google_monitoring_uptime_check_config.https_check.id
    ]
  }
}
```

## lifecycle ブロックとは

lifecycle ブロックは、Terraform の機能であり、定義することでリソースのライフサイクルをカスタマイズすることができます。
公式リファレンスには、**The lifecycle Meta-Argument** として記載されています。

https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle

2023/6/23 時点で以下の4つ設定が定義できます。

- `create_before_destroy`
- `prevent_destroy`
- `ignore_changes`
- `replace_triggered_by`

この記事では、`replace_triggered_by` についての説明に絞ります。

## 例. Cloud Monitoring の稼働時間チェックとアラートポリシー

Cloud Monitoring の稼働時間チェックとアラートポリシーには依存関係がありますが、APIとして明確に依存関係が定義されておらず、`replace_triggered_by` の設定が有効となります。

以降では、稼働時間チェックとアラートポリシーの機能を検証する際に用いたTerraformのコードを使い説明します。

※ 注意
Project IDやBackendは省略していたり、監視対象のホスト名は変更していたりしています。
参考にしていただく際は、適宜読み替えてください。
また、Googleプロバイダのバージョンによって `replace_triggered_by`は不要になるかもしれませんので、ご了承ください。

以下にコードの全体像を記載しますが、説明箇所は適宜取り上げるため読み飛ばして問題ありません。

<details>
<summary>検証に用いたTerraformコードの全体像</summary>
<div>

```provider.tf
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "4.50.0"
    }
  }
}
provider "google" {
  project = "＜省略＞"
  region  = "asia-northeast1"
}
```


```uptime_check_sample.tf
resource "google_monitoring_uptime_check_config" "https_check" {
  display_name = "外形監視のテスト"
  timeout      = "10s"
  period       = "600s"

  # 選択なしだと、GLOBAL（全リージョン）からののチェックとなる。
  selected_regions = [
    "ASIA_PACIFIC",
    "EUROPE",
    "USA_VIRGINIA",
  ]

  http_check {
    path         = "/"
    port         = "443"
    use_ssl      = true
    validate_ssl = true
    accepted_response_status_codes {
      status_value = 200
    }
  }

  monitored_resource {
    type = "uptime_url"
    labels = {
      project_id = local.project_id
      host       = "example.com"
    }
  }

  content_matchers {
    content = "OK"
    matcher = "CONTAINS_STRING"
  }

  checker_type = "STATIC_IP_CHECKERS"
}
```

```alert_policy_sample.tf
resource "google_monitoring_alert_policy" "https_check_alert" {
  display_name = "外形監視のアラート"
  combiner     = "OR"
  conditions {
    display_name = "外形監視のアラート"

    condition_threshold {
      filter          = "resource.type = \"uptime_url\" AND metric.type = \"monitoring.googleapis.com/uptime_check/check_passed\" AND metric.labels.check_id = \"${google_monitoring_uptime_check_config.https_check.uptime_check_id}\""
      duration        = "0s"
      threshold_value = 1
      comparison      = "COMPARISON_GT"
      aggregations {
        alignment_period     = "1200s"
        cross_series_reducer = "REDUCE_COUNT_FALSE"
        per_series_aligner   = "ALIGN_NEXT_OLDER"
      }
      trigger {
        count   = 1
        percent = 0
      }
    }
  }

  # 通知先の定義は、別途定義が必要です。
  # ちなみに、認証まわりをTerraformで管理したくないため、通知チャンネルは管理コンソールから手動で追加し、data source で管理しています。
  notification_channels = [
    data.google_monitoring_notification_channel.slack.id,
  ]

  documentation {
    content   = <<-EOT
    外形監視のアラートテストです。
    EOT
    mime_type = "text/markdown"
  }

  user_labels = { "severity" : "info" }
  enabled     = true

  depends_on = [
    google_monitoring_uptime_check_config.https_check
  ]

  lifecycle {
    replace_triggered_by = [
      google_monitoring_uptime_check_config.https_check.id
    ]
  }
}

```

</div>
</details>

### 使用するリソース

以下の２種類を使っています。

- 稼働時間チェック [google_monitoring_uptime_check_config](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/monitoring_uptime_check_config)
- アラートポリシー [google_monitoring_alert_policy](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/monitoring_alert_policy)

どちらも Google Cloud の Cloud Monitoring のリソースとなっています。
稼働時間チェックを用いることで、外形監視やVMの死活監視などを設定することができます。
本記事では、稼働時間チェックで外形監視を設定し、アラートポリシーにより異常を検知したらSlack通知する設定を想定します。

また、それぞれのリソースの詳細な設定内容については割愛します。
ご興味のある方は、公式ドキュメントを参照ください。

https://cloud.google.com/monitoring/uptime-checks/introduction?hl=ja

https://cloud.google.com/monitoring/alerts/types-of-conditions?hl=ja

### 依存関係

アラートポリシーが稼働時間チェックに依存している関係になっています。
詳しくは、以下のようにアラートの対象とするための条件を記載するfilterの項目で、稼働時間チェックのIDを参照しています。

```alert_policy_sample.tf
resource "google_monitoring_alert_policy" "https_check_alert" {
  ...略
  condition_threshold {
      filter          = "resource.type = \"uptime_url\" AND metric.type = \"monitoring.googleapis.com/uptime_check/check_passed\" AND metric.labels.check_id = \"${google_monitoring_uptime_check_config.https_check.uptime_check_id}\""
  ...略
}
```

本題の `replace_triggered_by` は、`lifecycle` の部分に記述しており、この設定で稼働時間チェックの修正をトリガーとして replace（作り直し）をするように定義できます。

```alert_policy_sample.tf
resource "google_monitoring_alert_policy" "https_check_alert" {
  ...略

  lifecycle {
    replace_triggered_by = [
      google_monitoring_uptime_check_config.https_check.id
    ]
  }
}
```

### replace_triggered_by なしで稼働時間チェックを削除すると...

稼働時間チェックは一度作成すると、チェックの時間間隔や対象のホスト名を変更することができず、変更するには一旦作り直しをする必要があります。
試しに、`google_monitoring_uptime_check_config`の`period`の値を600sから900sに変更してみます。

その際に、以下のように lifecycle をコメントアウトした状態で`terraform apply`してみます。

```alert_policy_sample.tf
resource "google_monitoring_alert_policy" "https_check_alert" {
  ...略
  period       = "600s"  # ここを 900s に修正
  ...略

  /* lifecycle {
    replace_triggered_by = [
      google_monitoring_uptime_check_config.https_check.id
    ]
  } */
}
```

実行結果は、以下のようになります。
（リソースIDなどはxxxでマスクしています）

```replace_triggered_by を設定しなかった場合の実行結果
$ terraform apply
...

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # google_monitoring_alert_policy.https_check_alert will be updated in-place
  ~ resource "google_monitoring_alert_policy" "https_check_alert" {
        id                    = "projects/xxx-project/alertPolicies/xxx"
        name                  = "projects/xxx-project/alertPolicies/xxx"
        # (7 unchanged attributes hidden)

      ~ conditions {
            name         = "projects/xxx-project/alertPolicies/xxx/conditions/xxx"
            # (1 unchanged attribute hidden)

          ~ condition_threshold {
              ~ filter          = "resource.type = \"uptime_url\" AND metric.type = \"monitoring.googleapis.com/uptime_check/check_passed\" AND metric.labels.check_id = \"xxx\"" -> (known after apply)
                # (3 unchanged attributes hidden)

                # (2 unchanged blocks hidden)
            }
        }

        # (1 unchanged block hidden)
    }

  # google_monitoring_uptime_check_config.https_check must be replaced
-/+ resource "google_monitoring_uptime_check_config" "https_check" {
      ~ id               = "projects/xxx-project/uptimeCheckConfigs/xxx" -> (known after apply)
      ~ name             = "projects/xxx-project/uptimeCheckConfigs/xxx" -> (known after apply)
      ~ period           = "600s" -> "900s" # forces replacement
      ~ project          = "xxx-project" -> (known after apply)
      ~ uptime_check_id  = "xxx" -> (known after apply)
        # (4 unchanged attributes hidden)

      ~ http_check {
          ~ headers        = {} -> (known after apply)
          - mask_headers   = false -> null
            # (5 unchanged attributes hidden)

          ~ accepted_response_status_codes {
                # (1 unchanged attribute hidden)
            }
        }

        # (2 unchanged blocks hidden)
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

google_monitoring_uptime_check_config.https_check: Destroying... [id=projects/xxx-project/uptimeCheckConfigs/xxx]
╷
│ Error: Error when reading or editing UptimeCheckConfig: googleapi: Error 400: Request contains an invalid argument.
│ 
│
```

実行計画の要点としては以下の2つであり、結果として`Plan: 1 to add, 1 to change, 1 to destroy.` のように出力されます。

- アラートポリシー google_monitoring_alert_policy.https_check_alert は filter の変更(chage)のみ
- 稼働時間チェック google_monitoring_uptime_check_config は period 修正に伴い、 destroy(削除)とadd(作成)


そして、実行結果では、Google APIに`Error 400: Request contains an invalid argument`と怒られてしまって、applyには失敗します。
これは、依存元であるアラートポリシーも、依存先の稼働時間チェックと一緒に一度作り直す必要があるためです。


### replace_triggered_by ありで稼働時間チェックを削除すると...

では、失敗するのを確認したところで、コメントアウトした `lifecycle` の設定を戻して再度 `terraform apply` してみます。修正内容は、前項と同じ period の変更のみとなります。

```alert_policy_sample.tf
resource "google_monitoring_alert_policy" "https_check_alert" {
  ...略

  lifecycle {
    replace_triggered_by = [
      google_monitoring_uptime_check_config.https_check.id
    ]
  }
}
```

（例によって、リソースIDなどはxxxでマスクしています）

```replace_triggered_by を設定した場合の実行結果
$ terraform apply
...

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # google_monitoring_alert_policy.https_check_alert will be replaced due to changes in replace_triggered_by
-/+ resource "google_monitoring_alert_policy" "https_check_alert" {
      ~ creation_record       = [
          - {
              - mutate_time = "YYYY-MM-SST11:25:03.808189933Z"
              - mutated_by  = "xxx-sa@xxx-project.iam.gserviceaccount.com"
            },
        ] -> (known after apply)
      ~ id                    = "projects/xxx-project/alertPolicies/xxx" -> (known after apply)
      ~ name                  = "projects/xxx-project/alertPolicies/xxx" -> (known after apply)
      ~ project               = "xxx-project" -> (known after apply)
        # (5 unchanged attributes hidden)

      ~ conditions {
          ~ name         = "projects/xxx-project/alertPolicies/xxx/conditions/xxx" -> (known after apply)
            # (1 unchanged attribute hidden)

          ~ condition_threshold {
              ~ filter          = "resource.type = \"uptime_url\" AND metric.type = \"monitoring.googleapis.com/uptime_check/check_passed\" AND metric.labels.check_id = \"xxx\"" -> (known after apply)
                # (3 unchanged attributes hidden)

              ~ aggregations {
                  - group_by_fields      = [] -> null
                    # (3 unchanged attributes hidden)
                }

                # (1 unchanged block hidden)
            }
        }

        # (1 unchanged block hidden)
    }

  # google_monitoring_uptime_check_config.https_check must be replaced
-/+ resource "google_monitoring_uptime_check_config" "https_check" {
      ~ id               = "projects/xxx-project/uptimeCheckConfigs/xxx" -> (known after apply)
      ~ name             = "projects/xxx-project/uptimeCheckConfigs/xxx" -> (known after apply)
      ~ period           = "600s" -> "900s" # forces replacement
      ~ project          = "xxx-project" -> (known after apply)
      ~ uptime_check_id  = "xxx" -> (known after apply)
        # (4 unchanged attributes hidden)

      ~ http_check {
          ~ headers        = {} -> (known after apply)
          - mask_headers   = false -> null
            # (5 unchanged attributes hidden)

          ~ accepted_response_status_codes {
                # (1 unchanged attribute hidden)
            }
        }

        # (2 unchanged blocks hidden)
    }

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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

google_monitoring_alert_policy.https_check_alert: Destroying... [id=projects/xxx-project/alertPolicies/xxx]
google_monitoring_alert_policy.https_check_alert: Destruction complete after 2s
google_monitoring_uptime_check_config.https_check: Destroying... [id=projects/xxx-project/uptimeCheckConfigs/xxx]
google_monitoring_uptime_check_config.https_check: Destruction complete after 0s
google_monitoring_uptime_check_config.https_check: Creating...
google_monitoring_uptime_check_config.https_check: Creation complete after 1s [id=projects/xxx-project/uptimeCheckConfigs/xxx]
google_monitoring_alert_policy.https_check_alert: Creating...
google_monitoring_alert_policy.https_check_alert: Creation complete after 2s [id=projects/xxx-project/alertPolicies/xxx]

Apply complete! Resources: 2 added, 0 changed, 2 destroyed.
```

今度の実行計画は、`Plan: 2 to add, 0 to change, 2 to destroy.`となり、リソースは2つも作り直し(replace)され、怒られることなくapplyできました。

## まとめ

Terraform 1.2 から追加された `lifecycle` の `replace_triggered_by` がないとエラーになってしまう例について書きました。
`replace_triggered_by` の設定によって、依存先のリソースの作り直しをトリガーに、依存元も同時に作り直すように定義することができます。

今回、稼働時間チェックとアラートポリシーの検証をしていた際に、設定変更でエラーとなって `replace_triggered_by` を知ったので記事にまとめてみました。
依存関係がありつつ、Google API側で明確に依存関係が定義されているわけではないという、なかなか稀な例かと思いますが、そこをTerraform自身の機能で良い感じに定義できるというのは個人的には面白いところだと思いました。

以上、この記事が誰かの役に立てれば幸いです。