Check! Terraform でモジュールを動的に選択する

4 min read読了の目安(約3700字

Prologue

Terraform でいろんな環境のサンプルを書いていて、必要に応じて出し分けたいというニーズがあり、試行錯誤の末に落ち着いたやり方を書き留めます。

Terraform でモジュールを動的に選択する

今回の構成はこのようになっています。

  • 配列の input variables を用意し、使用するモジュールを指定させる
  • 配列に一致する文字列があれば、そのモジュールを含める
  • 実行されたモジュールの実行結果を表示する

サンプルコード全体はこちらをご参照ください。

https://github.com/dzeyelid/terraform-playground/tree/main/module-selector

ポイントになるコードを抜粋します。 source のモジュールは module_name を返すだけのダミーです。

まず、モジュールを選択させる引数を指定させます。ここでは、文字列の配列を受け付ける引数 modules を用意します。文字列は出し分けに利用する "キーワード" として後述で扱います。モジュール名と同じか、モジュールを指定しやすい文字列にするとよいでしょう。

.auto.tfvars
modules = [
  "one",
  "two",
]
variables.tf
variable "modules" {
  type = list(string)
}

つぎに、各モジュールに対して、引数に"キーワード"に一致する文字列が指定されていたら、 countfor_each を使って出し分ける記述をします。

count を使う例では、var.modules にキーワードの文字列が含まれている場合は、count1回 を指定して、モジュールを実行させます。なければ 0回 を指定することで、実行させません。

main.tf
module "one" {
  count = 0 <= try(index(var.modules, "one"), -1) ? 1 : 0

  source = "./modules/base"
  name   = "one"
}

for_each を使う例では、var.modules にキーワードの文字列が含まれている場合は、1つの要素(ここでは "selected")を持つ配列を set に変換して実行させます。なければ、空の配列を指定することで実行させません。

main.tf
module "two" {
  for_each = toset(0 <= try(index(var.modules, "two"), -1) ? ["selected"] : [])

  source = "./modules/base"
  name   = "two"
}

実行結果の確認は利用するモジュールに寄りますが、ここでは、outputs でモジュールからの出力を出力して確認しましょう。

count を利用した module.one は、[*] シンボルを利用して、各モジュール(といっても 1個 または 0個)の出力を参照することができます。

for_each を利用した module.two は、該当するキーワードが指定されたとき(for_each に 1つ以上の要素が含まれる配列の set を指定したとき)は問題なく参照できますが、キーワードが指定されていないとき(for_each に空の配列の set を指定したとき)は参照エラーになってしまうので、 try() を利用して回避しています。

outputs.tf
output "one_module_name" {
  value = module.one[*].module_name
}

output "two_module_name" {
  value = try(module.two["selected"].module_name, "Not found")
}

それでは、コードの準備ができたら、確認してみましょう。Terraform のコマンドを実行します。

terraform init
terraform plan

すると、このような出力になると示されます。期待通り!✨

Changes to Outputs:
  + one_module_name = [
      + "one",
    ]
  + two_module_name = "two"

さらに、モジュールを指定しない確認もしてみましょう。引数の modules を空の配列にすると、それぞれ countfor_each で選ばれなくなるので、それぞれのモジュールは実行されません。

.auto.tfvars
modules = [
#  "one",
#  "two",
]

terraform plan の実行結果はこのようになります。

Changes to Outputs:
  + one_module_name = []
  + two_module_name = "Not found"

やったー、意図通り!すっきり!🙌✨

なお、エラーが出てしまう方は下記もご参考ください。

つまづきポイント

for_each を利用したときの参照は少し独特です。個々のインスタンスを参照するには、アトリビュートではなく、配列のような書き方 <TYPE>.<NAME>["<KEY>"]module.<NAME>["<KEY>"] として参照します。

https://www.terraform.io/docs/language/meta-arguments/for_each.html#referring-to-instances

文字列のリストを toset() を使って指定した場合

main.tf
module "example" {
  for_each = toset(["a_member"])
  ...
}
outputs.tf
output "output_example" {
  value = module.example["a_member"].output_a_string
}

map を使って指定した場合

main.tf
module "example" {
  for_each = { a_key = "a_value" }
  ...
}
outputs.tf
output "output_example" {
  value = module.example["a_key"].output_a_string
}

エラーの例

アトリビュートの指定を誤ると、たとえば terraform plan を実行した際には下記のようなエラーが表示されます。

outputs.tf
output "output_example" {
  value = module.example.output_a_string  # <- Wrong reference
}
Error: Unsupported attribute

  on outputs.tf line 6, in output "output_example":
   6:   value = module.example.output_a_string
    |----------------
    | module.example is object with 1 attribute "a_key"

This object does not have an attribute named "output_a_string".

Epilogue

このやり方でモジュールを選択させることにしたのは、このリポジトリのコードです。Azure PaaS をセキュアに接続するいくつかのサンプルを、モジュールに分けて紹介しています。ご興味ある方は是非ご参照ください😊

https://github.com/dzeyelid/learn-azure-functions-with-network-options/tree/main/terraform