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との最大の違いは管理方式です。
| 比較項目 | count | for_each |
|---|---|---|
| 管理キー | インデックス(0, 1, 2…) | 文字列キー(”dev”, “web”など) |
| 受け取る値 | number | map または 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.key | each.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 の参照
mapをfor_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_eachにlistを直接渡せないため、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を優先するか)
countとfor_eachの使い分けについては「count vs for_each — 違いと使い分けの完全ガイド」で詳しく解説しています。
一言でまとめると:
for_eachを使うべき場面: 複数のリソースをまとめて作成するほとんどのケースcountを使うべき場面: リソースの「有無」をtrue/falseで制御するだけのケース(count = var.enabled ? 1 : 0)
countでは中間要素を削除するとインデックスがずれ、意図しないリソースの再作成が発生します。for_eachではキーで管理するためこの問題が起きません。詳細は「count — 整数でリソースを複数作成するメタ引数」を参照してください。
⚠️ 注意:
countとfor_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_eachにlist型の値を直接渡している。
解決方法: 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. 関連記事
- count — 整数でリソースを複数作成するメタ引数
- count vs for_each — 違いと使い分けの完全ガイド
- for式の使い方 — リスト・mapの変換・フィルタリング
- dynamicブロックの使い方 — 繰り返しネストブロックを動的に生成する
- toset / tolist / tomap — コレクション型を変換する関数
12. まとめ
for_eachはmapまたはset(string)を受け取り、各要素に対してリソースを1つ作成する- ループ変数は
each.key(キー)とeach.value(バリュー) - リストを渡したいときは
toset()でset型に変換する - objectのmapを使うと、複数の設定値をまとめて管理できる
for式でlistをmapに変換してからfor_eachに渡すのが実務の定番パターン- 作成されたリソースは
aws_instance.web["key名"]の形式で参照する for_eachはmoduleにも使える(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