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

置き換えか更新か — 変えられない属性は、作り直しになる

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

宣言を書き換えたとき、世界の側で起きるのが「その場の更新」なのか「作り直し」なのか、それを誰が決めているのかを見ます。

まず結論

宣言の1行を書き換えたとき、世界の側で起きるのは「その場で直す」か「壊して作り直す」かのどちらかです。属性ごとに「その場で変えられるか」を申告しているのは、Terraform ではプロバイダのスキーマ、Kubernetes では API サーバの検証とコントローラの作りです。

作り直しの順序を「先に作る」へ入れ替える create_before_destroy は、新旧が同時に存在できる相手でしか成り立ちません。同じ名前を許さない相手では、実測で作成の段階で失敗しました。

? 読む前に予測

名前を固定した Docker コンテナを Terraform で管理しています。image のタグを上げ、create_before_destroy を付けて apply すると、どうなるでしょうか。

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

抽象原理

変えられない属性は、作り直しになる

宣言の1行を書き換えて apply したとき、世界の側で起きることは2通りあります。今ある実体をその場で直すか、壊して新しく作るかです。書き換えた行の見た目は同じでも、結果はまるで違います。作り直せば id が変わり、中にあったものは消え、壊してから作るまでの間には何も無い時間ができます。

Terraform の文書は、作り直しになる条件をこう書きます。

By default, when Terraform must change a resource argument that cannot be updated in-place due to remote API limitations, Terraform destroys the existing object and then creates a new replacement object with the new configured arguments.

鍵は due to remote API limitations です。変えられないのは Terraform の都合ではなく、相手の API にその場で変える手段が無いからです。その事情を知っているのは、相手の API を包んだプロバイダです。プロバイダ向けの SDKv2 の文書は、属性ごとの印をこう定義します。

ForceNew indicates that any change in this field requires the resource to be destroyed and recreated.

属性がその場で変えられるかどうかは、プロバイダがスキーマで申告しています。 Terraform 本体はその申告を読んで、plan に作り直しを載せます。書き手の側にも手段はあります。replace_triggered_by は Terraform replaces the resource when any of the referenced resources or specified attributes change.、plan の -replace は指定した実体の置き換えを指示し、逆に ignore_changes は指定した属性を更新の計画で無視させます。

構成の属性を変えたとき、その場で変えられるかを申告しているもの
構成の属性を書き換える変えたのは値だけ
プロバイダのスキーマを見る
その場で変えられる属性今の実体を直す。id は同じ
ForceNew の属性plan に # forces replacement
作り直しの順序(create_before_destroy で入れ替える)
既定: 壊してから作る-/+ 。間に何も無い時間
create_before_destroy+/- 。新旧が同時に存在する
  • ・属性がその場で変えられるかはプロバイダが申告し、既定の順序を入れ替えるのが create_before_destroy
  • ・「先に作る」は、新旧が同時に存在できる相手でしか成り立たない
具体実物

plan に出る # forces replacement

手元で確かめました。Terraform 1.16.4 を hashicorp/terraform:1.16.4 のコンテナで動かし、プロバイダは hashicorp/local v2.9.1 と kreuzwerker/docker v4.6.0 です。

まずファイル1つを作る local_file の content を書き換えました。2回とも plan に ~ content = <<-EOT # forces replacement が出て、中身の書き換えではなく削除と作成になりました。このログでは id が content_sha1 と同じ値なので、中身が変われば id も変わります。

次に busybox のコンテナを置く docker_container の image を busybox:1.36 から busybox:1.37 に変えました。下の出力は lifecycle を付けない状態の実行で、実際に流したのは、次の節で見る create_before_destroy の失敗2回の後です。

terraform plan / apply — docker_container の image を変更(lifecycle なし。後述の失敗2回の後に実行。検証ログから行を抜粋)コマンド出力
-/+ destroy and then create replacement
~ image = "busybox:1.36" -> "busybox:1.37" # forces replacement
Plan: 1 to add, 0 to change, 1 to destroy.
docker_container.web: Destroying... [id=40bcb904371b9958024c0200928c5eb2bb5f907235163e00d29f677ca9dd931b]
docker_container.web: Destruction complete after 0s
docker_container.web: Creating...
docker_container.web: Creation complete after 1s [id=456c9d11b529a2f30eca93c0244094448b7bf803adaaef1726f088dc8ae7e3bb]
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

2行目の # forces replacement が、image がその場で変えられない属性だという印です。このプロバイダの image が ForceNew だと書いた文書は、確認できた範囲では見つかっていません。ここで言えるのは、この版の plan がそう出したという実測までです。 4行目で古い id が消え、7行目で別の id ができています。

✓ ここまでの確認

plan にある属性の行に `# forces replacement` が付きました。この属性を「その場では変えられない」と決めているのは何ですか。

抽象原理

先に作るには、2つが同時に存在できなければならない

既定の「壊してから作る」には、間に何も無い時間があります。これを避ける規則が create_before_destroy ですが、文書はそれが既定でない理由を書いています。

This is an opt-in behavior because many remote object types have unique name requirements or other constraints that must be accommodated for both a new and an old object to exist concurrently.

先に作れば、一瞬、新旧の2つが並びます。名前が一意でなければならない相手では、その一瞬が成り立ちません。文書は続けて、名前に乱数の接尾辞を付ける選択肢を持つ型もあるが Terraform CLI cannot automatically activate such features と書きます。名前をずらすのは書く人の仕事です。

Kubernetes では、決める者の置き方が違います。Pod はコンテナを包む最小の単位、ReplicaSet は指定した数の Pod を保つコントローラ、Deployment は ReplicaSet を使って Pod の更新を進めるコントローラです。

1つ目の決め手は API サーバの検証 です。文書は A Deployment's label selector is immutable after creation; と書き、変えたいなら you have to delete the Deployment and recreate it. とします。Terraform のように自動で作り直すのではなく、要求そのものを拒否します。

2つ目は コントローラの作り です。Pod の中身を決める template を変えると、Deployment は今の Pod を直さず、新しい ReplicaSet を作って古いほうを減らします(A new ReplicaSet is created, and the Deployment gradually scales it up while scaling down the old ReplicaSet)。新しい ReplicaSet の名前には template から決まる pod-template-hash が付くので、新旧の名前はぶつかりません。これは create_before_destroy の文書が挙げた「名前をずらす」を、コントローラが毎回やっている形だと読めます(この対応づけは教材側の整理です)。

例外もあります。Pod の CPU とメモリの requests と limits は、Stable since Kubernetes v1.35 の in-place resize で、動いている Pod を作り直さずに変えられます。これは Pod を直接変える話で、Deployment の template を変えれば rollout になる規則は変わりません。

具体実物

Terraform は衝突し、Kubernetes は名前をずらす

同じ docker_container に lifecycle { create_before_destroy = true } を足し、名前は decl-web7 に固定したまま image のタグを上げました。

terraform apply — create_before_destroy + 固定名(検証ログから行を抜粋。Error の本文行は全文)コマンド出力
+/- create replacement and then destroy
# docker_container.web must be replaced
Plan: 1 to add, 0 to change, 1 to destroy.
docker_container.web: Creating...
Error: Unable to create container: Error response from daemon: Conflict. The container name "/decl-web7" is already in use by container "40bcb904371b9958024c0200928c5eb2bb5f907235163e00d29f677ca9dd931b". You have to remove (or rename) that container to be able to reuse that name.
### state after failed apply
image = "busybox:1.36"
docker: 40bcb904371b decl-web7 busybox:1.36 Up 20 minutes

記号が +/- に変わり、Creating で止まりました。失敗のあとも state は古い id と busybox:1.36 のまま、古いコンテナも動いています。2回流して2回とも同じエラーでした。失敗しても壊れてはいませんが、変更は1歩も進みません。 この2回の後に lifecycle の行を消すと、同じ変更は前の節の -/+ のとおり通りました。

Kubernetes 側は k3s v1.37.0(v1.37.0+k3s1)で、Deployment seven の selector と image をそれぞれ変えました。

kubectl apply / get rs — selector の変更と image の変更(検証ログから行を抜粋)コマンド出力
The Deployment "seven" is invalid: spec.selector: Invalid value: {"matchLabels":{"app":"seven2"}}: field is immutable
exit=1
deployment.apps/seven configured
exit=0
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
seven-557585bb49 0 0 0 11s c busybox:1.36 app=seven,pod-template-hash=557585bb49
seven-b85c9c48 1 1 1 7s c busybox:1.37 app=seven,pod-template-hash=b85c9c48

selector の変更は1行目で拒否されました。image の変更は通り、新しい ReplicaSet seven-b85c9c48 ができ、古い seven-557585bb49 は DESIRED 0 で残りました。2回目の実行でも同じハッシュになりました。template が同じなら同じ名前になる、という範囲の再現です。

✓ ここまでの確認

作成済みの Deployment の `spec.selector` を書き換えて kubectl apply しました。k3s v1.37.0 の実測で何が起きましたか。

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

Terraform: 固定名のコンテナに create_before_destroy

image の変更は作り直しになり、先に作ろうとした時点で同じ名前が既にあるため Docker が Conflict を返しました。名前をずらす仕組みは書く人が用意しない限りありません。

具体例 B

Kubernetes: Deployment の template を変更

image の変更で新しい ReplicaSet ができ、古いほうは 0 まで減らされました。名前に template から決まるハッシュが付くので、新旧は衝突せずに並びます。

同じ「先に新しいほうを作る」なのに、片方は失敗し片方は通りました。違いはどこにあるでしょうか。

新旧が並んだ一瞬に、2つの名前がそれぞれ何だったかを比べてください。

この章のまとめ

  • 変更は「その場で直す」か「壊して作り直す」か。作り直すと 別の id になる。
  • 属性がその場で変えられるかは プロバイダのスキーマ(ForceNew) が申告する。plan の # forces replacement が印。書き手も -replace・replace_triggered_by で作り直しを起こせる。docker provider の image は実測でそう出た(文書では未確認)。
  • create_before_destroy は順序だけを変える。新旧が同時に存在できる相手でしか成り立たず、固定名のコンテナでは Conflict で失敗した。state と古いコンテナは無傷。
  • Kubernetes では、Deployment の selector は API サーバが変更を拒否 し、template の変更は コントローラが新しい ReplicaSet を作る。名前にハッシュが付くので新旧が並ぶ。
  • prevent_destroy は、構成からリソースの定義ごと消すと削除を止めない。

自分の現場で確かめる

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

理解度チェック

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

問題 1 / 3正解: 0

prevent_destroy = true を付けたリソースについて、Terraform の lifecycle 文書の記述と合うのはどれですか。

用語集(4語)
置き換え(replacement)
その場で変えられない属性が変わったとき、既存の実体を壊して新しい実体を作ること。Terraform の plan では `-/+` や `+/-` の記号と `must be replaced` で示される。新しい実体は別の id になる。
ForceNew
Terraform プロバイダ(SDKv2)のスキーマで、属性の変更が作り直しを要すると申告する印。付けるのはプロバイダの実装で、Terraform 本体ではない。
create_before_destroy
置き換えの順序を「先に作ってから壊す」に変える lifecycle の規則。新旧が同時に存在できることが前提なので opt-in になっている。
pod-template-hash
Deployment のコントローラが、作ったり取り込んだりした ReplicaSet に付けるラベル。ReplicaSet の名前の末尾にも同じ文字列が付く。