tkak's tech blog

This is my technological memo.

Terraform moduleあるある

このエントリは HashiCorp Advent Calendar 2015 - Qiita 10日目の記事です。今回はTerraformのmodule機能に関する知見をご紹介します。

moduleとは?

module "consul" {
    source = "github.com/hashicorp/consul/terraform/aws"
    servers = 3
}

moduleは、Terraform resourceを抽象化するためのものです。よく使うパラメータをmodule内に隠蔽して入力項目を減らしたり、複数のresourceをまとめたり、tfファイルの見通しを良くするために使います。

生のresourceを使ってTerraformの設定ファイルを書くのには、ある程度インフラの知識が必要です。module機能を使って、可能な限り入力項目を簡潔にすれば、インフラの知識がない人でもTerraformを使ってインフラの構築作業ができるようになります。例えば、インフラインフラエンジニアがmoduleの作成と管理を行い、アプリケーションエンジニアがmoduleを利用する、というような具合です。moduleを制するものはTerraformを制す、と言っても過言ではないくらい、重要な機能だと思います。

moduleの作り方

moduleの作り方ですが、ドキュメントを読めばなんとなく概要はつかめると思います。ただ、正直色んな書き方ができてしまうので、最初は困ると思います(自分がそうでした…)。実はHashicorpがterraform-community-modulesというのを作っているので、最初はそれを参考にするのがいいかと思います。自分が作ってるterraform moduleもこれを参考にして作っていて、大体このようなファイル構成にしてます。

.
├── CHANGELOG.md
├── README.md
├── init.sh
├── main.tf
├── outputs.tf
├── templates
│   └── node.json.tpl
└── variables.tf

基本的にmoduleはgitリポジトリごとに分けて管理していて、main.tfにメイン処理、variables.tfに変数、outputs.tfにモジュール外で呼び出したいパラメータを記述するようにしています。

では実際に試行錯誤して得られた知見をみていきましょう。

Try 1: moduleの変数に配列を使いたい

module "example" {
  source = "..."
  servers = ["foo", "bar", "buz"]
  ...
  ...

ある時、上の設定のようにmoduleの変数に配列を渡したい時がありました。

試しにこの書き方で使ってみると、エラーが返ってきます。現在の最新バージョンv0.6.8でも、まだサポートされていないようです。moduleの変数に配列は渡せません。

module "example" {
  source = "..."
  servers = "foo,bar,buz"
  ...
  ...

ではどうしたかというと、色々とgithubのissueを探した結果、上の設定のようにカンマを使って文字列として変数を渡し、module内で配列に変換するという方法を見つけました。

module内部の記述は以下のようになります。

resource "openstack_compute_instance_v2" "default" {
    count = "${length(split(",", var.servers))}"
    name = "${element(split(",", var.servers), count.index)}"
    ...
    ...

countパラメータでは、splitで文字列を配列に変換し、lengthで配列のサイズを求めています。nameパラメータではsplitで文字列を配列に変換し、elementで配列の要素を取得しています。正直余り綺麗ではないですが、なんとかやりたいことは実現できました…。

Try 2: クラスタを組む時にAZを分けたい

ある時、サービスの可用性を高めるために異なるAZに分けてインスタンスを構築したい、そんな時がありました。

これは割と知られている方法かもしれませんが、count.indexと余りを計算する%を組み合わせて実現できました。

variables.tfファイルの中で、AZの定義を書き、

variable "availability_zones" {
  default = {
    "0" = "az-foo"
    "1" = "az-bar"
    "2" = "az-buz"
  }
}

main.tfの中で、lookupを使って、availability_zones変数から対応するAZを取得してます。こうすることで、1台目はaz-fooに、2台目はaz-barに、3台目はaz-buzに、4台目はaz-fooに、といった具合になります。

resource "openstack_compute_instance_v2" "default" {
  ...
  availability_zone = "${lookup(var.availability_zones, count.index % 3)}"
  ...
  ...

Try 3: Serverspecと連携させたい

ある時、Terraformで作ったインスタンスをServerspecで確認したい、Serverspecで使うyamlファイルをTerraformで生成したい、という時がありました。

そんな時は、template_file resourceを使って対象インスタンスごとにyamlファイルを生成し、Terraform実行後にServerspecを実行するというようなことで実現できました。以下は、設定例です。

resource "template_file" "example" {
    template = "${file("./templates/os_base.yml.tpl)}"

    vars {
        target = "${openstack_compute_instance_v2.example.access_ip_v4}"
    }

    provisioner "local-exec" {
        command = "echo '${template_file.example.rendered}' > /var/tmp/os_base.yml"
    }
}

local-execでechoする、…なかなか渋い方法になりました。

Try 4: module内の変数をmoduleの外で使いたい

ある時、moduleの中で生成される変数を他のresourceの変数として使いたい、というような時がありました。

普通にmodule内部のresourceパスを書けばできるだろうと思って、色んな書き方を試したんですが、うまくいきません。色々試した結果、moduleの外から呼び出したい変数はmoduleの中でoutput設定を使って、明示的に定義してやらないといけないことに気がつきました。moduleを作る際は、意外とoutput設定が重要です。

output "ipv4_address" {
    value = "${openstack_compute_instance_v2.default.access_ip_v4}"
}

Try 5: moduleはきちんとtagを打つ

moduleを使う際は、バージョンロックさせるためにgitのtagでバージョンを指定するようにしましょう。tagを指定しないとmoduleの入力パラメータが変わってしまった時にエラーになってしまうので、利用者にきちんと説明をした方がいいです。

まとめ

以上、Terraformのmoduleに関する知見を紹介しました。Terraformがリリースされて1年以上になりますが、まだまだノウハウが足りません。かゆい所にもうちょっとで手が届くか届かないかっていう感じなので、tfファイルのグッドプラクティス情報、ぜひ知りたいです。よろしくお願いします。