普段の開発でよく使っているTerraformのテンプレ構成をまとめてみました。
そのまま管理
.
├─ ec2.tf
├─ vpc.tf
├─ rds.tf
├─ main.tf
├─ provider.tf
└─ backend.tf
サンドボックスで検証する時や小規模なサービスを管理する際に利用することが多い。
ec2.tfやvpc.tfと分けずに全部main.tfで管理することもある。
開発、検証、本番環境などを管理しようとすると大変なことになるので、そういった場合はmoduleやworkspaceを使用する。
環境毎にtfvarsを作成して管理
.
├─ tfvars
| ├─ dev.tfvars
| ├─ stg.tfvars
| └─ prd.tfvars
├─ vpc.tf
├─ rds.tf
├─ main.tf
├─ provider.tf
├─ backend.tf
├─ variables.tf
└─ shell.sh
そのまま管理と似ているが、tfvarsを環境毎に分けることで複数環境でも管理することができる。
構成がシンプル、環境毎の差分がわかりやすいという点で個人的には好きな構成。
moduleを組み合わせたり、workspaceでstateを分けたりとカスタマイズ性が高い。
terraformへtfvarsを渡すために、以下のような引数を判断して処理できる簡単なシェルを用意することが多い。
if [ $1 = "plan" ]; then
terraform plan -out="$env.tfplan" -var-file=tfvars/$env.tfvars
fi
moduleで管理
.
├─ env
| ├─ dev
| | ├─ network.tf
| | ├─ compute.tf
| | ├─ ...
| | ├─ variables.tf
| | ├─ provider.tf
| | └─ backend.tf
| ├─ stg
| └─ prd
└─ modules
├─ network
| ├─ vpc.tf
| ├─ subnet.tf
| ├─ ...
├─ compute
| ├─ ecs.tf
| ├─ ...
ベンダーなどへ発注する際に何も注文を付けないと大体この構成になる(経験上)。
作る時は楽だが運用するとなると話は別で、
- stateが重たくなる
- ライフサイクルの違うリソースが同じstateで管理される
- プロバイダのバージョンを上げた時に大変なことになる
- 別のリポジトリで新サービスを作ろうとすると、何をoutputするのか考えないといけない
などなど、様々な問題が出てくる。
そのためこの構成で管理するのは中規模まで、かつ他とリソースを共有しないようなサービスになると思う。
moduleで管理+stateを分離
moduleで管理する点は変わらないが、
- ライフサイクルが違う
- 他のサービスとリソースを共有したい
- 間違ってdestroyしたら再構築できないようなリソース
などのリソース毎の特性に応じてstateを分離する構築方法。
この辺りを細かく設計していくことで、安心して運用できるterraformが出来上がる。
例:VPCを共有する場合
参照元
.
├─ env
| ├─ dev
| | ├─ network.tf
| | ├─ ...
| | ├─ variables.tf
| | ├─ provider.tf
| | └─ backend.tf
| ├─ stg
| └─ prd
└─ modules
└─ network
├─ vpc.tf
├─ ...
└─ output.tf
outputの中身
output "vpc" {
value = aws_vpc.main
}
別のサービスから参照する
backend.tf
data "terraform_remote_state" "ref" {
backend = "s3"
config = {
bucket = "ref-vpc-bucket"
key = "env/dev/network/terraform.tfstate"
region = "ap-northeast-1"
}
}
subnet.tf
resource "aws_subnet" "private" {
vpc_id = data.ref.outputs.vpc.id
...
}
terragrunt
前述のmoduleで管理+stateを分離は理想的な構成だが、構築に時間が掛かるという欠点がある。
またリポジトリの分割が必要となり、構成全体が見辛くなるという欠点もある。
そんな問題を解消できる、あったらいいなが詰まっているツールがterragruntになる。
https://terragrunt.gruntwork.io/docs/features/keep-your-terraform-code-dry/
terragruntのメリット
コードが生成できる
terraformの仕様として、バックエンドでは変数が利用できないという特徴がある。
そのため、各環境のbackend.tfに同じようなコードを記述しないといけないのは周知の通り。
しかしterragurntを使えばbackendがコードから生成できるので、以下のようなDRY(Don't Repeat Yourself)な記述が可能となる。
公式より
https://terragrunt.gruntwork.io/docs/features/keep-your-remote-state-configuration-dry/
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "frontend-app/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "my-lock-table"
}
}
これが
generate "backend" {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "my-lock-table"
}
}
EOF
}
こうなって環境毎のbackendが生成できる。
remote stateの依存関係を解決してくれる
remote stateを参照する際にリポジトリが違うと参照するのに手間が掛かるが、terragruntにはdependencyという仕組みがあり、remote stateを意識することなく参照が可能。
terraform {
source = "github.com/<org>/modules.git//app?ref=v0.1.0"
}
dependency "vpc" {
config_path = "../vpc"
}
dependency "mysql" {
config_path = "../mysql"
}
inputs = {
basename = "example-app"
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.subnet_ids
mysql_endpoint = dependency.mysql.outputs.endpoint
}
こちらのdependencyを使用するとremote stateが参照可能となる。
ただしplanの段階では実際に保存されている値までは確認してくれないので注意が必要。
一応、mockという仕組みを使ってplanで擬似的に確認することもできる。
dependency "vpc" {
# This will get overridden by child terragrunt.hcl configs
config_path = ""
mock_outputs = {
attribute = "hello"
old_attribute = "old val"
list_attr = ["hello"]
map_attr = {
foo = "bar"
}
}
mock_outputs_allowed_terraform_commands = ["apply", "plan", "destroy", "output"]
}
構成情報が出力できる
同様のことは terraform graph
でも実現できるが、terragruntの graph-dependencies
コマンドはremote state含めて依存関係をグラフ化することができる。
https://terragrunt.gruntwork.io/docs/reference/cli-options/#graph-dependencies
まとめ
結局は掛けられる工数に応じてテンプレを選択することになるが、運用するということを一番に考えて選択するべきだと思う。
またterragruntも決して最適な選択ではなく、記述方法の自由度が高すぎる、ツールに依存するという点で嫌がる会社が多いといったデメリットもある。