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

状態ファイルは世界の写しにすぎない

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

宣言と世界のあいだには、比べる者が持つ記録があります。今回はその記録が、世界とどうずれるかを見ます。

まず結論

Terraform の state は、構成に書いたリソースと世界の実体を結ぶ対応表に、属性の写しを添えたものです。世界そのものではありません。

写しは plan や apply のたびに provider へ問い合わせて取り直されます(plan ではメモリ上だけ)。同じかどうかを決めるのは provider の読み取り(Read)で、手で中身を書き換えたファイルを「削除された」と写すこともあります。

? 読む前に予測

Terraform で `hello` と書いたファイルを作り、その中身を手で `edited by hand` に書き換えました。構成は変えずに `terraform plan -refresh-only` を実行すると、何と報告されるでしょうか。

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

抽象原理

state は世界ではなく、世界との対応表

Terraform が「このリソースは作ってある」と分かるのは、世界を見ているからではありません。手元の記録を読んでいるからです。文書はその記録、state の目的をこう書きます。

The primary purpose of Terraform state is to store bindings between objects in a remote system and resource instances declared in your configuration.

構成の resource "local_file" "greeting" と、世界のどの実体が同じものかを結ぶ対応表です。Purpose のページは、これが無いと成り立たないと書きます。

Terraform requires some sort of database to map Terraform config to the real world.

初期の試作では state を持たず、クラウドのタグで対応を取っていたが、すぐ問題に当たった、とも書いています(Early prototypes of Terraform actually had no state files and used this method. However, we quickly ran into problems.)。

対応表に加えて、state は属性値の写しも持ちます。こちらの扱いは軽いものです。

This is the most optional feature of Terraform state and is done only as a performance improvement.

必須なのは対応、任意なのは属性の写しです。写しは古くなるので、plan の前に取り直されます。

Prior to any operation, Terraform does a refresh to update the state with the real infrastructure.

構成・state・世界の3つと、それぞれを誰が読むか
構成(main.tf)人が書いた望む状態
resource のアドレスで結ぶ
state対応表(必須)+ 属性の写し(任意)
plan / apply のたびに provider が読み取る
世界の実体ファイル・コンテナ・クラウドの資源
  • ・写しが更新されるのは Terraform が呼ばれたときだけ。それ以外の時間、state は最後に見た時点のまま
  • ・「同じ実体か」を答えるのは Terraform 本体ではなく provider の読み取り
具体実物

中身を手で書き換えたファイルは、どう写るか

hashicorp/terraform:1.16.4 のコンテナで、hashicorp/local 2.9.1 を使って1つのファイルを作りました。

main.tf設定ファイル
terraform {
required_providers {
local = { source = "hashicorp/local" }
}
}
variable "content" { default = "hello\n" }
resource "local_file" "greeting" {
filename = "${path.module}/out/hello.txt"
content = var.content
}

apply 後の state には "id": "f572d396fae9206628714fb2ce00f72e94f2258f" が記録されます。これは hello と改行の sha1 と一致します(python3 の hashlib で検算)。この provider では、中身がそのまま同一性の鍵です。ここで中身を手で書き換え、refresh-only の plan を取りました。

手で書き換えたあとの plan -refresh-only(Terraform 1.16.4。属性9行を省略)コマンド出力
$ printf 'edited by hand\n' > out/hello.txt; cat out/hello.txt
edited by hand
$ terraform plan -refresh-only -no-color
local_file.greeting: Refreshing state... [id=f572d396fae9206628714fb2ce00f72e94f2258f]
Note: Objects have changed outside of Terraform
Terraform detected the following changes made outside of Terraform since the
last "terraform apply" which may have affected this plan:
# local_file.greeting has been deleted
- resource "local_file" "greeting" {
- content = <<-EOT
hello
EOT -> null
...(content_base64sha256 から filename までの9行を省略)
- id = "f572d396fae9206628714fb2ce00f72e94f2258f" -> null
}
This is a refresh-only plan, so Terraform will not take any actions to undo
these. If you were expecting these changes then you can apply this plan to
record the updated values in the Terraform state without changing any remote
objects.

ファイルはあるのに has been deleted です。記録した id の実体を provider が見つけられなかった、という写り方です。provider 開発者向けの文書(Plugin Framework の Read)は、実体がもう無いときの扱いをこう勧めています。

Ignore returning errors that signify the resource is no longer existent, call the response state RemoveResource() method, and return early. The next Terraform plan will recreate the resource.

local provider の実装がこの指針に沿っているかは確かめていませんが、実演の出力はこの形と一致します。「無い」と判断するのは provider で、Terraform 本体ではありません。続けて通常の plan を取り、実物と state を確かめました。

続けて通常の plan と、その後の実物・state(抜粋。行の間を省略)コマンド出力
$ terraform plan -no-color
local_file.greeting: Refreshing state... [id=f572d396fae9206628714fb2ce00f72e94f2258f]
# local_file.greeting will be created
Plan: 1 to add, 0 to change, 0 to destroy.
$ cat out/hello.txt # still hand-edited, plan changed nothing
edited by hand
$ grep -o '"id": "[^"]*"' terraform.tfstate # state file on disk untouched by plan
"id": "f572d396fae9206628714fb2ce00f72e94f2258f"
$ terraform apply -refresh-only -auto-approve -no-color | tail -4
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
$ grep -c local_file terraform.tfstate # after refresh-only apply
0

読み取れることは3つです。通常の plan は「更新」ではなく 作成 を提案しました。plan の後も実物は書き換えたままで、ディスク上の state には古い id が残っています。apply -refresh-only を通して初めて、state から記録が消えました。同じ手順を2回流し、2回とも同じ出力でした。

✓ ここまでの確認

実演で、中身を書き換えただけのファイルが `has been deleted` と写ったのはなぜだと考えられますか。

抽象原理

写しは、ずれていることが普通の状態

実演は、よく聞く2つの言い方がどちらも外れていることを示しています。

1つ目は「Terraform が状態を壊した」。plan は実物にも、ディスク上の state にも触れていません。チュートリアルは、plan のときの refresh について The refresh updates your state file in-memory と書きます。写しを取り直して比べただけです。

2つ目は「state を見れば実物が分かる」。書き換えた後も state には古い id が残っていました。state は Terraform が最後に見た時点の世界で、その後に誰かが手で変えた分は、次に呼ばれるまで写りません。

写しを世界に合わせる操作は、以前は単独の terraform refresh でした。現行の文書はこう書きます。

This command is deprecated. Instead, add the -refresh-only flag to terraform apply and terraform plan commands.

refresh-only モードの用途として、plan の文書は障害対応中に手で変えた場合を挙げ、reconcile Terraform's records with those changes と書きます。向きに注意してください。合わせるのは記録の側で、実物は戻しません。 自動で記録を合わせる危うさは、refresh コマンドの文書に書かれています。

If you have misconfigured credentials for one or more providers, Terraform may be misled into thinking that all of the managed objects have been deleted, causing it to remove all of the tracked objects without any confirmation prompt.

読み取りが失敗すれば、写しは「全部消えた」になりえます。写しの正しさは、読み取りの正しさを超えません。

逆に、Terraform の外で作った実体は対応表に無いので、Terraform からは存在しないのと同じです。チュートリアルは、AWS CLI で新しく作ったセキュリティグループを not tracked in the Terraform state file と表現しています。文書は管理下に入れる手段として import を挙げ、The block also declares an address for the imported resource in state. と書きます。対応を1行書き足す操作です。

具体実物

Kubernetes は写しをどこに置くか

Kubernetes では、宣言と観測が同じオブジェクトの2つのフィールドとして説明されます。

The status describes the current state of the object, supplied and updated by the Kubernetes system and its components.

利用者が書くのは spec(望む状態)、status(今の状態)を書くのは Kubernetes 側の部品です。コントローラ(望む状態と今の状態を比べ、差を埋める制御ループ)について、文書はこう書きます。

The important point here is that the controller makes some changes to bring about your desired state, and then reports the current state back to your cluster's API server.

status もまた、誰かが観測して書いた写しです。だから「その写しがどの宣言を見て書かれたか」を示す欄があります。API の規約は observedGeneration を、宣言の変更に応じて動く部品が最後に観測した世代(generation)だと説明し、ensure that the reported status reflects the most recent desired status に使えると書きます。

違いは写しを書く者と時点です。Terraform では、呼び出した人の Terraform が plan や apply のたびに書きます。Kubernetes では、動き続けるコントローラが API サーバに書き戻します。

✓ ここまでの確認

Kubernetes のオブジェクトで「今どうなっているか」を表す status は、誰が書くと文書に書かれていますか。

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

Terraform の state

呼び出されたときに provider が実物を読み、写しを取り直す。実演では、手で中身を変えたファイルを削除として写し、plan の後もディスク上の state には古い id が残っていた。

写しの書き手と時点表
項目 Terraform
写しの場所 terraform.tfstate(既定)
書く者 呼び出した人の Terraform と provider
書く時点 plan / apply のとき
同一性の鍵 provider が決める(local_file は中身の sha1)
具体例 B

Kubernetes の status

宣言(spec)と同じオブジェクトに載り、Kubernetes の部品が書く。コントローラが現状を API サーバに報告し、observedGeneration がどの世代の宣言を見た写しかを示す。

写しの書き手と時点表
項目 Kubernetes
写しの場所 オブジェクトの status
書く者 Kubernetes のシステムと部品
書く時点 コントローラが観測して報告したとき
鮮度の目印 observedGeneration

state と status はどちらも「今の世界」を表しているように見えます。どちらを読むときにも共通して確かめるべきことは何でしょうか。

その記録を書いたのは誰で、それは何秒前、何回前の操作のときだったかを考えてみてください。

この章のまとめ

  • state の主目的は 構成のリソースと世界の実体を結ぶ対応表。属性の写しは the most optional feature で性能のため。
  • 写しは plan と apply のたびに provider が読んで取り直す。呼ばれていない間は古いまま。
  • 何が「同じ実体」かを決めるのは provider。local 2.9.1 の local_file は、中身を変えたファイルを has been deleted と写した。
  • plan は実物もディスク上の state も変えなかった。「Terraform が壊した」も「state を見れば実物が分かる」も外れ。
  • terraform refresh は非推奨。-refresh-only が合わせるのは 記録の側で、実物は戻さない。
  • Kubernetes では宣言(spec)と観測(status)が同じオブジェクトに載り、status は Kubernetes の部品が書く写し。鮮度の目印は observedGeneration。

自分の現場で確かめる

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

理解度チェック

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

問題 1 / 3正解: 0

Terraform の文書が state の主な目的として挙げているのはどれですか。

用語集(4語)
state(Terraform)
構成のリソースと世界の実体の対応表に、属性値の写しを添えた記録。既定ではローカルの terraform.tfstate に保存され、前の版が terraform.tfstate.backup に残る。
refresh
provider に実物を問い合わせて state の写しを更新すること。文書は plan と apply の前に行うと書く。単独の terraform refresh コマンドは非推奨。
-refresh-only
plan と apply のモード。Terraform の外で起きた変更に state を合わせることだけを目的とし、実物を元に戻す操作はしない。v0.15.4 以降。
status(Kubernetes)
オブジェクトの現在の状態を表すフィールド。宣言である spec と同じオブジェクトに載り、Kubernetes のシステムと部品が書く。