Production Checklist และ Kubernetes API Client
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”workload ที่พร้อมขึ้น production ไม่ใช่ feature เดียว แต่เป็น checklist ของ control ที่แยกจากกัน ทั้ง resource limit, probe, disruption budget, RBAC แบบ least-privilege, network policy, secret ที่ encrypt จริง, autoscaling ที่ bound ไว้, replica ที่กระจายตัว และ namespace quota แต่ละอันปิดช่องโหว่คนละแบบที่อันอื่นปิดไม่ได้
เช็กลิสต์
หัวข้อที่มีชื่อว่า “เช็กลิสต์”แต่ละข้อด้านล่างปิดช่องว่างเฉพาะเจาะจง ไม่มีข้อไหน optional ใน production cluster จริง และไม่มีข้อไหนแทนที่ข้ออื่นได้
- Resource
requests/limitsตั้งทั้งคู่ ไม่ใช่แค่อันใดอันหนึ่งrequestsเป็นตัวขับ scheduling และกำหนด QoS class ของ Pod (Guaranteed,Burstable, หรือBestEffort) ซึ่ง kubelet ใช้ตัดสิน eviction order ตอนโหนดขาดแคลนทรัพยากรlimitsจำกัดว่า container ที่ runaway จะแย่งทรัพยากรจากเพื่อนบ้านได้มากแค่ไหน - readiness AND liveness probe readiness probe ควบคุมว่า Pod จะรับ traffic จาก Service หรือไม่ (แก้ปัญหา “ส่ง traffic ไปหา Pod ที่ยังไม่พร้อม”) ส่วน liveness probe ควบคุมว่า kubelet จะ restart container หรือไม่ (แก้ปัญหา “process ยังมีชีวิตอยู่แต่ hang แล้ว traffic ก็ fail ต่อเนื่อง”) ทั้งสองตัวแก้คนละปัญหา workload เลยต้องมีทั้งคู่
- PodDisruptionBudget (PDB) สำหรับ service ที่มีหลาย replica จำกัดว่า disruption แบบ voluntary (node drain, cluster upgrade) จะเอา replica ออกพร้อมกันได้กี่ตัว ทำให้ rolling node upgrade ไม่พา service ทั้งตัวเหลือ replica เป็นศูนย์โดยไม่ตั้งใจ
- RBAC scope แบบ least-privilege Role กับ RoleBinding (หรือ ClusterRole/ClusterRoleBinding) ให้สิทธิ์เฉพาะ verb กับ resource ที่ ServiceAccount ของ workload ต้องใช้จริง ไม่ใช่
cluster-adminโดย default - default-deny NetworkPolicy ตรงจุดที่เหมาะสม ถ้าไม่มี Pod ทุกตัวจะเข้าถึง Pod อื่นได้หมดโดย default policy แบบ default-deny บวก allow rule ที่ระบุชัดทำให้การเคลื่อนที่แบบ lateral จาก Pod ที่ถูก compromise ยากขึ้นมาก
- การป้องกัน secret แบบจริงจัง encryption at rest สำหรับ etcd (Secret ถูก encode ด้วย base64 ไม่ใช่ encrypt เว้นแต่จะตั้ง
EncryptionConfiguration) หรือใช้ external secret manager ไม่ใช่พึ่ง base64 อย่างเดียวเป็น security boundary - HorizontalPodAutoscaler ที่ bound ไว้อย่างเหมาะสม
minReplicasป้องกันไม่ให้ scale ลงเหลือศูนย์สำหรับ service ที่ต้อง available ตลอดเวลาmaxReplicasจำกัด cost และ load ที่ไปกระทบ dependency ปลายทาง (database และอื่น ๆ) ตอน traffic พุ่ง - replica หลายตัวกระจายข้ามโหนด/zone Pod anti-affinity หรือ topology spread constraint ทำให้โหนดเดียวหรือ availability zone เดียวล่มไม่พา replica ทุกตัวหายไปพร้อมกัน จำนวน replica เฉยๆ ไม่ได้การันตีเรื่องนี้
- ResourceQuota/LimitRange ต่อ namespace
ResourceQuotaจำกัด resource รวมของ namespace ส่วนLimitRangeให้ค่า default ต่อ container ที่สมเหตุสมผล เพื่อไม่ให้ Pod ที่ deploy โดยไม่ตั้งrequests/limitsกลายเป็นBestEffortแบบเงียบ ๆ
apiVersion: apps/v1kind: Deploymentmetadata: name: orders-apispec: replicas: 3 selector: matchLabels: app: orders-api template: metadata: labels: app: orders-api spec: affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - topologyKey: 'kubernetes.io/hostname' labelSelector: matchLabels: app: orders-api containers: - name: orders-api image: myregistry/orders-api:2.3.0 resources: requests: cpu: '250m' memory: '256Mi' limits: cpu: '500m' memory: '512Mi' readinessProbe: httpGet: path: /healthz/ready port: 8080 periodSeconds: 5 livenessProbe: httpGet: path: /healthz/live port: 8080 periodSeconds: 10---apiVersion: policy/v1kind: PodDisruptionBudgetmetadata: name: orders-api-pdbspec: minAvailable: 2 selector: matchLabels: app: orders-apiปิดวงจรด้วย programmatic verification script
หัวข้อที่มีชื่อว่า “ปิดวงจรด้วย programmatic verification script”kubectl rollout status ครอบคลุมกรณีที่ใช้แบบ interactive ได้ แต่ deployment pipeline บ่อยครั้งต้องการเช็คแบบเดียวกันในรูป code เช่น gate ขั้นตอน promotion ด้วยการเช็คว่า Deployment ถึง replica count ที่ต้องการจริง ๆ ไม่ใช่แค่ kubectl apply คืนค่าสำเร็จ KubeConfig.loadFromDefault() ของ @kubernetes/client-node resolve credential แบบเดียวกันไม่ว่าจะรัน script บนเครื่อง laptop ของวิศวกร (ผ่าน KUBECONFIG หรือ ~/.kube/config) หรือรันอยู่ใน cluster เองในฐานะ CI job หรือ Job Pod (ผ่าน ServiceAccount token ที่ mount ไว้) ไม่ต้องเขียน logic แยกกรณีเลย
import * as k8s from '@kubernetes/client-node';
const kc = new k8s.KubeConfig();// Resolves KUBECONFIG env var, then ~/.kube/config, then in-cluster ServiceAccountkc.loadFromDefault();
const appsApi = kc.makeApiClient(k8s.AppsV1Api);
async function verifyRollout(name: string, namespace: string, timeoutMs = 120_000): Promise<void> { const start = Date.now();
while (Date.now() - start < timeoutMs) { const dep = await appsApi.readNamespacedDeployment({ name, namespace });
const desired = dep.spec?.replicas ?? 0; const ready = dep.status?.readyReplicas ?? 0; const updated = dep.status?.updatedReplicas ?? 0;
console.log(`${namespace}/${name}: ${ready}/${desired} ready, ${updated}/${desired} updated`);
if (ready === desired && updated === desired) { console.log('rollout complete'); return; }
await new Promise((resolve) => setTimeout(resolve, 3000)); }
throw new Error(`rollout of ${namespace}/${name} did not complete within ${timeoutMs}ms`);}
await verifyRollout('orders-api', 'default');นี่คือกรณีที่ programmatic client ชนะ shell script ที่ห่อ kubectl อยู่ตรง ๆ ผลลัพธ์ป้อนตรงเข้าการตัดสิน pass/fail ของ pipeline ได้เลย เข้าถึง status.readyReplicas กับ status.updatedReplicas แบบมี type แทนที่จะต้อง parse text output จาก CLI
ApiException: error handling จริง ไม่ใช่ string-matching stderr
หัวข้อที่มีชื่อว่า “ApiException: error handling จริง ไม่ใช่ string-matching stderr”ทุก request ที่ fail จาก @kubernetes/client-node 2.x throw ApiException แบบมี type พร้อม HTTP status code จริงและ Status body จริงจาก API server เอง — พิสูจน์กับ Deployment ที่ไม่มีอยู่จริง
import * as k8s from '@kubernetes/client-node';
const kc = new k8s.KubeConfig();kc.loadFromDefault();const appsApi = kc.makeApiClient(k8s.AppsV1Api);
try { await appsApi.readNamespacedDeployment({ name: 'does-not-exist', namespace: 'default' });} catch (err) { if (err instanceof k8s.ApiException) { console.log(`[ApiException] code=${err.code} body=${JSON.stringify(err.body)}`); } else { throw err; }}$ npx tsx src/ssa-and-errors.ts[ApiException] code=404 body="{\"kind\":\"Status\",\"apiVersion\":\"v1\",...,\"message\":\"deployments.apps \\\"does-not-exist\\\" not found\",\"reason\":\"NotFound\",\"code\":404}\n"err.code คือ HTTP status จริง (404) และ err.body คือ object Status จริงของ API server (reason: "NotFound" รูปแบบเดียวกับที่ kubectl เอง parse) — pipeline branch ตาม err.code === 404 แปลว่า “ยังไม่ deploy” เทียบกับ err.code === 409 แปลว่า “conflict” เทียบกับอย่างอื่นคือ failure จริง ๆ ได้เลย โดยไม่ต้อง parse string error ของ CLI เลย
Server-Side Apply จาก TypeScript client — รวมถึง conflict จริง
หัวข้อที่มีชื่อว่า “Server-Side Apply จาก TypeScript client — รวมถึง conflict จริง”กลไก Server-Side Apply ที่คอร์สนี้พูดถึงสำหรับ kubectl apply --server-side ใช้ได้จาก @kubernetes/client-node 2.x ด้วย ผ่าน k8s.setHeaderOptions ตั้ง header Content-Type: application/apply-patch+yaml กับ fieldManager บน request พิสูจน์ด้วยการพยายาม SSA-patch Deployment ที่ field manager ของ Helm เองจัดการอยู่แล้ว (Release orders-orders-api เดียวกับบทเรื่อง Helm)
// (excerpt, continues from ssa-and-errors.ts above -- reuses kc/appsApi, not independently valid)const patch: k8s.V1Deployment = { apiVersion: 'apps/v1', kind: 'Deployment', metadata: { name: 'orders-orders-api', namespace: 'default' }, spec: { replicas: 4, selector: { matchLabels: { app: 'orders' } }, template: { metadata: { labels: { app: 'orders' } }, spec: { containers: [{ name: 'orders-api', image: 'nginx:1.27-alpine' }] }, }, },};
try { const result = await appsApi.patchNamespacedDeployment( { name: 'orders-orders-api', namespace: 'default', body: patch, fieldManager: 'ts-client-demo' }, k8s.setHeaderOptions('Content-Type', 'application/apply-patch+yaml'), ); console.log(`[SSA patch] replicas now ${result.spec?.replicas}`);} catch (err) { if (err instanceof k8s.ApiException) { console.log(`[SSA conflict] code=${err.code} body=${JSON.stringify(err.body)}`); }}$ npx tsx src/ssa-and-errors.ts[SSA conflict] code=409 body="{\"kind\":\"Status\",...,\"message\":\"Apply failed with 1 conflict: conflict with \\\"helm\\\": .spec.replicas\",\"reason\":\"Conflict\",\"details\":{\"causes\":[{\"reason\":\"FieldManagerConflict\",\"field\":\".spec.replicas\",...}]},\"code\":409}\n"409 Conflict จริง ระบุ field ที่ชนกันเป๊ะ (.spec.replicas) และ field manager คู่แข่งเป๊ะ ("helm") — รูปแบบเดียวกับ conflict ของ kubectl apply --server-side ที่คอร์สนี้พิสูจน์ไว้ใน foundations lesson เพราะเป็นกลไก API server ตัวเดียวกันข้างใต้ วิธีแก้ก็แบบเดียวกัน คือไม่จัดการ field ที่ manager อื่นถืออยู่แล้ว หรือใส่ force: true ใน patch options (เทียบเท่า PatchStrategy/force-conflicts) เพื่อ take ownership โดยตั้งใจ
Watch: event stream จริง ไม่ใช่ polling
หัวข้อที่มีชื่อว่า “Watch: event stream จริง ไม่ใช่ polling”k8s.Watch เปิด HTTP connection แบบ long-lived จริงไปที่ API server แล้ว stream event ADDED/MODIFIED/DELETED ตามที่เกิดขึ้นจริง — พิสูจน์ด้วยการ watch Deployment ตัวเดียวแล้ว abort connection สะอาด ๆ ด้วย timer
import * as k8s from '@kubernetes/client-node';
const kc = new k8s.KubeConfig();kc.loadFromDefault();const watch = new k8s.Watch(kc);
const req = await watch.watch( '/apis/apps/v1/namespaces/default/deployments', { fieldSelector: 'metadata.name=orders-orders-api' }, (type: string, obj: any) => { console.log(`[${type}] replicas=${obj.spec?.replicas} readyReplicas=${obj.status?.readyReplicas}`); }, (err: any) => { if (err) console.error('watch ended with error', err); },);
setTimeout(() => req.abort(), 5000);$ npx tsx src/watch-deployment.ts[ADDED] replicas=3 readyReplicas=3watch ended with error DOMException [AbortError]: This operation was abortedevent ADDED แรกส่ง state ปัจจุบันของ object มาทันทีที่ watch เปิด (pattern เดียวกับ “list แล้ว watch ต่อจาก resourceVersion นั้น” ที่ kubectl get --watch ใช้ พิสูจน์แล้วที่อื่นในคอร์สนี้) การเปลี่ยนแปลงจริงทุกครั้งหลังจากนั้นส่งมาเป็น event MODIFIED ของตัวเอง ไม่มี polling interval ให้ต้องปรับแต่ง req.abort() คือการเรียก AbortController จริง นั่นคือเหตุผลที่ error callback ทำงานด้วย AbortError จริง ไม่ใช่ปล่อยให้ watch รั่ว connection เงียบ ๆ makeInformer (พิสูจน์แล้วในบท Operators) ห่อ class Watch นี้เป๊ะไว้พร้อม local cache กับ relist อัตโนมัติ และเกือบทุกครั้งคือตัวเลือกที่ดีกว่าสำหรับ controller จริง Watch ดิบ ๆ เหมาะกับ script แบบ “stream event N วินาทีแล้วจบ” แบบนี้มากกว่า
In-cluster auth พิสูจน์จาก Pod จริง
หัวข้อที่มีชื่อว่า “In-cluster auth พิสูจน์จาก Pod จริง”KubeConfig.loadFromDefault() ที่ resolve ไปใช้ ServiceAccount token ที่ mount ไว้ตอนรันใน cluster (loadFromCluster() ข้างใน trigger อัตโนมัติเมื่อ KUBERNETES_SERVICE_HOST ถูกตั้งไว้) ไม่ใช่ black box — พิสูจน์ด้วยการอ่านไฟล์และ environment variable เดียวกันเป๊ะด้วยมือ จาก Pod จริงที่มี ServiceAccount แบบ least-privilege เล็ก ๆ
# ServiceAccount + Role + RoleBinding + Pod applied in order (real apply, not dry-run) --# under dry-run=server nothing persists between documents in the same fence, so the# Pod's serviceAccountName lookup would fail a cross-document dry-run; no k8s/ path hereapiVersion: v1kind: ServiceAccountmetadata: name: api-client-demo namespace: default---apiVersion: rbac.authorization.k8s.io/v1kind: Rolemetadata: name: api-client-demo-role namespace: defaultrules: - apiGroups: ['apps'] resources: ['deployments'] verbs: ['get', 'list']---apiVersion: rbac.authorization.k8s.io/v1kind: RoleBindingmetadata: name: api-client-demo-binding namespace: defaultsubjects: - kind: ServiceAccount name: api-client-demo namespace: defaultroleRef: kind: Role name: api-client-demo-role apiGroup: rbac.authorization.k8s.io---apiVersion: v1kind: Podmetadata: name: api-client-demo namespace: defaultspec: serviceAccountName: api-client-demo containers: - name: curl image: curlimages/curl:8.11.1 command: ['sleep', '3600']$ kubectl exec api-client-demo -- sh -c 'TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)curl -sS --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ -H "Authorization: Bearer $TOKEN" \ "https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT/apis/apps/v1/namespaces/default/deployments/orders-orders-api" \ | head -c 400'{ "kind": "Deployment", "apiVersion": "apps/v1", "metadata": { "name": "orders-orders-api", ...นั่นคือทั้งหมดที่ loadFromCluster() ทำให้อัตโนมัติ คือ อ่าน token จาก /var/run/secrets/kubernetes.io/serviceaccount/token, CA จาก ca.crt ที่อยู่ข้าง ๆ กัน, และ address ของ API server จาก KUBERNETES_SERVICE_HOST/KUBERNETES_SERVICE_PORT — ไม่มี configuration ภายนอก ไม่มีไฟล์ kubeconfig ใน Pod เลย RBAC มีผลแบบเดียวกันเป๊ะกับตัวตนอื่นใด ๆ — พิสูจน์ด้วยการลอง verb ที่ Role ไม่ได้ให้
$ kubectl exec api-client-demo -- sh -c '... -X DELETE "https://.../deployments/orders-orders-api"'{ "kind": "Status", "status": "Failure", "message": "deployments.apps \"orders-orders-api\" is forbidden: User \"system:serviceaccount:default:api-client-demo\" cannot delete resource \"deployments\" in API group \"apps\" in the namespace \"default\"", "reason": "Forbidden", "code": 403}403 จริง ระบุตัวตน, verb, resource, namespace ตรง ๆ — Role ของ ServiceAccount api-client-demo ให้แค่ get/list ดังนั้น delete ถูก reject ที่ authorization layer เหมือนกับที่บท RBAC พิสูจน์ไว้กับตัวตนอื่นทุกตัวในคอร์สนี้เป๊ะ loadFromCluster() (และโดยผลพวง loadFromDefault() ตอนรันใน Pod) ไม่ใช่ authorization system คนละแบบ — มันคือตัวตนแบบ bound-token ที่ RBAC เช็คเดียวกันเป๊ะกับที่คอร์สนี้พูดถึงตลอด แค่เข้าถึงผ่าน bootstrap path ที่ต่างออกไปนิดหน่อย
Production checklist อัปเกรดด้วยสิ่งที่คอร์สนี้พิสูจน์แล้วตอนนี้
หัวข้อที่มีชื่อว่า “Production checklist อัปเกรดด้วยสิ่งที่คอร์สนี้พิสูจน์แล้วตอนนี้”แต่ละข้อด้านล่างลิงก์ไปยังบทที่พิสูจน์มันแบบสด ๆ ไม่ใช่แค่บรรยาย
- requests/limits ของ resource และ QoS class — พิสูจน์ใน The Scheduler and Resource Requests
- readiness และ liveness probe และ failure mode ของ restart loop จริงตอนชี้ทั้งคู่ไปที่ endpoint เดียวกัน — พิสูจน์ใน Probes: Liveness, Readiness, Startup
preStophook และ termination grace period — พิสูจน์ใน Pods: The Atomic Unit- PodDisruptionBudget และการที่มัน block voluntary drain แต่ไม่ block preemption — พิสูจน์ใน Pod Disruption and Cluster Autoscaling
- topology spread constraint และ
nodeTaintsPolicy: Honor— พิสูจน์ใน Node Affinity, Taints, and Tolerations - Pod Security Admission (
restricted) และ ValidatingAdmissionPolicy — พิสูจน์ใน Pod and Cluster Security - RBAC แบบ least-privilege, bound ServiceAccount token, และ anti-pattern ของ
cluster-admin— พิสูจน์ใน RBAC and Service Accounts และพิสูจน์ซ้ำด้านบนด้วย403จริง - วินัยของ field-manager ใน Server-Side Apply — พิสูจน์ใน kubectl and Declarative YAML และพิสูจน์ซ้ำด้านบนด้วย
409จริงจาก TypeScript client ตัวนี้เอง - HorizontalPodAutoscaler ที่ bound ไว้อย่างเหมาะสม — พิสูจน์ใน Horizontal Pod Autoscaling
flowchart TB dep["Deployment: orders-api (3 replicas, spread across nodes)"] dep --> probes["Readiness + liveness probes"] dep --> resources["requests/limits (QoS)"] dep --> pdb["PodDisruptionBudget (minAvailable: 2)"] dep --> rbac["RBAC: least-privilege ServiceAccount"] dep --> netpol["Default-deny NetworkPolicy + explicit allows"] dep --> hpa["HorizontalPodAutoscaler (bounded min/max)"] ns["Namespace"] --> quota["ResourceQuota / LimitRange"] quota --> dep
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”@kubernetes/client-node ไม่ใช่ authorization system คนละแบบจาก kubectl — พิสูจน์แล้วด้านบน ApiException code ของมัน, conflict ของ Server-Side Apply ของมัน, และ in-cluster bootstrap ของมันทั้งหมด resolve ไปที่กลไก API server เดียวกันเป๊ะ (object Status, field manager, bound ServiceAccount token, RBAC) ที่คอร์สนี้พิสูจน์ตลอดทั้งเล่มด้วย kubectl คุณค่าทั้งหมดของ TypeScript client คือ การเข้าถึงกลไกเดียวกันนั้นแบบ typed และ programmatic — err.code === 409 แทนที่จะ grep stderr หา “conflict”, result.spec?.replicas แทนที่จะ parse output ของ kubectl get -o jsonpath ไม่ใช่กฎคนละชุดข้างใต้เลย
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
kubectl ใน shell pipeline | เขียนเร็ว ไม่ต้องติดตั้ง dependency | พิสูจน์แล้วด้านบน: error handling แปลว่าต้อง parse text output 404 กับ 403 ดูเหมือนกันจนกว่าจะ grep string message |
@kubernetes/client-node typed client | พิสูจน์แล้วด้านบน: ApiException.code จริง, result.spec?.readyReplicas แบบมี type ไม่ต้อง parse string | ต้องมี Node/TS toolchain ใน pipeline setup มากกว่าเรียก kubectl เปล่า ๆ |
class Watch ดิบ ๆ | พิสูจน์แล้วด้านบน: เรียบง่าย event stream จริงสำหรับ script อายุสั้น | ไม่มี cache ไม่มี relist อัตโนมัติตอน error — พิสูจน์แล้วในบท Operators ว่า makeInformer เพิ่มทั้งสองอย่าง |
makeInformer (client-node) | local cache + relist-on-error อัตโนมัติ ตัวเลือกที่ถูกต้องสำหรับ controller ที่รันยาว | ชิ้นส่วนเคลื่อนไหวมากกว่า Watch ครั้งเดียวสำหรับ script ที่แค่ต้อง observe สั้น ๆ |
loadFromDefault() ทุกที่ (laptop และ in-cluster) | พิสูจน์แล้วด้านบน: code path เดียวกันเป๊ะ ไม่ต้องแยก logic ตาม environment ใน application code | RBAC ที่ ServiceAccount ใน cluster ถืออยู่ยังต้อง scope ให้ตั้งใจอยู่ดี — พิสูจน์แล้วด้านบน Role ที่กว้างเกินไปอันตรายพอ ๆ กับ kubeconfig บน laptop ที่มีสิทธิ์เกินจำเป็น |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “error ของ client library ก็แค่ text อะไรก็ตามที่ API server บังเอิญคืนมา” พิสูจน์แล้วด้านบน:
ApiException.codeคือ HTTP status ที่มีโครงสร้างจริง (404,409) และ.bodyคือ objectStatusที่ API server parse จริง — pipeline branch ตามมันได้อย่างเชื่อถือได้ ไม่ใช่ fuzzy-match string - “conflict ของ Server-Side Apply เป็นแนวคิดของ kubectl เท่านั้น TypeScript client ไม่มี” พิสูจน์แล้วด้านบนด้วย
409จริง: กลไก field-manager conflict เดียวกันเป๊ะมีผลกับ client ไหนก็ตามที่ส่ง apply-patch content type ไม่ใช่แค่kubectl - “Watch กับ Informer คือของเดียวกัน แค่ชื่อต่างกัน” พิสูจน์แล้วด้านบนและในบท Operators:
Watchคือ event stream ดิบ ๆ ไม่มี cache ไม่มี reconnect logicmakeInformerห่อทั้งสองอย่างไว้ นั่นคือเหตุผลที่ controller จริงเกือบทุกครั้งควรเลือกอย่างหลัง - “loadFromCluster() ต้องทำอะไรที่ต่างหรือมีสิทธิ์มากกว่า ServiceAccount ปกติ” พิสูจน์แล้วด้านบนด้วยการอ่านไฟล์ token/CA เดียวกันด้วยมือผ่าน curl: มันคือกลไก bound-token ที่ RBAC เช็คเดียวกันเป๊ะกับ ServiceAccount อื่นใด แค่เข้าถึงผ่าน bootstrap path ที่ต่างออกไป ไม่ใช่การ escalate แบบพิเศษ
- “ถ้า client library call ไม่ throw แปลว่า operation สำเร็จตามที่ตั้งใจไว้เต็มที่แล้ว” Server-Side Apply patch ที่สำเร็จยังสามารถทิ้ง field ที่ caller ตั้งใจจะ set ไปแบบเงียบ ๆ ได้ ถ้า field manager อื่นถือ field นั้นอยู่แล้วและไม่เกิด conflict เพราะค่าที่ตั้งบังเอิญตรงกัน — พิสูจน์โดยอ้อมด้านบน conflict จะโผล่ก็ต่อเมื่อ ค่า ต่างกันจริง ไม่ใช่แค่ manager สองตัวอ้างถึง field เดียวกัน
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review TypeScript/Node code ที่คุยกับ Kubernetes API ที่ AI สร้าง ให้เช็ค
try/catchเปล่า ๆ ที่ logerr.messageหรือString(err)แทนที่จะเช็คerr instanceof k8s.ApiExceptionแล้ว branch ตามerr.code— พิสูจน์แล้วด้านบน code กับ body ที่มีโครงสร้างอยู่ตรงนั้นแล้ว เชื่อถือได้กว่า string matching มาก- code แบบ
@kubernetes/client-node1.x ที่ใช้ positional argument (เช่นreadNamespacedDeployment(name, namespace)) แทนที่จะเป็นรูปแบบ object-parameter ของ 2.x (readNamespacedDeployment({ name, namespace })) ที่ใช้ตลอดคอร์สนี้ — code ของ assistant ที่ train มาจาก 1.x จะ type-check ไม่ผ่านกับ install ของ 2.x - Server-Side Apply patch ที่ไม่ตั้ง
fieldManagerเลย หรือใช้ตัวเดียวกันซ้ำข้าม logical owner ที่ต่างกันจริง — พิสูจน์แล้วด้านบน conflict message ระบุ field manager ที่ผิดตรงตัวเป๊ะ ดังนั้นตัวที่ไม่ตั้งหรือใช้ร่วมกันทำให้ attribute conflict จริงย้อนหลังยากขึ้นมาก - code ที่ใช้ polling loop เขียนเอง (
setInterval+readNamespacedDeployment) เพื่อจับการเปลี่ยนแปลง ทั้งที่WatchหรือmakeInformer(พิสูจน์แล้วทั้งคู่ด้านบน) ส่งข้อมูลเดียวกันแบบ push จริงทันทีที่เกิดขึ้น code น้อยกว่าและไม่ต้องปรับ polling-interval trade-off เลย - ServiceAccount ของ Pod ที่ได้
cluster-admin“เพื่อให้แน่ใจว่า client library มีสิทธิ์ที่ต้องการแน่ ๆ” — พิสูจน์แล้วด้านบนด้วย403จริง RBAC มีผลกับ client เหมือนกับตัวตนอื่นใด ๆ เป๊ะ วิธีแก้คือ Role ที่ scope แคบ ระบุแค่ verb ที่ code เรียกใช้จริง KubeConfig.loadFromDefault()ที่ถูกห่อด้วย branchif (inCluster) { loadFromCluster() } else { loadFromDefault() }— พิสูจน์แล้วด้านบนloadFromDefault()ทำ resolution นี้ให้เองข้างในอยู่แล้ว branch นี้คือ code ที่ไม่จำเป็นและอาจ diverge จากสิ่งที่ library เองทำแบบเงียบ ๆ ได้
🐛 หา bug ในโค้ดนี้
import * as k8s from '@kubernetes/client-node';
const kc = new k8s.KubeConfig();kc.loadFromDefault();const appsApi = kc.makeApiClient(k8s.AppsV1Api);
async function isRolledOut(name: string, namespace: string): Promise<boolean> {try { const dep = await appsApi.readNamespacedDeployment({ name, namespace }); return dep.status?.readyReplicas === dep.spec?.replicas;} catch (err) { console.log('rollout check failed: ' + err); return false;}}เฉลย
catch block กลืน error ทุกแบบให้กลายเป็น return false เหมือนกันหมด พร้อม log string เฉย ๆ ไม่ว่า Deployment จะยังไม่มีอยู่จริง (404 จริงและคาดหวังได้ช่วงต้นของ rollout), caller ไม่มีสิทธิ์ (403 จริงที่ควร fail ดัง ๆ ไม่ใช่รายงานเงียบ ๆ ว่า “ยังไม่ rolled out”), หรือ API server เข้าไม่ถึงเลย พิสูจน์แล้วด้านบน err instanceof k8s.ApiException ให้ err.code ที่มีโครงสร้างจริงไว้ branch ได้ — วิธีแก้คือแยก err.code === 404 (ยังไม่ถูกสร้างจริง ๆ poll ต่อไป) ออกจากอย่างอื่น (rethrow หรือ alert) แทนที่จะยุบทุก failure mode ให้เหลือ false เดียวกันหมด