permadiff: 何度applyしても消えない差分

terraformを使っていたら誰もが意図しない差分と格闘したことがあるはず。 その中でも特に何度applyしても差分が消えず残り続けるものがある。このようなdiffのことを permadiff と呼ぶ。

googlecloudplatform.github.io

permadiffについての解説はMagic ModulesというGoogle Cloud向けproviderのコード生成器におけるドキュメントで取り上げられている。このためどちらかというとGoogle系のエコシステムの中で用いられる用語かもしれない。 実際、terraform-provider-google ではpermadiffの単語は数多く登場するが、terraform-provider-awsではたまに用いられる程度である。

とはいえ、AWS向けかGoogle Cloud向けかに限らず、よく知られる概念でありterraformを使っていると多くの人が遭遇すると思われる。なのでこういったパターンをきちんと定義することは非常に有効に感じた。

具体例

terraform-provider-google v5.44.2のリリースノートにpermadiffという単語が記載されており初めて知った。

google_container_node_poolnode_config.0.kubelet_config の算出ロジックに不具合があり、kubelet_configの値を未指定にするとエラーになるというもの。 具体的には以下のコードをapplyした上で node_config.0.kubelet_config を削除するとapply エラーになる。

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "5.44.1"
    }
  }
}


resource "google_service_account" "default" {
  account_id   = "service-account-id"
  display_name = "Service Account"
}

resource "google_container_cluster" "primary" {
  name     = "my-gke-cluster"
  location = "asia-northeast1"

  remove_default_node_pool = true
  initial_node_count       = 1
}

resource "google_container_node_pool" "primary_preemptible_nodes" {
  name       = "my-node-pool"
  location   = "asia-northeast1"
  cluster    = google_container_cluster.primary.name
  node_count = 1

  node_config {
    preemptible  = true
    machine_type = "e2-medium"

    service_account = google_service_account.default.email
    oauth_scopes = [
      "https://www.googleapis.com/auth/cloud-platform"
    ]

    kubelet_config {
      cpu_manager_policy                     = "static"
      insecure_kubelet_readonly_port_enabled = "TRUE"
    }
  }
}

plan実行時にdiffが出て、applyは正常終了するけどdiffは解消しない、というケースを想定していたので このようにapplyエラーになるケースでもエラーの原因によってはpermadiffと呼ぶんだなという学びがあった。

もっとわかりやすい例は Cloud Runの metadata.annotations にlaunch-stageを指定する例。 Cloud Runにて preview featureを利用したい場合は metadata.annotations"run.googleapis.com/launch-stage": "BETA" の値を指定する必要がある。

run.googleapis.com/launch-stage sets the launch stage when a preview feature is used.

registry.terraform.io

Cloud Run のトラブルシューティング  |  Cloud Run Documentation  |  Google Cloud

resource "google_cloud_run_service" "default" {
  name     = "cloudrun-srv"
  location = "asia-northeast1"

  template {
    spec {
      containers {
        image = "gcr.io/cloudrun/hello"
      }
    }
  }

  metadata {
    annotations = {
      "run.googleapis.com/launch-stage" = "BETA"
    }
  }
}

ただし、preview featureを利用していない場合はmetadata.annotations に "run.googleapis.com/launch-stage": "BETA" を指定してもGoogle Cloud側で自動的に除去されてしまう。このため、次にplanを実行するときにはまた差分として出力されてしまう。
(この挙動は GitHubのコメント にある程度で、公式な資料としては見付けられなかった

  # google_cloud_run_service.default will be updated in-place
  ~ resource "google_cloud_run_service" "default" {
        id                         = "xxxxx"
        name                       = "cloudrun-srv"
        # (4 unchanged attributes hidden)

      ~ metadata {
          ~ annotations           = {
              ~ "run.googleapis.com/launch-stage" = null -> "BETA"
            }
          ~ effective_annotations = {
              + "run.googleapis.com/launch-stage"   = "BETA"
                # (6 unchanged elements hidden)
            }
            # (8 unchanged attributes hidden)
        }

        # (2 unchanged blocks hidden)
    }

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

preview featureを利用するときのみ metadata.annotations に "run.googleapis.com/launch-stage": "BETA" を指定し、利用しない場合は指定を解除する必要がある。