📚

Terraformでリソース、設定の出し分け

2023/04/16に公開

Overview

Terraformでは複数のリソースを作成したり、環境によってデプロイするリソースや設定を出し分けしたいケースがよくあると思いますが、使い方を忘れることが多いのでまとめました。

countとfor_each

同じリソースを複数作成するとき、countとfor_eachのどちらもループを回してリソースを作成することができます。

countの場合
locals {
  availability_zones        = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
  vpc_cidr_block            = "10.1.0.0/16"
  public_subnet_cidr_blocks = ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
}

resource "aws_vpc" "vpc" {
  cidr_block = local.vpc_cidr_block
}

resource "aws_subnet" "public" {
  count             = length(local.availability_zones)
  vpc_id            = aws_vpc.vpc.id
  availability_zone = local.availability_zones[count.index] # count.indexで値を参照
  cidr_block        = local.public_subnet_cidr_blocks[count.index]
}
for_eachの場合
locals {
  vpc_cidr_block            = "10.1.0.0/16"
  subnet_cidr_blocks = {
    ap-northeast-1a = {
      public = "10.1.1.0/24"
    }
    ap-northeast-1c = {
      public = "10.1.2.0/24"
    }
    ap-northeast-1d = {
      public = "10.1.3.0/24"
    }
  }
}

resource "aws_vpc" "vpc" {
  cidr_block = local.vpc_cidr_block
}

resource "aws_subnet" "public" {
  for_each          = local.subnet_cidr_blocks # mapでない場合は、tosetでsetに変換が必要
  vpc_id            = aws_vpc.vpc.id
  availability_zone = each.key # each.keyでループ要素のkeyを参照
  cidr_block        = each.value.public # each.valueでループ要素のvalueを取得
}

使い分け

基本的には、for_eachを使うべきだと思います。
両者の違いとして、countは配列、for_eachはmapでstate上、リソースが管理されます。そのため、リソース作成後に途中の要素を削除しようとすると配列の途中の番号が飛ぶことになり、詰め直しが行われるで、リソースの再作成が発生してしまいます。これは好ましくない動作を及ぼす場合や、他リソースから参照されていて失敗する可能性があります。

countの場合
% terraform state list | grep aws_subnet
aws_subnet.public[0]
aws_subnet.public[1]
aws_subnet.public[2]
for_eachの場合
% terraform state list | grep aws_subnet
aws_subnet.public["ap-northeast-1a"]
aws_subnet.public["ap-northeast-1c"]
aws_subnet.public["ap-northeast-1d"]

実際に途中の要素を消そうとすると以下のようなり、変更を加えたくないリソースに対して再作成が発生してしまいます。

countの場合
locals {
-  availability_zones        = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
+  availability_zones        = ["ap-northeast-1a", "ap-northeast-1d"]
   vpc_cidr_block            = "10.1.0.0/16"
-  public_subnet_cidr_blocks = ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
+  public_subnet_cidr_blocks = ["10.1.1.0/24", "10.1.3.0/24"]
}
% terraform plan

  # aws_subnet.public[1] must be replaced
-/+ resource "aws_subnet" "public" {
      ~ availability_zone                              = "ap-northeast-1c" -> "ap-northeast-1d" # forces replacement
      ~ cidr_block                                     = "10.1.2.0/24" -> "10.1.3.0/24" # forces replacement
    }

  # aws_subnet.public[2] will be destroyed
  
Plan: 1 to add, 0 to change, 2 to destroy.

一方、for_eachの場合は、削除したいリソースが削除されるのみです。

for_eachの場合
   subnet_cidr_blocks = {
     ap-northeast-1a = {
       public = "10.1.1.0/24"
     }
-    ap-northeast-1c = {
-      public = "10.1.2.0/24"
-    }
     ap-northeast-1d = {
       public = "10.1.3.0/24"
     }
   }
% terraform plan

  # aws_subnet.public["ap-northeast-1c"] will be destroyed
  # (because key ["ap-northeast-1c"] is not in for_each map)
  
Plan: 0 to add, 0 to change, 1 to destroy.

例外

ただし、countで複数のリソースを作成しておらず、リソース自体の出し分けをしている場合、indexは0にしかならないため、countを利用する形が良いかと思います。

resource "aws_vpc" "vpc" {
  count      = var.environment == "production" ? 1 : 0
  cidr_block = local.vpc_cidr_block
}

Tips

  • for_eachでループする要素にmapではなく、listを渡したい場合、tosetでsetに変換をしたものを渡すことになります(キーが重複することは許容されないため)。この場合、each.keyでもeach.valueでも同じ値が取得されます。
  • for_eachはネストして使うこともできますが、可読性が落ちるので、あまりネストしすぎないことを推奨します。

for_eachとdynamic、nullの利用

環境等によって設定するパラメータを変えたい場合がありますが、何らかの値を渡すのではなく、設定自体を行いたくない場合はfor_eachとdynamicブロックの組み合わせ、またはnullを利用することで実現できます。

resource "resource_type" "resource_name" {
  argument = "argument_value"
  nested_argument_block {
    nested_argument = "nested_argument_value"
  }
}

例えば、上記のようなリソースが存在する場合、以下のようにすることで引数自体をセットするorしないを制御することができます。

resource "resource_type" "resource_name" {
  argument = var.flag ? "argument_value" : null
  dynamic "nested_argument_block" {
    for_each = var.flag ? [1] : []
    content {
      nested_argument = "nested_argument_value"
    }
  }
}

参考

https://developer.hashicorp.com/terraform/language/meta-arguments/count
https://developer.hashicorp.com/terraform/language/meta-arguments/for_each

Discussion