terraformで複数リソースを作りたい時、countとfor_eachどちらを使うか

2021/12/06に公開

terraformで複数リソースを作成する時、countかfor_eachを使うと思いますが、
両者を適切に使い分けるためにそれぞれの挙動をまとめてみました。

ソースコードはこちら

目次

  1. countの場合
  2. for_eachの場合
  3. まとめ

1. countの場合

countを使って複数リソースを作る

countの挙動を確かめるために下記の様にサブネットを作成します。

resource "aws_vpc" "this" {
  cidr_block  = var.cidr_block
}

resource "aws_subnet" "public" {
  count = length(var.public_blocks)

  vpc_id            = aws_vpc.this.id
  cidr_block        = element(var.public_blocks, count.index)
  availability_zone = element(var.availability_zones, count.index)
}
locals {
  region = "ap-northeast-1"
}

module "count" {
  source = "./count"

  cidr_block         = "10.0.0.0/16"
  public_blocks      = ["10.0.1.0/24", "10.0.2.0/24"]
  availability_zones = ["${local.region}a", "${local.region}c"]
}

countは作成したいリソースの数を指定することで複数リソース作成出来ます。
作成したいリソースの数を指定するだけなので for_each よりもシンプルに記述出来ます。

作成した複数リソースをoutputしたい時

output "subnet_ids" {
  value = aws_subnet.public.*.id
}

countで作ったリソースを参照したい時、 * を使えば list で取得出来ます。

subnet_ids = [
  "subnet-01eb09ab26904e369",
  "subnet-06c6128ce474cd3eb",
]

ただ、 ap-northeast-1a のサブネットを参照したい場合、次の様に

module.count.subnet_ids[0]

と配列の番号を指定することになってしまいこれでは何のリソースを参照しているのか
分かりにくいですね。 後述しますが for_each だとこの問題を解消することが出来ます。

リソースを削除したい時

途中でAZが ap-northeast-1a のサブネットを削除したくなったとします。

module "count" {
  source = "./count"

  cidr_block         = "10.0.0.0/16"
  public_blocks      = ["10.0.2.0/24"]
  availability_zones = ["${local.region}c"]
}

この様に書き換えてplanを実行してみると・・・

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # module.count.aws_subnet.public[0] must be replaced
-/+ resource "aws_subnet" "public" {
      ~ arn                             = "arn:aws:ec2:ap-northeast-1:76345784206:subnet/subnet-06f995d8d74733a4b" -> (known after apply)
      ~ availability_zone               = "ap-northeast-1a" -> "ap-northeast-1c" # forces replacement
      ~ availability_zone_id            = "apne1-az4" -> (known after apply)
      ~ cidr_block                      = "10.0.1.0/24" -> "10.0.2.0/24" # forces replacement
      ~ id                              = "subnet-06f995d8d74733a4b" -> (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)
      - map_customer_owned_ip_on_launch = false -> null
      ~ owner_id                        = "762742784206" -> (known after apply)
      - tags                            = {} -> null
      ~ tags_all                        = {} -> (known after apply)
        # (3 unchanged attributes hidden)
    }

  # module.count.aws_subnet.public[1] will be destroyed
  - resource "aws_subnet" "public" {
      - arn                             = "arn:aws:ec2:ap-northeast-1:76274484206:subnet/subnet-0bd46cfab1a7d126e" -> null
      - assign_ipv6_address_on_creation = false -> null
      - availability_zone               = "ap-northeast-1c" -> null
      - availability_zone_id            = "apne1-az1" -> null
      - cidr_block                      = "10.0.2.0/24" -> null
      - id                              = "subnet-0bd46cfab1a7d126e" -> null
      - map_customer_owned_ip_on_launch = false -> null
      - map_public_ip_on_launch         = false -> null
      - owner_id                        = "762742784206" -> null
      - tags                            = {} -> null
      - tags_all                        = {} -> null
      - vpc_id                          = "vpc-00e075e18377e14b8" -> null
    }

Plan: 1 to add, 0 to change, 2 to destroy.

この様に module.count.aws_subnet.public[0] をreplaceする挙動となります。
こんな時、出来れば ap-northeast-1c のサブネットを残したまま、 ap-northeast-1a
サブネットを削除したいと思いますが、残念ながら出来ません。

ただstateを直接修正することで、既存のサブネットを残したまま修正することも可能です。

まず、stateを確認しましょう。

❯ terraform state list  
module.count.aws_subnet.public[0]
module.count.aws_subnet.public[1]
module.count.aws_vpc.this

次に module.count.aws_subnet.public[0]1 以外の適当な番号を割り当てます。

terraform state mv module.count.aws_subnet.public[0] module.count.aws_subnet.public[10]

次に module.count.aws_subnet.public[1]module.count.aws_subnet.public[0]
変更します。

terraform state mv module.count.aws_subnet.public[1] module.count.aws_subnet.public[0]

この状態でplanを実行すると...

Terraform will perform the following actions:

  # module.count.aws_subnet.public[10] will be destroyed
  - resource "aws_subnet" "public" {
      - arn                             = "arn:aws:ec2:ap-northeast-1:762742784206:subnet/subnet-0850badcdfdf2d6b9" -> null
      - assign_ipv6_address_on_creation = false -> null
      - availability_zone               = "ap-northeast-1a" -> null
      - availability_zone_id            = "apne1-az4" -> null
      - cidr_block                      = "10.0.1.0/24" -> null
      - id                              = "subnet-0850badcdfdf2d6b9" -> null
      - map_customer_owned_ip_on_launch = false -> null
      - map_public_ip_on_launch         = false -> null
      - owner_id                        = "762742784206" -> null
      - tags                            = {} -> null
      - tags_all                        = {} -> null
      - vpc_id                          = "vpc-079d79a4be16828a8" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

1 to destroy となってますね!
これで ap-northeast-1a のサブネットのみ削除することが出来ました。

2. for_eachを使う場合

for_eachで複数リソースを作成する

countと同じくfor_eachの挙動を確認したいので、リソースを作ってみます。

resource "aws_vpc" "this" {
  cidr_block = var.cidr_block
}

resource "aws_subnet" "public" {
  for_each = var.public_subnets

  vpc_id            = aws_vpc.this.id
  cidr_block        = each.value.cidr_block
  availability_zone = each.key
}
locals {
  public_subnets = {
    ap-northeast-1a = {
      cidr_block = "10.0.64.0/24"
    }
    ap-northeast-1c = {
      cidr_block = "10.0.65.0/24"
    }
  }
}

module "for_each" {
  source = "./for_each"

  cidr_block     = "10.0.0.0/16"
  public_subnets = local.public_subnets
}

for_eachには mapset を渡します。 each.value each.key
それぞれのkeyとvalueを参照することが出来ます。

作成したリソースをoutputしたい時

output "public_1a_id" {
  value = aws_subnet.public["ap-northeast-1a"].id
}

output "subnet_ids" {
  value = [ for value in aws_subnet.public : value.id ]
}

count で作成した時と違い list で取得したい時はfor文を使う所に注意してください。
また、個々のリソースを参照したい場合、上記の様に aws_subnet.public["ap-northeast-1a"]
とkeyで参照するので count で作成した時と比べて個々のリソースを参照しやすいですね。

リソースを削除したい時

locals {
  public_subnets = {
    ap-northeast-1c = {
      cidr_block = "10.0.65.0/24"
    }
  }
}

ap-northeast-1a のサブネットを削除して plan を実行してみます。

Terraform will perform the following actions:

  # module.for_each.aws_subnet.public["ap-northeast-1a"] will be destroyed
  - resource "aws_subnet" "public" {
      - arn                             = "arn:aws:ec2:ap-northeast-1:76276724206:subnet/subnet-0f966e9cd41aaf87c" -> null
      - assign_ipv6_address_on_creation = false -> null
      - availability_zone               = "ap-northeast-1a" -> null
      - availability_zone_id            = "apne1-az4" -> null
      - cidr_block                      = "10.0.64.0/24" -> null
      - id                              = "subnet-0f966e9cd41aaf87c" -> null
      - map_customer_owned_ip_on_launch = false -> null
      - map_public_ip_on_launch         = false -> null
      - owner_id                        = "762742784206" -> null
      - tags                            = {} -> null
      - tags_all                        = {} -> null
      - vpc_id                          = "vpc-06767e6aec20a67ca" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

countと違い、stateを修正することなく特定のリソースを削除することが出来ます。

3. まとめ

対象のリソースを他のリソースを作成する時に参照する必要がなく、単純に複数個作成したい
という場合は count を使い、サブネットの様に個々のリソースを参照する必要がある場合は
for_each と使い分けるのが良いと思っています。

参考資料

https://www.terraform.io/docs/language/meta-arguments/count.html
https://www.terraform.io/docs/language/meta-arguments/for_each.html

GitHubで編集を提案

Discussion