07. 反復構築(count / for_each / dynamic)
「同じものを 3 つ作る」「環境ごとに違う名前で複数作る」「内部のブロックを集合から動的に生成する」。これらに対応する 3 つのメタ引数があります。役割を間違えると、リファクタ時に大量の作り直しが起きるので慎重に。
どれを使うか早見表
| やりたいこと | 使うもの |
|---|---|
| 同じ設定で N 個 | count |
| 名前付きの集合(map / set)から複数 | for_each |
| resource ブロック内の 入れ子ブロック を集合から生成 | dynamic |
for_each を選ぶ機会が圧倒的に多いです。count は「途中の 1 個を消すと残り全部が再作成される」ため、削除や順序変更に弱い。
count
resource "aws_instance" "worker" {
count = 3
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
tags = {
Name = "worker-${count.index}" # worker-0, worker-1, worker-2
}
}
# 参照は配列っぽく
output "first_worker_id" {
value = aws_instance.worker[0].id
}
output "all_worker_ids" {
value = aws_instance.worker[*].id # splat 式
}
count = 0 で「条件付き作成」
resource "aws_cloudtrail" "audit" {
count = var.enable_audit ? 1 : 0
# ...
}
# 参照は要注意(count=0 のとき [0] は無効)
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 から(属性違いで複数)
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" }
}
}
resource "aws_instance" "app" {
for_each = var.instances
ami = each.value.ami_id
instance_type = each.value.type
tags = {
Name = each.key # "api", "worker", "cron"
}
}
# 参照は map のキーで
output "api_id" {
value = aws_instance.app["api"].id
}
set(string) から(同設定の名前付き複製)
resource "aws_iam_user" "team" {
for_each = toset(["alice", "bob", "carol"])
name = each.key # set の場合 each.value も同じ
}
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 ブロックを変数の数だけ作る:
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" {
for_each = var.ingress_rules
content {
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 引数で。
dynamic "ingress" {
for_each = var.ingress_rules
iterator = rule
content {
from_port = rule.value.from_port
# ...
}
}
count から for_each への移行
あとから count → for_each に変えると、Terraform は「foo[0] を消して foo["api"] を作る」と判定し、全リソースが再作成される。これを避けるのが moved ブロック:
moved {
from = aws_instance.app[0]
to = aws_instance.app["api"]
}
moved {
from = aws_instance.app[1]
to = aws_instance.app["worker"]
}
これだけで、destroy/create なしで state のアドレスだけ書き換わります。リファクタ時にめちゃくちゃ便利。