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

Terraform/OpenTofu基礎

依存グラフと状態ファイルの中身

TT Labで続きを見る

一言でいうと

Terraformは、ファイルに書かれた順序ではなく、参照が作るグラフの順序で動きます。そのグラフは、状態ファイルのdependenciesに化石のように残り、コードからリソースが消えたあとでも、削除の順序を教えてくれます。

なぜ必要なのか

シェルスクリプトでは、順序を人が決めます。リソースが5個のときは、上から下へ読めばよいのですが、30個になると、新しいリソースを差し込むたびに、「これはどれの後ろに来るべきか」を、毎回人が判断し直さなければなりません。しかも、互いに無関係なリソースも1列に並べて作ることになるので、作るのにかかる時間がそのまま合算されます。

宣言的なツールは、この問題をひっくり返します。人は関係だけを書き、順序は書きません。Aを作るときにBの値を使えば、それ自体が「Bが先」という宣言です。ツールは、この関係を集めて有向グラフを作り、トポロジカルソートして、互いに無関係なものは同時に処理します。削除するときは、同じグラフを逆にたどります。

どう動くのか

依存関係を作る方法は2つあります。

方式 どうやってできるか いつ使うか
暗黙の依存関係 他のリソースの属性を式で参照すると自動的に 基本。値が実際に必要なとき
明示的な依存関係 depends_on = [주소]を直接書いて(プレースホルダーはアドレスです) 参照はないのに、順序は必要なとき

大半は暗黙的で十分です。depends_onが必要なのは、値としては表に出ない副作用があるときです。たとえば、IAMポリシーが付く前は、リソースの作成が失敗するのに、そのポリシーの値をコードで使わない場合です。

depends_onを習慣のように付けると、グラフが太くなります。すると2つの損があります。1つ目は、同時に処理できたものが順番待ちをして、実行時間が延びることです。2つ目は、前のリソースが再作成されるときに、後ろのリソースまで一緒に再作成される範囲が広がることです。

状態ファイル側も一緒に見ましょう。terraform state listは、登録されたアドレスだけを1行ずつ出力する、最も速い確認手段です。機械で扱う必要があるときは、terraform show -jsonを使います。この出力のリソースの一覧はvalues.root_module.resourcesの下にあり、各項目にaddressが付いているので、jqで扱いやすいです。状態ファイル自体を直接読むと、serial、lineage、version、resourcesが見えます。

.terraform.lock.hclも、この時点で読んでおく価値があります。中には、provider "registry.opentofu.org/hashicorp/local"のようなブロックと、確定したversion、そしてh1:またはzh:で始まるハッシュが入っています。バージョンは「何を使うことにしたか」、ハッシュは「実際に受け取ったものが、それで間違いないか」を意味します。

現場での姿

1つ目は、depends_onドミノです。順序の問題が一度起きると、人はあちこちにdepends_onを付け始めます。数か月後、データベースのパラメーターを1つ直しただけなのに、プランに「再作成12件」と出ます。グラフを太くした代償は、いつもあとから請求されます。

2つ目は、削除の順序は、コードではなく状態が知っていることです。コードからリソースを削除すると、そのリソースの関係は、コードから消えます。それでもツールが正しい逆順で削除できるのは、状態のdependenciesに関係が残っているからです。状態を手で編集することが危険な理由の1つです。

3つ目は、ドリフトは特別な機能ではないことです。管理中のリソースをコードの外で直すと、次のplanが照会の段階でその差を見つけて、元に戻そうとします。障害対応中にコンソールで急いで直した設定が、次のデプロイで静かに消える事故は、ここから生まれます。急な変更は、必ずコードに戻しておく必要があります。

依存関係があるのに、Terraformが知らないとき

Terraformは、参照から順序を読み取ります。 aws_instance.web.subnet_idのように、他のリソースの属性を使うと、そのリソースが先に作られる必要があることを、自分で知ります。問題は、参照なしに存在する依存関係です。

resource "aws_iam_role_policy" "app" { ... }

resource "aws_instance" "app" {
  # 정책이 붙기 전에 인스턴스가 떠서 부팅 스크립트가 실패한다
  depends_on = [aws_iam_role_policy.app]
}

depends_onは、このようなときにだけ使います。習慣的に付けると、並列で作れるものまで一列に並ばせて、適用が遅くなり、グラフが人の目に見えなくなります。参照で表現できるなら、常に参照のほうがよいです。

プランに出てくる順序は、実行順序ではありません。 terraform planの出力は、アルファベット順に近いです。実際の順序は、グラフが決めます。

terraform graph | dot -Tsvg > graph.svg

リソースの置き換えが連鎖を起こします。 プランに# forces replacementが付いた属性があれば、そのリソースは、削除して作り直されます。そのリソースを参照するものも一緒に変わるので、1行直しただけなのに、プランに12個も出る状況が生じます。このとき、create_before_destroyを有効にすると、新しいものを先に作って古いものを削除しますが、名前がグローバルで一意でなければならないリソースでは、かえって衝突します。名前にrandom_idを付けたり、name_prefixを使ったりする理由がこれです。

lifecycle {
  create_before_destroy = true
  ignore_changes        = [tags["LastModified"]]
}

ignore_changesは、対象を狭く書きます。 丸ごと無視すると、そのリソースは、事実上管理の外に出ます。外から付くタグ1つのように、誰がなぜ変えるのかがわかっている属性だけを書きます。

countの代わりにfor_eachを使います。 countで作ったリストは、アドレスが番号です。途中の1つを削除すると、後ろのものが1つずつ前に詰まって、問題のないリソースがすべて置き換えられます。 for_eachのアドレスはキーなので、削除したものだけが消えます。

次のラボですること

/root/tf/stateにリソースを積みながら、参照だけで依存関係ができることを状態で確認し、参照がない箇所にはdepends_onを付けてみます。状態のアドレスの一覧とJSON表現を取り出し、serial・lineage・リソースの個数を含むメタデータファイルを作ります。そのあと、管理中のファイルを手で削除して、ドリフトがプランで捕捉されるのを見て、ロックファイルからプロバイダーのバージョンを読みます。最後に、3段階の依存の連鎖を作って、順序を記録します。