💡 はじめに
この記事では、Terraformを用いたインフラストラクチャのコード化において、for_eachとlocalsを効果的に活用した再利用性が高く、環境ごとの差分管理が容易な構成について解説します。特に、複数のリソースを定義する際のパラメータシートとしてのlocals.tfの使い方と、依存関係を持つリソースの扱い方に焦点を当てます。
📂 ファイル構成
まずはファイル構成です。
以下のように作ることが多いです。
├── ...
└── infra # terraform関連ファイルを格納
├── dev # dev環境用
|── stg # stg環境用
...
├── examples # 検証用/呼び出し側コードの雛形
│ ├── locals.tf
│ ├── main.tf
│ ├── provider.tf
│ ├── terraform.tf
│ ├── terraform.tfvars
│ └── variables.tf
└── modules
├── vm
│ ├── main.tf
│ ├── outputs.tf
│ ├── variables.tf
│ └── versions.tf # providerのversionを指定
├── vnet
│ ├── main.tf
│ ├── outputs.tf
│ ├── variables.tf
│ └── versions.tf # providerのversionを指定
...
-
dev,stg,prdなど作成したい環境ごとに1ディレクトリ用意してstateを管理します。 -
examplesはリソース作成検証用のファイルを作成。このディレクトリ内のファイルが、各環境(devやstg)ディレクトリにコピーされ、リソースを定義する呼び出し側のコードとして機能します。- 場合によっては
examples/の中でディレクトリを分けることもあります。 - 環境ごとの差分がない場合は環境ディレクトリ (dev, stgなど)をコピーしてもよいです。
- 場合によっては
🧩 module内の構成
再利用しやすいようにリソース毎に作成します。
基本的には一般的な構成かと思うため、割愛します。
イメージはこちらです:terraform-aws-vpcのflow-logモジュール
呼び出し側
この記事のメインです。
呼び出し側は「ファイル構成」内のexamples/main.tfなどのファイル群が該当します。これは、各環境ディレクトリ (devやstg)に配置され、実際にリソースをデプロイするために使用されます。
.
├── locals.tf
├── main.tf
├── provider.tf
├── terraform.tf
├── terraform.tfvars
└── variables.tf
provider.tf, terraform.tfはTerraformのバージョンや利用するproviderを記載します。一般的な書き方をすればよいです。
variables.tf, terraform.tfvarsは機密情報を扱います(locals.tfにハードコードしたくない部分を扱います)。
重要なのは**main.tfとlocals.tf**になります。
main.tf: for_eachで複数のリソースを簡潔に定義
main.tfでは、locals.tfで定義したデータ構造をfor_eachでループし、複数のリソース(ここではVM)を一括で定義します。これにより、リソースの追加・変更がlocals.tfのデータ操作だけで完結しやすくなります。
以下moduleはazurerm_linux_virtual_machine関連のモジュールと仮定します。
## 既存のリソースグループをデータソースとして参照(ここではモジュールとして抽象化していると仮定)
module "resource_groups" { ... }
module "availability_sets" { ... }
# VMを複数台定義
module "vms" {
source = "../modules/vm"
# local.vmsリストからvmのnameをkeyにしたmapを作成し、for_eachに指定
# key: vm01, vm02, ...
# value: { name = "vm01", resource_group_name = "..." }
for_each = { for vm in local.vms : vm.name => vm }
# モジュールへの入力パラメータ
name = each.key # nameはkeyから取得
resource_group_name = module.resource_groups[each.value.resource_group_name].name # 依存関係のあるリソース名を指定
location = local.location # 共通のlocationはlocalsから取得
size = each.value.size
zone = try(each.value.zone, null) # 定義がない場合を考慮しtry()を使用
availability_set_id = try(module.availability_sets[each.value.availability_set_name].id, null) # IDで依存関係を指定
...
tags = local.tags
}
重要なポイント
-
for_eachのKeyの選び方:for_each = { for vm in local.vms : vm.name => vm }のように、リソースを一意に識別できるnameをkeyに指定します。名前で重複することはまれ(普通プロジェクト内で同じ名前を使わないため)ですが、もし重複する可能性があれば、nameとresource_group_nameを結合した文字列などをkeyに利用すると確実です。 -
依存関係の解決: 既に作成済みのリソース(例:リソースグループ、アベイラビリティセット)のIDやNameを渡す場合、以下のようにリソースのKeyを用いて参照します。
module.resource_groups[<resource key>].name- これにより、Terraformがリソース間の依存関係を正しく認識します。
-
try()関数による省略可能なパラメータ: 定義として明示的に書かない場合があるときは、try()を使って値の指定がないことに起因するエラーを回避します。これにより、パラメータを省略した場合の挙動をモジュール内で制御できます。
a. モジュール側を直す必要があった場合でもtry()で対応できます。
locals.tf: リソース定義のパラメータシート
locals.tfは、リソース定義のパラメータシートとして機能します。main.tfをfor_eachで記載してあげれば、モジュールに渡したい値のリストをそのまま記載するだけです。
locals {
# 環境全体で共通のタグやリージョンなど
location = "japaneast"
tags = {
Environment = "Dev"
Project = "MyApp"
ManagedBy = "Terraform"
}
# Resource Groupなど定義
...
# VMの定義リスト
vms = [
# vm 1: Webサーバー
{
name = "web-vm01"
resource_group_name = "my-rg-web"
size = "Standard_B2s"
zone = 1 # Zone 1 に配置
availability_set_name = null # ASは使用しない
disk_size = 64
},
# vm 2: DBサーバー
{
name = "db-vm01"
resource_group_name = "my-rg-db"
size = "Standard_E4s_v3"
zone = 2 # Zone 2 に配置
availability_set_name = "my-as-db" # ASを使用
disk_size = 128
},
# vm 3: ...
]
...
}
重要なポイント
-
vmsリストの各{}が1つのVMのパラメータセット(インスタンス定義)に相当します。 -
main.tfでkeyとして利用する値 (nameやresource_group_name) は、依存関係解決のためにIDではなく名前を渡します。 - このファイルだけを見れば、どのリソースがどのような設定で作成されるのかが一目で分かります。環境(
dev,stg)ごとにこのファイルを書き換えるだけで、リソースの差分を管理できます。
🎉 さいごに
**for_eachによるモジュール呼び出しとlocals.tfによるパラメータシートの組み合わせによるTerraformコードはいかがでしょうか。私は最近この書き方にたどり着いて割といいのではと思っています。
どなたかの参考になれば幸いです!最後までお読みいただきありがとうございました。