★★ 中級

07. 反復構築(count / for_each / dynamic)

「同じものを 3 つ作る」「環境ごとに違う名前で複数作る」「内部のブロックを集合から動的に生成する」。これらに対応する 3 つのメタ引数があります。役割を間違えると、リファクタ時に大量の作り直しが起きるので慎重に。

どれを使うか早見表

やりたいこと使うもの
同じ設定で N 個count
名前付きの集合(map / set)から複数for_each
resource ブロック内の 入れ子ブロック を集合から生成dynamic
迷ったら for_each 実務では 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_eachmap または 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
    # ...
  }
}
使いすぎ注意 公式が「dynamic は便利だが、入れ子の dynamic を多用すると 読めなくなる」と警告しています。引数の数が決まっているなら素直に並べるほうが、レビューもデバッグも楽です。

count から for_each への移行

あとから countfor_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 のアドレスだけ書き換わります。リファクタ時にめちゃくちゃ便利。