GitOps และ CI/CD สำหรับ Kubernetes
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”GitOps ทำให้ Git repository เป็น single source of truth ของ desired state ของ cluster โดยมี reconciler ที่รันอยู่ใน cluster คอย pull แล้ว apply state นั้น แทนที่จะให้ CI job push state เข้ามาจากภายนอก
Push-based CI/CD เทียบกับ pull-based GitOps
หัวข้อที่มีชื่อว่า “Push-based CI/CD เทียบกับ pull-based GitOps”CI/CD pipeline แบบดั้งเดิมเป็น push-based CI job build artifact เสร็จแล้วรัน kubectl apply หรือ helm upgrade ตรงเข้า cluster เลย โดยใช้ credential ของ cluster ที่ตัว CI system เก็บไว้เอง ทำงานได้ก็จริง แต่หมายความว่า CI system ซึ่งบ่อยครั้งเป็น SaaS ของบุคคลที่สามที่อยู่นอก infrastructure boundary ของเรา ต้องมี access เข้า cluster แบบถาวรและมักเป็น privileged access ด้วย
GitOps กลับทิศทางนี้ reconciler อย่าง Argo CD หรือ Flux รันอยู่ ภายใน cluster ที่จัดการ คอย watch Git repository ที่เก็บ manifest หรือ Helm values ที่ต้องการอยู่ตลอด พอ repository เปลี่ยน reconciler จะ pull desired state ใหม่แล้ว apply เอง cluster เป็นฝ่ายเอื้อมไปหา Git ไม่ใช่ Git เอื้อมเข้ามาใน cluster
# Push-based: CI applies directly, using credentials CI itself holdskubectl apply -f manifests/helm upgrade myapp ./mychart -f values-prod.yaml
# Pull-based (GitOps): CI's job stops at committing to Git;# an in-cluster reconciler (Argo CD / Flux) takes it from theregit commit -am "bump myapp image tag to 1.5.0"git push origin mainทำไม pull-based model ถึงคุ้มค่า
หัวข้อที่มีชื่อว่า “ทำไม pull-based model ถึงคุ้มค่า”ย้ายขั้นตอน apply เข้าไปอยู่ใน cluster ได้ประโยชน์จริงสามอย่าง
- ไม่มี external system ตัวไหนต้องถือ credential ระดับ cluster-admin งานของ CI system จบแค่
git pushมีแค่ reconciler ที่อยู่ใน cluster ซึ่งเราคุมเองเท่านั้นที่ authenticate กับ API server ด้วยสิทธิ์เขียน - ตรวจจับ drift ได้ เพราะ reconciler เทียบสถานะจริงของ cluster กับสถานะที่ Git ประกาศไว้อยู่ตลอด การแก้แบบ out-of-band ด้วย
kubectl editหรือkubectl scaleจะถูกจับได้ และสามารถ revert กลับอัตโนมัติ แทนที่จะเบี่ยงเบนไปจากสิ่งที่ Git บอกไว้แบบเงียบ ๆ - มีประวัติที่ตรวจสอบได้ในตัว ทุกการเปลี่ยน desired state คือ Git commit หนึ่งอัน ใครเปลี่ยนอะไร เมื่อไหร่ ทำไม (ผ่าน commit message) อยู่ในประวัติเดียวกับที่เราใช้ review code อยู่แล้ว
รูปแบบ pipeline ที่ใช้งานจริง
หัวข้อที่มีชื่อว่า “รูปแบบ pipeline ที่ใช้งานจริง”การ build กับ deploy แบบ GitOps แยกเป็นสอง repository (หรือสอง stage ที่แยกกันชัดเจน) หนึ่งสำหรับ build แอป อีกหนึ่งสำหรับประกาศว่าควรรันอะไรอยู่
# Stage 1 — application repo CI: build and publish the imagedocker build -t myregistry/myapp:1.5.0 .docker push myregistry/myapp:1.5.0
# Stage 2 — update the manifests/values repo that GitOps watches# (a small CI step, or a bot, edits the tag and commits)sed -i 's/tag: .*/tag: "1.5.0"/' values-prod.yamlgit commit -am "myapp: bump image tag to 1.5.0"git push origin main
# From here, the GitOps reconciler (not CI) notices the commit and applies it.# In a push-based setup, stage 2 would instead run directly against the cluster:# helm upgrade myapp ./mychart -f values-prod.yamlใน flow แบบ GitOps ตัว CI system ไม่เคยรัน kubectl หรือ helm upgrade เข้า cluster จริงเลย หน้าที่ของตัวเองจบแค่ build image แล้ว commit desired state ใหม่เข้า Git
kubectl kustomize: base + overlay พิสูจน์ด้วย rename ที่มี hash suffix จริง
หัวข้อที่มีชื่อว่า “kubectl kustomize: base + overlay พิสูจน์ด้วย rename ที่มี hash suffix จริง”ทั้ง Argo CD และ Flux render manifest ของ Kubernetes ธรรมดาจาก source tree ก่อนจะ apply อะไรเลย — โมเดล base/overlay ของ Kustomize คือรูปแบบที่พบบ่อยที่สุดของ source tree นั้น และ kubectl kustomize (Kustomize ฝังอยู่ใน kubectl แล้ว) render มันได้โดยไม่ต้องใช้ tool เพิ่ม base ประกาศ Deployment กับ configMapGenerator (ตั้งใจ ไม่ใช่ ConfigMap แบบ literal ใน resources: — แบบนั้นจะปิด hash suffix) overlay prod patch replicas แล้วเติม key เพิ่มลงบน ConfigMap ที่ generate มา
$ kubectl kustomize base/apiVersion: v1data: LOG_LEVEL: infokind: ConfigMapmetadata: name: orders-api-config-hf678c7m2b---apiVersion: apps/v1kind: Deploymentmetadata: name: orders-apispec: replicas: 1 ... containers: - envFrom: - configMapRef: name: orders-api-config-hf678c7m2b image: myregistry/orders-api:1.0.0 name: orders-api
$ kubectl kustomize overlays/prod/apiVersion: v1data: LOG_LEVEL: warnkind: ConfigMapmetadata: name: orders-api-config-dfk4bdbtkk---apiVersion: apps/v1kind: Deploymentmetadata: name: orders-apispec: replicas: 5 ... containers: - envFrom: - configMapRef: name: orders-api-config-dfk4bdbtkk image: myregistry/orders-api:1.0.0 name: orders-apiมีสามอย่างที่พิสูจน์พร้อมกันตรงนี้ คือ strategic-merge patch ของ replicas: 5 apply ทับ replicas: 1 ของ base ได้สะอาด ชื่อของ ConfigMap ที่ generate มาเปลี่ยน hash suffix (-hf678c7m2b → -dfk4bdbtkk) ทันทีที่ data ของมันเปลี่ยน (LOG_LEVEL: info → warn ผ่าน behavior: merge ที่เติมทับ generator ของ base) และ envFrom.configMapRef.name ของ Deployment เองถูก rewrite ให้อัตโนมัติตาม hash ใหม่ทั้งสองครั้ง โดยไม่มีการแก้ cross-reference ด้วยมือที่ไหนใน overlay เลย การ rename-and-rewire อัตโนมัตินี้แหละคือกลไกที่บังคับให้เกิด rolling update จริงทุกครั้งที่ เนื้อหา ของ ConfigMap เปลี่ยน — ไม่ต้องมี restart step แยก ไม่ต้อง kubectl rollout restart เพราะ Pod template ของ Deployment เปลี่ยนจริง ๆ
Server-side apply คือสิ่งที่ Argo CD กับ Flux ใช้อยู่ข้างใน
หัวข้อที่มีชื่อว่า “Server-side apply คือสิ่งที่ Argo CD กับ Flux ใช้อยู่ข้างใน”กลไก Server-Side Apply ที่คอร์สนี้พูดถึงไปแล้วสำหรับ kubectl apply --server-side ไม่ใช่แค่ทางเลือกที่ reconciler อาจจะ ใช้ก็ได้ — มันคือกลไกที่เครื่องมือ GitOps ถูกสร้างบนมันเลย เพราะ reconciler ที่จัดการ resource เป็นร้อยตัวข้าม commit จำนวนมากต้องใช้ conflict tracking ของ API server เอง (managedFields, field manager ที่ตั้งชื่อไว้) แทนที่จะ diff แบบ three-way ฝั่ง client เทียบกับ last-applied-configuration ที่ cache ไว้ในเครื่อง นี่ไม่ใช่คำอ้างจาก docs ที่เชื่อไปเฉย ๆ — มันโผล่ขึ้นมาตรง ๆ ตอนติดตั้ง manifest upstream ของ Argo CD เองบน cluster ของบทนี้
$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml...The CustomResourceDefinition "applicationsets.argoproj.io" is invalid: metadata.annotations: Too long: may not be more than 262144 bytes
$ kubectl apply -n argocd -f install.yaml --server-side --field-manager=argocd-installer...Apply failed with 1 conflict: conflict with "kubectl-client-side-apply" using networking.k8s.io/v1: .spec.ingresserror แรกคือ ข้อจำกัดขนาด annotation ของ client-side apply ที่รู้จักกันดี คือ kubectl apply โหมด classic ยัด object ทั้งตัวลงใน annotation kubectl.kubernetes.io/last-applied-configuration และ CRD ของ Argo CD เองก็ใหญ่พอที่จะทะลุเพดาน annotation ของ etcd ที่ราว 256KiB — ซึ่งเป็นเหตุผลที่ docs การติดตั้งของทั้ง Argo CD และ Flux แนะนำให้ใช้ --server-side กับ manifest ของตัวเอง พอสลับมาใช้ --server-side ก็เจอ conflict จริงตัวที่สองทันที ที่ foundations lesson ของคอร์สนี้พูดถึงลึกไปแล้ว คือ .spec.ingress ของ NetworkPolicy มี field manager kubectl-client-side-apply ติดมาจาก apply ครั้งแรก (ที่สำเร็จบางส่วน) อยู่แล้ว และ server-side apply ปฏิเสธที่จะแอบขโมย ownership ของ field ที่ manager เดิมยังถืออยู่ — ทางออกแบบเดียวกับ --force-conflicts ที่ใช้ได้ทุกที่ที่ server-side apply เจอสถานการณ์แบบนี้
ติดตั้ง GitOps reconciler จริงบน kind แล้วพิสูจน์ reconcile จริง
หัวข้อที่มีชื่อว่า “ติดตั้ง GitOps reconciler จริงบน kind แล้วพิสูจน์ reconcile จริง”Argo CD ติดตั้งจาก manifest upstream ไฟล์เดียว และขึ้นสำเร็จบน kind cluster production ของบทนี้ในเวลาไม่ถึง 5 นาที หลัง step server-side-apply ด้านบนสำเร็จ — controller Pod ทุกตัว Running
$ kubectl -n argocd rollout status deployment/argocd-server --timeout=180sdeployment "argocd-server" successfully rolled out
$ kubectl -n argocd get podsNAME READY STATUS RESTARTS AGEargocd-application-controller-0 1/1 Running 0 90sargocd-applicationset-controller-... 1/1 Running 0 90sargocd-dex-server-... 1/1 Running 0 90sargocd-notifications-controller-... 1/1 Running 0 90sargocd-redis-... 1/1 Running 0 90sargocd-repo-server-... 1/1 Running 0 90sargocd-server-... 1/1 Running 0 90scustom resource Application (ไม่ต้องใช้ CLI argocd เลย — มันเป็นแค่ CRD instance) ที่ชี้ไปยัง public git repository จริง พิสูจน์ reconcile แบบ end-to-end ได้จริง โดยไม่ต้องมี git daemon ในเครื่องเลย
# Application CRD instance -- argoproj.io is only installed on this lesson's own# Argo CD cluster, not the shared harness cluster, so this excerpt carries no k8s/ pathapiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: name: guestbook namespace: argocdspec: project: default source: repoURL: https://github.com/argoproj/argocd-example-apps.git targetRevision: HEAD path: guestbook destination: server: https://kubernetes.default.svc namespace: guestbook syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true$ kubectl apply -f guestbook-app.yamlapplication.argoproj.io/guestbook created
$ kubectl -n argocd get application guestbook -o jsonpath='{.status.sync.status} {.status.health.status}'Synced Progressing # a few seconds later:Synced HealthyDrift detection กับ self-heal พิสูจน์ด้วยการ scale out-of-band ด้วยมือ
หัวข้อที่มีชื่อว่า “Drift detection กับ self-heal พิสูจน์ด้วยการ scale out-of-band ด้วยมือ”syncPolicy.automated.selfHeal: true ไม่ใช่แค่คำอ้างใน values file — พิสูจน์ด้วยการ scale Deployment ที่ reconcile ไว้แล้วโดยไม่บอก Argo CD แล้วดูมันถูก revert กลับ โดยไม่มีมนุษย์ไปรันอะไรซ้ำเลย
$ kubectl -n guestbook get deployment guestbook-ui -o jsonpath='{.spec.replicas}'1
$ kubectl -n guestbook scale deployment guestbook-ui --replicas=3deployment.apps/guestbook-ui scaled
$ kubectl -n guestbook get deployment guestbook-ui -o jsonpath='{.spec.replicas}'1ภายในประมาณสิบวินาทีหลัง scale ด้วยมือ spec.replicas กลับมาเป็น 1 — ค่าที่ Git ประกาศไว้จริง ๆ — โดย Argo CD ริเริ่มเองล้วน ๆ syncPolicy.automated.prune: true ทำแบบเดียวกันกับ resource ที่ ถูกลบ ออกจาก Git — ถ้าไม่มีมัน การลบ manifest ออกจาก source repo จะปล่อย object ของ cluster ที่ตรงกันค้างอยู่ตลอดไป เพราะ reconciler โดย default จะแค่เพิ่มกับอัปเดตเท่านั้น ไม่เคยลบ นอกจากจะบอกให้ prune ชัดเจน
Image update automation (docs-only — บทนี้ไม่ได้ push image เข้า registry จริง)
หัวข้อที่มีชื่อว่า “Image update automation (docs-only — บทนี้ไม่ได้ push image เข้า registry จริง)”loop ที่สมบูรณ์จะปิด pipeline ที่แสดงไว้ก่อนหน้านี้ด้วยการให้ reconciler — ไม่ใช่ CI — หยิบ image tag ใหม่มาเองอัตโนมัติ คู่มือ image update automation ของ Flux อธิบาย CRD สามตัวทำงานร่วมกัน คือ ImageRepository ที่ scan registry หา tag ใหม่, ImagePolicy ที่ “select[s] the latest tag based on the defined policy (semver, calver, regex)” และ ImageUpdateAutomation ที่ “replace[s] the tag in Kubernetes manifests” แล้ว commit การเปลี่ยนแปลงกลับเข้า Git เอง ปิด loop โดยไม่ต้องมีมนุษย์หรือ CI job มาเขียน tag ใหม่ด้วยมือเลย คอร์สนี้ไม่ได้ push image จริงเข้า registry ดังนั้นส่วนนี้ยังเป็นแค่ citation จาก docs ไม่ใช่การรันที่พิสูจน์แล้ว
รูปร่างของ CI pipeline, kubectl diff ใน CI, และการ lint manifest ของคอร์สนี้เอง
หัวข้อที่มีชื่อว่า “รูปร่างของ CI pipeline, kubectl diff ใน CI, และการ lint manifest ของคอร์สนี้เอง”CI pipeline ที่ใช้งานจริงสำหรับ cluster ที่จัดการด้วย GitOps มีสี่ stage คือ build image ของแอป, push เข้า registry, bump image reference ใน repository ของ manifest/values (ด้วยมือ, ด้วย script เล็ก ๆ, หรือด้วย image-automation controller ด้านบน), แล้วให้ reconciler reconcile ต่อ มี verification step สองอย่างที่ควรอยู่ใน pipeline นั้น ก่อน ที่การเปลี่ยนแปลงจะไปถึง reconciler
kubectl diff -f <rendered manifests>(หรือkubectl kustomize overlays/prod | kubectl diff -f -) — การเปรียบเทียบจริงกับสิ่งที่รันอยู่ตอนนี้ ใช้กลไกเดียวกับkubectl apply --dry-run=serverที่พิสูจน์ตลอดคอร์สนี้ จับ field ที่เปลี่ยนโดยไม่ตั้งใจได้ (resources.requestsหายไป,replicasลดลง) ก่อนที่จะ merge- static manifest linter ที่รันโดยไม่ต้องติดตั้งในเครื่องเลย ผ่าน
docker runหรือnpx— พิสูจน์ตรง ๆ กับ output จริงของoverlays/prodที่ render ในบทนี้เอง
$ kubectl kustomize overlays/prod > rendered-prod.yaml$ docker run --rm -i ghcr.io/yannh/kubeconform:latest -summary - < rendered-prod.yamlSummary: 2 resources found parsing stdin - Valid: 2, Invalid: 0, Errors: 0, Skipped: 0
$ docker run --rm -v "$(pwd):/work" -w /work stackrox/kube-linter:latest lint rendered-prod.yamlError: found 5 lint errorsKubeLinter 0.8.3.../rendered-prod.yaml: (object: orders-api apps/v1, Kind=Deployment) object has 5 replicas but does not specify inter pod anti-affinity (check: no-anti-affinity, ...).../rendered-prod.yaml: (object: orders-api apps/v1, Kind=Deployment) container "orders-api" does not have a read-only root file system (check: no-read-only-root-fs, ...).../rendered-prod.yaml: (object: orders-api apps/v1, Kind=Deployment) container "orders-api" is not set to runAsNonRoot (check: run-as-non-root, ...).../rendered-prod.yaml: (object: orders-api apps/v1, Kind=Deployment) container "orders-api" has cpu request 0 (check: unset-cpu-requirements, ...).../rendered-prod.yaml: (object: orders-api apps/v1, Kind=Deployment) container "orders-api" has memory limit 0 (check: unset-memory-requirements, ...)kubeconform ยืนยันว่า manifest ถูกต้องเชิงโครงสร้างตาม OpenAPI schema ของ Kubernetes (schema อย่างเดียว — ไม่มีความเห็นเรื่อง security posture เลย) kube-linter (หรือ Polaris เครื่องมือคล้ายกัน) จับช่องโหว่ด้าน production-hardening ที่ production checklist ของคอร์สนี้เอง ระบุไว้พอดี คือ ไม่มี anti-affinity, ไม่มี read-only root filesystem, ไม่มี non-root user, ไม่มี resource requests/limits — จับได้กับ manifest ที่บทนี้เขียนไว้ก่อนหน้าด้วยความตั้งใจดี ทั้งสอง tool รันโดยไม่ต้องติดตั้งในเครื่องเลย ตรงจาก container image ที่ publish ไว้ ซึ่งเป็นวิธีเดียวกับที่ CI runner จะใช้มันด้วย
flowchart LR dev["Developer"] -->|"push manifest / values change"| git["Git repository (desired state)"] ci["CI pipeline"] -->|"build & push image"| registry["Container registry"] git -->|"watched / polled"| reconciler["In-cluster GitOps reconciler (Argo CD / Flux)"] reconciler -->|"apply diff"| cluster["Live cluster state"] cluster -->|"pulls referenced image"| registry
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”GitOps ไม่ใช่กลไก apply ใหม่ — พิสูจน์แล้วตลอดบทนี้ มันคือ kubectl kustomize/server-side-apply machinery เดียวกับที่คอร์สนี้พูดถึงไปแล้ว แค่ถูก trigger ด้วย Git watch loop แทนที่จะเป็นมนุษย์หรือ CI job ทุกอย่างที่ดูแตกต่างของมันมาจาก credential อยู่ที่ไหนและใครเป็นคนกดปุ่ม คือ reconciler ในเครื่อง cluster ที่มี field manager ของตัวเอง คอย diff Git กับ live state ตลอด (พิสูจน์แล้วด้านบนด้วยการ revert จริงภายในไม่กี่วินาทีหลัง scale แบบ out-of-band) แทนที่จะเป็น CI job ภายนอกที่ถือ write access ประจำ โมเดล base/overlay/generator ของ Kustomize เป็นวิธีที่พบบ่อยที่สุดในการอธิบาย “อะไรควรรันอยู่” เป็น source tree ที่ Git เก็บได้ reconciler ก็แค่สิ่งที่คอยทำให้ source tree นั้นเป็นจริงตลอดเวลา
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
| Push-based CI/CD | ง่าย pipeline เดียวทำทุกอย่าง ไม่ต้องมี component ใน cluster | พิสูจน์แล้วด้านบนเชิงแนวคิด: CI ถือ credential ของ cluster แบบถาวร มักเป็น privileged ด้วย |
| Pull-based GitOps (Argo CD/Flux) | พิสูจน์แล้วด้านบน: external system ไม่ต้องถือ cluster-admin เลย drift ถูกจับและ revert อัตโนมัติ | ต้องรันและดูแลความปลอดภัยของ reconciler เองใน cluster |
configMapGenerator (มี hash suffix) | พิสูจน์แล้วด้านบน: content เปลี่ยนบังคับ rolling update จริงอัตโนมัติ ไม่ต้อง restart ด้วยมือ | manifest ทุกอันที่อ้างถึง ConfigMap ต้องใช้ชื่อที่ generate มา ไม่ใช้ชื่อ hardcode เด็ดขาด |
kubectl apply (client-side, default) | ง่าย ไม่ต้องเรียนรู้แนวคิด field-manager | พิสูจน์แล้วด้านบนด้วยการติดตั้ง Argo CD จริง: object ใหญ่ทะลุขีดจำกัดขนาด annotation ของ last-applied-configuration ได้ |
--server-side | พิสูจน์แล้วด้านบน: ไม่มีขีดจำกัดขนาด annotation ตรวจ field-manager conflict ได้จริง | พิสูจน์แล้วด้านบน: field ที่ apply แบบ client-side ไว้แล้วอาจเกิด conflict จริงที่ต้องแก้ด้วย --force-conflicts |
syncPolicy.automated.prune: true | ลบ manifest ออกจาก Git แล้ว cluster object ถูกลบตามจริง | selector ที่ตั้งผิดหรือ manifest ที่ย้ายข้าม Application อาจ prune เกินกว่าที่ตั้งใจ — พิสูจน์เชิงแนวคิดด้วยกลไกเดียวกับที่พิสูจน์ selfHeal ด้านบน |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “GitOps แปลว่า CI ยัง apply การเปลี่ยนแปลงอยู่ แค่หลัง Git commit เท่านั้น” พิสูจน์แล้วด้านบน: ใน pipeline ที่บทนี้รัน หน้าที่ของ CI system จบแค่
git push— resourceApplicationของ Argo CD ใน cluster ต่างหากที่ทำ apply จริงทุกครั้ง ไม่ใช่ CI - “Server-side apply แค่ flag ทางเลือก ไม่เกี่ยวกับวิธีที่ GitOps tool ทำงานจริง ๆ” พิสูจน์แล้วด้านบนด้วย manifest install ของ Argo CD เอง:
--server-sideจำเป็นเพื่อผ่าน annotation-size failure จริง แล้วโผล่ field-manager conflict จริงทันที — นี่คือกลไกจริง ไม่ใช่รายละเอียดเล็กน้อย - “selfHeal สำคัญแค่กับการเปลี่ยนแปลงโดยไม่ตั้งใจ ไม่ใช่การเปลี่ยนแบบตั้งใจ” พิสูจน์แล้วด้านบน:
kubectl scaleที่ทำโดยตั้งใจและถูกต้องถูก revert เท่า ๆ กับที่เป็นอุบัติเหตุ — selfHeal ไม่แยกความตั้งใจ แค่แยกว่าต่างจาก Git หรือไม่ - “GitOps reconciler ลบ resource ที่ถูกลบออกจาก Git ให้อัตโนมัติ เป็น default” ไม่ได้พิสูจน์ว่าจริงตรงนี้ —
prune: trueถูกตั้งไว้ชัดเจนในsyncPolicyของบทนี้ ถ้าไม่มีมัน reconciler จะแค่เพิ่มกับอัปเดตเท่านั้น ไม่เคยลบ ปล่อย object กำพร้าไว้เมื่อ manifest ถูกลบออกจาก source - “manifest ที่ render แล้วผ่าน kubeconform แปลว่า production-ready แล้ว” พิสูจน์แล้วด้านบน:
kubeconformรายงาน 0 error กับ overlay output ของบทนี้เอง ในขณะที่kube-linterเจอช่องโหว่ด้าน production-hardening จริง 5 ข้อในทันทีจาก manifest ชุดเดียวกันเป๊ะ — schema ถูกต้องกับ production-ready เป็นคนละเรื่อง จำเป็นทั้งคู่
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอน review CI/CD pipeline, Kustomize overlay, หรือ GitOps Application manifest ที่ AI สร้าง ให้เช็ค
- CI pipeline step ที่รัน
kubectl apply/helm upgradeตรงเข้า production cluster ใน repo ที่อ้างว่าเป็น “GitOps” ด้วย — พิสูจน์แล้วด้านบน GitOps จริงหยุดแค่ commit เข้า Git step apply ตรงจาก CI คือ push-based ไม่ใช่ GitOps ไม่ว่า pipeline จะตั้งชื่ออะไร kustomization.yamlของ base ที่มีConfigMapแบบ literal ในresources:และconfigMapGeneratorสำหรับชื่อเดียวกัน — พิสูจน์แล้วด้านบนbehavior: replaceบน resource ที่ประกาศตรง ๆ ปิด hash suffix ทำลายกลไก automatic-rolling-update ที่ pattern นี้มีอยู่เพื่อสิ่งนั้นpatchesStrategicMergeในkustomization.yaml— พิสูจน์แล้วด้านบนด้วยการรันkubectl kustomizeจริงในปัจจุบัน:# Warning: 'patchesStrategicMerge' is deprecated. Please use 'patches' instead. Run 'kustomize edit fix' to update your Kustomization automatically.มัน render ถูกต้องอยู่ในวันนี้ (kustomize v5.8.1 ของคอร์สนี้ยอมรับพร้อม warning เฉย ๆ) แต่ Kustomize output ที่ AI generate จาก example เก่าควรใช้ field รวมpatches:ที่บทนี้ใช้ตลอดแทนkubectl apply -f <ทั้ง directory ของ repo>โดยไม่มี--prune(หรือ GitOps reconciler ที่ปิด pruning) — พิสูจน์แล้วด้านบน ถ้าไม่มี prune step ชัดเจน resource ที่ถูกลบออกจาก source จะไม่มีวันถูกลบออกจาก cluster เลย สะสม object กำพร้าไปเรื่อย ๆ- GitOps
Application/Kustomizationresource ที่ชี้ไปยัง credential ของ service account ของ CI system หรือ CI pipeline ที่ได้cluster-admin“เพื่อความปลอดภัย” — พิสูจน์แล้วด้านบนเชิงแนวคิด: ประโยชน์ด้าน security ทั้งหมดของ GitOps ขึ้นอยู่กับที่ external CI system ไม่ ถือ cluster-admin แบบถาวร CI SA ที่มีcluster-adminสร้างปัญหาเดิมที่ GitOps มีไว้เพื่อกำจัดขึ้นมาใหม่ - CI gate ที่ render manifest แล้วรันแค่ schema validation (kubeconform) แล้วเรียกมันว่า “production-ready” — พิสูจน์แล้วด้านบน kube-linter เจอช่องโหว่ด้าน hardening จริงแยกต่างหากที่ schema check ของ kubeconform ไม่มีทางจับได้เลย
🐛 หา bug ในโค้ดนี้
# kustomization.yaml is a Kustomize build input, never applied to the API server directly -- no k8s/ pathapiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- deployment.yaml- configmap.yamlconfigMapGenerator:- name: orders-api-config behavior: replace literals: - LOG_LEVEL=infoเฉลย
configmap.yaml คือ ConfigMap แบบ literal ที่ประกาศไว้ใน resources: แล้ว และ generator ใช้ behavior: replace กับชื่อเดียวกัน พิสูจน์แล้วด้านบนด้วยการตั้งค่าแบบเดียวกัน: behavior: replace (และ resource ที่ประกาศตรง ๆ ชื่อเดียวกัน) ให้ ConfigMap ที่ไม่มี hash suffix คือ orders-api-config ไม่ใช่ orders-api-config-hf678c7m2b นั่นแปลว่า content ของ LOG_LEVEL เปลี่ยนไม่มีวันเปลี่ยนชื่อ object เลย ดังนั้นไม่มีอะไรบังคับให้ Pod ของ Deployment roll — config ที่เปลี่ยนไม่ propagate ไปถึง Pod ที่รันอยู่แล้วเงียบ ๆ จนกว่าจะมีอะไรอื่น (manual rollout restart) มาทำ วิธีแก้คือตัด configmap.yaml แบบ literal ออกจาก resources: แล้วปล่อยให้ generator สร้างมันขึ้นใหม่ (default behavior ซึ่งเพิ่ม hash suffix และ auto-update ทุก reference envFrom/volumes ที่ชี้มาที่มันให้)