countとfor_each — インデックスかキーか
一言でいうと
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に移行させ、破棄なしでアドレスだけを変えることを、プランで証明します。