Dev Study
← 宣言的に動かす — Kubernetes と Terraform に共通する仕組み
レッスン 5 / 12·約 8 分·第26期 宣言的に動かす

plan は見積もり — 保存した計画が見張っているもの

アプリと DB の下に何が積まれているか(左が下、右が上)
  1. 物理機リージョン/AZ
  2. ハイパーバイザVM exit で捕まえる
  3. VMゲストOS今ここ
  4. コンテナnamespace / cgroup今ここ
  5. プロセスアプリ / DB

宣言は望む状態の記録で、世界がそれに近づくのは誰かが比べて差を埋めたときだけです。今回は、差を埋める前に「どう埋めるか」を見せる仕組みが、何を前提にしているかを見ます。

まず結論

terraform plan は、読んだ時点の世界に対して「適用したらこう動かす」という操作の列を提案するだけで、何も実行しません。保存した plan は、その時点の世界を前提にした見積もりです。

実演では、plan の後に別の操作で state が変わると保存した plan は stale として拒まれ、Terraform の外で実物だけが変わったときは止まらずにそのまま実行されました。Kubernetes の dry-run と kubectl diff は、見積もりではなく保存直前の姿を返すもので、Pod が動くかは最初から見ていません。

? 読む前に予測

terraform plan -out で計画をファイルに保存しました。その後、別の人が同じ構成で apply して state が変わりました。保存した計画を terraform apply に渡すと、Terraform 1.16.4 ではどうなるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

plan は、読んだ時点の世界に対する提案

plan の文書は、既定で行うことを3つ挙げます。実物を読んで state を最新にし、設定と前の state を比べ、そして

Proposes a set of change actions that should, if applied, make the remote objects match the configuration.

should, if applied、つまり「適用すれば、そうなるはず」の操作の列です。続けて The plan command alone does not actually carry out the proposed changes とあり、plan だけでは何も変わりません。

-out=FILE で保存した計画を apply に渡すと、文書の Saved Plan Mode は Terraform performs the operations in the saved plan without prompting you for confirmation と書きます。承認されるのはファイルの中の操作の列で、apply する瞬間の世界ではありません。保存した plan は、plan を取った時点の世界を前提にした見積もりです。

第13期で扱うデータベースの実行計画も「見積もり」ですが、推定しているものが違います。あちらは行数とコストの推定、こちらは「適用したら何を作り、何を壊すか」という操作の列です。

保存した plan と、plan から apply までの間に起きる変化(今回の実演)
plan -out実物を読み、設定と比べ、操作の列を提案
ファイルに保存。apply に渡すことが承認になる
保存した planplan 時点の世界が前提
apply までの間に何が変わったか
別の操作で state が変わったSaved plan is stale で停止
実物だけが外で変わった止まらずに実行された
  • ・Terraform 1.16.4 と hashicorp/local v2.9.1 での実演。各2回実行して同じ結果
  • ・stale の判定方法そのものは文書に書かれていない。ここに描いたのは実演で観察した結果
具体実物

state が変わると、保存した plan は拒まれる

ファイルを1つ作るだけの構成(local_file)で、plan を保存したあと、別の apply で state を変えてから保存した plan を渡しました。

Terraform 1.16.4 の実出力(実演1回目。空行と一部の行を省いて抜粋。2回目も同じ結果)コマンド出力
$ terraform plan -out=p.tfplan -no-color
# local_file.greeting will be created
+ resource "local_file" "greeting" {
+ content = <<-EOT
hello
EOT
+ content_base64sha256 = (known after apply)
+ filename = "./out/hello.txt"
+ id = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
$ terraform apply -auto-approve # another operation changes state
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ terraform apply p.tfplan
Error: Saved plan is stale
The given plan file can no longer be applied because the state was changed by
another operation after the plan was created.
exit=1

見積もりの中には、最初から未知の値が入っています。id もハッシュも (known after apply) で、適用するまで決まりません。

そして、別の操作で state が変わった後の適用は拒まれました。メッセージは「plan を作った後に、別の操作が state を変えた」と理由を述べています。stale という語は plan と apply の文書には無く、1.16.4 の実出力です。

✓ ここまでの確認

plan の出力に `id = (known after apply)` が並んでいました。この行が示していることとして妥当なのはどれですか。

抽象原理

止まるのは、Terraform 自身の記録が変わったとき

ここで「保存した plan なら、何が起きても確認どおりに適用される」という理解は崩れます。では逆に「世界が変わっていれば stale で止まるから安全」と言えるでしょうか。

エラー文が挙げた理由は the state was changed でした。state は Terraform が持つ世界の写しで、世界そのものではありません。Terraform を通さずに実物だけが変わったとき、写しは変わりません。 文書には stale をどう判定するかの記述が無いので、次の実演で確かめます。

Kubernetes にも適用前に見る仕組みがありますが、性質が違います。Pod(Kubernetes が動かすコンテナのまとまりの最小単位)を何個・どのイメージで動かすかを宣言する Deployment を --dry-run=server で送ると、API サーバは要求を

up until persisting objects to storage

まで通します。アドミッション(保存の前に要求を検査し、書き換えることもある段)・検証・既定値の補完を経て、保存されたであろう最終オブジェクトを返すだけで、何も保存しません。予測ではなく「保存直前の姿」です。イメージが取れるか、Pod が動くかは、最初から範囲の外です。

具体実物

実物だけ変えても、保存した plan は止まらない

今度は中身を v2 に変える plan を保存し、その後 Terraform を通さずにファイルを手で書き換えました。state には触れていません。

Terraform 1.16.4 の実出力(実演1回目。実演スクリプトが grep で絞った出力のまま。2回目も同じ結果)コマンド出力
$ terraform plan -out=p.tfplan -var content='v2\n'
# local_file.greeting must be replaced
~ content = <<-EOT # forces replacement
- hello
+ v2\n
Plan: 1 to add, 0 to change, 1 to destroy.
$ printf 'edited by hand\n' > out/hello.txt # state untouched
$ terraform apply p.tfplan
local_file.greeting: Destroying... [id=f572d396fae9206628714fb2ce00f72e94f2258f]
local_file.greeting: Destruction complete after 0s
local_file.greeting: Creating...
local_file.greeting: Creation complete after 0s [id=7ae37e7433866e88a0516a258b54eaadef25ca05]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.
exit=0
$ cat out/hello.txt
v2\n

stale にはならず、計画どおりに壊して作り直しました(v2\n がそのまま文字列として入っているのは、実演で -var にリテラルの \n を渡したためで、結論には関わりません)。

手で書いた edited by hand は、レビューされたどの plan にも映らないまま消えています。plan は既定で実物を読み直すので、取り直していれば、その時点の世界に対する見積もりになっていたはずです。保存した plan を渡すと計画は作り直されず、ファイルの中の操作がそのまま実行されます。

✓ ここまでの確認

plan -out の後、Terraform を通さずに実物のファイルだけを手で書き換え、保存した plan を apply しました(state は触っていない)。実演の結果はどれですか。

⇄ 2つを見比べる
具体例 A

Terraform の保存した plan

plan 時点の世界に対する操作の列です。実演では state が変わると拒まれ、実物だけが変わると止まらずに実行されました。

実演で観察したこと(Terraform 1.16.4)表
plan と apply の間の変化 結果
別の apply で state が変わる Saved plan is stale、exit 1
実物だけを手で書き換える 計画どおり実行、exit 0
具体例 B

Kubernetes の dry-run と kubectl diff

保存直前の姿を返すだけで、存在しないタグでも created と答えます。diff の終了コードは差分の有無です。diff の出力に出た uid は、その後に実際に作られたオブジェクトの uid と違っていました。

k3s v1.37.0 の実出力(実演1回目から抜粋)コマンド出力
$ kubectl apply --dry-run=server -f l2-dep-bad.yaml
deployment.apps/web created (server dry run)
exit=0
$ kubectl get deploy
No resources found in default namespace.
exit=0

どちらも「適用前に確かめる」仕組みです。それぞれ、何を見ていないでしょうか。

保存した plan は apply の瞬間の実物を、dry-run は保存の後に起きることを、それぞれ見ているかどうか。

この章のまとめ

  • plan は should, if applied の操作の列を提案するだけで、何も実行しない。
  • 保存した plan は plan 時点の世界が前提の見積もり。apply に渡すことが承認になる。
  • 見積もりには最初から (known after apply) の未知が含まれる。
  • 実演(1.16.4)では、state が変わると Saved plan is stale で止まり、実物だけが変わると止まらずに実行された。stale が出ないことは、世界が変わっていない証拠ではない。
  • Kubernetes の dry-run は 保存直前の姿を返すだけで、存在しないタグでも created。Pod が動くかは範囲の外。
  • plan ファイルには機密値が 平文で入りうる。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

保存した plan ファイルを terraform apply に渡したとき、承認の扱いについて apply の文書は何と書いていますか。

用語集(4語)
保存した plan(saved plan)
terraform plan -out=FILE で書き出した計画。apply に渡すと確認なしで中の操作を実行する。-out なしの plan は speculative plan と呼ばれ、適用する意図を持たない。
Saved plan is stale
Terraform 1.16.4 の実出力。plan の作成後に別の操作で state が変わると、保存した plan の適用を拒む。文書の plan・apply のページにこの語は無い。
(known after apply)
plan の時点では値が決まっておらず、適用して初めて分かることを示す表示。
dry-run(Kubernetes)
変更の要求を保存の直前まで通し、保存しない仕組み。1.19 から stable。返るのは保存されたであろう最終オブジェクトで、生成される値は本番と違いうる。