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

2回流して同じになるのは、宣言に書いた部分だけ

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

「宣言的だから何度流しても同じ」という言い方が、どこまで成り立つのかを見ます。

まず結論

同じ宣言を2回流しても2回目は何もしない。これが成り立つのは、道具が記録と比べられる範囲だけです。

Terraform の provisioner が作ったファイルを消しても、次の apply は差が無いと答えます。管理対象のリソースとしては、そのファイルは宣言にも記録にも無いからです。

? 読む前に予測

terraform_data に local-exec を付け、apply でファイルを1つ書かせました。そのファイルを手で消して、もう一度 apply するとどうなるでしょうか。

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

抽象原理

apply が揃えると約束しているもの

Terraform の文書は、apply がすることを5つ挙げ、最後をこう書きます。

Updates the state file so that the configuration, real infrastructure, and state match.

揃えるのは 構成・実物・state の3つ です。ただし下の実演が示すとおり、ここでの「実物」は provider が読んで state に書ける属性です。そこに現れないものは、比べる相手になりません。

その極端な例が terraform_data です。

The terraform_data resource type implements the standard resource lifecycle, but does not directly take any other actions.

ライフサイクルだけを持ち、それ以外は何もしない型です。文書はこれを provisioner の入れ物として紹介しています(serves as a container for arbitrary operations taken by the provisioner "local-exec" block)。入れ物の中のコマンドが外に何を残しても、記録の中で値を持つのは入れ物の id だけです。

2回目の apply が比べるものと、比べないもの
宣言構成ファイル / マニフェスト
apply が比べる
記録state / API オブジェクト
読める実物provider が読む属性
比べる相手がいない
記録の外provisioner の副作用・手で消したファイル
  • ・2回目が何もしないのは、上の2段で差が無いとき
  • ・3段目で何が起きても、2回目の答えは変わらない
具体実物

ファイルを消しても、差は無いと答える

実演の構成はこれだけです。

main.tf(Terraform 1.16.4、組み込みの terraform_data のみ)設定ファイル
resource "terraform_data" "setup" {
provisioner "local-exec" { command = "echo created >> ${path.module}/side-effect.log" }
}

1回目で side-effect.log ができます。2回目は No changes でした。ここまでは期待どおりです。そこでファイルを消して、3回目を流しました。

実演ログ(抜粋。1回目の計画表示と2回目を省略、ほかに一部の行を省略、空行は除去)コマンド出力
$ terraform apply -auto-approve # 1st
Plan: 1 to add, 0 to change, 0 to destroy.
terraform_data.setup (local-exec): Executing: ["/bin/sh" "-c" "echo created >> ./side-effect.log"]
terraform_data.setup: Creation complete after 0s [id=001a3f90-1674-e629-558f-274fe7a732b7]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ rm side-effect.log; terraform apply -auto-approve # 3rd, after rm
terraform_data.setup: Refreshing state... [id=001a3f90-1674-e629-558f-274fe7a732b7]
No changes. Your infrastructure matches the configuration.
Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
$ ls side-effect.log
ls: l3-tf/side-effect.log: No such file or directory

compared your real infrastructure と言いながら、ファイルは無いままです。嘘ではありません。比べた「実物」は terraform_data の記録で、そこにファイルは載っていないからです。

戻すには、記録された実体を作り直すしかありません。-replace=terraform_data.setup を付けると 1 added, 0 changed, 1 destroyed となり、local-exec が再び走ってファイルが戻りました。id は ec43ce7d-... に変わっています。2回流して2回とも同じ結果でした(id の値は回ごとに異なる)。

✓ ここまでの確認

ファイルを消した後の apply が `found no differences` と答えたのはなぜですか。

抽象原理

何と比べるかで、2回目の答えが決まる

Kubernetes でも同じ形が見えます。Deployment は「この中身の Pod(コンテナをまとめた実行単位)をいくつ動かすか」を書いたオブジェクトで、API サーバに記録されます。

2回目に何が起きるかは、比べる相手で決まります。

  • 名前と比べる: API 規約は All compatible Kubernetes APIs MUST support "name idempotency" と書き、同名の POST には 409 を返せと求めます。create の2回目はここで止まります。
  • 今の実物と前回の宣言と比べる: apply はファイルを今の実物(live)と、前回の宣言(注釈 last-applied-configuration)の両方と比べます。値を設定するかは live と、消すかは注釈と比べて決めます(calculates this patch request using the configuration file, the live configuration, and the last-applied-configuration annotation)。create はこの注釈を残しません。

名前という歯止めにも外があります。metadata.generateName で名前をサーバに作らせると、the name returned to the client will be different than the name passed。名前が毎回変わるので、409 の歯止めは掛かりません。

第12期で扱う「冪等」は、HTTP メソッドの意図された効果の話です。ここで見ているのは層が違い、宣言と記録を比べて差を埋める道具が、どこまでを比べているかの話です。

具体実物

create 2回と apply 2回

同じマニフェスト(busybox:1.36 の Deployment、replicas 1)を、k3s v1.37.0 で流しました。

実演ログ(kubectl v1.37.0。警告文の中ほどを … で省略、途中の delete と一部の exit 行を省略)コマンド出力
$ kubectl create -f l3-dep-ok.yaml
deployment.apps/web created
$ kubectl create -f l3-dep-ok.yaml # again
Error from server (AlreadyExists): error when creating "/l3-dep-ok.yaml": deployments.apps "web" already exists
exit=1
$ kubectl apply -f l3-dep-ok.yaml # apply after create
Warning: resource deployments/web is missing the kubectl.kubernetes.io/last-applied-configuration annotation … The missing annotation will be patched automatically.
deployment.apps/web configured
$ kubectl apply -f l3-dep-ok.yaml
deployment.apps/web created
$ kubectl apply -f l3-dep-ok.yaml # again
deployment.apps/web unchanged
exit=0

create の2回目は名前でぶつかり、終了コード1です。apply の2回目は unchanged で終了コード0。create の後の apply は警告を出して注釈を補い、configured で通りました。公式文書は混用を not supported と書きますが(Last modified August 24, 2023)、この版で失敗はしていません。「失敗する」と覚えるのは誤りで、原文は「サポートしない」です。2回流して2回とも同じでした。

✓ ここまでの確認

kubectl create で作った Deployment に、同じファイルで kubectl apply を流すとどうなりましたか(kubectl v1.37.0)。

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

Terraform: terraform_data と local-exec

値を持つのは id だけです。local-exec が書いたファイルを消しても、次の apply は No changes と答えます。やり直すには -replace で実体ごと作り直します。

比べる相手表
もの 記録にあるか 2回目の扱い
terraform_data ある(id) 差なし → 何もしない
side-effect.log ない 消えても気づかない
具体例 B

Kubernetes: create と apply

create は名前と比べ、同名なら 409 で止まります。apply は今の実物と前回の宣言の両方と比べ、差が無ければ unchanged です。generateName は名前が毎回変わり、名前の歯止めが掛かりません。

比べる相手表
操作 比べる相手 2回目の結果
create 名前 AlreadyExists
apply 今の実物と前回の宣言 unchanged

片方は黙って通り、片方はエラーで止まります。2回目の結果を決めているのは何でしょうか。

それぞれの道具が、2回目に「何と」比べているかを探してください。

この章のまとめ

  • apply が揃えるのは 構成・実物・state。実物とは provider が読んで記録できる範囲。
  • terraform_data の記録で値を持つのは id だけ。local-exec が書いたファイルを消しても No changes。
  • やり直すには -replace で実体ごと作り直す。id は変わる。
  • Kubernetes の create は 名前 と比べ、同名なら AlreadyExists。apply は 今の実物と前回の宣言 の両方と比べ、差が無ければ unchanged。
  • create の後の apply は警告付きで通る。公式は混用を not supported と書くが、「失敗する」ではない。
  • generateName は名前が毎回変わり、名前の歯止めが掛からない。
  • 「何度流しても同じ」は、記録と比べられる範囲でだけ成り立つ。

自分の現場で確かめる

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

環境:

理解度チェック

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

問題 1 / 3正解: 0

「宣言的な道具だから、同じ定義を何度流しても安全だ」という主張の扱いとして適切なのはどれですか。

用語集(4語)
terraform_data
provider を要らない組み込みのリソース型。標準のライフサイクルを持つが、それ以外の動作はしない。provisioner の置き場所に使われる。
provisioner(local-exec)
リソースの作成に合わせて手元でコマンドを走らせる仕組み。コマンドが外に残したものは state に記録されない。
name idempotency
Kubernetes の API 規約が求める性質。既存と同じ名前での作成要求には 409(AlreadyExists)を返す。
last-applied-configuration
kubectl apply が前回送った宣言を残す注釈。次の apply で、ファイルから消えたフィールドを見つけるのに使う。create は残さない。