Server-Side Apply Internals
ไอเดียหลักในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียหลักในหนึ่งประโยค”managedFields ไม่ใช่ list แบนของ “ใครแตะ object นี้บ้าง” — มันคือ tree ต่อ manager ว่า field ไหนบ้าง คีย์ด้วยอะไร ที่ manager นั้นเป็นเจ้าของ คำนวณโดย library structured-merge-diff เทียบกับ marker x-kubernetes-list-type ของ OpenAPI schema จริงที่ใช้งานอยู่ นี่คือเหตุผลที่ manager สองตัวเป็นเจ้าของ element คนละตัวของ list เดียวกันได้โดยไม่ conflict เลย ในขณะที่ manager ตัวที่สามที่แตะ list คนละชนิด จะ conflict ตั้งแต่ครั้งแรกที่ลอง
kubectl and Declarative YAML แสดง --server-side, block managedFields จริงและ error conflict ของ field manager จริงไปแล้ว บทนี้พิสูจน์ว่าอะไรตัดสินว่า manager สองตัวจะ conflict กันไหมเลย
managedFields อ่านเป็น tree: f:, k:, v:
หัวข้อที่มีชื่อว่า “managedFields อ่านเป็น tree: f:, k:, v:”ทุก entry ใน metadata.managedFields คือ tree fieldsV1 ที่ใช้ marker นำหน้าสามแบบ f:<name> สำหรับ field struct ธรรมดา k:<key> สำหรับ element หนึ่งตัวของ list ที่คีย์ด้วย identity (ไม่ใช่ index) และ (พบน้อยกว่า) v:<value> สำหรับ element หนึ่งตัวของ set ที่คีย์ด้วยค่าของตัวเอง manager สองตัว ต่างก็ SSA-apply Deployment เดียวกันและตั้ง environment variable ของ container คนละตัวชื่อ
apiVersion: apps/v1kind: Deploymentmetadata: name: ssa-merge-demo namespace: internals-demospec: replicas: 1 selector: matchLabels: { app: ssa-merge-demo } template: metadata: labels: { app: ssa-merge-demo } spec: containers: - name: c image: registry.k8s.io/pause:3.10 env: - name: FROM_MANAGER_A value: "a"apiVersion: apps/v1kind: Deploymentmetadata: name: ssa-merge-demo namespace: internals-demospec: replicas: 1 selector: matchLabels: { app: ssa-merge-demo } template: metadata: labels: { app: ssa-merge-demo } spec: containers: - name: c image: registry.k8s.io/pause:3.10 env: - name: FROM_MANAGER_B value: "b"apply ด้วย field manager สองตัวที่ต่างกันจริง ไม่มี conflict เลย
$ kubectl apply --server-side --field-manager=manager-a -f ssa-env-a.yamldeployment.apps/ssa-merge-demo serverside-applied$ kubectl apply --server-side --field-manager=manager-b -f ssa-env-b.yamldeployment.apps/ssa-merge-demo serverside-applied
$ kubectl -n internals-demo get deployment ssa-merge-demo -o jsonpath='{.spec.template.spec.containers[0].env}'[{"name":"FROM_MANAGER_A","value":"a"},{"name":"FROM_MANAGER_B","value":"b"}]environment variable ทั้งสองรอด — ไม่มี manager ตัวไหนเขียนทับอีกตัว managedFields จริง อ่านกลับมา แสดงเหตุผลตรง ๆ
$ kubectl -n internals-demo get deployment ssa-merge-demo -o yaml --show-managed-fields managedFields: - manager: manager-a operation: Apply fieldsV1: f:spec: f:template: f:spec: f:containers: k:{"name":"c"}: f:env: k:{"name":"FROM_MANAGER_A"}: .: {} f:name: {} f:value: {} f:image: {} f:name: {} - manager: manager-b operation: Apply fieldsV1: f:spec: f:template: f:spec: f:containers: k:{"name":"c"}: f:env: k:{"name":"FROM_MANAGER_B"}: .: {} f:name: {} f:value: {} f:image: {} f:name: {}อ่านตรงตัว k:{"name":"c"} แปลว่า “element ใน list containers ที่ identity key name เท่ากับ c” — ไม่ใช่ “index 0” ลึกลงไปอีกชั้น k:{"name":"FROM_MANAGER_A"} ก็แปลว่าเรื่องเดียวกันเป๊ะสำหรับ list env อีกชั้นหนึ่ง manager-a เป็นเจ้าของ env[name=FROM_MANAGER_A] manager-b เป็นเจ้าของ env[name=FROM_MANAGER_B] มันคือ field path ที่แยกจากกันในเชิงโครงสร้าง ไม่มีอะไรให้ conflict เลย เพราะ SSA ไม่เคยมองว่า env เป็น field เดียวที่ต้องเป็นเจ้าของ มันมอง element ที่มีคีย์แต่ละตัวเป็น field ของตัวเอง
marker ของ schema ที่ทำให้สิ่งนี้เป็นไปได้
หัวข้อที่มีชื่อว่า “marker ของ schema ที่ทำให้สิ่งนี้เป็นไปได้”การ address ด้วย k: ข้างบนทำงานได้เพราะ OpenAPI schema จริงบอก API server ว่า env เป็น map-type list คีย์ด้วย name — อ่านตรง ๆ จาก cluster 1.37 ที่รันอยู่จริง
$ kubectl get --raw /openapi/v3/api/v1 | jq '.components.schemas."io.k8s.api.core.v1.Container".properties.env'{ "type": "array", "items": { "$ref": "#/components/schemas/io.k8s.api.core.v1.EnvVar" }, "x-kubernetes-list-map-keys": ["name"], "x-kubernetes-list-type": "map", "x-kubernetes-patch-merge-key": "name", "x-kubernetes-patch-strategy": "merge"}เทียบกับ args output จริงจาก schema เดียวกัน property ถัดไป
$ kubectl get --raw /openapi/v3/api/v1 | jq '.components.schemas."io.k8s.api.core.v1.Container".properties.args'{ "type": "array", "items": { "type": "string" }, "x-kubernetes-list-type": "atomic"}x-kubernetes-list-type: atomic แปลว่าตรงตามตัวหนังสือ ทั้ง list คือ field เดียว เป็นเจ้าของเป็นก้อนเดียว ไม่มีการคีย์ต่อ element เลย (ไม่มี identity ให้คีย์ด้วย args เป็น string array ธรรมดา ไม่มีอะไรแยก element แต่ละตัวออกจากกันเหมือนที่ name ทำให้ env)
Atomic list พิสูจน์แล้ว: array ทั้งก้อน conflict ไม่ใช่ element เดียว
หัวข้อที่มีชื่อว่า “Atomic list พิสูจน์แล้ว: array ทั้งก้อน conflict ไม่ใช่ element เดียว”การทดลอง two-manager แบบเดียวกัน คราวนี้กับ args
apiVersion: apps/v1kind: Deploymentmetadata: name: ssa-atomic-demo namespace: internals-demospec: replicas: 1 selector: matchLabels: { app: ssa-atomic-demo } template: metadata: labels: { app: ssa-atomic-demo } spec: containers: - name: c image: registry.k8s.io/pause:3.10 args: ["--flag-a"]$ kubectl apply --server-side --field-manager=manager-a -f ssa-args-a.yamldeployment.apps/ssa-atomic-demo serverside-applied
$ kubectl apply --server-side --field-manager=manager-b -f ssa-args-b.yaml # args: ["--flag-b"]error: Apply failed with 1 conflict: conflict with "manager-a": .spec.template.spec.containers[name="c"].argsPlease review the fields above--they currently have other managers. Hereare the ways you can resolve this warning:* If you intend to manage all of these fields, please re-run the apply command with the `--force-conflicts` flag....field path ที่ conflict คือ .args ทั้งก้อน ไม่ใช่ .args[0] — manager-b ไม่ได้เป็นเจ้าของ “แค่ element ของตัวเอง” เหมือนตอนที่ทำกับ env เพราะไม่มี identity ต่อ element ให้ args คีย์ ownership ด้วย managedFields จริงของ manager-a สำหรับ object นี้ยืนยันว่ามันถือ field นี้เป็นก้อนทึบก้อนเดียว
$ kubectl -n internals-demo get deployment ssa-atomic-demo -o yaml --show-managed-fields managedFields: - manager: manager-a fieldsV1: f:spec: f:template: f:spec: f:containers: k:{"name":"c"}: f:args: {} # field ทึบก้อนเดียว -- ไม่มี k:/v: ลูก ต่างจาก env ข้างบน f:image: {} f:name: {}f:args: {} ไม่มี key ลูกเลย เทียบกับ f:env ที่มี k:{"name":...} เป็นลูกต่อ element ข้างบน — นี่คือ field-ownership tree ที่สะท้อน marker map กับ atomic ของ schema ตรง ๆ ไม่ใช่เรื่องบังเอิญของสองตัวอย่างนี้
marker ของ schema ไม่ overrides กฎ immutability ที่ hardcode ไว้
หัวข้อที่มีชื่อว่า “marker ของ schema ไม่ overrides กฎ immutability ที่ hardcode ไว้”ผลลัพธ์จริงอีกอันที่เข้าใจผิดง่าย containers เองก็เป็น x-kubernetes-list-type: map (คีย์ด้วย name เหมือน env) —
$ kubectl get --raw /openapi/v3/api/v1 | jq '.components.schemas."io.k8s.api.core.v1.PodSpec".properties.containers'{ "description": "List of containers belonging to the pod. Containers cannot currently be added or removed. ... Cannot be updated.", "type": "array", "x-kubernetes-list-map-keys": ["name"], "x-kubernetes-list-type": "map", ...}— ซึ่งดูเหมือนจะให้ manager ตัวที่สองเพิ่ม container ใหม่ทั้งตัวได้เหมือนที่ env ให้ manager ตัวที่สองเพิ่มตัวแปรใหม่ แต่มันไม่ได้ บน Pod เปล่า ๆ เพราะกฎ validation ระดับ strategy ที่ hardcode แยกต่างหาก (ไม่ใช่อะไรที่ SSA หรือ list-type ของ schema ควบคุม)
$ kubectl apply --server-side --field-manager=manager-b -f add-sidecar.yamlThe Pod "ssa-demo" is invalid: spec.containers: Forbidden: pod updates may not add or remove containersdescription ของ schema เองบอกตรง ๆ “Containers cannot currently be added or removed… Cannot be updated” — นี่คือ Kubernetes เลือกที่จะห้าม mutation นี้ทั้งหมดที่ชั้น update-validation ไม่ขึ้นกับว่า merge algorithm จะยอมให้อะไรก็ตาม list-type: map อธิบาย วิธีที่ merge รวมค่าของสอง manager ถ้า update ได้รับอนุญาตแล้ว มันไม่ได้บอกว่า update ได้รับอนุญาตหรือเปล่า (Deployment’s spec.template.spec.containers ตรงข้ามกัน เป็น template ข้างใน object ที่ mutable เต็มที่คนละตัว นี่คือเหตุผลเป๊ะที่การทดลอง env/args ข้างบนใช้ Deployment ไม่ใช่ Pod เปล่า ๆ)
structured-merge-diff: library ที่คำนวณสิ่งนี้จริง ๆ
หัวข้อที่มีชื่อว่า “structured-merge-diff: library ที่คำนวณสิ่งนี้จริง ๆ”ไม่มีการ merge ข้างบนอันไหนเป็นโค้ด Kubernetes เฉพาะทางเลย มันคือ sigs.k8s.io/structured-merge-diff (pin ไว้ที่ v6.4.2 ใน go.mod ของคอร์สนี้) library อเนกประสงค์สำหรับคำนวณ field ownership บน typed schema vendor เข้ามาใน k8s.io/apimachinery package managedfields ของ API server เอง (staging/src/k8s.io/apimachinery/pkg/util/managedfields/internal/manager.go) ห่อมันไว้หลัง interface จริงตัวเดียว
// staging/src/k8s.io/apimachinery/pkg/util/managedfields/internal/manager.go, v1.37.0type Manager interface { Apply(liveObj, appliedObj runtime.Object, managed Managed, fieldManager string, force bool) (runtime.Object, Managed, error) // ...}--server-side apply ทุกอันที่ API server ประมวลผลคือการเรียก Apply() ตัวเดียวกันนี้ ซึ่งก็คือฟังก์ชันเดียวกับที่ fieldmanager.NewManagedFieldsValidatingAdmissionController (เห็นใน excerpt จริงของ create.go ใน Admission Chain) ห่อไว้เป็นส่วนหนึ่งของ admission — การคำนวณ field-ownership ไม่ใช่ side effect ที่แปะเพิ่มบน request path มันคือขั้นตอนที่ใกล้ admission อีกขั้นที่ create/update ทุกอันวิ่งผ่าน
controller ใช้ SSA อย่างไร: Apply Configuration ใน client-go
หัวข้อที่มีชื่อว่า “controller ใช้ SSA อย่างไร: Apply Configuration ใน client-go”controller ที่เขียนเองได้พฤติกรรม merge ที่ปลอดภัยจาก conflict แบบเดียวกัน ผ่าน type Apply Configuration ที่ generate มา — DeploymentApplyConfiguration และเพื่อน ๆ (staging/src/k8s.io/client-go/applyconfigurations/apps/v1/deployment.go) สร้างขึ้นเฉพาะเพื่อแสดง “field ที่ฉันเป็นเจ้าของ” แทนที่จะเป็น object เต็ม
// staging/src/k8s.io/client-go/applyconfigurations/apps/v1/deployment.go, v1.37.0func Deployment(name, namespace string) *DeploymentApplyConfiguration { /* ... */ }func ExtractDeployment(deployment *appsv1.Deployment, fieldManager string) (*DeploymentApplyConfiguration, error) { /* ... */ }ExtractDeployment คือครึ่งการอ่านของ idea เดียวกัน มันดึงกลับจาก object จริงเฉพาะ field ที่ fieldManager ที่ระบุเป็นเจ้าของอยู่ตอนนี้ ทำให้ controller สร้าง apply ครั้งถัดไปจาก “สิ่งที่ฉันเป็นเจ้าของจริงตอนนี้” แทนที่จะสมมติว่ายังเป็นเจ้าของทุกอย่างที่ apply ไปครั้งที่แล้ว — ภาพสะท้อนฝั่ง controller ของการอ่าน managedFields เองที่พิสูจน์ตลอดบทนี้
flowchart TD schema["Live OpenAPI schema: x-kubernetes-list-type"] -->|map, คีย์ด้วย name| mapPath["env: manager-a เป็นเจ้าของ env[name=A]\nmanager-b เป็นเจ้าของ env[name=B]\nไม่ conflict (พิสูจน์ข้างบน)"] schema -->|atomic| atomicPath["args: manager-a เป็นเจ้าของ array ทั้งก้อน\nการ apply ของ manager-b CONFLICT (พิสูจน์ข้างบน)"] mapPath --> smd["structured-merge-diff คำนวณ field ownership\n(sigs.k8s.io/structured-merge-diff v6.4.2)"] atomicPath --> smd smd --> managedFields["metadata.managedFields: tree f:/k:/v: ต่อ manager"]
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”managedFields ไม่ใช่ “log ว่าใคร apply ล่าสุด” มันคือ field-ownership tree ปัจจุบัน และคำถามว่า manager สองตัวจะ conflict ไหมลดรูปเหลือคำถามเดียว schema ให้ identity แก่ element ของ list นี้ให้คีย์ ownership ด้วยไหม x-kubernetes-list-type: map (หรือ set) บอกว่าใช่ ดังนั้น structured-merge-diff ให้ ownership ที่แยกกันได้บน element ที่แยกกัน — พิสูจน์ข้างบน ไม่ conflict เลยบน env x-kubernetes-list-type: atomic บอกว่าไม่ใช่ ดังนั้นทั้ง list คือ field เดียว เจ้าของเดียวในแต่ละช่วงเวลา — พิสูจน์ข้างบน conflict จริงทันทีบน args กฎ immutability ที่ hardcode ไว้ (Pod containers) เป็นแกนที่สาม แยกอิสระโดยสิ้นเชิง มันห้าม update ได้ตรง ๆ ไม่ว่า merge algorithm จะคำนวณอะไรไว้ก็ตาม
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ชนิด list | ข้อดี | ข้อเสีย |
|---|---|---|
map (คีย์ด้วย identity เช่น env, ports, containers) | manager หลายตัวเป็นเจ้าของ element คนละตัวได้อย่างปลอดภัย ไม่ conflict เลย (พิสูจน์ข้างบน) | ต้องมี identity key จริงใน schema ไม่ใช่ทุก list จะมี |
atomic (เช่น args, command) | ownership ชัดเจน ไม่กำกวม เจ้าของเดียวต่อ field ทั้งก้อน | manager ตัวที่สองแตะ elementไหนก็ตาม บังคับให้เกิด conflict จริง แม้จะแค่เพิ่มค่าที่ดูไม่มีพิษภัย |
immutability ที่ hardcode ไว้ (ห้ามเพิ่ม/ลบ containers ของ Pod) | ป้องกันความผิดพลาดทั้งกลุ่ม “แก้รูปร่าง Pod ที่รันอยู่แบบเงียบ ๆ” | override สิ่งที่ marker list-type อย่างเดียวจะบอกว่าเป็นไปได้ พิสูจน์ข้างบน เดาผิดง่าย |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
managedFieldsก็แค่เวอร์ชัน server-side ของlast-applied-configuration” พิสูจน์ข้างบน มันคือ tree ต่อ manager ต่อ field-path คีย์ด้วย marker schema จริง ไม่ใช่ blob ก้อนเดียว manager สองตัวเป็นเจ้าของ list เดียวกันโดยไม่ conflict เลยได้ ซึ่งกลไก annotation เก่าไม่มี concept แบบนี้เลย - “ถ้า field เป็น list manager สองตัวแตะ element คนละตัวไม่มีวัน conflict” พิสูจน์ข้างบนว่าจริงแค่กับ list type
map/setการทดลองเดียวกันเป๊ะบน list แบบatomic(args) เกิด conflict จริงทันทีบน array ทั้งก้อน - “list แบบ
mapแปลว่าเปลี่ยนอะไรก็ได้เสมอ” พิสูจน์ข้างบนด้วย Podcontainersmarkerlist-type: mapของ schema เองอยู่ร่วมกับกฎที่ hardcode แยกต่างหากที่ห้ามเพิ่มหรือลบ element เลยบน object นั้นเฉพาะ - “
managedFieldsคือ logic เฉพาะทางของ Kubernetes เอง” พิสูจน์ข้างบน การคำนวณจริงคือsigs.k8s.io/structured-merge-difflibrary อเนกประสงค์ ห่อด้วย interface จริงตัวเดียวManager.Apply()ในk8s.io/apimachinery
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลา review การใช้ SSA ที่ generate มา การแก้ conflict ที่อ้างว่าถูกต้อง หรือ logic apply ของ controller เช็คสิ่งเหล่านี้
- ใช้
--force-conflictsทันทีที่เจอ conflict โดยไม่อ่านว่า field path ไหนที่มันเอ่ยชื่อ — พิสูจน์ข้างบน field path (.argsเทียบกับ.env[name=...]) บอกตรง ๆ ว่าคุณกำลังจะขโมย field แบบ atomic ทั้งก้อนจาก manager อื่น หรือแค่เคลม map key ที่แยกกันอยู่แล้ว - โค้ดที่สมมติว่า list field ไหนก็ตามรองรับ “แค่เพิ่ม element ของตัวเองอย่างปลอดภัย” — พิสูจน์ข้างบน การรับประกันนั้นมีแค่กับ list type
map/setเช็ค schema จริง (kubectl get --raw /openapi/v3/...) ก่อนจะสมมติ ไม่ใช่หลังจาก conflict โผล่มาบน production - controller ที่สร้าง apply ครั้งถัดไปจากสิ่งที่ apply ไปครั้งที่แล้ว แทนที่จะใช้
Extract*(เช่นExtractDeployment) เทียบกับ object จริง — พิสูจน์ข้างบนExtract*ให้สิ่งที่คุณเป็นเจ้าของจริง ตอนนี้ ซึ่งอาจต่างไปถ้า manager อื่นเข้าครอบครอง field ที่คุณเลิกตั้งค่าไปแล้ว - สมมติว่า
x-kubernetes-list-type: mapอย่างเดียวรับประกันว่า update ได้รับอนุญาตแน่ ๆ — พิสูจน์ข้างบนด้วย Podcontainersกฎ immutability ที่ hardcode ไว้ห้าม update ได้ตรง ๆ ไม่ขึ้นกับ merge type เลย
🐛 หา bug ในโค้ดนี้
# A teammate's fix for a recurring SSA conflict on a Deployment's argskubectl apply --server-side --force-conflicts --field-manager=my-controller -f patch.yamlเฉลย
วิธีนี้หยิบ --force-conflicts มาใช้โดยไม่เคยอ่านเลยว่า field ไหนที่ conflict เอ่ยชื่อ พิสูจน์ข้างบน บน list แบบ atomic อย่าง args field path ที่ conflict คือ array ทั้งก้อน (.spec.template.spec.containers[name="c"].args) การ force ทิ้งค่า args ของ manager อีกตัวไปเงียบ ๆ ไม่ใช่แค่ “เพิ่มของฉันเข้าไปข้าง ๆ ของเขา” แบบที่จะเกิดกับ list แบบ map อย่าง env การตอบสนองที่ถูกต้อง พิสูจน์ตลอดบทนี้ คืออ่าน field path ที่แน่ชัดของ conflict ก่อน ถ้าเป็น list แบบ atomic --force-conflicts คือการเขียนทับค่าทั้งหมดของคนอื่นจริง ตั้งใจ ไม่ใช่ merge ที่ปลอดภัย — การตัดสินใจนั้นควรมาจากคนอ่าน field เฉพาะนั้นจริง ๆ ไม่ใช่ฝังไว้ใน script ที่ยิงทุกครั้งที่เจอ conflict ไม่ว่าจะ field ไหน