Terraformでmappingをmoduleに渡す方法

2016.05.26

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは、中山です。

みなさんはTerraformのmappingやmoduleを利用していますか。mappingについては弊社八幡のTerraformで複数台のEC2インスタンスを構築する場合のTIPSに詳しいです。簡単に説明すると、mappingとはいわゆるhashのように変数を管理するデータ構造です。moduleとは要するに関数ですね。Terraformで利用するresourceをひとまとめにして再利用可能な形にパッケージ化できます。Terraformにはこのようにプログラミング言語と同等の機能があるので表現力がとても高く、小さなプログラミング言語のような側面があります。

私はこれらの機能をよく利用しているのですが、ふとこう思ったことがあります。「mappingで定義した変数をmoduleに渡せたら便利だな」と。私はTerraformを利用するとき main.tf に全てのresourceを書くのではなくコンポーネント毎(NW/EC2/RDSなど)にmoduleでtfファイルを分割しています。そしてそれらを束ねる大本のtfファイルから terraform.tfvars を参照するという方式でコードを書くことが多いです。

こうすることによって以下のメリットがあると考えています。

  1. 全ての変数を terraform.tfvars で管理するので変数の管理が容易
  2. コンポーネント毎にコードを分割することでコードの見通しが良くなる

参考にしているのはhashicorp/best-practicesというリポジトリです。Terraformの開発元、HashiCorpがbestと言っているのだからそれなりの理由があるんだろうという訳で今のところこの方式でコードを書いています(これはこれできついものがあるのですがそれはまた別エントリにまとめます)。

話を戻します。Terraformはmoduleに変数を渡す際、変数を渡すtfファイル/変数を受け取るmodule両方に変数を定義する必要があります。変数の数が増えるとこの変数定義が結構面倒くさいです。そこでmappingとして変数をまとめれば便利なのではと思った次第です。

本エントリではこれが実現できるのか、その試行錯誤をメインにしてご紹介します。

結論

先に結論を書きます。v0.7.0対応するようです!!!111。つまりそれ以下のバージョンでは対応してません。なので下の文章読まなくてもいいです。もしお時間ありましたらお付き合いください。

出力結果の確認方法

Terraformの output サブコマンドは terraform.tfstate から入力を得ているため、適宜 plan サブコマンドでそのファイルを作成する必要があります。本エントリのサンプルコードは出力結果を確認する前に plan サブコマンドで terraform.tfstate ファイルを作成してください。

Terraformのバージョン

v0.6.16です。

テスト1: *.tf ファイルでmappingを定義してそのまま出力してみる

まずは通常の使い方で期待する動作を確認してみましょう。 terraform.tfvars ではなく *.tf でmappingを定義し、moduleではなくそのまま出力してみます。

  • main.tf
variable "images" {
  default = {
    us-east-1 = "image-1234"
    us-west-2 = "image-4567"
  }
}

output "us-east-1_image_id_via_lookup" {
  value = "${lookup(var.images, "us-east-1")}"
}

output "us-east-1_image_id_via_dot" {
  value = "${var.images.us-east-1}"
}

output "us-west-2_image_id_via_lookup" {
  value = "${lookup(var.images, "us-west-2")}"
}

output "us-west-2_image_id_via_dot" {
  value = "${var.images.us-west-2}"
}

mappingはlookup関数だけではなく、 ${var.map.key} の形式でも参照可能です。実行してみましょう。

$ terraform output
us-east-1_image_id_via_dot = image-1234
us-east-1_image_id_via_lookup = image-1234
us-west-2_image_id_via_dot = image-4567
us-west-2_image_id_via_lookup = image-4567

予想通り images の値がそれぞれ参照できているようですね。

テスト2: terraform.tfvars でmappingを定義してそのまま出力してみる

*.tf ではなく terraform.tfvars でもmappingは定義できます。こちらも期待する動作をするのか実際に試してみましょう。

  • terraform.tfvars
images.us-east-1 = "image-1234"
images.us-west-2 = "image-4567"
  • main.tf
variable "images" {
  default = {}
}

output "us-east-1_image_id_via_lookup" {
  value = "${lookup(var.images, "us-east-1")}"
}

output "us-east-1_image_id_via_dot" {
  value = "${var.images.us-east-1}"
}

output "us-west-2_image_id_via_lookup" {
  value = "${lookup(var.images, "us-west-2")}"
}

output "us-west-2_image_id_via_dot" {
  value = "${var.images.us-west-2}"
}

ポイントは main.tf で定義している images 変数の default = {} という指定です。これを指定しないとmappingとして認識されないようです。実行してみましょう。

$ terraform output
us-east-1_image_id_via_dot = image-1234
us-east-1_image_id_via_lookup = image-1234
us-west-2_image_id_via_dot = image-4567
us-west-2_image_id_via_lookup = image-4567

先程と同じような出力結果が得られました。

テスト3: *.tf で定義したmappingをmoduleに渡して出力してみる

期待する動作をテスト1/テスト2で確認しました。今度はmoduleにmappingで定義した変数を渡し、上記のような期待する動作になるのか確認してみましょう。まずは、 mappingを *.tf で定義してみます。

  • main.tf
variable "images" {
  default = {
    us-east-1 = "image-1234"
    us-west-2 = "image-4567"
  }
}

module "test" {
  source = "./test"

  images = "${var.images}"
}

output "us-east-1_image_id_via_lookup" {
  value = "${module.test.us-east-1_image_id_via_lookup}"
}

output "us-east-1_image_id_via_dot" {
  value = "${module.test.us-east-1_image_id_via_dot}"
}

output "us-west-2_image_id_via_lookup" {
  value = "${module.test.us-west-2_image_id_via_lookup}"
}

output "us-west-2_image_id_via_dot" {
  value = "${module.test.us-west-2_image_id_via_dot}"
}
  • test/test.tf
variable "images" {
  default = {}
}

output "us-east-1_image_id_via_lookup" {
  value = "${lookup(var.images, "us-east-1")}"
}

output "us-east-1_image_id_via_dot" {
  value = "${var.images.us-east-1}"
}

output "us-west-2_image_id_via_lookup" {
  value = "${lookup(var.images, "us-west-2")}"
}

output "us-west-2_image_id_via_dot" {
  value = "${var.images.us-west-2}"
}

実行してみます。

$ terraform plan
Error configuring: 1 error(s) occurred:

* variable images in module test should be type map, got type string

どうやらmappingとして定義したはずの images 変数が文字列として解釈されてしまうようです。Terraformは変数を定義する際にそのタイプを明示的に宣言することができます。具体的には type = "map" と指定すればmappingとして定義できます。が、こちらを指定してみても同じ結果でした。残念。

テスト4: terraform.tfvars で定義したmappingをmoduleに渡して出力してみる

今度は terraform.tfvars でmappingを定義してみましょう。動くのでしょうか。

  • terraform.tfvars
images.us-east-1 = "image-1234"
images.us-west-2 = "image-4567"
  • main.tf
variable "images" {
  default = {}
}

module "test" {
  source = "./test"

  images = "${var.images}"
}

output "us-east-1_image_id_via_lookup" {
  value = "${module.test.us-east-1_image_id_via_lookup}"
}

output "us-east-1_image_id_via_dot" {
  value = "${module.test.us-east-1_image_id_via_dot}"
}

output "us-west-2_image_id_via_lookup" {
  value = "${module.test.us-west-2_image_id_via_lookup}"
}

output "us-west-2_image_id_via_dot" {
  value = "${module.test.us-west-2_image_id_via_dot}"
}
  • test/test.tf
variable "images" {
  default = {}
}

output "us-east-1_image_id_via_lookup" {
  value = "${lookup(var.images, "us-east-1")}"
}

output "us-east-1_image_id_via_dot" {
  value = "${var.images.us-east-1}"
}

output "us-west-2_image_id_via_lookup" {
  value = "${lookup(var.images, "us-west-2")}"
}

output "us-west-2_image_id_via_dot" {
  value = "${var.images.us-west-2}"
}

実行してみます。

$ terraform plan
Error configuring: 1 error(s) occurred:

* variable images in module test should be type map, got type string

残念ながら同じようなエラーが出てしまいますね。 type = "map" にしても同じようです。

テスト5: keys/values関数を使う

Terraformにはドキュメントには記載されていないkeys/values関数があります。この関数を利用することで「擬似的」にmappingをmoduleに渡すことができます。

  • terraform.tfvars
images.us-east-1 = "image-1234"
images.us-west-2 = "image-4567"
  • main.tf
variable "images" {
  default = {}
}

module "test" {
  source = "./test"

  image_keys   = "${join(",", keys(var.images))}"
  image_values = "${join(",", values(var.images))}"
}

output "us-east-1_image_id" {
  value = "${module.test.us-east-1_image_id}"
}

output "us-west-2_image_id" {
  value = "${module.test.us-west-2_image_id}"
}
  • test/test.tf
variable "image_keys" {}

variable "image_values" {}

output "us-east-1_image_id" {
  value = "${element(split(",", var.image_values), index(split(",", var.image_keys), "us-east-1"))}"
}

output "us-west-2_image_id" {
  value = "${element(split(",", var.image_values), index(split(",", var.image_keys), "us-west-2"))}"
}

keys関数でmappingのkeyを取得、values関数でmappingのvalueを取得し、join関数でカンマ区切りの文字列に変換してmoduleに渡しています。変数を受け取った方のmoduleではカンマ区切りの変数をsplit関数でlistに変換し、index関数で目的のkeyを参照、element関数でvalueを最終的に取得しています。この関数を利用することで、一つのmapping定義だけで済むという利点があります。ちなみにですがjoin関数を利用しないとどうやら「B780FFEC...」という文字列に変換されてしまうようなので、指定する必要があります。

実行してみましょう。

$ terraform output
us-east-1_image_id = image-1234
us-west-2_image_id = image-4567

ちゃんと値を取得できているようですね。今回の例では terraform.tfvars を利用しましたが、 *.tf に直接mapping定義する場合でも同じく出力してくれます。

テスト6: module側でmappingを再作成する

テスト5の例を見た方の中には「module側でmappingを再作成すると良いのでは」と思った方もいらっしゃるのではないでしょうか。テスト5の例は、確かに一つのmappingだけを定義すれば良いという利点がありますが、module側では単なる文字列を渡しているだけです。そのためさまざまな関数を利用して文字列処理を実施しています。

それならいっそmoduleに渡した変数からmodule側でmappingを再作成すれば、妙な文字列処理せず素直なコードになるのではないでしょうか。試してみまょう。

  • terraform.tfvars
images.us-east-1 = "image-1234"
images.us-west-2 = "image-4567"
  • main.tf
variable "images" {
  default = {}
}

module "test" {
  source = "./test"

  image_keys   = "${join(",", keys(var.images))}"
  image_values = "${join(",", values(var.images))}"
}

output "us-east-1_image_id_via_lookup" {
  value = "${module.test.us-east-1_image_id_via_lookup}"
}

output "us-east-1_image_id_via_dot" {
  value = "${module.test.us-east-1_image_id_via_dot}"
}

output "us-west-2_image_id_via_dot" {
  value = "${module.test.us-west-2_image_id_via_dot}"
}

output "us-west-2_image_id_via_lookup" {
  value = "${module.test.us-west-2_image_id_via_lookup}"
}
  • test/test.tf
variable "image_keys" {}

variable "image_values" {}

variable "images" {
  default = {
    us-east-1 = "${element(split(",", var.image_values), index(split(",", var.image_keys), "us-east-1"))}"
    us-west-2 = "${element(split(",", var.image_values), index(split(",", var.image_keys), "us-west-2"))}"
  }
}

output "us-east-1_image_id_via_lookup" {
  value = "${lookup(var.images, "us-east-1")}"
}

output "us-east-1_image_id_via_dot" {
  value = "${var.images.us-east-1}"
}

output "us-west-2_image_id_via_lookup" {
  value = "${lookup(var.images, "us-west-2")}"
}

output "us-west-2_image_id_via_dot" {
  value = "${var.images.us-west-2}"
}

変数定義の中で文字列処理しているのであまり意味ないのではという気もしなく無いですが、実行してみましょう。

$ terraform plan
There are warnings and/or errors related to your configuration. Please
fix these before continuing.

Errors:

  * 1 error(s) occurred:

* module test.root: 1 error(s) occurred:

* Variable 'images': cannot contain interpolations

残念ながら現在のところTerraformは変数定義の中での変数展開をサポートしていません。一応null_resourceやtemplate_fileを利用したワークアラウンドがあるようです。

v0.7.0では対応されているのか

では肝心のv0.7.0ではmappingをmoduleへ渡す機能に対応しているのでしょうか。「a47ad103e2320a40071a54400be46cf24071091b(v7.0.0-dev)」をコンパイルし、確認してみましょう。コンパイルの方法はREADMEを参照してください。コンパイルが完了すると $GOPATH/bin ディレクトリ以下にTerraformのバイナリが出力されます。

$ $GOPATH/bin/terraform version
Terraform v0.7.0-dev (a47ad103e2320a40071a54400be46cf24071091b)

結果

テスト3-4実行してみたのですがうまく動作してないようでした。CHANGELOGを見るとjsonencode関数がそれらしい記述をしているのですが、これのことなのか。v0.7.0がでたら追記したいと思います。

追記
2016/08/02にTerraform v0.7.0がリリースされましたv0.7.0へのアップグレードガイドの「Migrating to native lists and maps」に記述されていますが、Moduleに対してlist及びmapを渡すことが可能になりました!

まとめ

いかがだったでしょうか。

はやくv0.7.0リリースしてくれ!!!111。

本エントリがみなさんの参考になれば幸いです。

参考リンク