Terraform 基礎 #6 依存関係と繰り返し — depends_on、count と for_each、dynamic ブロック

読了 5分

リソースが 2、3 個のうちは考える必要のなかった 2 つの疑問が、インフラが大きくなると現れます。互いに依存するリソースはどの順序で作られるのか、そして、ほぼ同じリソース 10 個を 10 回コピーして書かなければならないのか、という疑問です。今回は Terraform が順序を決める仕組みと、繰り返しをなくす 3 つの文法である count、for_each、dynamic ブロックを扱います。特に count には、実務でリソースを消し飛ばすことで有名な落とし穴があるので、その動作を #5 で学んだ state とつなげて理解することが今回の中心です。

参照がそのまま依存関係です #

#2 では、バージョニングのリソースが aws_s3_bucket.hello.id を参照していました。この参照は値を取得する文法であると同時に、順序の宣言でもあります。バケットが存在して初めてその id が分かるので、Terraform はバケットを先に作り、バージョニングを後に作ります。コード全体の参照を集めると有向グラフができ、Terraform はこのグラフに沿って実行しつつ、依存関係のないリソースは並列で作ります。 順序を指定する専用の文法がなくても、値を参照でつなぐ良い習慣だけで順序の問題が自然に解決される構造です。

depends_on — 参照のない依存関係の例外処理 #

まれに、参照はないのに順序は必要という場合があります。典型例が IAM 権限です。EC2 インスタンスが起動スクリプトで S3 にアクセスするのに、その権限を与えるポリシーアタッチメントのリソースとインスタンスの間に何の参照もないとしましょう。Terraform は 2 つを並列で作るので、インスタンスが先に起動して、権限のないまま S3 アクセスを試みて失敗する可能性があります。こういうときに明示的に順序を強制します。

main.tf
resource "aws_instance" "web" {
  ami           = data.aws_ami.al2023.id
  instance_type = "t3.micro"

  depends_on = [aws_iam_role_policy_attachment.web_s3]
}

注意すべきなのは、depends_on が最後の手段だという点です。参照で表現できる依存関係を depends_on で書くとグラフがコードから見えなくなり、不要な depends_on は並列性を減らして apply を遅くするだけです。参照が可能なら参照で、本当に不可能なときだけ depends_on で書きます。

count — 個数で複製する #

同じリソースを複数作るときの最初の道具が count です。

main.tf
resource "aws_instance" "web" {
  count = 3

  ami           = data.aws_ami.al2023.id
  instance_type = "t3.micro"

  tags = { Name = "web-${count.index}" }
}

リソースアドレスが aws_instance.web[0][1][2] に増え、ブロックの中では count.index で自分の番号を使えます。#4 の条件式と組み合わせた条件付き作成も count の役目です。

main.tf
count = var.env == "prod" ? 1 : 0

本番環境にだけ作るリソースをこの 1 行で表現するのは、広く使われるイディオムです。

count の落とし穴 — 番号がずれると再作成されます #

count で作ったリソースの識別子は、state に番号で記録されます。ここに落とし穴があります。リストをもとにサブネットを 3 つ作ったとしましょう。

main.tf
variable "azs" {
  default = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
}

resource "aws_subnet" "public" {
  count             = length(var.azs)
  availability_zone = var.azs[count.index]
  # ...
}

ここでリストの途中の 2b を外すとどうなるでしょうか。2c がインデックス 2 から 1 にずれます。Terraform から見ると、[1] は 2b だったのに今度は 2c にならなければいけないので置き換えの対象で、[2] は消えたので削除の対象です。意図は「2b を 1 つ削除」だったのに、plan は 2c まで消して作り直すと提案してきます。そのサブネットの上で何かが動いていたら事故です。原因は、リソースの識別子が内容ではなく順番に結び付いていることです。

for_each — キーで複製する #

この落とし穴への答えが for_each です。番号の代わりに、マップのキーまたはセットの値が識別子になります。

main.tf
variable "azs" {
  default = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
}

resource "aws_subnet" "public" {
  for_each          = toset(var.azs)
  availability_zone = each.value
  # ...
}

アドレスが aws_subnet.public["ap-northeast-2a"] のようにキーで記録されるので、2b を外すと正確に ["ap-northeast-2b"] だけが削除の対象になり、残りには触れません。マップを渡せば each.keyeach.value を一緒に使えるので、名前と設定を対にするときに便利です。リストはそのまま渡せず、toset() で包む必要があるという点だけ覚えておけば大丈夫です。

使い分けの基準を整理するとこうなります。完全に同一のものを N 個作る、または条件付き作成(0 個か 1 個)なら count、それぞれ名前や設定が違う集合なら for_each です。実務のコードでは、for_each のほうがはるかに頻繁に正解になります。

dynamic ブロック — ネストしたブロックの繰り返し #

count と for_each はリソース自体を複製します。ところが、繰り返しがリソースの中のネストしたブロックで必要になることがあります。セキュリティグループの ingress ルールが代表例です。

main.tf
variable "service_ports" {
  default = [80, 443]
}

resource "aws_security_group" "web" {
  name = "web-sg"

  dynamic "ingress" {
    for_each = var.service_ports
    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }
}

dynamic "ブロック名" の中に for_each と content を書くと、content が繰り返しの回数だけ展開されて ingress ブロックになります。繰り返し変数の名前が each ではなくブロック名(ここでは ingress.value)である点に注意します。便利ですが乱用するとコードが読みにくくなるので、ルールが 2、3 個で固定ならブロックを 2 回書いたほうが明確な場合も多いです。

まとめ #

今回扱った内容です。

  • 参照がそのまま依存関係です。Terraform は参照グラフで順序を決め、依存のないリソースは並列で作ります
  • depends_on は参照のない順序依存のための最後の手段で、乱用するとグラフが不明瞭になり apply が遅くなります
  • count は番号で複製します。条件 ? 1 : 0 の条件付き作成イディオムは有用ですが、リストの途中削除で番号がずれ、意図しない再作成が起きます
  • for_each はキーで複製するので、途中削除に対して安全です。名前を持つ集合なら for_each、同一の複製と条件付き作成なら count です
  • ネストしたブロックの繰り返しは dynamic ブロックで解決し、繰り返し変数の名前がブロック名と同じである点に注意します

次回(#7 リソースのライフサイクル制御)では、#2 で出会った置き換え(replace)を制御する方法を扱います。無停止の置き換えを実現する create_before_destroy、本番リソースを守る prevent_destroy、ドリフトを許容する ignore_changes です。

X