for_each — map/setで動的にリソースを作成するメタ引数

1. 概要

この記事では、以下の内容を解説します。

  • for_eachの基本構文とeach.key/each.valueの使い方
  • setで使う方法(toset()でリストをsetに変換する)
  • mapで使う方法(文字列map・objectのmap)
  • for式でmapを生成してfor_eachに渡すパターン
  • for_eachで作ったリソースの参照方法
  • moduleへのfor_each適用
  • countとの違いとよくあるエラー

for_eachは、同じ構成のリソースを複数作成するための最も重要なメタ引数です。countと違い、各リソースをインデックス(0, 1, 2…)ではなくキー(文字列)で管理するため、中間要素を削除しても他のリソースが再作成されません。本番環境ではcountよりfor_eachを優先することを推奨します。


2. for_eachとは

for_eachはリソース・モジュールブロック内で使えるメタ引数です。mapまたはset(string)を渡すと、その要素数分だけリソースが作成されます。

resource "aws_s3_bucket" "logs" {
  for_each = toset(["access", "error", "audit"])  # 3つのバケットを作成

  bucket = "${each.key}-logs-bucket"  # each.key = "access" / "error" / "audit"
}

countとの最大の違いは管理方式です。

比較項目countfor_each
管理キーインデックス(0, 1, 2…)文字列キー(”dev”, “web”など)
受け取る値numbermap または set(string)
要素の参照aws_instance.web[0]aws_instance.web["web"]
中間要素を削除した場合それ以降すべて再作成削除した要素のみ削除
リソースの識別位置依存(不安定)キー依存(安定)

詳細な比較は「count vs for_each — 違いと使い分けの完全ガイド」を参照してください。


3. 基本構文

resource "<リソースタイプ>" "<リソース名>" {
  for_each = <map または set(string)>

  # ループ変数
  # each.key   → mapのキー / setの要素値
  # each.value → mapの値(setの場合はeach.keyと同じ)

  <引数> = each.key   # または each.value
}

for_eachに渡せる値は以下の2種類です。

each.keyeach.value
set(string)set内の要素(文字列)each.keyと同じ
map(any)mapのキー(文字列)mapのバリュー

4. setで使う

文字列のsetから直接使う

set(string)を直接for_eachに渡せます。

terraform {
  required_version = ">= 1.9"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

# IAMグループをsetから一括作成
resource "aws_iam_group" "teams" {
  for_each = toset(["developers", "designers", "devops"])

  name = each.key  # "developers" / "designers" / "devops"
}

toset()でリストをsetに変換する

for_eachにリスト(list)を直接渡すことはできませんtoset()でset型に変換する必要があります。

# ❌ エラー: for_eachにlistを直接渡すことはできない
# variable "bucket_names" {
#   type    = list(string)
#   default = ["access-logs", "error-logs"]
# }
#
# resource "aws_s3_bucket" "logs" {
#   for_each = var.bucket_names  # Error!
# }

# ✅ toset()でset(string)に変換してから渡す
variable "bucket_names" {
  description = "作成するS3バケット名のリスト"
  type        = list(string)
  default     = ["access-logs", "error-logs", "audit-logs"]
}

resource "aws_s3_bucket" "logs" {
  for_each = toset(var.bucket_names)  # listをsetに変換

  bucket = each.key  # "access-logs" / "error-logs" / "audit-logs"

  tags = {
    Name        = each.key
    Environment = "production"
    ManagedBy   = "terraform"
  }
}

# バケットのバージョニングを個別に設定
resource "aws_s3_bucket_versioning" "logs" {
  for_each = aws_s3_bucket.logs  # 作成済みリソースのmapをそのまま渡せる

  bucket = each.value.id

  versioning_configuration {
    status = "Enabled"
  }
}

⚠️ 注意: toset()はリストから重複を除去し、順序を消失させます。要素の順序に依存するロジックは使えません。詳細は「toset / tolist / tomapの使い方」を参照してください。


5. mapで使う

each.key / each.value の参照

mapfor_eachに渡すと、each.keyにキー、each.valueにバリューが入ります。

# 環境ごとにEC2インスタンスを作成
locals {
  # 環境名 → インスタンスタイプのmap
  environments = {
    dev = "t3.micro"
    stg = "t3.small"
    prd = "t3.medium"
  }
}

# AMIをdata sourceで取得(IDのハードコード禁止)
data "aws_ami" "amazon_linux_2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_instance" "web" {
  for_each = local.environments

  ami           = data.aws_ami.amazon_linux_2023.id
  instance_type = each.value  # "t3.micro" / "t3.small" / "t3.medium"

  tags = {
    Name        = "${each.key}-web"  # "dev-web" / "stg-web" / "prd-web"
    Environment = each.key
    ManagedBy   = "terraform"
  }
}

objectのmapを使う

バリューをobjectにすることで、複数の設定値をまとめて管理できます。

locals {
  # 環境名 → 設定objectのmap
  server_configs = {
    dev = {
      instance_type = "t3.micro"
      volume_size   = 20
      multi_az      = false
    }
    stg = {
      instance_type = "t3.small"
      volume_size   = 50
      multi_az      = false
    }
    prd = {
      instance_type = "t3.medium"
      volume_size   = 100
      multi_az      = true
    }
  }
}

resource "aws_instance" "app" {
  for_each = local.server_configs

  ami           = data.aws_ami.amazon_linux_2023.id
  instance_type = each.value.instance_type  # objectの属性にアクセス

  root_block_device {
    volume_size = each.value.volume_size
    volume_type = "gp3"
    encrypted   = true  # EBSは暗号化を有効にする
  }

  # IAMインスタンスプロファイルを設定(SSM接続用)
  iam_instance_profile = aws_iam_instance_profile.ssm[each.key].name

  tags = {
    Name        = "${each.key}-app"
    Environment = each.key
    ManagedBy   = "terraform"
  }
}

6. for式でmapを生成してfor_eachに渡す

for_eachlistを直接渡せないため、for式でmapに変換してから渡すパターンが実務でよく使われます。

listからmapへの変換パターン

# 例: サブネット設定のリストからリソースを作成
locals {
  # 設定をオブジェクトのリストで定義(可読性が高い)
  subnet_configs = [
    { name = "public-1a",  cidr = "10.0.1.0/24", az = "ap-northeast-1a" },
    { name = "public-1c",  cidr = "10.0.2.0/24", az = "ap-northeast-1c" },
    { name = "private-1a", cidr = "10.0.11.0/24", az = "ap-northeast-1a" },
    { name = "private-1c", cidr = "10.0.12.0/24", az = "ap-northeast-1c" },
  ]

  # for式でlistをmap(name → object)に変換
  # for_eachに渡すためのキーとしてnameを使う
  subnets_map = { for s in local.subnet_configs : s.name => s }
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name      = "main-vpc"
    ManagedBy = "terraform"
  }
}

resource "aws_subnet" "main" {
  for_each = local.subnets_map  # mapを渡す

  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value.cidr  # "10.0.1.0/24" など
  availability_zone = each.value.az

  tags = {
    Name        = each.key  # "public-1a" / "public-1c" など
    Environment = "production"
    ManagedBy   = "terraform"
  }
}

💡 ポイント: for式の詳細は「for式の使い方」を参照してください。listからmapへの変換はfor_eachと組み合わせる最重要パターンです。


7. for_eachで作ったリソースの参照方法

TYPE.NAME["key名"] 形式

for_eachで作ったリソースは、インスタンスマップとして管理されます。特定のキーのリソースを参照するには["key名"]を使います。

# 特定のリソースをキーで参照
resource "aws_route53_record" "app" {
  zone_id = var.hosted_zone_id
  name    = "app.example.com"
  type    = "A"

  alias {
    name                   = aws_lb.env["prd"].dns_name  # "prd"キーのLBを参照
    zone_id                = aws_lb.env["prd"].zone_id
    evaluate_target_health = true
  }
}

values() / keys() で全要素を取得する

# 全インスタンスのIDをリストで取得
output "all_instance_ids" {
  description = "作成したEC2インスタンスのIDリスト"
  value       = values(aws_instance.web)[*].id
  # values()でmapのバリュー(リソースオブジェクト)のリストに変換
  # [*]でsplat式を使いidを取得
}

# 全インスタンスのIPアドレスをmap形式で出力
output "instance_ips" {
  description = "環境名→IPアドレスのmap"
  value       = { for k, v in aws_instance.web : k => v.private_ip }
}

for_eachで作ったリソースを個別削除する

for_eachで作ったリソースのうち1つだけを削除したい場合は、for_eachに渡しているmapやsetから対象のキーを削除してterraform applyを実行します。対象のリソースだけが削除され、他のリソースは影響を受けません。

# before: "dev" / "stg" / "prd" の3つが存在する
locals {
  environments = {
    dev = "t3.micro"
    stg = "t3.small"
    prd = "t3.medium"
  }
}

# after: "stg" を削除 → aws_instance.web["stg"] だけが削除される
locals {
  environments = {
    dev = "t3.micro"
    prd = "t3.medium"
  }
}

⚠️ 注意: terraform apply -target=aws_instance.web["stg"] で削除もできますが、-targetは応急処置用のオプションです。通常はfor_eachの元データを変更してapplyする方法を使ってください。

8. moduleにfor_eachを使う

for_eachはリソースだけでなくmoduleブロックにも使えます(Terraform 0.13以降)。

# モジュールを環境ごとに呼び出す
module "app" {
  for_each = {
    dev = { instance_type = "t3.micro",  min_size = 1, max_size = 2  }
    prd = { instance_type = "t3.medium", min_size = 2, max_size = 10 }
  }

  source = "./modules/app"  # ローカルモジュール

  environment   = each.key
  instance_type = each.value.instance_type
  min_size      = each.value.min_size
  max_size      = each.value.max_size
}

# moduleのoutputを参照
output "app_endpoints" {
  value = { for k, v in module.app : k => v.endpoint }
}

9. countとの違い(なぜfor_eachを優先するか)

countfor_eachの使い分けについては「count vs for_each — 違いと使い分けの完全ガイド」で詳しく解説しています。

一言でまとめると:

  • for_eachを使うべき場面: 複数のリソースをまとめて作成するほとんどのケース
  • countを使うべき場面: リソースの「有無」をtrue/falseで制御するだけのケース(count = var.enabled ? 1 : 0

countでは中間要素を削除するとインデックスがずれ、意図しないリソースの再作成が発生します。for_eachではキーで管理するためこの問題が起きません。詳細は「count — 整数でリソースを複数作成するメタ引数」を参照してください。


⚠️ 注意: countfor_eachを同一リソースブロックに同時に設定することはできません。両方を書くとエラーになります。

10. よくあるエラー

Error: The for_each value depends on resource attributes that cannot be determined until apply

│ Error: Invalid for_each argument
│
│   on main.tf line 10, in resource "aws_subnet" "main":
│   10:   for_each = aws_security_group.dynamic
│
│ The "for_each" value depends on resource attributes that cannot be
│ determined until apply, so Terraform cannot predict how many instances
│ will be created. To work around this, use the -target argument to first
│ apply only the resources that for_each depends on.

原因: for_eachに渡す値が、まだ作成されていないリソースの属性(apply後にしか確定しない値)に依存している。

解決方法1(推奨): for_eachに渡す値を、既知の値(local変数やvariable)から計算するように変更する。

# ❌ 問題: for_eachがapply後に確定する属性に依存
resource "aws_subnet" "main" {
  for_each = aws_security_group.dynamic  # apply前は内容が不明
  ...
}

# ✅ 解決: 既知の値から構築したmapをfor_eachに渡す
locals {
  subnet_map = { for s in var.subnet_configs : s.name => s }
}

resource "aws_subnet" "main" {
  for_each = local.subnet_map  # 既知の値なのでOK
  ...
}

解決方法2(応急処置): -targetオプションで依存元リソースを先にapplyする。

# 先に依存元リソースだけapply
terraform apply -target=aws_security_group.dynamic
# その後、全体をapply
terraform apply

Error: for_each requires a map or set of strings

│ Error: Invalid for_each argument
│
│   on main.tf line 3, in resource "aws_s3_bucket" "example":
│    3:   for_each = var.bucket_names
│
│ The given "for_each" argument value is unsuitable: the "for_each" argument
│ must be a map, or set of strings, and you have provided a value of type
│ list of string.

原因: for_eachlist型の値を直接渡している。

解決方法: toset()でset型に変換する。

# ❌ エラー: listを直接渡している
resource "aws_s3_bucket" "example" {
  for_each = var.bucket_names  # list型はNG
}

# ✅ toset()でset型に変換
resource "aws_s3_bucket" "example" {
  for_each = toset(var.bucket_names)  # OK
}

Error: Duplicate object key

│ Error: Invalid for_each argument
│
│ Two different items produced the key "web", which is not allowed.

原因: for式でmapを生成する際に、同じキーが複数生成された。

解決方法: キーが一意になるようにデータを修正するか、キー生成ロジックを変更する。

# ❌ エラー: キーが重複する可能性がある
locals {
  bad_map = { for item in var.items : item.type => item }
  # typeが同じ要素が2つあるとエラー
}

# ✅ 一意のキーを使う(idなど)
locals {
  good_map = { for item in var.items : item.id => item }
  # idは一意なのでOK
}

11. 関連記事


12. まとめ

  • for_eachmapまたはset(string)を受け取り、各要素に対してリソースを1つ作成する
  • ループ変数はeach.key(キー)とeach.value(バリュー)
  • リストを渡したいときはtoset()でset型に変換する
  • objectのmapを使うと、複数の設定値をまとめて管理できる
  • for式でlistをmapに変換してからfor_eachに渡すのが実務の定番パターン
  • 作成されたリソースはaws_instance.web["key名"]の形式で参照する
  • for_eachmoduleにも使える(Terraform 0.13以降)
  • 本番環境ではcountよりfor_eachを優先することを強く推奨する

動作確認バージョン: Terraform >= 1.9 / AWS Provider ~> 5.0 対象リージョン: ap-northeast-1(東京) 公式ドキュメント: https://developer.hashicorp.com/terraform/language/meta-arguments/for_each