Terraform moduleでトグル

こんにちは、エリックです。今回はTerraformの話にします。

Terraformでworkspaceを使う時に、dev/stg/prodなどの環境を統一するのがベストだけど、たまにコストや機能により「ここにはこれは要らないな」などの場合がある。その時、各環境に有無トグルをどうやって設定するか、どうやって依存などを解決するか軽く語ろう。

例として、foo/bar/bazというGKEクラスタのnode poolがあり、stg環境にbazが不要だとする。

  • dev: foo, bar, baz
  • prod: foo, bar, baz
  • stg: foo, bar

各node poolはこんな風:

main.tf:
module node-pool-baz {
  source          = ".../container_node_pool/google"
  version         = "1.0.0"
  cluster         = module.cluster.cluster
  node_pool       = var.node_pool_baz
  node            = var.node_baz
  service_account = module.sa-baz.sa
}

まず、node poolのモジュールに、有無のトグルを追加:

.terraform/modules/node-pool-baz/main.tf:
resource google_container_node_pool default {
+ count      = var.node_pool.enabled ? 1 : 0
  [...]
.terraform/modules/node-pool-baz/variables.tf:
variable node_pool {
  type = object({
+   enabled    = bool,
    [...]
  })
}

見れば分かると思うが、そのcount行は三項演算子で、boolのenabledとの変数で、構築するかどうか決める。 trueなら、1つ構築、falseなら0構築(構築しない)。countはTerraformで、どのリソースにでも使える。

環境(workspace)のtfvarsファイルはこんな風に:

stg.tfvars:
node_pool_foo = {
  enabled     = true,
  [...]
}
node_pool_bar = {
  enabled     = true,
  [...]
}
node_pool_baz = {
  enabled     = false,
  [...]
}

ここまでは大丈夫だけど、各node poolに、サービスアカウントも依存するから、 そのmoduleにもenabled追加しよう(そうしないと、enabled:falseの場合にでもサービスアカウントが作成されちゃう)。

まず、サービスアカウントのmoduleにそのenabled変数を追加して、リスースのところに以上の三項演算子を:

.terraform/modules/sa-baz/variables.tf:
+variable enabled {
+  type    = bool
+  default = true
+}
.terraform/modules/sa-baz/main.tf:
resource google_service_account default {
   [...]
+  count        = var.enabled ? 1 : 0
 }

 resource google_project_iam_member default {
   [...]
-  for_each = toset(var.roles)
+  for_each = var.enable ? toset(var.roles) : []

   project = var.project
   role    = each.key
   member  = format("serviceAccount:%s", google_service_account.default.email)
 }

あと、呼び出し側にもenabledを追加(混乱・重複しないためnode poolのenabledを利用して)。

main.tf:
 module sa-baz {
[...]
   roles        = var.sa_baz_roles
+  enabled      = var.node_pool_baz.enabled
 }

OK. 実行してみよう!

Error: Missing resource instance key

  on .terraform/modules/sa-baz/main.tf line 15, in resource "google_project_iam_member" "default":
  15:   member  = format("serviceAccount:%s", google_service_account.default.email)

Because google_service_account.default has "count" set, its attributes must be
accessed on specific instances.

For example, to correlate with indices of a referring resource, use:
    google_service_account.default[count.index]

なるほど。count追加すると、変数が配列になり、indexで示さないと。 もうちょい修正:

.terraform/modules/sa-baz/main.tf:
-  member  = format("serviceAccount:%s", google_service_account.default.email)
+  member  = format("serviceAccount:%s", google_service_account.default[0].email)

で、もう一回実行してみる:

Error: Unsupported attribute

  on .terraform/modules/node-pool-baz/main.tf line 13, in resource "google_container_node_pool" "default":
  13:     service_account = var.service_account.email
    |----------------
    | var.service_account is tuple with 1 element

This value does not have any attributes.

なるほど。配列になったけど、使用してるnode-poolのところが単独の変数を期待してる。どこを修正すれば良いかは、モジュールの出力のところを見てみよう。

.terraform/modules/sa-baz/output.tf:
output sa {
  value = google_service_account.default
}

ああ。では、こっちで配列の[0]にすれば、配列じゃなくて普通の変数が出てくるわけですね。

  value = google_service_account.default[0]

もう一回実行:

Error: Invalid index

  on .terraform/modules/sa-baz/output.tf line 2, in output "sa":
   2:   value = google_service_account.default[0]
    |----------------
    | google_service_account.default is empty tuple

The given key does not identify an element in this collection value.

あ〜、enabled: falseのところは、サービスアカウントがないから配列の[0]もないね。 じゃあ、これはどう?

   value = length(google_service_account.default) > 0 ? google_service_account.default[0] : ""

やってみよう:

Error: Error in function call

  on .terraform/modules/sa-baz/output.tf line 2, in output "sa":
   2:   value = length(google_service_account.default > 0) ? google_service_account.default[0] : ""

Call to function "length" failed: argument must be a string, a collection
type, or a structural type.

ダメだな。配列がないならlengthも使えない。ちょいググったら、こんな形が出てきた:

  value = try(google_service_account.default[0], null)

saの配列[0]の要素を返してみて、それがなければnullを。

No changes. Infrastructure is up-to-date.

これで解決〜!

…とはいえ、moduleで色んな書き方があるし、0.13から呼び出しの方からでもfor_eachcountが使えるようになったらしい。とゆーところで、 0.13でどうやって扱うか、良い方法考えてみるのは宿題!w

〜終〜