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

誰が最後に書いたか — 黙って戻す apply と、拒む apply

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

宣言が世界に近づくのは、誰かが差を埋めたときだけでした。今回は、差を埋めようとする者が2人いるときに何が起きるかを見ます。

まず結論

同じフィールドに書き手が2人いると、宣言は「最後に書いた者の値」でしかありません。既定の kubectl apply は誰が値を変えたかを知らず、ファイルに書いた値へ黙って戻します。

Server-Side Apply はフィールドごとに書き手を記録し、別の書き手の値を変える apply を拒みます。ただし強制すれば上書きでき、所有も移ります。揺れを止めるのは、どちらかがそのフィールドを手放すことです。

? 読む前に予測

Deployment を replicas: 2 のファイルで kubectl apply(既定の方式)し、kubectl scale で 5 にしました。同じファイルをもう一度 apply すると、v1.37 の kubectl では何が起きたでしょうか。

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

抽象原理

既定の apply は、3つを比べて誰かを知らない

Deployment は、望む Pod の数(replicas)と Pod の型を記録したオブジェクトで、コントローラがその数に合わせ続けます。では、その replicas を書くのが1人でなかったらどうなるでしょうか。第8期で扱った HPA も、手で打つ kubectl scale も、ファイルを流す kubectl apply も、同じ spec.replicas を書きます。

既定の kubectl apply(client-side apply)の計算方法は公式文書に書かれています。

The kubectl apply command calculates this patch request using the configuration file, the live configuration, and the last-applied-configuration annotation stored in the live configuration.

材料は、今のファイル・今の live・前回 apply したファイルの写し(注釈)の3つです。そこから、注釈にあってファイルに無いフィールドを消し、fields present in the configuration file whose values don't match the live configuration を設定します。

誰が live を今の値にしたかは、材料に入っていません。

client-side apply は3つを比べてパッチを作る
ファイルreplicas: 2
last-applied 注釈前回のファイル。replicas: 2
livereplicas: 5(scale が書いた)
ファイルにあって live と違う
設定し直す対象replicas を 2 に
API サーバへパッチ
replicas: 25 を書いた者には何も知らされない
  • ・消す対象は、注釈にあってファイルに無いフィールド
  • ・ファイルにも注釈にも無いフィールドは触らない(公式文書の例では scale の値が残る)
具体実物

scale の 5 が、次の apply で 2 に戻る

k3s rancher/k3s:v1.37.0-k3s1(kubectl v1.37.0+k3s1)で、replicas: 2 のファイルを apply し、scale で 5 にしてから同じファイルを流しました。

client-side apply(run1。コマンドのエコー、注釈の JSON、Pod 数の行、空行を省略)コマンド出力
deployment.apps/web created
replicas=2
deployment.apps/web scaled
replicas=5
deployment.apps/web configured
exit=0
replicas=2
kubectl-client-side-apply Update
k3s Update status

configured、終了コード 0、replicas は 2。2回流して同じ結果でした。エラーも警告もなく、scale の 5 が消えています。 省いた注釈の JSON には "replicas":2 が入っており、scale の 5 は入っていません。公式文書も、scale した値は注釈に入らないと書きます(# because it was not updated through apply)。

戻ったのは、apply が scale を嫌ったからではありません。ファイルに replicas を書いていたからです。 公式文書の例では、ファイルに replicas が無いので The replicas field retains the value of 2 set by kubectl scale. となります。

✓ ここまでの確認

既定の kubectl apply が、kubectl scale で 5 にした replicas を 2 に戻したのはなぜですか。

抽象原理

書き手をフィールドごとに記録する

Server-Side Apply(SSA)は、比べる材料に「誰が」を加えます。

A Server-Side Apply patch request requires the client to provide its identity as a field manager. When using Server-Side Apply, trying to change a field that is controlled by a different manager results in a rejected request unless the client forces an override.

所有の意味も定義されています。The user who last made an assertion about the value of a field will be recorded as the current field manager. 衝突を返す理由は This prevents an applier from unintentionally overwriting the value set by another user. です。

ただし拒むのは既定の動きです。強制すると changes the value of the field, and removes the field from all other managers' entries in managedFields。原文は、書き手がコントローラのような自動処理なら強制すべきだとも書きます。SSA は上書きを禁じる仕組みではなく、上書きを黙って起こさない仕組みです。

具体実物

同じ replicas の書き換えを、SSA は拒む

同じ環境で --field-manager を alice・bob と分けて SSA を流しました。

server-side apply(run1 抜粋。解決策の案内文・B2・他の managedFields 行を省略。{...} は f:template の中身の省略)コマンド出力
=== B. server-side apply: alice applies replicas 2, bob applies replicas 3
deployment.apps/web serverside-applied
replicas=2
error: Apply failed with 1 conflict: conflict with "alice": .spec.replicas
exit=1
replicas=2
=== B3. kubectl scale (a different manager, subresource scale) then alice re-applies replicas 2 with SSA
deployment.apps/web scaled
bob Apply sub= fields.spec={"f:selector":{},"f:template":{...}}
kubectl Update sub=scale fields.spec={"f:replicas":{}}
error: Apply failed with 1 conflict: conflict with "kubectl" with subresource "scale" using apps/v1: .spec.replicas
exit=1
replicas=5

bob の apply は alice の名前を挙げて拒まれ、live は 2 のままでした。省いた B2 では、bob が --force-conflicts を付けると replicas 3 になり、f:replicas は alice の項から消えて bob の項に移りました。

B3 が client-side との対照です。kubectl scale は kubectl という manager、subresource scale として記録され、alice が 2 を apply し直すと拒まれて 5 のまま残りました。 client-side では同じ操作が黙って 2 に戻していました。

もう1つ、B3 の scale は、B2 で bob が持った replicas を拒まれずに書き換え、所有も kubectl に移しています。拒まれるのは apply の要求で、apply 以外の更新は値を変えた者に所有が移ります(Whenever a field's value does change, ownership moves ...)。scale が manager として記録されるのは文書の逐語ではなく、この実演(2回とも同じ結果)で確かめたことです。

✓ ここまでの確認

「Server-Side Apply に切り替えれば、他の書き手の値が上書きされることは無くなる」という理解は妥当ですか。

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

HPA と spec.replicas(Kubernetes)

HPA の公式文書は、HPA を有効にしたら Deployment のマニフェストから spec.replicas を外すよう推奨します。残すと apply のたびにその値へ Pod 数を合わせることになり、thrashing or flapping を招きうると書きます。外すと既定値 1 へ一度落ちうる注意も併記されています。

具体例 B

ignore_changes(Terraform)

Terraform は既定で、実物と構成の差を見つけると構成に合わせる計画を立てます。ignore_changes は、別のプロセスと1つのオブジェクトの管理を分け合うために、指定した属性の差を更新の計画で見なくします。拒むのではなく見ないだけです。本回では実演していません。

片方はマニフェストから値を消し、片方は差を見ない設定を足します。共通しているのは何でしょうか。

そのフィールドについて、宣言する側が何をやめているかを見てください。

この章のまとめ

  • 同じフィールドに書き手が2人いると、宣言は 最後に書いた者の値 にすぎない。
  • 既定の kubectl apply は ファイル・live・注釈 の3つで差を出す。誰が変えたかは知らない。v1.37 の実演では scale の 5 を 終了コード 0・警告なし で 2 に戻した。
  • 戻すのは ファイルに書いたフィールドだけ。書いていなければ scale の値は残る。
  • SSA は フィールドごとに manager を記録 し、別の manager の値を変える apply を拒む。scale も manager として記録された。
  • 拒否は既定の動き。強制すれば上書きされ所有が移る。apply 以外の更新は拒まれない。
  • 揺れを止めるのは手放すこと。HPA 文書は spec.replicas を外す よう推奨し、Terraform の ignore_changes は 差を見ない(拒むのではない)。

自分の現場で確かめる

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

環境:

理解度チェック

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

問題 1 / 3正解: 0

Server-Side Apply で alice が replicas 2 を適用した後、bob が別の manager 名で replicas 3 を適用しました(強制なし)。結果はどうなりましたか。

用語集(4語)
last-applied-configuration
既定の kubectl apply が、前回 apply したファイルの中身を live のオブジェクトに残す注釈。ファイルから消えたフィールドを見分けるのに使う。apply 以外で変えた値は入らない。
field manager / managedFields
Server-Side Apply で、フィールドごとの書き手の名前。記録は .metadata.managedFields にあり、API サーバが管理する。
conflict
別の manager が持つフィールドを apply が別の値に変えようとしたときに返るエラー。--force-conflicts で強制すると上書きされ、所有が移る。
ignore_changes
Terraform の lifecycle の引数。指定した属性の差を更新の計画で見なくする。外部の変更を拒むものではない。