はじめに
記事を書こうとしたきっかけ
GoogleCloudのリソース作成を業務で行うにあたって、IaCでやるべくTerraform利用を基本としています。
いくつかのアプリケーションインフラ構築のためにこれまでコードを書いてきてみたのですが、毎度毎度後から見返した時に「このコード、イケてない」と悲しい気持ちになることがありました。
何度かのマイナーチェンジを繰り返してみていますが、「ぼくがかんがえたさいきょうのTerraformコード」はまだ作れていません。
それでも、こういう作り方をするとこういうところが良さそう/悪そう、というので知見が増えてきたのでまとめてみることにしました。
インフラ要件例
- GoogleCloud上のプロジェクトに、ComputeInstanceやサービスアカウントを作成。付随するリソースも作る。
- 本番環境と非本番環境、のように複数環境があり、基本的には同一構成を取るものの一部パラメータやリソースに差異がある
- 本番環境だとすべて内部通信にするためVMに外部IPを持たせない
- リソース名に環境名が入る(
prd
,test
など)
試してきたパターン
1. ディレクトリ構成
1.1. [Bad Pattern] ディレクトリを環境ごとに分けて、ファイル複製
src/
└ terraform/
└ prd/
│ └ a-prd.tf
│ └ b-prd.tf
│ └ prd.tfvars
└ test/
└ a-test.tf
└ b-test.tf
└ c-test.tf
└ test.tfvars
※x-prd.tf/x-test.tfなど、環境名を入れたファイル名にしていますが、別ファイルであることを明示したいため書いています(以下同様)。
環境ごとに差異がある場合にはこの構成が拾いやすいですが、ソース複製という無駄が非常に大きいです。全環境に対して修正をかけるときに漏れ無く修正する必要があり、他方で環境ごとでソース差異があった時に「ここって環境ごとで違うのは正しいのか?コピペミスor漏れかも?」という迷いも生じることになりそうです
1.2. [Bad Pattern] 同じディレクトリ下で、tfvarsだけ環境ごとに用意する
src/
└ terraform/
└ a.tf
└ b.tf
└ c.tf
└ prd.tfvars
└ test.tfvars
同じソースを使いながらtfvarsだけで切り替えられる点では、本番系/非本番系で同じように作れることを保証しやすいです。ただ、非本番系では作らないリソースがあるなどリソース差異がある場合もありますので、そこがうまくやりにくいです。
1.3. ディレクトリごとに環境を分けて、共通部分は切り出し
上記2パターンで課題であった「全環境共通部分の管理をまとめたいけど、けど差分がある箇所も上手くやりたい」の解決案としてはやはり、共通部分の外出しということになります。その中でも2パターンやり方がありそうです。
1.3.1 module化
src/
└ terraform/
└ prd/
│ └ a-prd.tf
│ └ prd.tfvars
└ test/
│ └ a-test.tf
│ └ test.tfvars
└ modules/
└ vm-module/
│ └ main.tf
│ └ variables.tf
│ └ output.tf
└ sa-module/
└ main.tf
└ variables.tf
└ output.tf
moduleという仕組みを提供していることからして、この構成が基本的にはベストプラクティスなのだと思います。
ただし、実際にこれでやってみると、案外上手くいかないなと思うところもありました。
- モジュール化の単位が悪いと拡張しにくい
- VM1つに対しては必ずIPは1つ、とか構成が確定していれば問題ないが、後で変更が出た時に対応しづらい。
- モジュール内で依存関係を生まないようにモジュール内で単一リソースしか作らないようにするのでも良いかもしれないが、そうなるとモジュール化してもソース量が多い。
- モジュール呼び出しのための記述が環境ごとで複製されてしまう。VM構築は特に渡すパラメータが多く、これを各環境のモジュール呼び出し側で記述するのはやや手間になる。
1.3.2 ファイルをシンボリックリンクで呼び出し
src/
└ terraform/
└ prd/
│ └ a.tf ※シンボリックリンク
│ └ b.tf ※シンボリックリンク
│ └ prd.tfvars
└ test/
│ └ a.tf ※ シンボリックリンク
│ └ b.tf ※ シンボリックリンク
│ └ c.tf ※ test環境でだけ必要なリソースを記述
│ └ test.tfvars
└ common/
└ a.tf
└ b.tf
基本的にはリソースを共通化しつつも、モジュール呼び出しではなくしています。これは、モジュールの場合の欠点を解消する方法として考えたものです。
ただしこの場合にもデメリットはあります。
例としては、共通化した側が環境差異を意識するように作る必要が生じうることです。
ソース例
VMを作るときに、環境によって外部IPを付与するかどうかを設定する場合
resource "google_compute_address" "ip_external" {
count = (var.env) == "test" ? 1 : 0 // ※環境差異を共通リソース側で意識する必要がある
name = "external-ip-vm"
address_type = "EXTERNAL"
region = var.region
}
resource "google_compute_instance" "default" {
name = var.vm_name
machine_type = var.vm_type
zone = var.var_vm_zone
network_interface {
network = var.network
subnetwork = var.subnet
network_ip = google_compute_address.internal_ip.address
dynamic "access_config" {
for_each = toset(google_compute_address.ip_external)
content {
nat_ip = google_compute_address.ip_external[0].address
}
}
}
}
どの言語で書くソースにしろ、共通化された"モジュール的"な位置づけのものが、呼び出し元側のことを気にするような仕組みなのはイマイチでしょう。
ただ、このようなロジックで書かなければならない箇所が多くないのであれば、許容できないこともないと思っています。
2. ファイル分割方法
ディレクトリ構成として3.1 モジュール化、3.2 シンボリックリンクそれぞれの方法が採択できそうなので、その2パターンでのファイル分割方法を考えてみます。
2.1. [Bad Pattern] リソース種類ごとにファイル分割
例
- computeinstance.tf
- serviceaccount.tf
ソースコードを読む上ではわかりやすそうです。ただ、このやり方だとリソースが増えるたびにファイルが増えます。
特に3.2 シンボリックリンク案の場合にはシンボリックリンクが貼られるファイルも増え、取り扱いがやや面倒です。
2.2. 機能ごとでファイル分割
- vm-hoge.tf
- VMのために1つのSA、1つの内部IPリソースが必要なので、それらもまとめて1ファイルに記述する。
1.3.2シンボリックリンク案の時であっても、ファイル数が増えることはありません。1.3.1モジュール案の場合でも同様に、モジュールを呼び出す側(a-prd.tf, a-test.tf)はこのまとめ方でよさそうです。
併せて言うと、variables.tfも分けずに書いて良いのではと思っています。
たしかに1ファイルの行数は増えますが、リソースに渡す変数定義とリソース定義を分けておくことでソースが見やすくなるわけでもありません。環境が増えた場合でもその1ファイルだけシンボリックリンクを貼ってあげればいい、という点で拡張しやすいのがこの方法のメリットといえます。
個人的に3.2を推したい理由
例えば、「VMを1つ追加したいけどSAは同じものを使う」要件になった時には
- vm.base.tf (VM構築に共通する部分 (= SA)だけを作る)
- vm.hoge.tf (hogeVM固有部分だけ作る)
- vm.fuga.tf (fugaVM固有部分だけ作る)
という形でファイル分割により対応ができます。この時、各リソースはフラットに作っているのでモジュール依存せず、terraform stateを調整するような必要もないのがメリットかと思います。
おわりに
Terraformは決してニッチなサービスでは無いでしょうが、インフラ構築という多種多様なアウトプットが考えられるようなものなのもあり、デザインパターンだったりスタンダードな書き方、みたいなのはまだないのかなと思っています。
いずれそれが出てくることにも期待しつつ、この記事が誰かの助けになれば幸いです。