1. ホーム
  2. 記事一覧
  3. Terraformの基礎 countとfor_eachの基礎を学び繰り返し処理を効率化しよう

2023.09.25

Terraformの基礎 countとfor_eachの基礎を学び繰り返し処理を効率化しよう

こちらの記事では、Terraformの繰り返し処理の基本であるcount、for_eachの構文の基礎と使い方について解説します。この構文を使うことによって、Terraformで複数のリソースを作成する際のソースコードの冗長化を防ぐことができます。

繰り返し処理の重要性

Terraformで複数のリソースを作成する際、繰り返しの構文を使用しないと以下のようになります。

前提条件として、VPCは作成済みです。

resource "aws_subnet" "subnet_1" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
  tags = {
    Name = "subnet-1"
  }
}

resource "aws_subnet" "subnet_2" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.2.0/24"
  tags = {
    Name = "subnet-2"
  }
}

resource "aws_subnet" "subnet_3" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.3.0/24"
  tags = {
    Name = "subnet-3"
  }
}

3つのサブネットを作成しようとした場合、リソースの数だけコードを記述することになります。これを10個、20個と記述していくことを想像すると、ものすごく冗長なコードになることが分かります。

Terraformの繰り返し処理の構文であるcountやfor_eachを使うことで、この状況を避けることができます。結果として、コードの冗長化を防ぐとともに、可読性の向上にもつながります。

Terraformとは

そもそもTerraformとは?という方へ概要を説明します。

Terraformは、HashiCorp社によって開発されたオープンソースのIaCツールです。Terraformを使うことで、従来の手動でのインフラ管理や設定作業を自動化し、コードベースでの管理が可能になります。Terraformを使用することで、インフラの設定変更やデプロイを迅速かつ確実に行うことができます。

クラウド環境でのリソース管理において、Terraformは多くのクラウドプロバイダとの互換性を持っています。具体的にはAWS、GCP、Azureなど主要なクラウドサービスに対応しており、インフラ構築を一元的にコードで管理することができます。

IaCのメリットなどについては以下のリンクで解説しています。

https://envader.plus/article/136

Terraformを実際に触ってみたい!という方へ向けて、環境構築、EC2インスタンスの作成方法について以下の記事で解説しています。

https://envader.plus/article/162

countによる繰り返し処理

countはモジュールや全てのリソースブロックで使用することができる、Meta-Argumentと呼ばれる特別な引数のうちの一つです。

countを使用することで、一つのリソースブロックで複数のリソースを管理することができます。

countの基本的な使い方

基本的な使い方は以下です。

resource "aws_instance" "server" {
  count = 4 # 作成したいリソースの数

  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"

  tags = {
    Name = "Server ${count.index}" # 0から始まるindexを取得することができる
  }
}

Terraform公式ドキュメントより引用: Basic Syntax

count作成したいリソースの数値を入力し、count.index作成したリソースのindex番号を取得できます。

countの注意点

countで指定できるのは、整数のみです。リソース作成後に生成されるIDなどを直接参照することはできません。

# これはエラーになります
resource "aws_instance" "server" {
  count = aws_other_resource.example.id
...
}

IDを指定する場合には、length関数などを使って指定する必要がありました。

variable "subnet_ids" {
  type = list(string)
}

resource "aws_instance" "server" {
  # length関数を使用してサブネットのIDを参照している
  count = length(var.subnet_ids)

  ami           = "ami-xxxxxxxxx"
  instance_type = "t2.micro"
  subnet_id     = var.subnet_ids[count.index]

  tags = {
    Name = "Server ${count.index}"
  }
}

Terraform公式ドキュメントより引用: When to Use for_each Instead of count

また、countではリソースの削除などによりindexが変わってしまうと、参照するIDなどが変わる可能性があります。

# subnet-002が削除された場合、subnet-003のインスタンスIDも変わってしまう。
variable "subnets" {
  description = "List of subnets"
  default = ["subnet-001", "subnet-002", "subnet-003"]
}

resource "aws_instance" "example" {
  count      = length(var.subnets)
  subnet_id  = var.subnets[count.index]
  // ... other configuration ...
}

リソースの削除などによって参照先が変わってしまう可能性があるリソースに対しては、後述するfor_eachを使用することを検討しましょう。

countを使ったリソースの作成

countを使用して、3つのサブネットを作成する例を紹介します。

resource "aws_subnet" "subnet" {
  count = 3 # 作成したいリソースの数

  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.${count.index + 1}.0/24" # indexは0から始まるため、1を足す
  tags = {
    Name = "subnet-${count.index + 1}"
  }
}

冒頭で紹介したソースコードを再度掲載します。

resource "aws_subnet" "subnet_1" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
  tags = {
    Name = "subnet-1"
  }
}

resource "aws_subnet" "subnet_2" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.2.0/24"
  tags = {
    Name = "subnet-2"
  }
}

resource "aws_subnet" "subnet_3" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.3.0/24"
  tags = {
    Name = "subnet-3"
  }
}

今回の例は簡単な例ですが、21行あったコードが8行まで削減できているのが分かります。countを使用することで、ソースコードの冗長化と可読性を向上させることができます。

for_eachによる繰り返し処理

for_eachはTerraformのバージョン0.13で追加された機能です。count同様、Meta-Argumentと呼ばれる特別な引数のうちの一つです。for_eachもモジュールや全てのリソースタイプで使用することができます。

The for_each Meta-Argument

for_eachの基本的な使い方

for_eachへ渡す値で使用できるのは、Terraform がサポートするコレクション型(mapまたはset)です。以下にmapを使用した簡単な例を示します。

# 3人のユーザーを変数で定義
variable "user_map" {
  default = {
    alice = "admin"
    bob   = "guest"
    carol = "editor"
  }
}

resource "aws_iam_user" "example" {
  for_each = var.user_map

  name = each.key
  tags = {
    Role = each.value
  }
}

この例では、aws_iam_userリソースを作成する際にfor_eachを使用しています。var.user_map変数は3つのユーザー(alice、bob、carol)とそれぞれのロール(admin、guest、editor)を定義しています。

each.keyでは、var.user_mapの各キー(alice、bob、carol)を指定しています。

each.valueでは、var.user_mapの各値(admin、guest、editor)を指定しています。

このようにeach.keyeach.valueを使用することで、動的に値を設定できます。

for_eachを使ったリソースの作成

countと同じように、サブネットを3つ作成する例です。

resource "aws_subnet" "subnet" {
  for_each = {
# mapの値を渡している
# each.key = subnet_1、subnet_2、subnet_3
# each.valueでmapの値を取得している
    subnet_1 = {
      cidr_block        = "10.0.1.0/24"
      availability_zone = "ap-northeast-1a"
    },
    subnet_2 = {
      cidr_block        = "10.0.2.0/24"
      availability_zone = "ap-northeast-1c"
    },
    subnet_3 = {
      cidr_block        = "10.0.3.0/24"
      availability_zone = "ap-northeast-1a"
    }
  }

  cidr_block        = each.value.cidr_block
  availability_zone = each.value.availability_zone
  vpc_id            = aws_vpc.main.id

  tags = {
    Name = each.key
  }
}

mapの値を変数化すると、for_eachの部分をもう少しスッキリ書くこともできます。

variable "subnets" {
  default = {
    subnet_1 = {
      cidr_block        = "10.0.1.0/24"
      availability_zone = "ap-northeast-1a"
    },
    subnet_2 = {
      cidr_block        = "10.0.2.0/24"
      availability_zone = "ap-northeast-1c"
    },
    subnet_3 = {
      cidr_block        = "10.0.3.0/24"
      availability_zone = "ap-northeast-1a"
    }
  }
}

resource "aws_subnet" "subnet" {
# for_eachで変数を参照し、一つのkeyごとにループしながら値を設定していくことができる。
  for_each = var.subnets

  cidr_block        = each.value.cidr_block
  availability_zone = each.value.availability_zone
  vpc_id            = aws_vpc.main.id

  tags = {
    Name = each.key
  }
}

このコードでは、for_eachに変数subnetsを指定しています。each.value で変数内で定義した内部マップを指定し、each.value.~ でその内部マップの値を参照しています

for_each = var.subnets の部分で var.subnets の各エントリ(subnet_1, subnet_2subnet_3 など)が順番に処理されます。それぞれの繰り返し処理で、each.key は外側のマップのキー( など)を、each.value はそのキーに対応する値(各サブネットの設定マップ)を参照します。

そして、each.value.cidr_blockeach.value.availability_zone のようにして、その内部マップの「値」を取得しています。

for_eachの注意点

for_eachを使う際に理解しておくべきポイントを公式ドキュメントを元にまとめました。

for_eachに渡す値はmapかsetである必要がある。

for_eachで使用する値はmapかsetでなければいけません。この値を使用することによって、for_eachでリソースを作成する際にそれぞれのリソースが重複して作成されることを防ぐことができます。

mapやsetなどTerraformの型については、以下の記事で解説しています。

https://envader.plus/article/190

setの値を渡す場合は、each.keyもeach.valueも同じ値になる。

setの値はmapのようにkey=value形式ではないため、each.keyeach.valueどちらを指定しても同じ値を参照します。

locals {
  subnet_ids = toset([
    "subnet-abcdef",
    "subnet-012345",
  ])
}

resource "aws_instance" "server" {
  for_each = local.subnet_ids

  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
  subnet_id     = each.key # each.valueを指定しても同じ値を参照する

  tags = {
    Name = "Server ${each.key}"
  }
}

Terraform公式ドキュメントより引用: Using Sets

plan実行時に判断できない値は使用できない

random_petなど動的に生成されるリソースタイプをfor_eachで使用することはできません。このリソースはランダムな「ペットネーム」(通常は人間が読みやすい形式の文字列)を生成しますが、planを実行する時点では値が確定していません。

# 悪い例
locals {
  users = tomap({
    name = random_pet.name.id
    role = "admin"
  })
}

# randomな値を生成。
resource "random_pet" "name" {
  length = 2
}

resource "aws_iam_user" "example" {
	# local.usersにはランダムな値が含まれるため、エラーになる。
  for_each = local.users

  name = each.key
  tags = {
    Role = each.value
  }
}

このように、plan実行時には値がわからないものを参照している場合、for_eachではエラーになります。

uuidやtimestampなど毎回異なる結果を返す関数は使用できない

uuid、timestamp、bcryptなど同じ引数に対して毎回異なる結果を返す関数を使用した場合もエラーになります。これは、関数を実行するタイミングで生成される値が毎回異なる可能性があるためです。

# 悪い例
locals {
  resources = {
		# timestamp関数を使ってローカル変数のmapにキーを生成している
    "${timestamp()}" = "value1"
  }
}

resource "aws_s3_bucket" "my_buckets" {
	# timestamp関数はplanやapply実行時に結果が変わる可能性があるため、エラーになる
  for_each = local.resources
	
  bucket = "${each.key}-bucket"
 }

パスワードなどの機密性の高い値は使用できない

for_eachではsensitive=trueとしている値を使用することができません。

Terraformでは機密性の高い情報を使用する場合、sensitive属性をtrueにする場面があります。こうすることにより、planapplyを実行する際に値が表示されるのを防ぐことができます。

この値をfor_eachで指定するとエラーが発生します。

# 悪い例
variable "sensitive_values" {
  type = map(string)
	# sensitiveの値をtrueにしている
  sensitive = true
  default = {
    password1 = "supersecret"
    password2 = "evenmoresecret"
  }
}

resource "aws_s3_bucket" "my_buckets" {
	# sensitive=trueの値を指定しているためエラーになる
  for_each = var.sensitive_values

  bucket = "${each.key}-bucket"
 
  tags = {
    SensitiveValue = each.value
  }
}

まとめ

こちらの記事では、Terraformのループ処理であるcount、for_eachについて解説しました。

count、for_eachを使用することでソースコードの冗長化を防ぐことができ、可読性も向上させることができます。

シンプルなリストや数値に基づいてリソースを生成する場合にはcountが適しています。注意点として、countでは作成したリソースのIDなどを参照するには向いていないため、使用する場面は検討する必要があります。

for_eachはmapやsetを使用し、繰り返しリソースを作成する場面に適しています。

countとfor_eachを同時には使用できないため、ループ処理を行いたい場合には両者の適切な使い分けが必要になってくるでしょう。

【番外編】USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

IT未経験者必見 USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

プログラミング塾に半年通えば、一人前になれると思っているあなた。それ、勘違いですよ。「なぜ間違いなの?」「正しい勉強法とは何なの?」ITを学び始める全ての人に知って欲しい。そう思って書きました。是非読んでみてください。

「フリーランスエンジニア」

近年やっと世間に浸透した言葉だ。ひと昔まえ、終身雇用は当たり前で、大企業に就職することは一種のステータスだった。しかし、そんな時代も終わり「優秀な人材は転職する」ことが当たり前の時代となる。フリーランスエンジニアに高価値が付く現在、ネットを見ると「未経験でも年収400万以上」などと書いてある。これに釣られて、多くの人がフリーランスになろうとITの世界に入ってきている。私もその中の1人だ。数年前、USBも知らない状態からITの世界に没入し、そこから約2年間、毎日勉学を行なった。他人の何十倍も努力した。そして、企業研修やIT塾で数多くの受講生の指導経験も得た。そこで私は、伸びるエンジニアとそうでないエンジニアをたくさん見てきた。そして、稼げるエンジニア、稼げないエンジニアを見てきた。

「成功する人とそうでない人の違いは何か?」

私が出した答えは、「量産型エンジニアか否か」である。今のエンジニア市場には、量産型エンジニアが溢れている!!ここでの量産型エンジニアの定義は以下の通りである。

比較的簡単に学習可能なWebフレームワーク(WordPress, Rails)やPython等の知識はあるが、ITの基本概念を理解していないため、単調な作業しかこなすことができないエンジニアのこと。

多くの人がフリーランスエンジニアを目指す時代に中途半端な知識や技術力でこの世界に飛び込むと返って過酷な労働条件で働くことになる。そこで、エンジニアを目指すあなたがどう学習していくべきかを私の経験を交えて書こうと思った。続きはこちらから、、、、

note記事3000いいね超えの殿堂記事 今すぐ読む

エンベーダー編集部

エンベーダーは、ITスクールRareTECHのインフラ学習教材として誕生しました。 「遊びながらインフラエンジニアへ」をコンセプトに、インフラへの学習ハードルを下げるツールとして運営されています。

RareTECH 無料体験授業開催中! オンラインにて実施中! Top10%のエンジニアになる秘訣を伝授します! RareTECH講師への質疑応答可

関連記事