TT Lab
はじめる
学ぶ 学習パス コース

Terraform実戦

countとfor_each — インデックスかキーか

TT Labで続きを見る

一言でいうと

countとfor_eachの違いは、「いくつ作るか」ではなく、各インスタンスにどんなラベルを付けるかです。そして、そのラベルは、状態ファイルにそのまま刻まれます。

なぜ必要なのか

サーバーを3台作るために、count = 3を使います。うまく動作します。数か月後、リストが変数に変わり、count = length(var.servers)になります。ここまでも問題ありません。事故は、真ん中の項目を1つ削除するときに起きます。

countで作成したインスタンスのアドレスは、[0]、[1]、[2]です。リストから2つ目の項目を削除すると、3つ目が2つ目の位置に繰り上がります。ツールが見ている世界では、「[1]の内容が変わり、[2]がなくなった」ことになります。そのため、プランには、削除しようとした1台ではなく、2台の再作成が表示されます。実際のクラウドだったなら、問題なく動いていたサーバーが破棄され、作り直されます。

for_eachは、この問題を名前で解決します。アドレスが["web-a"]、["web-b"]のように意味のあるキーになるため、リストから1つ除くと、そのキー1つだけがなくなります。残りは、びくともしません。

どう動くのか

項目 count for_each
受け取る値 数値 セット(set)またはマップ(map)
インスタンスのアドレス res[0] res["kr"]
内部で使う参照 count.index each.key、each.value
途中の項目の削除 後ろがずれて再作成 該当のキーだけ削除
向いている場面 個数だけに意味があるとき、オン・オフのスイッチ 各項目に固有の識別性があるとき

実務の基準は単純です。各インスタンスが、互いに区別できる名前を持てるならfor_each、単に個数だけに意味があるならcountです。countを使い続けてよい代表的な場面は、機能スイッチです。count = var.enable_debug ? 1 : 0は、「あるかないか」を表現する慣用句で、インスタンスが1つだけなので、ずれる問題がありません。

リストをfor_eachにそのまま渡すことはできません。リストには順序があり、for_eachは順序のないキーを要求するため、toset(...)に変換する必要があります。このとき、キーは要素の値そのものになります。マップを渡すと、キーはマップのキー、each.valueはその値になります。こちらは、各項目に複数の属性を付けやすいので、実務でより多く使われます。

for式は、また別のものです。for_eachがリソースを複数作るメタ引数なら、forは、値1つを別の形に変換する式です。{ for k, v in var.services : k => v.port }のように、マップを再びマップに畳み込むのに使います。出力やローカル値を整えるときに、ほぼ必ず使うことになります。

現場での姿

1つ目に、移行は必ずmovedで行います。すでにcountで適用されたリソースをfor_eachに変えると、ツールは、古いアドレスが消えて新しいアドレスができたと判断します。つまり、すべてを破棄して、すべてを作り直します。movedブロックで「res[0]はres["n0"]になった」と伝えれば、破棄なしで、状態アドレスだけが移ります。プランに破棄が1件でも見えたら、移行ではなく再作成が起きているというサインです。

2つ目に、移す前に、現在のアドレスを残します。移行作業の最初のステップは、コードの修正ではなく、記録です。state listの結果と状態のコピーを、先にファイルとして残す必要があります。移行が終わったあとで、「漏れたリソースはない」と言うには、移す前の一覧と、移したあとの一覧を並べて、個数と対応関係を確認する必要がありますが、前者を残していなければ、確認する方法自体がありません。事故は、たいてい20個のうち19個だけを移したときに起き、残った1つは、次のapplyが黙って破棄します。

3つ目に、キーを計算しないでください。for_eachのキーが、ほかのリソースの属性のように、applyの時点でようやく決まる値だと、プランの段階でインスタンスの一覧を把握できず、エラーになります。キーは、変数や定数のように、プランの時点で確定する値である必要があります。

4つ目に、キー名は変えにくいです。一度決めたキーは、事実上リソースの識別子になるため、あとでaz-aをap-northeast-2aに直すことも、移行作業になります。このときも答えはmovedです。そのため、最初にキーを決めるときに、「この名前は2年後にも通用するか」をもう一度考えるほうが、安く済みます。

アドレスが変わるとリソースが置き換えられる

countとfor_eachの本当の違いは、文法ではなく、状態に書き込まれるアドレスです。 そのアドレスが変わると、Terraformは別のリソースとみなして、削除して作り直します。

count:    aws_instance.web[0]      aws_instance.web[1]      aws_instance.web[2]
for_each: aws_instance.web["a"]    aws_instance.web["b"]    aws_instance.web["c"]

リストの真ん中のbを削除するとします。countでは、cがインデックス1の位置に繰り上がるため、 インデックス1と2の両方が置き換えの対象になります。for_eachでは、["b"]だけがなくなります。 データベースやディスクがそのリストにあったなら、この違いが、事故と普通のデプロイを分けます。

for_eachのキーは、プランの時点でわかっている必要があります。ほかのリソースの出力のように、 (known after apply)である値をキーに使うと、「The for_each value depends on resource attributes that cannot be determined until apply」というエラーになります。値はあとで 決まってもよいですが、キーは今の時点で決まっている必要があります。

# 이름을 열쇠로 삼는다 — 만들기 전에 이미 안다
resource "aws_instance" "web" {
  for_each      = { for s in var.servers : s.name => s }
  instance_type = each.value.type
  subnet_id     = aws_subnet.app[each.value.az].id
}

すでにcountで作ったものを移すときは、movedを使います。そうしないと、すべてを 削除して作り直します。

moved {
  from = aws_instance.web[0]
  to   = aws_instance.web["a"]
}

countを使ってよい場面もあります。作るかどうかを決めるオン・オフのように、個数が 0または1の場合です。

resource "aws_nat_gateway" "this" {
  count = var.enable_nat ? 1 : 0
}

このとき参照はaws_nat_gateway.this[0].idになり、オフのときはその参照がエラーに なるため、one(aws_nat_gateway.this[*].id)かtryで包みます。

次のラボですること

countで3つ作り、数値のインデックスが状態に刻まれることを確認して、移す前のアドレスの一覧をファイルに残します。続いて、tosetとマップで、for_eachを2通りの方法で使ってみます。for式で出力マップを作り、条件付きcountで機能スイッチを表現します。最後に、movedブロックでcountをfor_eachに移行させ、破棄なしでアドレスだけを変えることを、プランで証明します。