07. 反復構築(count / for_each / dynamic)
「同じものを 3 つ作る」「環境ごとに違う名前で複数作る」「内部のブロックを集合から動的に生成する」。これらに対応する 3 つのメタ引数があります。役割を間違えると、リファクタ時に大量の作り直しが起きるので慎重に。
どれを使うか早見表
| やりたいこと | 使うもの |
|---|---|
| 同じ設定で N 個 | count |
| 名前付きの集合(map / set)から複数 | for_each |
| resource ブロック内の 入れ子ブロック を集合から生成 | dynamic |
for_each を選ぶ機会が圧倒的に多いです。count は「途中の 1 個を消すと残り全部が再作成される」ため、削除や順序変更に弱い。
count
# count = 3 を付けると、このリソースを 3 個作成
# 各インスタンスは count.index(0, 1, 2)で識別
resource "aws_instance" "worker" {
count = 3
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
tags = {
# ${count.index} で 0, 1, 2 を埋め込み → worker-0, worker-1, worker-2 という名前に
Name = "worker-${count.index}"
}
}
# count を使ったリソースは「配列」として参照(添字 [N] で個別アクセス)
output "first_worker_id" {
value = aws_instance.worker[0].id # 1 番目(インデックス 0)
}
# [*] は splat 式 = 「全要素から .id を取り出して list にする」
output "all_worker_ids" {
value = aws_instance.worker[*].id
}
count = 0 で「条件付き作成」
# count = 三項演算子で 0 or 1 → リソースを「作る/作らない」の分岐
# enable_audit = true なら 1 個作成、false なら作らない(count=0 = リソースなし)
resource "aws_cloudtrail" "audit" {
count = var.enable_audit ? 1 : 0
# ...
}
# 参照は要注意(count=0 のとき aws_cloudtrail.audit[0] は存在しないのでエラー)
# → 三項演算子で「無効時は null」とガードする
output "audit_arn" {
value = var.enable_audit ? aws_cloudtrail.audit[0].arn : null
}
for_each
for_each は map または set(string) を取ります。各インスタンスは each.key / each.value でアクセス。
map から(属性違いで複数)
# 「キーごとに違う設定で複数のインスタンスを作る」設計
# map(object(...)) で「キー = 名前、値 = 設定セット」を表現
variable "instances" {
type = map(object({
ami_id = string
type = string
}))
default = {
api = { ami_id = "ami-aaa", type = "t3.small" }
worker = { ami_id = "ami-bbb", type = "t3.micro" }
cron = { ami_id = "ami-aaa", type = "t3.nano" }
}
}
# for_each に map を渡すと、キーの数だけリソースを作成
# 各ループで each.key(キー)、each.value(値の object)が使える
resource "aws_instance" "app" {
for_each = var.instances
ami = each.value.ami_id # 各 instance の ami_id を参照
instance_type = each.value.type # 各 instance の type を参照
tags = {
Name = each.key # "api", "worker", "cron" がそれぞれ Name タグになる
}
}
# for_each のリソースは map のキーでアクセス(配列添字ではない)
output "api_id" {
value = aws_instance.app["api"].id
}
set(string) から(同設定の名前付き複製)
# for_each には set(string) を渡すこともできる(list を toset() で変換)
# 各要素ごとにリソースを作成(同じ設定で名前だけ変える用途)
resource "aws_iam_user" "team" {
for_each = toset(["alice", "bob", "carol"])
name = each.key # set のときは each.value も each.key と同じ値
}
count vs for_each ─ 何が決定的に違うのか
count は 添字(0, 1, 2...)で state を管理します。途中の worker[1] を消すと、もともと worker[2] だったやつが worker[1] に繰り上がる = 同じ実体に違う添字が割り当たる。Terraform は「これは違うリソースだ」と判定して、無関係な再作成が発生します。
for_each は キー("api"、"worker" など)で state を管理します。"worker" を消しても "api" や "cron" には影響しません。順序ではなく名前で識別 しているからです。
dynamic ブロック
resource の中に書く 入れ子ブロック を、集合から動的に生成する仕組み。たとえば SG の ingress ブロックを変数の数だけ作る:
# 「許可するポート群」を変数で受け取る(list の中に object が複数並ぶ型)
variable "ingress_rules" {
type = list(object({
description = string
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
default = [
{ description = "HTTP", from_port = 80, to_port = 80, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] },
{ description = "HTTPS", from_port = 443, to_port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] },
]
}
resource "aws_security_group" "web" {
name = "web"
vpc_id = aws_vpc.main.id
# dynamic "ingress" = 「ingress という入れ子ブロックを動的に複数個生成」
# for_each で渡した要素の数だけ、content { ... } の中身が展開される
dynamic "ingress" {
for_each = var.ingress_rules
content {
# 各ループでは「ingress.value」で現在の要素の object にアクセスできる
description = ingress.value.description
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
イテレータ名は ブロック名と同じ(上の例では ingress)。明示的に変えたい時は iterator 引数で。
# iterator = rule で、ループ変数の名前を「ingress」から「rule」に変更
# → 入れ子の dynamic で同名衝突を避けたい時などに使う
dynamic "ingress" {
for_each = var.ingress_rules
iterator = rule
content {
from_port = rule.value.from_port # ← ingress.value ではなく rule.value
# ...
}
}
count から for_each への移行
あとから count → for_each に変えると、Terraform は「foo[0] を消して foo["api"] を作る」と判定し、全リソースが再作成される。これを避けるのが moved ブロック:
# moved ブロック: 「state 上の名前を変える」だけの指示
# 実物リソースは destroy / create されず、state 内の参照が書き換わるだけ
# → count → for_each に切り替えるときに「再作成事故」を防ぐ
moved {
from = aws_instance.app[0] # 以前は配列インデックス [0]
to = aws_instance.app["api"] # 今後は map のキー ["api"] で参照
}
moved {
from = aws_instance.app[1]
to = aws_instance.app["worker"]
}
これだけで、destroy/create なしで state のアドレスだけ書き換わります。リファクタ時にめちゃくちゃ便利。