Terraform 依存関係エラーの総整理 — Cycle、depends_on の誤用、known after apply

読了 5分

Terraform はリソースの参照から依存グラフを作り、その順序で実行します。このグラフから出てくる問題は 3 つの系統に整理できます。グラフが循環して順序を決められない Error: Cycle、依存を人が過剰に宣言して生まれる depends_on の乱用、そしてグラフはできたのに値が計算前で個数を確定できない known after apply の問題です。 3 つともエラーメッセージは違いますが根は同じなので、1 つの記事にまとめて扱います。依存と繰り返しの基本文法は基礎 #6 で扱いました。

Error: Cycle — グラフが循環するとき #

エラーメッセージに Cycle という単語とともにリソースの一覧が出たら、その一覧が互いを参照する輪になっているという意味です。Terraform は循環したグラフの実行順序を決められないため、plan の段階で止まります。

実務の代表例はセキュリティグループの相互参照です。Web SG のインバウンドルールがアプリ SG を参照し、アプリ SG のアウトバウンドルールが Web SG を参照する構成をインラインルール(ingress・egress ブロック)で書くと、2 つのリソースが互いを先に必要とする循環になります。解決策はルールを別のリソースに分離することです。SG 2 つをルールなしで先に作り、aws_vpc_security_group_ingress_rule のような分離リソースが両方の SG を参照するようにすれば、グラフが SG → ルールの一方向になって循環が消えます。SG のチェーンを最初から分離リソースで設計する理由は実践 #3 EC2 とセキュリティグループで扱ったとおりで、Cycle エラーはその設計原則を守らなかったときにぶつかる結果の側です。

モジュール単位の循環も同じ構造です。モジュール A の入力がモジュール B の出力を、B の入力が A の出力を受けると、モジュール全体が輪になります。この場合の解決策はルール分離のモジュール版です。両側が互いに必要とする値を第三の先行リソース(または上位スタック)に引き上げ、A と B の両方がそれを参照するようにします。診断の難しい大きなグラフは terraform graph | dot -Tsvg > graph.svg で描いてみると輪が視覚的に現れますが、大部分はエラーメッセージのサイクル一覧を上から下に読みながら「誰が誰をなぜ参照しているか」をたどれば十分です。

depends_on — 基本は暗黙の依存、宣言は例外です #

Terraform の依存関係は参照から自動的に生まれます。subnet_id = aws_subnet.main.id と書いた瞬間、サブネット → インスタンスの順序は確定し、別の宣言は要りません。depends_on は参照で表現されない隠れた依存にだけ使う例外の道具です。 典型的な正当な用途は、IAM ポリシーのアタッチが終わった後にその権限を使うリソースを作る必要がある場合のように、データの流れはないが時間の順序はある関係です。

問題は「念のため」に付ける習慣です。乱用の副作用は 2 つあります。1 つ目は、グラフが必要以上に直列化されて並列実行の幅が減り、plan・apply が遅くなることです。2 つ目はもっと静かな副作用で、depends_on の対象の変更がそれを宣言したリソースの再計算を誘発し、変えていないリソースに差分や再作成の連鎖が現れることがあります。 特にモジュールに掛けた depends_on はモジュール内のすべてのリソースにその依存を強制するため、波及が大きいです。レビューで depends_on に出会ったら「この依存は参照で表現できないか」を先に問い、参照に変えられるなら変えるのが整理の方向です。

known after apply — 個数は plan の時点で確定している必要があります #

plan 出力の (known after apply) はエラーではありません。新しく作るリソースの ID のように、apply が終わって初めて決まる値の正常な表記です。問題になるのは、この値が count や for_each のキーに入るときです。Terraform は plan の時点でリソースが何個かを確定する必要があり、個数を決める値が計算前だと「Invalid count argument」や「Invalid for_each argument」のエラーで止まります。

典型例は、新しく作るリソースの属性の一覧で for_each を回すコードです。解決策はキーを計算前の値から静的な値に変えることです。作るサブネットの ID(計算後の値)ではなく、コードにすでに書かれている AZ 名・ラベルのような入力値(静的な値)をキーにすれば、個数は plan の時点で確定し、計算後の値は各項目の属性の側でだけ使われます。基礎 #6 で for_each のキーを安定した文字列に取れとした原則と同じ話です。

構造を変えられない場合の回避策は 2 段階の apply です。先行リソースを先に apply して値を確定させた後、残りを apply する方式ですが、これは -target と同じく部分適用状態を作る一時的な手段であって、設計ではありません。2 段階が常時必要になるなら、そこをスタック分割の境界の合図として読むのが正しいです。

まとめ #

  • 3 つの問題の根は 1 つです。Terraform は参照からグラフを作り、グラフは循環してはならず、個数は plan の時点で確定している必要があります。
  • Error: Cycle の代表は SG インラインルールの相互参照です。ルールを分離リソースに出せばグラフが一方向になります。モジュールの循環は共有する値を上位に引き上げて断ちます。
  • depends_on は参照で表現できない隠れた依存の専用です。乱用すると実行が直列化され、無関係なリソースに差分・再作成の連鎖が広がります。
  • known after apply の値が count・for_each のキーに入るとエラーです。キーを静的な入力値に変えるのが解決策で、2 段階 apply は一時的な手段です。
  • 診断の道具はエラーメッセージのサイクル一覧と terraform graph です。回避策が常時必要になる箇所は、スタック分割の合図として読みます。
X