1. 概要
この記事では、以下の内容を解説します。
countとfor_eachの根本的な違い(インデックス管理 vs キー管理)countの「中間要素削除で全リソース再作成」問題の仕組みfor_eachがこの問題を解決する理由- どちらを使うべきかの明確な判断基準
countからfor_eachへの移行方法(movedブロック)
結論を先に述べます: 本番環境で複数リソースを作成するときは、原則としてfor_eachを使ってください。countを使うべきケースは「リソースの有無を0/1で制御するだけ」のケースに限定されます。
2. 一言で言うとどう違うか
| 比較項目 | count | for_each |
|---|---|---|
| 管理の仕組み | インデックス(0, 1, 2…) | 文字列キー(”dev”, “web”など) |
| 受け取る値 | number | map または set(string) |
| ループ変数 | count.index(整数) | each.key / each.value |
| リソース参照 | aws_instance.web[0] | aws_instance.web["web"] |
| 中間要素を削除した場合 | ❌ 以降すべて再作成 | ✅ 削除した要素のみ削除 |
| 意味のある識別子 | ❌ 数字のみ | ✅ 文字列キーで識別 |
| リストを直接使える | ✅(length()で件数指定) | ❌(toset()変換が必要) |
| オブジェクトを渡せる | ❌ | ✅(mapのバリューに) |
3. countの仕組みと問題点
インデックスで管理するリスク
countはリソースを配列のインデックス(0から始まる連番)で管理します。
# countで3つのEC2インスタンスを作成
variable "server_names" {
type = list(string)
default = ["web", "app", "db"]
}
resource "aws_instance" "servers" {
count = length(var.server_names) # → 3
ami = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.micro"
tags = {
Name = var.server_names[count.index] # 0→"web" / 1→"app" / 2→"db"
ManagedBy = "terraform"
}
}
Terraformは内部で以下のように管理します。
aws_instance.servers[0] → "web"
aws_instance.servers[1] → "app"
aws_instance.servers[2] → "db"
中間要素を削除すると全部再作成される理由
ここがcountの最大の問題点です。"app"(インデックス1)を削除しようとすると何が起きるか見てみます。
# "app"を削除してみる
variable "server_names" {
type = list(string)
default = ["web", "db"] # "app"を削除
}
Terraformは「インデックス0は”web”、インデックス1は”db”」と解釈します。しかし以前のStateは「インデックス1は”app”、インデックス2は”db”」だったため、次のような計画になります。
# 期待: "app"だけ削除
# 実際のplan出力:
~ aws_instance.servers[1] # "app"→"db"に変更(置き換え)
- aws_instance.servers[2] # "db"を削除
“web”以外のすべてのリソースが影響を受けます。 インデックスがずれることで、削除していないはずの"db"まで一度削除されて作り直される危険があります。
countが安全に使えるケース
中間要素の削除が発生しない、「リソースの有無だけを制御する」ケースではcountが有効です。
# ✅ 0/1でリソースの存在を制御するだけ → countで良い
variable "enable_bastion" {
description = "踏み台サーバーを作成するか"
type = bool
default = false
}
resource "aws_instance" "bastion" {
count = var.enable_bastion ? 1 : 0 # 0か1しかとらない
ami = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.micro"
tags = {
Name = "bastion"
ManagedBy = "terraform"
}
}
4. for_eachの仕組みとメリット
キーで管理するため途中削除が安全
for_eachはリソースを文字列キーで管理します。インデックスのように順番に依存しません。
# for_eachで同じ3つのEC2インスタンスを作成
resource "aws_instance" "servers" {
for_each = toset(["web", "app", "db"])
ami = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.micro"
tags = {
Name = each.key # "web" / "app" / "db"
ManagedBy = "terraform"
}
}
Terraformの内部管理:
aws_instance.servers["web"] → "web"
aws_instance.servers["app"] → "app"
aws_instance.servers["db"] → "db"
ここで"app"を削除すると:
resource "aws_instance" "servers" {
for_each = toset(["web", "db"]) # "app"を削除
...
}
planの出力は期待通りになります。
- aws_instance.servers["app"] # "app"だけ削除 ✅
aws_instance.servers["web"] # 変更なし ✅
aws_instance.servers["db"] # 変更なし ✅
キーで管理しているため、他のリソースはまったく影響を受けません。
each.key / each.value で意味のある名前が使える
countのcount.index(0, 1, 2…)と異なり、each.keyは意味のある文字列です。タグやリソース名に使うと、コードの可読性が大幅に上がります。
# ✅ for_each: キーから意味のある名前・タグを生成できる
locals {
environments = {
dev = { instance_type = "t3.micro", volume_size = 20 }
stg = { instance_type = "t3.small", volume_size = 50 }
prd = { instance_type = "t3.medium", volume_size = 100 }
}
}
resource "aws_instance" "app" {
for_each = local.environments
ami = data.aws_ami.amazon_linux_2023.id
instance_type = each.value.instance_type
root_block_device {
volume_size = each.value.volume_size
volume_type = "gp3"
encrypted = true
}
tags = {
Name = "${each.key}-app" # "dev-app" / "stg-app" / "prd-app"
Environment = each.key # 環境名がそのままタグになる
ManagedBy = "terraform"
}
}
countのもう一つのデメリット: splat式でしか全要素を取得できない
countで作ったリソースの全IDを取得するにはaws_instance.servers[*].id(splat式)しかなく、キーに意味がありません。for_eachなら{ for k, v in aws_instance.servers : k => v.id }でキー付きのmapが得られます。
5. どちらを使うべきか(判断基準)
判断フローチャート
複数のリソースを作りたい
│
▼
「リソースを作るか作らないか(0/1)だけ制御したい?」
│
Yes ─┤ No
│ │
▼ ▼
count = 0 or 1 を使う 「要素の追加・削除が起きる?」
(条件式との組み合わせ) │
Yes ─┤ No
│ │
▼ ▼
for_each for_each
(必須) (推奨)
基本ルール: for_each を使う
複数リソースを作成するケースでは、原則for_eachを選びます。
for_eachを選ぶ理由:
- 要素の削除・順序変更が安全
each.keyで意味のある識別子が使えるmapを使って複数の設定値をまとめて管理できる- 実務でのトラブルが少ない
countで良い場合: リソースの0/1制御だけ
countが適切なのは、次のような「このリソースを作るか作らないか」だけを制御するケースです。
# ✅ count が適切なケース1: 本番環境のみCloudWatchアラームを作成
resource "aws_cloudwatch_metric_alarm" "cpu_high" {
count = var.environment == "prd" ? 1 : 0
alarm_name = "prd-cpu-high"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 300
statistic = "Average"
threshold = 80
tags = {
Environment = var.environment
ManagedBy = "terraform"
}
}
# ✅ count が適切なケース2: オプション機能のON/OFF
resource "aws_wafv2_web_acl_association" "main" {
count = var.enable_waf ? 1 : 0
resource_arn = aws_lb.main.arn
web_acl_arn = aws_wafv2_web_acl.main[0].arn
}
⚠️ 注意:
countで作ったリソースを後からfor_eachに変えようとすると、Stateファイル上でリソースアドレスが変わり、一度削除して再作成されます。移行にはmovedブロックが必要です(後述)。
count と for_each は同時に使えない
同一リソースブロックにcountとfor_eachを同時に設定することはできません。コンパイル時エラーになります。どちらか一方のみ使用してください。
# ❌ エラー: 同一ブロックにcountとfor_eachは書けない
resource "aws_instance" "web" {
count = 3
for_each = toset(["a", "b", "c"]) # Error!
...
}
6. countからfor_eachへの移行方法
既存のcountリソースをfor_eachに変更すると、リソースアドレスが[0]から["key"]に変わります。そのままapplyすると削除→再作成が発生します。movedブロックを使うことでリソースを維持したまま移行できます(Terraform 1.1以降)。
# 移行前のコード(count使用)
resource "aws_instance" "web" {
count = 3
# ...
}
# Stateのアドレス: aws_instance.web[0], [1], [2]
# 移行後のコード(for_each使用)
resource "aws_instance" "web" {
for_each = toset(["web-1", "web-2", "web-3"])
# ...
}
# Stateのアドレス: aws_instance.web["web-1"], ["web-2"], ["web-3"]
# ↑ このままapplyすると[0][1][2]が削除されて["web-1"]["web-2"]["web-3"]が作成される
# moved ブロックで「[0]は["web-1"]に移動した」と宣言することで再作成を防ぐ
moved {
from = aws_instance.web[0]
to = aws_instance.web["web-1"]
}
moved {
from = aws_instance.web[1]
to = aws_instance.web["web-2"]
}
moved {
from = aws_instance.web[2]
to = aws_instance.web["web-3"]
}
movedブロックを追加した状態でterraform planを実行すると、削除・再作成なしに移行できることが確認できます。移行完了後、movedブロックは削除しても構いません(Stateに変更が反映されるため)。
7. 関連記事
- count — 整数でリソースを複数作成するメタ引数
- for_each — map/setで動的にリソースを作成するメタ引数
- for式の使い方 — リスト・mapの変換・フィルタリング
- toset / tolist / tomap — コレクション型を変換する関数
- state管理とは — tfstateファイルの役割とリスク
- locals vs variable — 違いと使い分けの完全ガイド
- Terraformよくあるエラー集
8. まとめ
countはインデックス(0, 1, 2…)で管理し、for_eachはキー(文字列)で管理するcountでは中間要素を削除するとインデックスがずれ、意図しないリソースの再作成が発生するfor_eachはキー管理のため、削除した要素のみが削除され他のリソースに影響しない- 原則は
for_eachを使う。countは「0/1の存在制御のみ」のケースに限定する - 既存の
countリソースをfor_eachに移行するにはmovedブロックを使う(再作成を防ぐため)
動作確認バージョン: Terraform >= 1.9 / AWS Provider ~> 5.0 対象リージョン: ap-northeast-1(東京) 公式ドキュメント: https://developer.hashicorp.com/terraform/language/meta-arguments/for_each / https://developer.hashicorp.com/terraform/language/meta-arguments/count