ข้ามไปยังเนื้อหา

GitOps และ CI/CD สำหรับ Kubernetes

GitOps ทำให้ Git repository เป็น single source of truth ของ desired state ของ cluster โดยมี reconciler ที่รันอยู่ใน cluster คอย pull แล้ว apply state นั้น แทนที่จะให้ CI job push state เข้ามาจากภายนอก

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

Terminal window
# Push-based: CI applies directly, using credentials CI itself holds
kubectl 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 there
git commit -am "bump myapp image tag to 1.5.0"
git push origin main

ย้ายขั้นตอน 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 อยู่แล้ว

การ build กับ deploy แบบ GitOps แยกเป็นสอง repository (หรือสอง stage ที่แยกกันชัดเจน) หนึ่งสำหรับ build แอป อีกหนึ่งสำหรับประกาศว่าควรรันอะไรอยู่

Terminal window
# Stage 1 — application repo CI: build and publish the image
docker 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.yaml
git 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

ทั้ง 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: v1
data:
LOG_LEVEL: info
kind: ConfigMap
metadata:
name: orders-api-config-hf678c7m2b
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
spec:
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: v1
data:
LOG_LEVEL: warn
kind: ConfigMap
metadata:
name: orders-api-config-dfk4bdbtkk
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
spec:
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 ที่คอร์สนี้พูดถึงไปแล้วสำหรับ 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.ingress

error แรกคือ ข้อจำกัดขนาด 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 เจอสถานการณ์แบบนี้

Argo CD ติดตั้งจาก manifest upstream ไฟล์เดียว และขึ้นสำเร็จบน kind cluster production ของบทนี้ในเวลาไม่ถึง 5 นาที หลัง step server-side-apply ด้านบนสำเร็จ — controller Pod ทุกตัว Running

$ kubectl -n argocd rollout status deployment/argocd-server --timeout=180s
deployment "argocd-server" successfully rolled out
$ kubectl -n argocd get pods
NAME READY STATUS RESTARTS AGE
argocd-application-controller-0 1/1 Running 0 90s
argocd-applicationset-controller-... 1/1 Running 0 90s
argocd-dex-server-... 1/1 Running 0 90s
argocd-notifications-controller-... 1/1 Running 0 90s
argocd-redis-... 1/1 Running 0 90s
argocd-repo-server-... 1/1 Running 0 90s
argocd-server-... 1/1 Running 0 90s

custom 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/ path
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: guestbook
namespace: argocd
spec:
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.yaml
application.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 Healthy

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=3
deployment.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 ชัดเจน

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.yaml
Summary: 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.yaml
Error: found 5 lint errors
KubeLinter 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
การเปลี่ยน manifest ถูก push เข้า Git แล้วถูก pull มา apply โดย GitOps reconciler ใน cluster ส่วน CI แยก build แล้ว publish image ต่างหาก

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 — resource Application ของ 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 เป็นคนละเรื่อง จำเป็นทั้งคู่

ตอน 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/Kustomization resource ที่ชี้ไปยัง 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/ path
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- configmap.yaml
configMapGenerator:
- 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 ที่ชี้มาที่มันให้)

การที่ Git เป็น single source of truth ใน GitOps workflow หมายความว่ายังไงในทางปฏิบัติ
ความต่างหลักระหว่าง push-based CI/CD pipeline กับ pull-based GitOps model คืออะไร
ทำไมชื่อ ConfigMap ถึงเปลี่ยนระหว่าง base กับ overlay
$ kubectl kustomize base/
...
  name: orders-api-config-hf678c7m2b
$ kubectl kustomize overlays/prod/
...
  name: orders-api-config-dfk4bdbtkk
error จริงสองอันนี้ร่วมกันแสดงอะไร
$ kubectl apply -n argocd -f install.yaml
The CustomResourceDefinition "applicationsets.argoproj.io" is invalid: metadata.annotations: Too long...
$ 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.ingress
หลังรัน kubectl scale ด้วยมือบน Deployment ที่ Argo CD Application ที่มี selfHeal: true จัดการอยู่ เกิดอะไรขึ้น
ทำไม kube-linter ถึงเจอปัญหาใน manifest ที่ kubeconform รายงานว่าถูกต้องสมบูรณ์
$ docker run --rm -i ghcr.io/yannh/kubeconform:latest -summary - < rendered-prod.yaml
Summary: 2 resources found parsing stdin - Valid: 2, Invalid: 0, Errors: 0, Skipped: 0
$ docker run --rm ... stackrox/kube-linter:latest lint rendered-prod.yaml
Error: found 5 lint errors