count vs for_each — 違いと使い分けの完全ガイド

1. 概要

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

  • countfor_eachの根本的な違い(インデックス管理 vs キー管理)
  • countの「中間要素削除で全リソース再作成」問題の仕組み
  • for_eachがこの問題を解決する理由
  • どちらを使うべきかの明確な判断基準
  • countからfor_eachへの移行方法(movedブロック)

結論を先に述べます: 本番環境で複数リソースを作成するときは、原則としてfor_eachを使ってください。countを使うべきケースは「リソースの有無を0/1で制御するだけ」のケースに限定されます。


2. 一言で言うとどう違うか

比較項目countfor_each
管理の仕組みインデックス(0, 1, 2…)文字列キー(”dev”, “web”など)
受け取る値numbermap または 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 で意味のある名前が使える

countcount.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 は同時に使えない

同一リソースブロックにcountfor_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. 関連記事


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