บทที่ 14 Kubernetes พื้นฐานด้วย kind: Pod, Deployment และ Service
แผนบริหารการสอนประจำบทที่ 14
หัวข้อเนื้อหา
- 14.1 เมื่อ Compose บนเครื่องเดียวไม่พอ
- 14.2 control plane และ worker node
- 14.3 Pod, Deployment, Service และ ConfigMap
- 14.4 เทียบ
compose.yamlของทีมกับ manifest ทีละส่วน - 14.5 kubectl พื้นฐาน
- 14.6 desired state กับสิ่งที่เกิดจริงในคลัสเตอร์
- 14.7 Service เลือก Pod ด้วย label ไม่เลือกจากชื่อ Deployment
- 14.8 image ในเครื่อง host กับ image ที่ node มองเห็น
- 14.9 ConfigMap, environment และการเปลี่ยนค่าระหว่างรัน
- ปฏิบัติการที่ 14 นำแอปของทีมขึ้นคลัสเตอร์ kind (Lab A–E)
วัตถุประสงค์เชิงพฤติกรรม
เมื่อศึกษาบทนี้และทำปฏิบัติการครบถ้วนแล้ว ผู้เรียนสามารถปฏิบัติได้ตามผลลัพธ์การเรียนรู้ต่อไปนี้ โดยแต่ละข้อมีงานที่ใช้ให้ feedback กำกับไว้
| รหัส | ผลลัพธ์การเรียนรู้ | วัดจาก (งานข้อ) | CLO ของรายวิชา |
|---|---|---|---|
| 14.1 | สร้างคลัสเตอร์ kind และ deploy image ของทีมจาก ghcr.io ด้วย Deployment และ Service จาก manifest แม่แบบ | งานข้อ 1 | CLO2 |
| 14.2 | อธิบายว่าโจทย์ที่กำหนดควรใช้ Docker Compose (บท 7) หรือ Kubernetes เพราะอะไร | งานข้อ 2 | CLO1 |
CLO ที่เกี่ยวข้อง (ข้อความจากหลักสูตร):
- CLO1 สามารถนำความต้องการและสถาปัตยกรรมการส่งมอบซอฟต์แวร์ที่กำหนดมาใช้ โดยอธิบาย Git workflow, CI/CD pipeline flow และการใช้ Containerization ได้อย่างเหมาะสมกับบริบทของงาน
- CLO2 สามารถกำหนดค่าและใช้งาน DevOps workflow ที่กำหนดมา เพื่อให้กระบวนการพัฒนา–ทดสอบ–ส่งมอบซอฟต์แวร์เป็นไปอย่างถูกต้องและมีประสิทธิภาพ
บทนี้ไม่นับคะแนน คะแนนทั้งหมดของรายวิชาปิดที่บทที่ 13 งานที่ต้องส่งของบทนี้มีไว้เพื่อรับ feedback จากผู้สอน ตาราง LLO ข้างต้นจึงเป็นโอกาสฝึกเพิ่มเติม ไม่ใช่หลักฐานตัดสินการบรรลุ CLO แต่ควรส่งงานเพราะผลงานของบทนี้เป็นพื้นของบทที่ 15
วิธีสอนและกิจกรรมการเรียนการสอน
- อธิบายแนวคิดตามหัวข้อ 14.1–14.5 โดยเริ่มจากข้อจำกัดของระบบ Compose เครื่องเดียวที่ทีมพบในบทที่ 12 แล้วใช้คำถามตรวจความเข้าใจท้ายหัวข้อเป็นกิจกรรมอภิปรายเป็นคู่
- เปิด
compose.yamlของทีมบนจอคู่กับ manifest สามไฟล์ แล้วให้ผู้เรียนช่วยกันจับคู่ทีละส่วน (หัวข้อ 14.4) - สาธิตสั้น ๆ ตั้งแต่
kind create clusterจนถึงkubectl get nodesเพื่อให้เห็นว่า node ของ kind คือ container หนึ่งตัวบนเครื่อง - ปฏิบัติการ Lab A–E ตามลำดับ สมาชิกทุกคนสร้างคลัสเตอร์บนเครื่องของตน (หรือใช้เครื่องเดียวร่วมกันถ้า RAM ไม่พอ) และแต่ละคนเปิด pull request เพิ่มไฟล์ใน
k8s/หนึ่งรายการ - มอบหมายใบงานเทียบ Compose กับ Kubernetes แบบฝึกหัดท้ายบท และ Learning Log ครั้งที่ 14
สื่อการเรียนการสอน
- เอกสารประกอบการสอนบทที่ 14 (บทความนี้) และหน้ารวมรายวิชา 10306343
- repository ทีม
devops-appและ imageghcr.io/<owner>/devops-app:0.5.2บน ghcr.io จากบทที่ 8 และบทที่ 12 และcompose.yamlจากบทที่ 7 - เครื่อง Ubuntu ในห้องปฏิบัติการที่ติดตั้ง Docker, kind และ kubectl แล้ว (RAM อย่างน้อย 8 GB ตามสมมติฐานของแผนการสอน)
- เอกสารทางการ Kubernetes หัวข้อ Kubernetes Components, Pods, Deployments, Service, ConfigMaps และหน้า Quick Start ของ kind
- แบบฟอร์มใบงานเทียบ Compose กับ Kubernetes (โจทย์ที่ผู้สอนกำหนด) และ rubric CLO1
การวัดผลและการประเมินผล
กำหนดส่ง: งานข้อ 1 ก่อนเริ่มเรียนบทที่ 15 เพราะ Lab A ของบทที่ 15 สร้างคลัสเตอร์ใหม่จากโฟลเดอร์ k8s/ บน main งานข้อ 2 และ 3 ส่งภายในสัปดาห์ที่ 15 งานทุกข้อของบทนี้ไม่นับคะแนน ผู้สอนให้ feedback เท่านั้น
| ข้อ | งาน | หลักฐาน | ผลลัพธ์ | CLO |
|---|---|---|---|---|
| 1 | M12 แอปทีมรันบนคลัสเตอร์ kind | ลิงก์ pull request ของตนเองที่ merge แล้ว (Lab C, D หรือ E) · ผล kubectl get pods,svc ที่เห็น Pod ของ devops-app และ Service devops-app · ผล curl ผ่าน kubectl port-forward ที่เรียก /health และ / | 14.1 | CLO2 |
| 2 | ใบงานเทียบ Compose กับ Kubernetes (รายบุคคล) | คำอธิบายไม่เกิน 1 หน้า ตอบโจทย์ที่ผู้สอนกำหนดว่าควรใช้ Docker Compose หรือ Kubernetes เพราะอะไร | 14.2 | CLO1 |
| 3 | Learning Log ครั้งที่ 14 | ตอบ 4 หัวข้อ: สิ่งที่เรียนในบทนี้ · คำสั่งหรือข้อความ error ที่พบและแก้อย่างไร · แหล่งที่ใช้ค้นหาคำตอบ (ระบุลิงก์เอกสารทางการอย่างน้อย 1 แหล่ง) · ส่วนใดของ compose.yaml ที่ยังจับคู่กับ Kubernetes ไม่ได้ | — | สะสมการเรียนรู้ต่อเนื่อง (portfolio ส่งไปแล้วในบทที่ 13) |
เกณฑ์ให้ feedback
| ข้อ | ผ่าน | ยังไม่ผ่าน |
|---|---|---|
| 1 | main มี k8s/configmap.yaml, k8s/deployment.yaml และ k8s/service.yaml ที่ใช้ image ของทีม · Pod ของ devops-app ทั้ง 2 ตัวอยู่สถานะ Running · curl ผ่าน port-forward ได้ HTTP 200 จาก /health และ / แสดงค่า APP_TITLE จาก ConfigMap | ไม่มี pull request ของตนเอง หรือ Pod ไม่อยู่สถานะ Running หรือเรียกแอปผ่าน port-forward ไม่ได้ หรือใช้ image ที่ไม่ใช่ของทีมโดยไม่ระบุเหตุผล |
| 2 | ได้ระดับ 2 ขึ้นไปตาม rubric CLO1 ด้านล่าง | ได้ระดับ 1 |
| 3 | ตอบครบ 4 หัวข้อ และมีลิงก์เอกสารทางการ | ขาดหัวข้อ หรือไม่มีแหล่งอ้างอิง |
ข้อ 2 ใช้ rubric CLO1 3 ระดับ เกณฑ์ผ่านคือระดับ 2 ขึ้นไป
| ระดับ | พฤติกรรมที่สังเกตได้ |
|---|---|
| 3 ดี | เลือก Compose หรือ Kubernetes ได้สอดคล้องกับโจทย์ อธิบายเหตุผลโดยอ้างระบบของทีม (เช่น ผลวัดช่วงไม่ตอบระหว่าง deploy ในบทที่ 12 หรือสิ่งที่เห็นในปฏิบัติการบทนี้) และเปรียบเทียบข้อดีข้อเสียของทั้งสองเครื่องมืออย่างน้อย 2 ด้าน เช่น ความซับซ้อนในการดูแลกับความสามารถกู้ตัวเองและอัปเดตโดยไม่หยุดระบบ |
| 2 ผ่าน | เลือกได้สอดคล้องกับโจทย์ และอธิบายเหตุผลที่เชื่อมกับความต้องการในโจทย์และระบบของทีมได้ถูกต้อง |
| 1 ยังไม่ผ่าน | ท่องนิยามทั่วไปของ Compose หรือ Kubernetes ได้ แต่เชื่อมกับโจทย์หรือระบบของทีมไม่ได้ หรือเลือกโดยไม่มีเหตุผล |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
การคิดคะแนนฉบับปัจจุบัน
มติผู้สอน 28 ต.ค. 2569: เข้าเรียน 10% · ใบงานบทที่ 1–13 รวม 40% · FP 20% · FW 20% · โครงงาน 10% ส่วนบทที่ 14–15 ฝึกเพื่อรับข้อเสนอแนะ ไม่นับคะแนน
บทนี้ไม่รวมในหมวดใบงาน FP หรือ FW ที่ใช้คำนวณเกรด ให้ข้อเสนอแนะตามหลักฐานเท่านั้น
| ผลลัพธ์ | หลักฐานตามงานส่ง | คะแนนดิบเต็ม |
|---|---|---|
| LLO14.1 | งานข้อ 1 | ไม่นับคะแนน |
| LLO14.2 | งานข้อ 2 | ไม่นับคะแนน |
ประเมินงานแต่ละข้อจากหลักฐานของข้อนั้นโดยตรง ไม่ตัดสิทธิ์รับคะแนนของอีกข้อเพราะ milestone หรือจุดตรวจอื่นยังไม่ผ่าน งานที่ยังทำไม่ได้ให้บันทึกปัญหาและหลักฐานที่มีตามจริง
ใช้เกณฑ์ความถูกต้อง เหตุผล และหลักฐานที่ระบุในงานแต่ละข้อประกอบการประเมินราย LLO งานเตรียมพื้นและ Learning Log ที่ไม่มีรหัส LLO ใช้ให้ข้อเสนอแนะ ไม่เพิ่มคะแนนดิบให้ใบงาน
บทนำ
ตลอดบทที่ 7–12 ทีมรันแอปด้วย Docker Compose บนเครื่องปลายทางเครื่องเดียว ระบบนี้เพียงพอสำหรับโครงงานของรายวิชา แต่บทที่ 12 แสดงข้อจำกัดให้เห็นแล้ว คือมีช่วงสั้น ๆ ที่ระบบไม่ตอบทุกครั้งที่ deploy และเมื่อเวอร์ชันใหม่พัง ต้องมีคนสังเกตและสั่ง rollback เอง บทนี้แนะนำ Kubernetes ซึ่งเป็นระบบจัดการ container บนกลุ่มเครื่อง (cluster) โดยใช้ kind สร้างคลัสเตอร์ขนาดเล็กในเครื่องของห้องปฏิบัติการ แล้วนำ image ของทีมจาก ghcr.io ขึ้นคลัสเตอร์ด้วย object สี่ชนิด ได้แก่ Pod, Deployment, Service และ ConfigMap
ความสำคัญ: Kubernetes พบบ่อยในงาน DevOps และ Platform Engineering การเห็นว่าแต่ละส่วนของ compose.yaml ของทีมไปอยู่ที่ใดใน manifest ทำให้เรียนเครื่องมือใหม่ได้เร็วขึ้น และช่วยตัดสินใจว่าเมื่อใดควรใช้เครื่องมือที่ซับซ้อนกว่า และเมื่อใด Compose ก็เพียงพอ
ความเชื่อมโยงกับบทอื่น: บทนี้ใช้ image และ container จากบทที่ 6 compose.yaml จากบทที่ 7 image บน ghcr.io จากบทที่ 8 ไวยากรณ์ YAML จากบทที่ 5 และข้อจำกัดของ rollback ด้วยมือจากบทที่ 12 ผลผลิตของบทนี้ถูกใช้ต่อตามตารางต่อไปนี้
| สิ่งที่ได้จากบทนี้ | ใช้ต่อในบท |
|---|---|
k8s/configmap.yaml, k8s/deployment.yaml, k8s/service.yaml บน main | 15 (เพิ่ม probe, rolling update และ rollout undo) |
| คำสั่ง kind และ kubectl พื้นฐาน | 15 (สร้างคลัสเตอร์ใหม่และติดตามการ rollout) |
| ใบงานเทียบ Compose กับ Kubernetes | 15 (หัวข้อสรุปวิชาและเส้นทางเรียนต่อ) |
ความรู้พื้นฐาน เครื่องมือ และรุ่นซอฟต์แวร์
ความรู้พื้นฐาน: ตามบทที่ระบุในความเชื่อมโยงข้างต้น โดยเฉพาะ environment และ healthcheck ใน compose.yaml (บทที่ 7) และการ login ghcr.io ด้วย personal access token (บทที่ 8)
เครื่องมือ: เครื่องมือใหม่ของบทนี้มีสองตัว คือ kind (Kubernetes in Docker) ซึ่งสร้างคลัสเตอร์โดยใช้ container เป็น node และ kubectl ซึ่งเป็นเครื่องมือบรรทัดคำสั่งสำหรับสั่งงานคลัสเตอร์ ทั้งสองตัวผู้สอนติดตั้งไว้บนเครื่องห้องปฏิบัติการแล้ว ตรวจได้ด้วยคำสั่งต่อไปนี้ และต้องมีโฟลเดอร์ ~/devops-app ที่ตรงกับ main ล่าสุด
docker version --format '{{.Server.Version}}'
kind version
kubectl version --clientรุ่นซอฟต์แวร์ที่ใช้เตรียมบทนี้:
| ซอฟต์แวร์ | รุ่น |
|---|---|
| Docker Engine | 29.8.2 ทดสอบเมื่อ 19 ต.ค. 2569 |
| kind | หน้า Quick Start ของ kind ระบุรุ่นเสถียร v0.33.0 ซึ่งใช้ node image kindest/node:v1.37.0 เป็นค่าเริ่มต้น (ตรวจจากเอกสาร ยังไม่ได้ติดตั้งจริง) |
| kubectl | ใช้รุ่นที่ห่างจากคลัสเตอร์ไม่เกินหนึ่ง minor version ตามหน้า Install kubectl on Linux |
| manifest | ตรวจ schema ด้วย kubernetes-validate 1.37.0 แบบ --strict กับ Kubernetes 1.37 และ 1.34 ผ่านทุกไฟล์ |
| แอป | devops-app 0.5.2 (Node.js 22, Express 5, ฟังพอร์ต 3000) |
สภาพแวดล้อมที่ใช้เตรียมบทนี้สร้างคลัสเตอร์ไม่ได้ เพราะดาวน์โหลด kind, kubectl และ image kindest/node ไม่ได้ สิ่งที่ทดสอบแล้วมีเพียง (1) manifest ทุกไฟล์ผ่านการตรวจ schema แบบเข้มงวด (2) แอปเวอร์ชัน 0.5.2 ใน Docker ตอบ /health และแสดงค่า APP_TITLE ตามตัวแปรสภาพแวดล้อม คำสั่ง kind และ kubectl ทุกคำสั่ง รวมถึง kind load docker-image port-forward และการอ่านค่าจาก ConfigMap ในคลัสเตอร์ยังไม่ได้ทดสอบ ผลของคำสั่งเหล่านี้จึงเขียนเป็น "ผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง)" ตามเอกสารทางการ ถ้าผลบนเครื่องต่างจากที่อธิบาย ให้บันทึกใน Learning Log และแจ้งผู้สอน
ค่าใช้จ่ายและข้อมูลลับ: ไม่มีค่าใช้จ่าย ทุกอย่างรันบนเครื่องห้องปฏิบัติการ ไม่ใช้คลาวด์ ถ้า package ของทีมบน ghcr.io เป็นแบบ private ต้อง docker login ghcr.io ด้วย personal access token (classic) ที่มีสิทธิ์ read:packages ตามบทที่ 8 ห้ามใส่ token ลงในไฟล์ใดใน k8s/ และ ConfigMap ใช้เก็บค่าที่ไม่ใช่ความลับเท่านั้น เพราะ ConfigMap ไม่เข้ารหัสข้อมูล
14.1 เมื่อ Compose บนเครื่องเดียวไม่พอ
ระบบของทีมหลังบทที่ 12 รันบนเครื่องปลายทางเครื่องเดียว บริการ app มี container เดียว และการ deploy แต่ละครั้งหยุด container เดิมก่อนสร้าง container ใหม่ ตารางต่อไปนี้สรุปข้อจำกัดที่ทีมพบแล้ว และแนวทางที่ Kubernetes ใช้จัดการ
| ข้อจำกัดของระบบ Compose ของทีม | ผลที่เกิด | แนวทางของ Kubernetes |
|---|---|---|
| เครื่องปลายทางเครื่องเดียว | เครื่องดับเมื่อใด ระบบหยุดทั้งหมด | คลัสเตอร์มีหลาย node ถ้า node หนึ่งล้ม controller จะสร้าง Pod ทดแทนบน node ที่ยังทำงาน (Kubernetes: Pods) |
| แอปมี container เดียว | ผลวัดในบทที่ 12 พบ 502 หนึ่งครั้งจาก 150 ครั้งระหว่าง deploy ปกติ | Deployment รักษาจำนวนสำเนา (replica) ตามที่กำหนด และ rolling update ค่อย ๆ แทนที่ทีละส่วน (บทที่ 15) |
| container ที่ล้มไม่ถูกเริ่มใหม่อัตโนมัติ | ต้องมีคนสังเกตและสั่งเริ่มใหม่ | self-healing เริ่ม container ใหม่ตาม restartPolicy และสร้าง Pod ทดแทนให้ครบจำนวน (Kubernetes: Self-Healing) |
| เพิ่มจำนวนสำเนาต้องแก้ระบบเอง | รับผู้ใช้เพิ่มช่วงเร่งด่วนได้ยาก | ปรับจำนวนสำเนาด้วยคำสั่งเดียว (Kubernetes: Overview) |
ระดับที่ 1: แนวคิดหลักของ Kubernetes คือ สถานะที่ต้องการ (desired state) ทีมเขียนไว้ใน manifest ว่าต้องการอะไร เช่น "แอป devops-app จาก image นี้ 2 สำเนา" แล้ว controller ในคลัสเตอร์จะปรับสถานะจริงให้ตรงกับสถานะที่ต้องการอย่างต่อเนื่อง (Kubernetes: Deployments) ต่างจาก scripts/deploy.sh ของทีมที่สั่งเป็นขั้นตอนครั้งเดียวแล้วจบ ถ้า container ล้มหลังสคริปต์จบ ไม่มีใครทำให้กลับมา
ระดับที่ 2: Kubernetes มีที่มาจากระบบ Borg และ Omega ภายใน Google ตามที่บทความ Borg, Omega, and Kubernetes (Burns et al., 2016) เล่าไว้ หน้า Overview สรุปความสามารถหลัก เช่น service discovery และ load balancing, automated rollouts and rollbacks, self-healing และการจัดการค่าตั้งและความลับ บทนี้ใช้ Deployment ที่รักษาจำนวน Pod, Service ที่เป็นทางเข้าเดียวของหลาย Pod และ ConfigMap ที่แยกค่าตั้งออกจาก image ส่วน rollout และ self-healing อยู่ในบทที่ 15
ระดับที่ 3: Kubernetes ไม่ใช่คำตอบของทุกระบบ หน้า Overview ระบุว่า Kubernetes ไม่ใช่ PaaS แบบครบวงจร ไม่ build แอปให้ และไม่มีฐานข้อมูลหรือบริการระดับแอปในตัว ทีมยังต้องมี CI/CD และต้องเรียนรู้และดูแลคลัสเตอร์เพิ่ม ระบบเล็กที่มีผู้ใช้น้อยและหยุดสั้น ๆ ได้ Compose บนเครื่องเดียวอาจเหมาะกว่า การเลือกเครื่องมือให้เหมาะกับโจทย์คือสิ่งที่ใบงาน (งานข้อ 2) ประเมิน
คำถามตรวจความเข้าใจ 14.1: ระบุว่าข้อจำกัดใดในตารางข้างต้นที่ kind ในห้องปฏิบัติการแก้ไม่ได้ เพราะอะไร และข้อจำกัดใดที่ยังสาธิตได้บน kind
14.2 control plane และ worker node
คลัสเตอร์ Kubernetes ประกอบด้วย control plane และ worker node ตั้งแต่หนึ่งเครื่องขึ้นไป (Kubernetes: Kubernetes Components)
| ส่วน | องค์ประกอบ | หน้าที่ |
|---|---|---|
| control plane | kube-apiserver | เซิร์ฟเวอร์ที่เปิด Kubernetes HTTP API ทุกคำสั่งของ kubectl ส่งมาที่นี่ |
| control plane | etcd | ที่เก็บข้อมูลแบบ key-value ของข้อมูลทั้งหมดใน API server |
| control plane | kube-scheduler | หา Pod ที่ยังไม่ถูกจัดให้ node ใด แล้วเลือก node ที่เหมาะสมให้ |
| control plane | kube-controller-manager | รัน controller ที่ทำให้พฤติกรรมของ API เกิดขึ้นจริง เช่น controller ของ Deployment |
| control plane | cloud-controller-manager (เลือกใช้) | เชื่อมกับผู้ให้บริการคลาวด์ (คลัสเตอร์ kind ในห้องปฏิบัติการไม่ใช้) |
| node | kubelet | ดูแลให้ Pod และ container ใน Pod ทำงาน |
| node | kube-proxy (เลือกใช้) | ดูแลกฎเครือข่ายบน node เพื่อให้ Service ทำงาน |
| node | container runtime | ซอฟต์แวร์ที่รัน container |
ระดับที่ 1: เมื่อสั่ง kubectl apply -f k8s/deployment.yaml คำขอไปที่ kube-apiserver ซึ่งบันทึก Deployment ลง etcd จากนั้น controller ของ Deployment สร้าง Pod ตามจำนวนที่ต้องการ kube-scheduler เลือก node ให้ Pod แต่ละตัว และ kubelet บน node นั้นสั่ง container runtime ให้ดึงและรัน image ลำดับนี้อธิบายว่าทำไม kubectl apply จบเร็ว แต่ Pod อาจยังไม่พร้อมทันที เพราะงานส่วนใหญ่เกิดหลังคำสั่งจบ
ระดับที่ 2: kind สร้างคลัสเตอร์โดยใช้ container เป็น node (kind) คำสั่ง kind create cluster --name devops แบบไม่มีไฟล์ค่าตั้งสร้าง node เดียวที่มีบทบาท control-plane node นี้คือ container หนึ่งตัวที่เห็นได้ด้วย docker ps ภายในมีทั้งองค์ประกอบของ control plane และส่วนของ node Pod ของแอปทีมจึงรันอยู่ใน container ของ node อีกชั้นหนึ่ง (ผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง))
ระดับที่ 3: คลัสเตอร์ node เดียวช่วยให้เห็นภาพและฝึกคำสั่งได้ แต่ไม่ได้ความทนทานของคลัสเตอร์จริง ถ้าเครื่องห้องปฏิบัติการดับ คลัสเตอร์ทั้งหมดหยุด เหมือนเครื่องปลายทางเครื่องเดียวของ Compose อีกเรื่องที่ต้องระวังคือ kind อยู่ในเครื่องที่รัน Docker อยู่แล้ว image ที่อยู่ใน Docker ของเครื่องจึงไม่ได้อยู่ใน container runtime ของ node โดยอัตโนมัติ ต้องนำเข้าด้วย kind load docker-image (Lab C)
คำถามตรวจความเข้าใจ 14.2: สมาชิกคนหนึ่งสั่ง kubectl apply แล้วได้ข้อความว่าสร้าง Deployment สำเร็จ แต่ Pod ยังไม่ทำงาน ระบุว่าองค์ประกอบใดในตารางข้างต้นทำงานแล้ว และองค์ประกอบใดที่อาจยังทำงานไม่เสร็จ
14.3 Pod, Deployment, Service และ ConfigMap
บทนี้ใช้ object สี่ชนิด ซึ่งเชื่อมกันด้วย label (ป้ายแบบ key-value ที่ติดกับ object) และ selector (เงื่อนไขที่เลือก object ตาม label)
| object | คืออะไร | ในระบบของทีม |
|---|---|---|
| Pod | หน่วยเล็กที่สุดที่สร้างและจัดการได้ใน Kubernetes เป็นกลุ่มของ container ตั้งแต่หนึ่งตัวที่ใช้เครือข่ายและพื้นที่เก็บข้อมูลร่วมกัน (Kubernetes: Pods) | Pod ละหนึ่ง container ชื่อ app จาก image ของทีม |
| Deployment | กำหนดสถานะที่ต้องการของ Pod และ ReplicaSet แล้ว controller ปรับสถานะจริงให้ตรง (Kubernetes: Deployments) | Deployment devops-app จำนวน 2 replica |
| Service | ทางเข้าทางเครือข่ายของกลุ่ม Pod ที่ตรงกับ selector (Kubernetes: Service) | Service devops-app ชนิด ClusterIP รับพอร์ต 80 ส่งต่อไปพอร์ต 3000 ของ Pod |
| ConfigMap | เก็บค่าตั้งที่ไม่ใช่ความลับแยกจากโค้ดของแอป (Kubernetes: ConfigMaps) | ConfigMap devops-app-config คีย์ APP_TITLE |
ภาพความสัมพันธ์ของ object ในบทนี้ เขียนเป็นข้อความได้ดังนี้
Deployment devops-app (replicas: 2, selector app=devops-app)
└─ ReplicaSet devops-app-<hash>
├─ Pod devops-app-<hash>-<id> label app=devops-app container app :3000
└─ Pod devops-app-<hash>-<id> label app=devops-app container app :3000
Service devops-app (ClusterIP, port 80 → targetPort 3000, selector app=devops-app)
ConfigMap devops-app-config (APP_TITLE) → env ของ container appชื่อ ReplicaSet มีรูปแบบ [DEPLOYMENT-NAME]-[HASH] และเป็นฐานของชื่อ Pod ที่ถูกสร้าง ตามที่หน้า Deployments ระบุ ส่วน <id> ท้ายชื่อ Pod เป็นอักขระที่ระบบสุ่มให้
ระดับที่ 1: หน้า Pods ระบุว่ามักไม่สร้าง Pod โดยตรง แม้ต้องการเพียงตัวเดียว เพราะ Pod ถูกออกแบบให้เป็นของชั่วคราวที่ทิ้งได้ (ephemeral, disposable) ถ้า Pod ถูกลบหรือ node ล้ม Pod ที่สร้างเองโดยไม่มี controller ไม่มีใครสร้างให้ใหม่ จึงใช้ Deployment ซึ่งสร้างและดูแล Pod ให้ตามจำนวน Lab B สร้าง Pod เดี่ยวเพื่อให้เห็นความต่างนี้ ส่วน Lab C ใช้ Deployment
ระดับที่ 2: Service เลือก Pod ด้วย selector controller ของ Service สแกนหา Pod ที่ตรงกับ selector อยู่ตลอด และปรับรายการปลายทาง (EndpointSlice) ให้ตามทัน Pod ที่เกิดใหม่หรือหายไป หน้า Service ระบุชนิดของ Service ไว้สี่ชนิด
| ชนิด | ความหมายตามเอกสาร | ใช้ในบทนี้ |
|---|---|---|
| ClusterIP | เปิด Service ด้วย IP ภายในคลัสเตอร์ และเป็นค่าเริ่มต้นเมื่อไม่ระบุ type | ใช้ (เข้าจากเครื่องด้วย kubectl port-forward) |
| NodePort | เปิด Service ที่พอร์ตคงที่บน IP ของทุก node | ไม่ใช้ |
| LoadBalancer | เปิด Service สู่ภายนอกผ่าน load balancer ภายนอก | ไม่ใช้ (ต้องมีผู้ให้บริการรองรับ) |
| ExternalName | จับคู่ Service กับค่าในฟิลด์ externalName | ไม่ใช้ |
ConfigMap ส่งค่าให้ container ได้หลายแบบ บทนี้ใช้แบบตัวแปรสภาพแวดล้อมด้วย valueFrom.configMapKeyRef ซึ่งตรงกับที่แอปอ่าน APP_TITLE จากตัวแปรสภาพแวดล้อมอยู่แล้ว แอปจึงไม่ต้องแก้โค้ด
ระดับที่ 3: มีสามจุดที่ต้องระวัง (1) spec.selector ของ Deployment ต้องตรงกับ label ใน spec.template.metadata.labels มิฉะนั้น API จะปฏิเสธ และ selector แก้ไม่ได้หลังสร้าง Deployment แล้ว (2) ConfigMap ไม่ให้ความลับหรือการเข้ารหัส ขนาดข้อมูลไม่เกิน 1 MiB ข้อมูลลับต้องใช้ Secret ซึ่งค่าเริ่มต้นก็ยังเก็บใน etcd แบบไม่เข้ารหัส (3) ตัวแปรสภาพแวดล้อมที่มาจาก ConfigMap ไม่อัปเดตอัตโนมัติ ต้องเริ่ม Pod ใหม่ (Kubernetes: ConfigMaps) ซึ่ง Lab E ให้ลองเอง
คำถามตรวจความเข้าใจ 14.3: ถ้าแก้ selector ของ Service เป็น app: devops (พิมพ์ขาด) แต่ Pod ยังมี label app: devops-app จะเกิดอะไรขึ้นกับการเข้าถึงแอปผ่าน Service และ Pod ยังทำงานอยู่หรือไม่
14.4 เทียบ compose.yaml ของทีมกับ manifest ทีละส่วน
บริการ app ใน compose.yaml ของทีม (สถานะหลังบทที่ 12) เป็นดังนี้
services:
app:
build: .
image: ghcr.io/devops-team-01/devops-app:${IMAGE_TAG:-dev}
init: true
environment:
APP_TITLE: ${APP_TITLE:-DevOps App}
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5sนอกจากนี้ยังมีบริการ proxy (nginx:1.28 เปิดพอร์ต 80 และส่งต่อไปที่ http://app:3000) และบริการ db (postgres:16 กับ volume db-data) แอปยังไม่ได้เชื่อมฐานข้อมูลจริง บทนี้จึงนำขึ้นคลัสเตอร์เฉพาะบริการ app
ส่วนใน compose.yaml | ใน Kubernetes | ไฟล์ในบทนี้ |
|---|---|---|
services.app | Deployment ที่สร้าง Pod ของแอป | k8s/deployment.yaml |
| (จำนวนสำเนา ไม่ได้ระบุ = 1) | spec.replicas: 2 | k8s/deployment.yaml |
image: | spec.template.spec.containers[].image (ใช้ tag ตายตัว เช่น 0.5.2 ไม่ใช้ตัวแปร) | k8s/deployment.yaml |
build: . | ไม่มี Kubernetes ไม่ build แอป ใช้ image ที่ CD ของบทที่ 8 สร้างไว้แล้ว | — |
environment: APP_TITLE | ConfigMap + env[].valueFrom.configMapKeyRef | k8s/configmap.yaml และ k8s/deployment.yaml |
พอร์ต 3000 ที่ proxy เรียก (http://app:3000) | containerPort: 3000 และ Service ที่ส่งพอร์ต 80 ไป targetPort: 3000 | k8s/deployment.yaml และ k8s/service.yaml |
ชื่อบริการ app ที่ใช้เรียกกันใน network ของ Compose | ชื่อ Service devops-app เป็นชื่อเดียวสำหรับกลุ่ม Pod | k8s/service.yaml |
proxy เปิดพอร์ต 80 บนเครื่อง | บทนี้ใช้ kubectl port-forward แทน (ไม่ได้นำ Nginx ขึ้นคลัสเตอร์) | — |
healthcheck: | readinessProbe และ livenessProbe ที่เรียก /health | บทที่ 15 |
depends_on กับ condition: service_healthy | manifest ในบทนี้ไม่มีส่วนที่ทำหน้าที่นี้ | — |
init: true | manifest ในบทนี้ไม่มีส่วนที่ทำหน้าที่นี้ | — |
db และ volume db-data | อยู่นอกขอบเขตของบทนี้ | — |
ระดับที่ 1: Compose รวม image ตัวแปรสภาพแวดล้อม พอร์ต และ healthcheck ไว้ในบริการเดียว ส่วน Kubernetes แยกเป็น object คนละชนิด Deployment ดูแลว่า Pod รันอะไรและกี่ตัว Service ดูแลทางเข้า ConfigMap ดูแลค่าตั้ง จึงเปลี่ยนแต่ละส่วนได้อิสระ เช่น เปลี่ยน APP_TITLE โดยไม่แก้ Deployment
ระดับที่ 2: compose.yaml ใช้ ${IMAGE_TAG:-dev} ให้ scripts/deploy.sh ส่งเลขเวอร์ชันเข้ามาตอน deploy แต่ manifest ในบทนี้เขียน tag ตายตัวไว้ในไฟล์ การเปลี่ยนเวอร์ชันจึงเป็นการแก้ไฟล์ใน repository ผ่าน pull request ซึ่งทำให้ประวัติใน Git บอกได้ว่าคลัสเตอร์ควรรันเวอร์ชันใด บทที่ 15 ใช้หลักนี้ตอนทำ rolling update
ระดับที่ 3: การจับคู่ไม่ได้เป็นหนึ่งต่อหนึ่งเสมอ บางส่วนของ Compose ไม่มีคู่ใน manifest ของบทนี้ (build, depends_on, init) และบางส่วนของ Kubernetes ไม่มีใน Compose ของทีม (replicas, label และ selector) ควรจับคู่ตามหน้าที่ ไม่ใช่ตามชื่อฟิลด์ เช่น ฟิลด์ ports ในบริการ proxy ของ Compose มีหน้าที่ใกล้กับการเปิดทางเข้าจากภายนอก ไม่ใช่ containerPort ซึ่งเป็นเพียงการบอกพอร์ตที่ container ฟัง
คำถามตรวจความเข้าใจ 14.4: สมาชิกคนหนึ่งเสนอให้ใส่ image: ghcr.io/devops-team-01/devops-app:latest ใน k8s/deployment.yaml เพื่อจะได้ไม่ต้องแก้ไฟล์ทุกครั้งที่ออกเวอร์ชัน ระบุข้อเสียอย่างน้อย 2 ข้อ (ข้อหนึ่งเกี่ยวกับ kind)
14.5 kubectl พื้นฐาน
kubectl ส่งคำสั่งไปที่ kube-apiserver ของคลัสเตอร์ที่เลือกไว้ใน context ปัจจุบัน kind ตั้งชื่อ context เป็น kind- ตามด้วยชื่อคลัสเตอร์ เช่น หน้า Quick Start ของ kind ใช้ kubectl cluster-info --context kind-kind กับคลัสเตอร์ชื่อ kind คลัสเตอร์ devops ของบทนี้จึงมี context ชื่อ kind-devops ตามรูปแบบเดียวกัน คำสั่งในตารางต่อไปนี้มาจากหน้า kubectl Quick Reference (Kubernetes: kubectl Quick Reference)
| คำสั่ง | ใช้ทำอะไร |
|---|---|
kubectl config current-context | ดูว่ากำลังสั่งคลัสเตอร์ใด |
kubectl get nodes / kubectl get pods / kubectl get services | แสดงรายการ object (ใส่หลายชนิดพร้อมกันได้ เช่น kubectl get pods,svc) |
kubectl get pods -o wide / --show-labels | แสดงรายละเอียดเพิ่ม เช่น IP ของ Pod และ label |
kubectl describe pods <ชื่อ> | แสดงรายละเอียดและเหตุการณ์ (events) ของ object ใช้หาสาเหตุเมื่อ Pod ไม่ทำงาน |
kubectl logs <ชื่อ Pod> / kubectl logs deploy/<ชื่อ> | ดู log ที่ container เขียนออก stdout |
kubectl apply -f <ไฟล์หรือโฟลเดอร์> | สร้างหรือปรับ object ตาม manifest |
kubectl delete -f <ไฟล์> | ลบ object ตาม manifest |
kubectl exec <ชื่อ Pod> -- <คำสั่ง> | รันคำสั่งใน container ที่ทำงานอยู่ |
kubectl explain pods | อ่านคำอธิบายฟิลด์ของ manifest จากคลัสเตอร์ |
kubectl port-forward service/<ชื่อ> <พอร์ตเครื่อง>:<พอร์ต Service> | ส่งต่อพอร์ตจากเครื่องไปยัง Pod ที่ Service เลือก (Kubernetes: kubectl port-forward) |
ระดับที่ 1: kubectl apply -f เป็นวิธีแบบ declarative คือส่งสถานะที่ต้องการทั้งไฟล์ให้คลัสเตอร์ ถ้า object ยังไม่มีจะถูกสร้าง ถ้ามีแล้วจะถูกปรับให้ตรงไฟล์ จึงสั่งซ้ำได้ ทีมใช้ kubectl apply -f k8s/ กับทั้งโฟลเดอร์ได้ ซึ่งเป็นเหตุผลที่โฟลเดอร์ k8s/ ใน repository มีเฉพาะสามไฟล์ของแอป ไม่มีไฟล์ฝึกหัดอื่นปน
ระดับที่ 2: ลำดับการหาสาเหตุเมื่อแอปไม่ทำงานคือ kubectl get pods ดูสถานะ แล้ว kubectl describe pods <ชื่อ> ดูเหตุการณ์ท้ายผลลัพธ์ (เช่น ดึง image ไม่ได้) แล้ว kubectl logs <ชื่อ> ดูข้อความของแอป (เช่น แอปล้มตอนเริ่ม) ลำดับนี้ตรงกับที่ทีมใช้ docker compose ps และ docker compose logs ในบทที่ 7 สถานะที่พบบ่อยเมื่อดึง image ไม่ได้คือ ErrImagePull และ ImagePullBackOff
ระดับที่ 3: ผลทุกคำสั่งขึ้นกับ context ปัจจุบัน เครื่องที่มีหลายคลัสเตอร์ (เช่น ใช้เครื่องร่วมกันในทีม) อาจสั่งผิดคลัสเตอร์โดยไม่รู้ตัว จึงควรตรวจ kubectl config current-context ก่อน apply หรือ delete ทุกครั้ง ส่วน kubectl port-forward เป็นเครื่องมือตรวจและแก้ปัญหาจากเครื่องของตน ไม่ใช่ทางเข้าของผู้ใช้จริง และเมื่อ forward ผ่าน Service จะส่งไปที่ Pod เพียงตัวเดียวที่ Service เลือก ไม่ได้กระจาย request
คำถามตรวจความเข้าใจ 14.5: Pod ตัวหนึ่งมีสถานะ ImagePullBackOff เรียงลำดับคำสั่ง kubectl ที่ใช้หาสาเหตุ และระบุว่าคาดว่าจะพบข้อมูลสำคัญในผลของคำสั่งใด
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
14.6 desired state กับสิ่งที่เกิดจริงในคลัสเตอร์
manifest อธิบายสถานะที่ต้องการ ส่วน controller ทำงานเพื่อให้สถานะจริงเข้าใกล้สถานะนั้น การส่ง kubectl apply สำเร็จจึงหมายถึง API รับการเปลี่ยนตามเงื่อนไข ไม่ได้ยืนยันว่า Pod พร้อมให้บริการแล้ว ต้องติดตาม Deployment, ReplicaSet, Pod และเหตุการณ์ที่เกี่ยวข้องจนถึงการตรวจ request จริง (Kubernetes: Deployments)
สถานการณ์สมมติคือประกาศ replicas เท่ากับ 2 แต่มีหนึ่ง Pod Running และอีกหนึ่ง Pending การมี Deployment object ครบไม่ได้แปลว่าได้สองสำเนาพร้อมใช้ Pending อาจเกิดจากทรัพยากรหรือข้อจำกัด scheduling ต้องอ่าน Events ก่อนแก้ image หากเป็น ImagePullBackOff ต้องตรวจชื่อ image สิทธิ์ registry และเครือข่าย ส่วน CrashLoopBackOff ชี้ไปยังการเริ่มกระบวนการแล้วล้มซ้ำ ทั้งสามอาการต้องเริ่มตรวจคนละจุด
ตัวอย่างที่ไม่ถูกต้องคือเห็น STATUS=Running แล้วสรุปว่าแอปพร้อมรับคำขอ Running เป็นสถานะของวงจร Pod ขณะที่ readiness เกี่ยวกับการรับ traffic ต้องตรวจคอลัมน์ READY เงื่อนไขของ Pod และผล probe ด้วย Pod อาจมีกระบวนการทำงานอยู่แต่ยังเตรียมข้อมูลไม่เสร็จหรือ readiness ล้ม
อีกตัวอย่างคือแก้ไฟล์ภายใน Pod แล้วคาดว่าการเปลี่ยนจะคงอยู่เมื่อ controller สร้างตัวใหม่ วิธีนี้ขัดกับการประกาศสถานะ หากต้องเปลี่ยนแอปให้สร้าง image ใหม่ หากต้องเปลี่ยนค่าตั้งให้ใช้กลไก config ที่เหมาะสม แล้วปรับ manifest ที่เป็นแหล่งอ้างอิง การแก้ instance ชั่วคราวควรใช้เพื่อสืบเหตุในขอบเขตที่ควบคุมและบันทึกสิ่งที่ทำ
14.7 Service เลือก Pod ด้วย label ไม่เลือกจากชื่อ Deployment
Service แบบมี selector ใช้ชุด labels เพื่อเลือก endpoints ของ Pod ชื่อ Service กับชื่อ Deployment ไม่ต้องเหมือนกัน และการตั้งชื่อเหมือนกันไม่ได้สร้างความเชื่อมโยงเอง ต้องตรวจ selector ของ Service เทียบกับ labels ใน Pod template รวมทั้งพอร์ตของ Service และพอร์ตที่แอปฟัง (Kubernetes: Service)
ตัวอย่างส่วนตัดต่อไปนี้แสดงข้อผิดพลาด โดยสมมติว่า Pod ที่ต้องการมี label app: devops-app
# ไม่ถูกต้องในสถานการณ์นี้: selector ไม่ตรง labels ของ Pod
apiVersion: v1
kind: Service
metadata:
name: devops-app
spec:
selector:
app: devops-api
ports:
- port: 80
targetPort: 3000ไฟล์อาจผ่านการตรวจรูปแบบและสร้าง object ได้ แต่ไม่ได้เลือก Pod ที่ตั้งใจ วิธีแก้คือกำหนด selector ให้ตรง labels ที่ถูกต้อง แล้วตรวจ EndpointSlice ของ Service พร้อม readiness ของ endpoints ไม่แก้ด้วยการตั้งชื่อทุก object ให้เหมือนกันโดยไม่ดู labels หากแก้ label ของ Pod ต้องพิจารณา selector ของ Deployment ที่เกี่ยวข้องด้วย
อีกกรณีคือ selector ตรงแต่ targetPort ผิด สมมติแอปฟัง 3000 แต่ Service ส่งไป 8080 ชื่อ resolve ได้และ endpoints มีอยู่ แต่คำขอยังไม่ถึงโปรแกรมที่ถูกต้อง การวิเคราะห์ควรไล่จากผู้เรียก → Service port → targetPort → พอร์ตที่โปรแกรมฟัง โดยแยก containerPort ที่ประกาศใน manifest ออกจากการทำให้โปรแกรมเปิดพอร์ตจริง การประกาศตัวเลขไม่ได้เปลี่ยนโค้ดแอปให้ฟังเลขนั้นเอง
การใช้ port-forward ไป Pod ช่วยตรวจแอปเฉพาะตัว แต่ถ้าทดสอบผ่าน Pod แล้วสรุปว่า Service ถูกต้อง จะข้าม selector และเส้นทาง Service ที่ต้องตรวจ การใช้ port-forward เป็นเครื่องมือสืบเหตุจึงต้องระบุว่ากำลังทดสอบขอบเขตใด และไม่ถือเป็นวิธีเปิด production ให้ผู้ใช้ทั่วไป
14.8 image ในเครื่อง host กับ image ที่ node มองเห็น
kind ใช้ container เป็น node สำหรับคลัสเตอร์ในเครื่อง การมี image ใน Docker ของ host ไม่ได้แปลว่า node ใช้ image นั้นได้เสมอ เอกสาร kind อธิบายการโหลด image ด้วย kind load docker-image และระบุชื่อคลัสเตอร์เมื่อใช้ named cluster รวมถึงข้อควรพิจารณา image pull policy (kind: Quick Start)
ตัวอย่างที่ผิดคือ build devops-app:lab14 บน host แล้ว apply manifest โดยไม่ push ไป registry และไม่โหลดเข้า node Pod อาจพยายามดึงจาก registry ที่ไม่มี image ดังกล่าว วิธีที่เหมาะสมในงานฝึกคือเลือกให้ชัดว่าจะใช้ registry ที่ node เข้าถึงได้ หรือโหลด image เข้า named cluster ที่ถูกต้อง แล้วตั้งชื่อและ pull policy ให้สอดคล้องกับวิธีนั้น
อีกตัวอย่างคือโหลด image เข้า cluster devops แต่ kubectl ใช้ context ของคลัสเตอร์อื่น คำสั่ง load สำเร็จกับที่หนึ่ง ขณะที่ apply เกิดอีกที่หนึ่ง ก่อนสาธิตจึงต้องตรวจ kubectl config current-context และชื่อ kind cluster หากใช้ namespace เฉพาะต้องระบุให้ตรงกันด้วย การหา object ไม่พบอาจเป็นเพราะ context หรือ namespace ผิด ไม่ใช่ว่า object ถูกลบ
การใช้ tag latest กับ image ในเครื่องอาจทำให้ pull policy ไม่ตรงความคาดหมาย จึงควรใช้ชื่อรุ่นที่เฉพาะสำหรับการฝึกและตรวจ policy ที่ถูกบันทึกใน object จริง ไม่สรุปจากชื่อในไฟล์ต้นทางเพียงอย่างเดียว เมื่อใช้ registry จริงต้องตรวจสิทธิ์จากบริบทของ node ไม่ใช่พิสูจน์ด้วยการ pull บนบัญชี host เท่านั้น
14.9 ConfigMap, environment และการเปลี่ยนค่าระหว่างรัน
ConfigMap ใช้เก็บ config ที่ไม่เป็นความลับ สามารถอ้างเป็น environment หรือไฟล์ที่ mount ได้ แต่รูปแบบที่เลือกมีผลต่อการอัปเดต เอกสาร Kubernetes ระบุว่า ConfigMap ที่ใช้เป็น environment ของ container ไม่ปรับค่าของกระบวนการที่รันอยู่โดยอัตโนมัติ ต้องสร้าง Pod ใหม่เพื่อรับค่าใหม่ ส่วนการ mount มีเงื่อนไขและความล่าช้าของการกระจายค่า และแอปต้องอ่านค่าที่เปลี่ยนด้วย (Kubernetes: ConfigMaps)
ตัวอย่างที่ผิดคือเปลี่ยนข้อความ APP_MESSAGE ใน ConfigMap แล้วตรวจ Pod เดิมซึ่งรับผ่าน environment จากนั้นสรุปว่า Kubernetes ไม่บันทึกค่าตั้ง ควรแยกค่าที่ object เก็บออกจากค่าที่กระบวนการได้รับตอนเริ่ม ตรวจ ConfigMap ปัจจุบัน ตรวจ Pod และใช้การเริ่ม Pod ใหม่ตามวิธีที่ควบคุมได้เมื่อจำเป็น
อีกความผิดคือใส่รหัสผ่านใน ConfigMap เพราะเห็นว่าไม่ต้อง commit ลงโค้ด ConfigMap ไม่ได้ออกแบบเป็นที่เก็บความลับ และการใช้ Kubernetes Secret ก็ยังต้องออกแบบสิทธิ์ การเข้ารหัสและการจัดการวงจรชีวิต ไม่ใช่เพียงเปลี่ยนชื่อชนิด object ในรายวิชาควรใช้เฉพาะค่าที่ไม่ลับใน ConfigMap และไม่เพิ่มข้อมูลจริงเพื่อสาธิต
ขอบเขตของ kind ต้องระบุชัดว่าเป็นสภาพแวดล้อมฝึก หาก host เครื่องเดียวหยุด node containers ทั้งหมดอาจหยุดพร้อมกัน การมีหลาย Pod หรือหลาย node containers บน host เดียวไม่พิสูจน์ความทนต่อการเสียของเครื่องจริง เช่นเดียวกับ replicas ของแอปไม่ทำให้ฐานข้อมูลสำรองหรือข้อมูลคงอยู่โดยอัตโนมัติ การสรุปผลจึงควรแยกสิ่งที่แสดงกลไกได้จากสิ่งที่ยังไม่ได้ทดสอบด้านความทนทาน
กรณีอธิบายต่อเนื่อง: Service มีชื่อถูกแต่เลือก Pod ไม่ได้
ข้อมูลสมมติมี Deployment ชื่อ devops-app และ Pod ที่มี label app: devops-app แต่ Service ชื่อเดียวกันใช้ selector app: devops-api การดูรายชื่อ object จะเห็นครบทั้งหมด ทว่าความสัมพันธ์ไม่ตรง วิธีตรวจคือเปิด labels และ selector เทียบกัน แล้วอ่าน EndpointSlice ของ Service ไม่ใช้ชื่อ object เป็นตัวแทนความสัมพันธ์
เมื่อแก้ selector ตรงแล้ว หากยังเรียกไม่ได้ต้องตรวจ readiness และ targetPort ต่อ สมมติ endpoints เลือก Pod ได้แต่ Pod ยังไม่ Ready เพราะ probe เรียก path ผิด การแก้ label อย่างเดียวไม่ทำให้ปลายทางพร้อม ข้อผิดพลาดสองชั้นอาจเกิดพร้อมกันได้ การพบและแก้หนึ่งเหตุจึงยังต้องตรวจข้อกำหนดสุดท้ายอีกครั้ง
ตัวอย่างที่ผิดคือปิด readiness probe เพื่อให้มี endpoints โดยไม่แก้ path ของตัวตรวจ วิธีนี้ทำให้เงื่อนไขความพร้อมหายไปและอาจส่ง traffic ไปหาแอปที่ยังไม่พร้อม การแก้ที่เหมาะสมคือเทียบ endpoint จริงกับ probe path ตรวจผลทั้งกรณีพร้อมและไม่พร้อม แล้วรักษาสัญญาของ probe ไว้
ผลสรุปควรมี manifest ที่แก้ diff ของ selector หรือ probe และหลักฐาน request ผ่าน Service หลังแก้ หากแสดงเฉพาะ port-forward ไป Pod จะยังข้ามเส้นทาง Service ที่โจทย์ต้องการพิสูจน์ ต้องระบุขอบเขตของทุกคำสั่งที่ใช้ทดสอบ
ใบงานเลือกแนวทางดูแลระบบ
สถานการณ์สำหรับใบงานเปรียบเทียบ Compose กับ Kubernetes (LLO 14.2)
ทีม 3 คนดูแลแอป Node.js กับฐานข้อมูลหนึ่งชุดบน VM เพียงเครื่องเดียว ผู้ดูแลมีประสบการณ์ Docker Compose และยอมรับช่วงหยุดบริการสั้น ๆ ระหว่าง deploy ได้ ยังไม่มีข้อกำหนดขยายหลายเครื่องและไม่มีผู้ดูแล Kubernetes โดยเฉพาะ เลือกแนวทางที่เหมาะกับสถานการณ์ปัจจุบัน พร้อมเหตุผล 2 ด้านและข้อจำกัด จากนั้นอธิบายว่าหากต้องรันแอปหลายเครื่องและกระจายงานข้ามเครื่อง เงื่อนไขใดทำให้ควรพิจารณา Kubernetes เพิ่มเติม
ตอบรายบุคคลไม่เกิน 1 หน้า อ้างสิ่งที่สังเกตจากแลบเมื่อมีผลจริง หรือระบุว่าเป็นการวิเคราะห์จากโจทย์เมื่อยังไม่ได้ทดลอง ไม่อ้างว่า kind บนเครื่องเดียวทำให้ระบบทนต่อการเสียของเครื่อง host ได้
ปฏิบัติการที่ 14 นำแอปของทีมขึ้นคลัสเตอร์ kind
ปฏิบัติการนี้ประกอบด้วย Lab A–E Lab A และ B เป็นงานรายบุคคลบนคลัสเตอร์ของตนเองและไม่มีไฟล์เข้า repository ส่วน Lab C–E เพิ่มไฟล์ในโฟลเดอร์ k8s/ ของ repository ทีมผ่าน pull request ตามปกติ โดยแบ่งผู้รับผิดชอบดังนี้ เพื่อให้ทุกคนมี pull request ของตนเองใน milestone M12
| Lab | สมาชิก | branch | ไฟล์ | ผู้ตรวจทาน |
|---|---|---|---|---|
| C | สมาชิกคนที่ 1 | k8s/deployment | เพิ่ม k8s/deployment.yaml | สมาชิกคนที่ 2 |
| D | สมาชิกคนที่ 2 | k8s/service | เพิ่ม k8s/service.yaml | สมาชิกคนที่ 3 |
| E | สมาชิกคนที่ 3 | k8s/configmap | เพิ่ม k8s/configmap.yaml และเพิ่ม env ใน k8s/deployment.yaml | สมาชิกคนที่ 1 |
ทุก pull request ใช้ template ของทีมใน .github/pull_request_template.md (บทที่ 3) หัวข้อ "วิธีทดสอบ" ระบุคำสั่ง kubectl ที่ผู้ตรวจทานใช้ตรวจบนคลัสเตอร์ของตนได้ และต้องผ่าน review และ required status check ตาม branch protection ของ main แม้ CI ไม่ได้ทดสอบไฟล์ใน k8s/ สมาชิกทุกคนยังต้องทำทุกขั้นของ Lab C–E บนคลัสเตอร์ของตนเอง โดยดึงไฟล์จาก branch ของเพื่อนหรือจาก main หลัง merge แต่ละ Lab แบ่งเป็นส่วน ฝึกตามตัวอย่าง และ ตัดสินใจเอง
ข้อความที่แอปตอบ (เช่น {"status":"ok","version":"0.5.2"} และ DevOps App on Kubernetes v0.5.2) ยืนยันแล้วด้วยการรัน image เดียวกันใน Docker ส่วนผลของคำสั่ง kind และ kubectl ทุกคำสั่งเป็น ผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง) จึงไม่แสดงเป็นข้อความผลลัพธ์เต็ม ให้บันทึกผลจริงบนเครื่องของตนเป็นหลักฐาน
Lab A: สร้างคลัสเตอร์ kind และสำรวจด้วย kubectl
สถานการณ์: ก่อนนำแอปขึ้นคลัสเตอร์ ทีมต้องมีคลัสเตอร์ที่ทำงาน รู้ว่ากำลังสั่งคลัสเตอร์ใด และเห็นด้วยตาว่าองค์ประกอบของ control plane ในหัวข้อ 14.2 อยู่ที่ใด
เป้าหมาย: สร้างคลัสเตอร์ชื่อ devops แล้วใช้ kubectl สำรวจ node และ Pod ของระบบ (เตรียมสำหรับ LLO 14.1)
ฝึกตามตัวอย่าง
A.1 ตรวจเครื่องมือและหน่วยความจำ
kind version
kubectl version --client
docker info --format '{{.MemTotal}}'
free -hถ้าเครื่องมี RAM น้อยกว่า 8 GB หรือคำสั่งใดหาไม่พบ ให้แจ้งผู้สอนและตกลงกันในทีมว่าจะใช้เครื่องของสมาชิกคนใดเป็นเครื่องกลาง สมาชิกคนอื่นทำ Lab ต่อบนเครื่องนั้นผลัดกันพิมพ์คำสั่ง
A.2 สร้างคลัสเตอร์
kind create cluster --name devops
kind get clustersผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): ครั้งแรกจะใช้เวลาดึง node image kindest/node จากนั้น kind แสดงความคืบหน้าของการสร้างทีละขั้นและจบโดยไม่มีข้อผิดพลาด kind get clusters แสดงชื่อ devops
A.3 ตรวจ context และข้อมูลคลัสเตอร์
kubectl config current-context
kubectl cluster-info --context kind-devopsผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): context ปัจจุบันชื่อ kind-devops และ cluster-info แสดงที่อยู่ของ control plane บนเครื่องของตนเอง (localhost)
A.4 สำรวจ node และ Pod ของระบบ
kubectl get nodes
kubectl get pods -A
docker psผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): kubectl get nodes แสดง node เดียวที่มีบทบาท control-plane และมีสถานะ Ready เมื่อพร้อม kubectl get pods -A แสดง Pod ใน namespace kube-system ซึ่งชื่อสอดคล้องกับองค์ประกอบในหัวข้อ 14.2 ส่วน docker ps แสดง container ของ node หนึ่งตัว ซึ่งตามรูปแบบในเอกสารของ kind ควรชื่อ devops-control-plane
ตัดสินใจเอง A.5: จากผล kubectl get pods -A จับคู่ Pod แต่ละตัวกับองค์ประกอบในตารางหัวข้อ 14.2 แล้วระบุว่าองค์ประกอบใดในตารางไม่ปรากฏเป็น Pod และคาดว่ามันทำงานอยู่ที่ใด บันทึกเป็นตารางสั้น ๆ ใน Learning Log
ผลลัพธ์ที่คาดหวัง A.5: ตารางที่จับคู่ชื่อ Pod กับองค์ประกอบ อย่างน้อยครบ kube-apiserver, etcd, kube-scheduler และ kube-controller-manager พร้อมคำอธิบายองค์ประกอบที่ไม่เป็น Pod
จุดตรวจ A: kind get clusters แสดง devops · kubectl config current-context เป็น kind-devops · node อยู่สถานะ Ready
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| kind แจ้งว่าเชื่อมต่อ Docker ไม่ได้ หรือไม่มีสิทธิ์ | ผู้ใช้ไม่อยู่ในกลุ่ม docker หรือ Docker ไม่ทำงาน | ตรวจด้วย docker ps ตามบทที่ 6 แล้วสร้างคลัสเตอร์ใหม่ |
สร้างคลัสเตอร์ไม่ได้เพราะมีชื่อ devops อยู่แล้ว | เคยสร้างไว้ในคาบก่อนหรือสร้างซ้ำ | ใช้คลัสเตอร์เดิม หรือ kind delete cluster --name devops แล้วสร้างใหม่ |
ดึง image kindest/node นานมากหรือไม่สำเร็จ | เครือข่ายไปยัง Docker Hub ช้าหรือถูกจำกัด | แจ้งผู้สอน ผู้สอนเตรียม image ไว้ล่วงหน้าตามบันทึกสำหรับผู้สอน |
| node ค้างที่สถานะ NotReady หรือเครื่องช้ามาก | RAM ไม่พอเมื่อเปิด container อื่นค้างไว้ | หยุดระบบ Compose ของบทก่อน (docker compose ... down) หรือย้ายไปเครื่องกลางของทีม |
kubectl สั่งคลัสเตอร์อื่น | context ปัจจุบันไม่ใช่ kind-devops | kubectl config use-context kind-devops |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
Lab B: image สำเร็จรูปเป็น Pod เดี่ยว
สถานการณ์: ก่อนนำแอปของทีมขึ้นคลัสเตอร์ ทีมทดลองกับ image สำเร็จรูปที่รู้จักแล้วก่อน เพื่อแยกปัญหาของคลัสเตอร์ออกจากปัญหาของ image ของทีม nginx:1.28 คือ image เดียวกับบริการ proxy ใน compose.yaml
เป้าหมาย: สร้าง Pod เดี่ยวจาก manifest ตรวจสถานะ เหตุการณ์ และ log แล้วสังเกตว่าเกิดอะไรขึ้นเมื่อ Pod ที่ไม่มี controller ถูกลบ (เตรียมสำหรับ LLO 14.1)
ฝึกตามตัวอย่าง
B.1 เขียน manifest ของ Pod ไฟล์นี้เป็นไฟล์ฝึกหัด เก็บไว้นอก repository ทีม
mkdir -p ~/k8s-practice
cd ~/k8s-practice
nano pod-hello.yaml# Lab B: Pod เดี่ยวจาก image สำเร็จรูป (nginx:1.28 ใช้มาแล้วในบท 7 และ 9)
apiVersion: v1
kind: Pod
metadata:
name: hello
labels:
app: hello
spec:
containers:
- name: nginx
image: nginx:1.28
ports:
- containerPort: 80| ส่วน | หน้าที่ |
|---|---|
apiVersion: v1 และ kind: Pod | บอกชนิดของ object และรุ่นของ API |
metadata.name และ metadata.labels | ชื่อของ Pod และ label สำหรับให้ object อื่นเลือก |
spec.containers | รายการ container ใน Pod ในที่นี้มีหนึ่งตัวชื่อ nginx |
containerPort: 80 | บอกพอร์ตที่ container ฟัง (เป็นข้อมูลประกอบ ไม่ได้เปิดพอร์ตออกนอกคลัสเตอร์) |
B.2 สร้างและตรวจ Pod
kubectl apply -f pod-hello.yaml
kubectl get pods -o wide
kubectl describe pods hello
kubectl logs helloผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): หลังดึง image เสร็จ Pod hello อยู่สถานะ Running และ READY 1/1 คอลัมน์ IP แสดงที่อยู่ภายในคลัสเตอร์ ส่วนท้ายของ describe เป็นรายการเหตุการณ์ เช่น การจัด Pod ให้ node การดึง image การสร้างและเริ่ม container ส่วน logs แสดงข้อความเริ่มทำงานของ Nginx
B.3 เข้าถึง Pod จากเครื่องของตน เปิดสองหน้าต่าง terminal
# หน้าต่างที่ 1 (ค้างไว้จนกว่าจะกด Ctrl+C)
kubectl port-forward pod/hello 8081:80# หน้าต่างที่ 2
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8081/ผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): รหัส 200 จากหน้าเริ่มต้นของ Nginx เมื่อเสร็จแล้วกด Ctrl+C ในหน้าต่างที่ 1
B.4 ลบ Pod แล้วสังเกต
kubectl delete -f pod-hello.yaml
kubectl get podsผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): Pod hello หายไปและไม่มี Pod ใหม่มาแทน เพราะไม่มี controller ดูแล ซึ่งตรงกับหัวข้อ 14.3 ที่ว่า Pod เป็นของชั่วคราวและควรสร้างผ่าน workload เช่น Deployment
ตัดสินใจเอง B.5: แก้ image ใน pod-hello.yaml เป็น tag ที่ไม่มีอยู่จริง เช่น nginx:1.28-not-exist แล้ว apply ใหม่ หาสาเหตุที่ Pod ไม่ทำงานโดยใช้คำสั่งจากหัวข้อ 14.5 แล้วตอบว่าคำสั่งใดให้ข้อมูลที่ชี้สาเหตุได้ชัดที่สุด คำสั่งใดไม่ช่วย และเพราะอะไร เสร็จแล้วลบ Pod นี้
ผลลัพธ์ที่คาดหวัง B.5: สถานะ Pod ที่สังเกตได้ ข้อความสำคัญจาก describe และคำอธิบายว่าเหตุใด logs ไม่ช่วย บันทึกใน Learning Log
จุดตรวจ B: สมาชิกอธิบายได้ว่าเหตุใด Pod hello ไม่กลับมาหลังถูกลบ และหาสาเหตุของ Pod ที่ดึง image ไม่ได้ด้วย describe
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
Pod ค้างที่ ContainerCreating นาน | กำลังดึง image จาก Docker Hub ครั้งแรก | รอแล้วดูความคืบหน้าใน describe |
Pod เป็น ErrImagePull หรือ ImagePullBackOff ทั้งที่ชื่อ image ถูก | node เข้าถึง Docker Hub ไม่ได้ | ดึงบนเครื่องด้วย docker pull nginx:1.28 แล้ว kind load docker-image nginx:1.28 --name devops |
port-forward แจ้งว่าพอร์ตถูกใช้อยู่ | มีโปรแกรมอื่นใช้พอร์ต 8081 บนเครื่อง | เปลี่ยนพอร์ตฝั่งซ้าย เช่น 8082:80 |
| kubectl แจ้งว่าไม่รู้จักชนิดของ object | สะกดชนิดของ object ผิด | ตรวจตัวสะกด เช่น pods, deployments, services |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
Lab C: Deployment ของแอปทีมจาก ghcr.io
สถานการณ์: ทีมต้องการรันแอป devops-app เวอร์ชัน 0.5.2 จำนวน 2 สำเนาบนคลัสเตอร์ โดยใช้ image เดียวกับที่ CD ของบทที่ 8 ส่งขึ้น ghcr.io
เป้าหมาย: นำ image ของทีมเข้าคลัสเตอร์ด้วย kind load docker-image แล้ว deploy ด้วย Deployment จาก manifest แม่แบบ (LLO 14.1)
ฝึกตามตัวอย่าง
C.1 นำ image เข้าคลัสเตอร์ (สมาชิกทุกคน บนคลัสเตอร์ของตน) แทน devops-team-01 ด้วยชื่อ owner บน ghcr.io ของทีม ถ้า package เป็นแบบ private ให้ login ก่อนตามบทที่ 8 โดยใช้ personal access token (classic) ที่มีสิทธิ์ read:packages
read -rs CR_PAT && echo "$CR_PAT" | docker login ghcr.io -u <github-user> --password-stdin
unset CR_PAT
docker pull ghcr.io/devops-team-01/devops-app:0.5.2
kind load docker-image ghcr.io/devops-team-01/devops-app:0.5.2 --name devops
docker exec devops-control-plane crictl images| คำสั่ง | หน้าที่ |
|---|---|
docker login ghcr.io ... --password-stdin | login ด้วย token ที่ส่งทาง stdin ไม่ให้ token อยู่ในประวัติคำสั่ง |
docker pull | ดึง image ลง Docker ของเครื่อง |
kind load docker-image ... --name devops | คัดลอก image จาก Docker ของเครื่องเข้า node ของคลัสเตอร์ชื่อ devops (ต้องระบุ --name เมื่อคลัสเตอร์ไม่ได้ชื่อ kind) |
docker exec devops-control-plane crictl images | ดูรายการ image ภายใน node ตามวิธีในเอกสารของ kind |
วิธีนี้ใช้ได้ทั้ง package แบบ public และ private เพราะการดึงเกิดที่ Docker ของเครื่องซึ่ง login ไว้แล้ว node ไม่ต้องติดต่อ ghcr.io เอง ผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): รายการจาก crictl images มี image ghcr.io/devops-team-01/devops-app tag 0.5.2
C.2 เขียน Deployment (สมาชิกคนที่ 1) Lab นี้ยังไม่มีส่วน env ส่วนนั้นจะเพิ่มใน Lab E
cd ~/devops-app
git switch main
git pull
git switch -c k8s/deployment
mkdir -p k8s
nano k8s/deployment.yamlapiVersion: apps/v1
kind: Deployment
metadata:
name: devops-app
labels:
app: devops-app
spec:
replicas: 2
selector:
matchLabels:
app: devops-app
template:
metadata:
labels:
app: devops-app
spec:
containers:
- name: app
# เปลี่ยน devops-team-01 เป็นชื่อ owner บน ghcr.io ของทีม
image: ghcr.io/devops-team-01/devops-app:0.5.2
ports:
- containerPort: 3000| ส่วน | หน้าที่ |
|---|---|
replicas: 2 | สถานะที่ต้องการ: Pod ของแอป 2 ตัว |
selector.matchLabels | บอก Deployment ว่า Pod ใดเป็นของตน ต้องตรงกับ label ใน template |
template | แม่แบบของ Pod ทุกตัวที่ Deployment สร้าง |
name: app | ชื่อ container (บทที่ 15 ใช้ชื่อนี้ในคำสั่ง kubectl set image) |
image: ...:0.5.2 | tag ตายตัว ไม่ใช้ latest ค่าเริ่มต้นของ pull policy จึงเป็น IfNotPresent node ใช้ image ที่ kind load ไว้ได้ |
containerPort: 3000 | พอร์ตที่แอปฟัง ตรงกับ EXPOSE 3000 ใน Dockerfile |
C.3 deploy และตรวจผล
kubectl config current-context
kubectl apply -f k8s/deployment.yaml
kubectl get deployments
kubectl get pods -l app=devops-app -o wide
kubectl logs deploy/devops-appผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): Deployment devops-app มีคอลัมน์ READY เป็น 2/2 เมื่อพร้อม มี Pod 2 ตัวชื่อรูปแบบ devops-app-<hash>-<id> สถานะ Running และ log ของแอปมีบรรทัด listening on port 3000 (ข้อความเดียวกับที่ทดสอบแล้วใน Docker)
C.4 เปิด pull request (สมาชิกคนที่ 1)
git add k8s/deployment.yaml
git commit -m "k8s: add Deployment for devops-app 0.5.2"
git push -u origin k8s/deploymentในคำอธิบาย pull request หัวข้อ "วิธีทดสอบ" ให้ระบุคำสั่ง kind load docker-image ..., kubectl apply -f k8s/deployment.yaml และ kubectl get pods -l app=devops-app ผู้ตรวจทานรันคำสั่งเหล่านี้บนคลัสเตอร์ของตนก่อน approve
ตัดสินใจเอง C.5: แก้ replicas ในไฟล์บนเครื่องเป็น 3 แล้ว kubectl apply ใหม่ สังเกตจำนวน Pod และการใช้หน่วยความจำของเครื่องด้วย free -h จากนั้นตัดสินใจว่าค่าที่จะ commit ใน k8s/deployment.yaml ควรเป็นเท่าใด โดยให้เหตุผลจาก RAM ของเครื่องที่ทีมใช้ และความต้องการให้ระบบไม่หยุดระหว่างอัปเดตในบทที่ 15 ถ้าเลือกค่าอื่นที่ไม่ใช่ 2 ให้ระบุเหตุผลในคำอธิบาย pull request
ผลลัพธ์ที่คาดหวัง C.5: จำนวน Pod ที่สังเกตได้หลังเปลี่ยนค่า ค่าที่เลือก และเหตุผลอย่างน้อย 2 ข้อในคำอธิบาย pull request
จุดตรวจ C: Pod ของ devops-app อยู่สถานะ Running ครบตามจำนวน และ pull request k8s/deployment ถูก merge แล้ว
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
docker pull แจ้งว่าไม่มีสิทธิ์หรือไม่พบ image | package เป็น private และยังไม่ login หรือ token ไม่มี read:packages หรือชื่อ owner ผิด | login ใหม่ตามบทที่ 8 และตรวจชื่อ image ในหน้า Packages ของ repository |
Pod เป็น ErrImagePull หรือ ImagePullBackOff | ไม่ได้ kind load หรือ load เข้าคลัสเตอร์อื่น (ลืม --name devops) หรือชื่อ image ใน manifest ไม่ตรงกับที่ load | load ใหม่ด้วยชื่อที่ตรงกันทุกตัวอักษร แล้วรอให้ Pod ลองใหม่ |
| ทีมไม่มี image ของตนบน ghcr.io | CD ของบทที่ 8 ไม่เคยผ่าน | แจ้งผู้สอนเพื่อใช้ branch เฉลย (solution/m7 ขึ้นไป) ตามแผนรับมือ |
kubectl apply ถูกปฏิเสธเรื่อง selector | label ใน selector กับ template ไม่ตรงกัน | แก้ให้ตรงกันทั้งสองที่ |
| YAML ผิดรูปแบบ | เยื้องบรรทัดไม่ตรง หรือใช้ tab | ใช้ช่องว่าง 2 ตัวตามตัวอย่าง |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
Lab D: Service และเข้าถึงผ่าน port-forward
สถานการณ์: Pod ของแอปมี IP ที่เปลี่ยนเมื่อ Pod ถูกสร้างใหม่ และมี 2 ตัว ทีมต้องการชื่อเดียวที่คงที่สำหรับเรียกแอป เหมือนชื่อบริการ app ที่ proxy เรียกใน Compose และต้องการทดสอบแอปจากเครื่องของตน
เป้าหมาย: สร้าง Service ชนิด ClusterIP ที่เลือก Pod ของแอปด้วย label แล้วเรียกแอปผ่าน kubectl port-forward (LLO 14.1)
ฝึกตามตัวอย่าง
D.1 เขียน Service (สมาชิกคนที่ 2)
cd ~/devops-app
git switch main
git pull
git switch -c k8s/service
nano k8s/service.yamlapiVersion: v1
kind: Service
metadata:
name: devops-app
labels:
app: devops-app
spec:
type: ClusterIP
selector:
app: devops-app
ports:
- port: 80
targetPort: 3000type: ClusterIP เป็นค่าเริ่มต้นที่เขียนไว้ให้อ่านชัด selector ต้องตรงกับ label ใน template ของ Deployment และ targetPort: 3000 ต้องตรงกับ containerPort ส่วน port: 80 คือพอร์ตของ Service เอง
D.2 สร้างและตรวจ Service
kubectl apply -f k8s/service.yaml
kubectl get svc devops-app
kubectl get pods -l app=devops-app -o wide
kubectl describe svc devops-appผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): Service devops-app ชนิด ClusterIP มี CLUSTER-IP ภายในคลัสเตอร์และพอร์ต 80/TCP ใน describe ส่วนที่แสดงปลายทาง (endpoints) ควรมี IP ของ Pod 2 ตัวที่ตรงกับคอลัมน์ IP ของ kubectl get pods -o wide ต่อด้วยพอร์ต 3000
D.3 เรียกแอปผ่าน port-forward เปิดสองหน้าต่าง terminal
# หน้าต่างที่ 1 (ค้างไว้)
kubectl port-forward service/devops-app 8080:80# หน้าต่างที่ 2
curl -s http://localhost:8080/health
curl -s http://localhost:8080/
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/healthผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): /health ตอบ {"status":"ok","version":"0.5.2"} / ตอบ DevOps App v0.5.2 และรหัส 200 (ข้อความของแอปเมื่อไม่ได้ตั้ง APP_TITLE ยืนยันแล้วด้วย Docker ส่วนเส้นทางผ่าน Service และ port-forward ยังไม่ได้ทดสอบ)
D.4 เปิด pull request (สมาชิกคนที่ 2)
git add k8s/service.yaml
git commit -m "k8s: add ClusterIP Service for devops-app"
git push -u origin k8s/serviceหัวข้อ "วิธีทดสอบ" ระบุคำสั่ง kubectl apply -f k8s/service.yaml, kubectl port-forward service/devops-app 8080:80 และ curl -s http://localhost:8080/health
ตัดสินใจเอง D.5: kubectl port-forward รับเป้าหมายได้หลายแบบ เช่น pod/<ชื่อ Pod>, deployment/devops-app และ service/devops-app ทดลองอย่างน้อยสองแบบ แล้วตัดสินใจว่าแบบใดเหมาะกับการเก็บหลักฐาน M12 ที่ต้องการพิสูจน์ว่า Service ของทีมตั้งค่าถูกต้อง เพราะอะไร จากนั้นแก้ selector ในไฟล์บนเครื่องให้ผิดชั่วคราว (เช่น app: devops) apply แล้วลอง port-forward ทั้งสองแบบอีกครั้ง บันทึกความต่าง แล้วแก้กลับ
ผลลัพธ์ที่คาดหวัง D.5: แบบที่เลือกพร้อมเหตุผล และผลที่สังเกตได้เมื่อ selector ผิด บันทึกใน Learning Log
จุดตรวจ D: curl ผ่าน service/devops-app ได้ 200 จาก /health และ pull request k8s/service ถูก merge แล้ว
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| port-forward ไปที่ Service ไม่สำเร็จ | ไม่มี Pod ที่ตรง selector หรือ Pod ยังไม่ Running | ตรวจ kubectl get pods --show-labels เทียบกับ selector |
curl ได้ connection refused ทั้งที่ port-forward ทำงาน | targetPort ไม่ตรงกับพอร์ตที่แอปฟัง | แก้ targetPort เป็น 3000 |
curl ค้างหรือเชื่อมต่อไม่ได้ | ปิดหน้าต่างที่ 1 ไปแล้ว หรือกด Ctrl+C | เปิด port-forward ใหม่ทิ้งไว้ |
| พอร์ต 8080 ถูกใช้อยู่ | มีโปรแกรมอื่นบนเครื่องใช้พอร์ตนี้ | เปลี่ยนพอร์ตฝั่งซ้าย เช่น 18080:80 |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
Lab E: ย้ายค่าตั้งไป ConfigMap
สถานการณ์: ใน Compose ทีมตั้ง APP_TITLE ผ่าน environment และไฟล์ .env บน Kubernetes ทีมต้องการเก็บค่านี้แยกจาก Deployment เพื่อเปลี่ยนข้อความหน้าแรกได้โดยไม่ต้อง build image ใหม่หรือแก้แม่แบบของ Pod
เป้าหมาย: สร้าง ConfigMap devops-app-config แล้วส่งค่า APP_TITLE ให้ container ผ่านตัวแปรสภาพแวดล้อม และเก็บหลักฐาน M12 ให้ครบ (LLO 14.1)
ฝึกตามตัวอย่าง
E.1 เขียน ConfigMap (สมาชิกคนที่ 3)
cd ~/devops-app
git switch main
git pull
git switch -c k8s/configmap
nano k8s/configmap.yaml# ค่าตั้งของแอป (ไม่ใช่ความลับ) — แอปอ่าน APP_TITLE จากตัวแปรสภาพแวดล้อม
apiVersion: v1
kind: ConfigMap
metadata:
name: devops-app-config
labels:
app: devops-app
data:
APP_TITLE: "DevOps App on Kubernetes"E.2 เพิ่ม env ใน Deployment เปิด k8s/deployment.yaml แล้วเพิ่มส่วน env ต่อจาก ports ในระดับเดียวกัน ไฟล์ทั้งหมดหลังแก้เป็นดังนี้
apiVersion: apps/v1
kind: Deployment
metadata:
name: devops-app
labels:
app: devops-app
spec:
replicas: 2
selector:
matchLabels:
app: devops-app
template:
metadata:
labels:
app: devops-app
spec:
containers:
- name: app
# เปลี่ยน devops-team-01 เป็นชื่อ owner บน ghcr.io ของทีม
image: ghcr.io/devops-team-01/devops-app:0.5.2
ports:
- containerPort: 3000
env:
- name: APP_TITLE
valueFrom:
configMapKeyRef:
name: devops-app-config
key: APP_TITLE| ส่วน | หน้าที่ |
|---|---|
env[].name: APP_TITLE | ชื่อตัวแปรสภาพแวดล้อมใน container ซึ่งแอปอ่านอยู่แล้ว |
configMapKeyRef.name | ชื่อ ConfigMap ที่ต้องมีอยู่ใน namespace เดียวกัน |
configMapKeyRef.key | คีย์ใน data ของ ConfigMap |
E.3 apply และตรวจ สร้าง ConfigMap ก่อน Deployment เพื่อให้ Pod ใหม่หาค่าที่อ้างถึงพบ
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/deployment.yaml
kubectl get pods -l app=devops-app
kubectl get configmap devops-app-config -o yaml
kubectl exec <ชื่อ Pod หนึ่งตัว> -- printenv APP_TITLEการเพิ่ม env เปลี่ยนแม่แบบของ Pod (.spec.template) Deployment จึงสร้าง Pod ชุดใหม่แทนชุดเดิม ผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): ชื่อ Pod เปลี่ยนจากใน Lab C และ printenv แสดง DevOps App on Kubernetes
E.4 ตรวจผ่าน Service และเก็บหลักฐาน M12 เปิด kubectl port-forward service/devops-app 8080:80 ในหน้าต่างที่ 1 แล้วรันในหน้าต่างที่ 2
kubectl apply -f k8s/
kubectl get pods,svc
curl -s http://localhost:8080/health
curl -s http://localhost:8080/
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/ผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): kubectl apply -f k8s/ รายงานว่า object ทั้งสามไม่เปลี่ยน และ / ตอบ DevOps App on Kubernetes v0.5.2 (ข้อความนี้ยืนยันแล้วใน Docker ด้วยค่า APP_TITLE เดียวกัน) ให้บันทึกผลของ kubectl get pods,svc และ curl ทั้งหมดเป็นหลักฐานงานข้อ 1 ของตนเอง
E.5 เปิด pull request (สมาชิกคนที่ 3)
git add k8s/configmap.yaml k8s/deployment.yaml
git commit -m "k8s: move APP_TITLE into ConfigMap"
git push -u origin k8s/configmapตัดสินใจเอง E.6: เปลี่ยนค่า APP_TITLE ใน k8s/configmap.yaml บนเครื่องเป็นชื่อทีม แล้ว kubectl apply -f k8s/configmap.yaml เรียก / ผ่าน port-forward ทันที บันทึกว่าข้อความเปลี่ยนหรือไม่ จากนั้นค้นในหน้า ConfigMaps ว่าเหตุใดจึงเป็นเช่นนั้น และเลือกวิธีทำให้ Pod ใช้ค่าใหม่ (เช่น kubectl rollout restart deployment/devops-app) อธิบายผลกระทบต่อผู้ใช้ของวิธีที่เลือก สุดท้ายตัดสินใจว่าจะ commit ชื่อทีมเข้า main หรือไม่ เพราะอะไร
ผลลัพธ์ที่คาดหวัง E.6: ข้อความก่อนและหลังเริ่ม Pod ใหม่ ประโยคจากเอกสารทางการที่อธิบายผล และเหตุผลของการตัดสินใจเรื่อง commit บันทึกใน Learning Log
จุดตรวจ E: / ผ่าน port-forward แสดงค่าจาก ConfigMap บน main มีไฟล์ใน k8s/ ครบสามไฟล์ และสมาชิกทุกคนมีหลักฐาน M12 ของตนเอง
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
Pod ใหม่ไม่เริ่ม และ describe แจ้งว่าหา ConfigMap หรือคีย์ไม่พบ | apply Deployment ก่อน ConfigMap หรือชื่อ ConfigMap หรือคีย์สะกดผิด | apply k8s/configmap.yaml แล้วตรวจชื่อและคีย์ให้ตรงกัน |
/ ยังแสดง DevOps App v0.5.2 | เรียกผ่าน port-forward ที่เปิดไว้ก่อน Pod ถูกแทน หรือ env เยื้องผิดระดับ | เปิด port-forward ใหม่ และตรวจว่า env อยู่ระดับเดียวกับ ports |
| แก้ ConfigMap แล้วหน้าแรกไม่เปลี่ยน | ตัวแปรสภาพแวดล้อมจาก ConfigMap ไม่อัปเดตจนกว่าจะเริ่ม Pod ใหม่ | ดูตัดสินใจเอง E.6 |
kubectl exec แจ้งว่าไม่พบ Pod | ใช้ชื่อ Pod เก่าที่ถูกแทนไปแล้ว | ดูชื่อใหม่จาก kubectl get pods -l app=devops-app |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
เก็บกวาด
เมื่อเก็บหลักฐาน M12 ครบและ pull request ทั้งสามรายการ merge แล้ว ลบคลัสเตอร์เพื่อคืนหน่วยความจำ บทที่ 15 จะสร้างคลัสเตอร์ใหม่จาก k8s/ บน main
kind delete cluster --name devops
kind get clusters
docker psผลที่คาดว่าจะเห็น (ยังไม่ได้ทดสอบบนคลัสเตอร์จริง): kind get clusters ไม่แสดง devops และ docker ps ไม่มี container ของ node อีก image ของทีมใน Docker ของเครื่องยังอยู่ จึงใช้ kind load ได้ทันทีในบทที่ 15 โฟลเดอร์ ~/k8s-practice เก็บไว้ได้ ลบ branch ที่ merge แล้วตามปกติ และ docker logout ghcr.io ถ้าใช้เครื่องร่วมกับผู้อื่น
สรุป
Docker Compose บนเครื่องเดียวเพียงพอสำหรับระบบขนาดเล็ก แต่มีข้อจำกัดที่ทีมพบในบทที่ 12 คือเครื่องเดียว container เดียว มีช่วงไม่ตอบระหว่าง deploy และต้องมีคนสั่งเริ่มใหม่หรือ rollback Kubernetes ใช้แนวคิดสถานะที่ต้องการ ทีมเขียน manifest แล้ว controller ปรับสถานะจริงให้ตรงอย่างต่อเนื่อง คลัสเตอร์ประกอบด้วย control plane และ node ซึ่ง kind ย่อไว้ใน container บนเครื่องห้องปฏิบัติการ
แอปของทีมใช้ object สี่ชนิดที่เชื่อมกันด้วย label app: devops-app Deployment รักษา Pod 2 ตัว Service ชนิด ClusterIP เป็นชื่อคงที่ของกลุ่ม Pod และส่งพอร์ต 80 ไปที่ 3000 และ ConfigMap แยกค่า APP_TITLE ออกจาก image การเทียบกับ compose.yaml ต้องดูที่หน้าที่ ไม่ใช่ชื่อฟิลด์ เพราะบางส่วนไม่มีคู่ (build, depends_on) และบางส่วนอยู่ในบทที่ 15 (healthcheck กับ probe) ลำดับ get → describe → logs ใช้หาสาเหตุเมื่อ Pod ไม่ทำงาน และ kind load docker-image ได้ผลเพราะ manifest ใช้ tag ตายตัว
สิ่งที่ส่งต่อไปยังบทที่ 15
| สิ่งที่ได้ | นำไปใช้ที่ |
|---|---|
k8s/configmap.yaml, k8s/deployment.yaml, k8s/service.yaml บน main (M12) | บทที่ 15 Lab A สร้างคลัสเตอร์ใหม่แล้ว kubectl apply -f k8s/ ก่อนเพิ่ม probe |
image 0.5.2 ใน Docker ของเครื่อง และวิธี kind load docker-image | บทที่ 15 rolling update ไป tag ใหม่ และ rollback จาก 0.6.0 |
ชื่อ container app และ label app: devops-app | บทที่ 15 คำสั่ง kubectl set image และการสังเกต Pod ทดแทน |
| ใบงานเทียบ Compose กับ Kubernetes | บทที่ 15 หัวข้อสรุปวิชาและเส้นทางเรียนต่อ |
| Learning Log ครั้งที่ 14 | สะท้อนการเรียนรู้ของตนเอง (portfolio ส่งไปแล้วในบทที่ 13) |
สรุปคำสั่งในบทนี้
| คำสั่ง | ความหมาย |
|---|---|
kind create cluster --name devops | สร้างคลัสเตอร์ kind ชื่อ devops (context kind-devops) |
kind get clusters | แสดงรายชื่อคลัสเตอร์ kind บนเครื่อง |
kind load docker-image <image> --name devops | คัดลอก image จาก Docker ของเครื่องเข้า node ของคลัสเตอร์ |
kind delete cluster --name devops | ลบคลัสเตอร์และคืนหน่วยความจำ |
kubectl config current-context | ดูคลัสเตอร์ที่กำลังสั่ง |
kubectl cluster-info --context kind-devops | แสดงที่อยู่ control plane ของคลัสเตอร์ |
kubectl get nodes / kubectl get pods -A | แสดง node และ Pod ทุก namespace |
kubectl get pods,svc | แสดง Pod และ Service พร้อมกัน (หลักฐาน M12) |
kubectl get pods -l app=devops-app -o wide | แสดง Pod ที่มี label ที่กำหนดพร้อม IP |
kubectl describe pods <ชื่อ> | แสดงรายละเอียดและเหตุการณ์ของ Pod |
kubectl logs deploy/devops-app | ดู log ของ Pod ใน Deployment |
kubectl apply -f k8s/ | สร้างหรือปรับ object ตาม manifest ทุกไฟล์ในโฟลเดอร์ |
kubectl delete -f <ไฟล์> | ลบ object ตาม manifest |
kubectl exec <Pod> -- printenv APP_TITLE | อ่านตัวแปรสภาพแวดล้อมใน container |
kubectl port-forward service/devops-app 8080:80 | ส่งต่อพอร์ต 8080 ของเครื่องไปยัง Pod ที่ Service เลือก |
kubectl rollout restart deployment/devops-app | เริ่ม Pod ของ Deployment ใหม่ เช่น หลังแก้ ConfigMap |
docker exec devops-control-plane crictl images | ดูรายการ image ภายใน node ของ kind |
แบบฝึกหัดท้ายบท
แบบฝึกหัดแบ่งเป็นสามกลุ่ม ได้แก่ ความเข้าใจ การประยุกต์ และการวิเคราะห์ แต่ละข้อระบุผลลัพธ์การเรียนรู้ที่ฝึก ข้อ ข1 คือแบบฝึกหัดจับคู่ส่วนของ compose.yaml กับ object ใน Kubernetes ตามแผนการสอน แบบฝึกหัดของบทนี้ไม่นับคะแนน
ก. ความเข้าใจ
- ก1 (LLO 14.2) อธิบายความหมายของ "สถานะที่ต้องการ" (desired state) โดยยกตัวอย่างจาก
k8s/deployment.yamlของทีม และเทียบกับการทำงานของscripts/deploy.shในบทที่ 8 - ก2 (LLO 14.1) เรียงลำดับสิ่งที่เกิดขึ้นตั้งแต่สั่ง
kubectl apply -f k8s/deployment.yamlจนถึง container ของแอปเริ่มทำงาน พร้อมระบุองค์ประกอบของคลัสเตอร์ที่เกี่ยวข้องในแต่ละขั้น - ก3 (LLO 14.1) อธิบายว่าเหตุใดทีมต้องใช้
kind load docker-imageทั้งที่ image อยู่ใน Docker ของเครื่องแล้ว และเหตุใด manifest จึงไม่ใช้ taglatest
ข. การประยุกต์
- ข1 (LLO 14.2) จับคู่ส่วนต่อไปนี้ของ
compose.yamlของทีมกับ object หรือฟิลด์ใน Kubernetes ถ้าไม่มีในบทนี้ ให้เขียนว่า "ไม่มีในบทนี้" และระบุคำที่จะใช้ค้นในเอกสารทางการ
ส่วนใน compose.yaml | object หรือฟิลด์ใน Kubernetes |
|---|---|
services.proxy กับ image: nginx:1.28 | |
ports: - "${PROXY_PORT:-80}:80" ของ proxy | |
volumes: - ./nginx:/etc/nginx/conf.d:ro ของ proxy | |
services.db กับ image: postgres:16 | |
POSTGRES_PASSWORD ใน environment ของ db | |
healthcheck ของ db (pg_isready) | |
volume db-data |
- ข2 (LLO 14.1) แอปของทีมอ่านพอร์ตจากตัวแปร
PORTถ้าต้องการให้แอปฟังพอร์ต 8080 โดยกำหนดค่าผ่าน ConfigMapdevops-app-configระบุว่าต้องแก้ไฟล์ใดในk8s/บ้าง แก้ส่วนใด และต้องสั่งคำสั่งใดหลังแก้ (เขียนเฉพาะส่วนที่เปลี่ยนของ YAML) - ข3 (LLO 14.1) เขียนลำดับคำสั่งทั้งหมดที่ใช้สร้างระบบของทีมขึ้นใหม่บนเครื่องที่เพิ่งลบคลัสเตอร์ไปแล้ว ตั้งแต่สร้างคลัสเตอร์จนเรียก
/healthผ่าน Service ได้ (ลำดับนี้คือจุดเริ่มของบทที่ 15 Lab A)
ค. การวิเคราะห์
- ค1 (LLO 14.2) โจทย์: "ระบบจองห้องประชุมของภาควิชา ใช้เฉพาะเวลาราชการ ผู้ใช้ไม่เกิน 30 คน ทีมดูแลมีคนเดียว และหยุดระบบช่วงเย็นเพื่ออัปเดตได้" วิเคราะห์ว่าควรใช้ Docker Compose หรือ Kubernetes โดยเปรียบเทียบอย่างน้อย 2 ด้าน แล้วระบุว่าถ้าความต้องการข้อใดเปลี่ยน คำตอบจะเปลี่ยน
- ค2 (LLO 14.1) สมาชิกคนหนึ่งคัดลอกไฟล์ค่าตั้งของ kind (
kind: Cluster,apiVersion: kind.x-k8s.io/v1alpha4) ไปไว้ในk8s/แล้วสั่งkubectl apply -f k8s/วิเคราะห์ว่าน่าจะเกิดอะไรขึ้น เพราะอะไร และทีมควรวางกติกาอย่างไรเกี่ยวกับโฟลเดอร์k8s/ - ค3 (LLO 14.2) ConfigMap
devops-app-configมี labelapp: devops-appเหมือน Pod สมาชิกคนหนึ่งกังวลว่า Service จะส่ง request ไปที่ ConfigMap ด้วย วิเคราะห์ว่าข้อกังวลนี้ถูกหรือไม่ และ label บน ConfigMap มีประโยชน์อะไร
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
การศึกษาค้นคว้าเพิ่มเติม
- อ่าน Kubernetes Components แล้ววาดแผนภาพคลัสเตอร์
devopsของตนเองใน Learning Log โดยระบุว่าแต่ละองค์ประกอบอยู่ใน container ใดบนเครื่อง - อ่าน Pods หัวข้อ Working with Pods และ Deployments ส่วนแรกถึง Updating a Deployment เพื่อเตรียมบทที่ 15
- อ่าน Service ส่วนชนิดของ Service แล้วอธิบายว่าถ้าต้องการให้ผู้ใช้เข้าแอปบนคลัสเตอร์โดยไม่ใช้ port-forward ต้องศึกษาอะไรเพิ่ม
- อ่าน ConfigMaps และ Secrets แล้วเปรียบเทียบกับการใช้
.envและ GitHub Secrets ในบทที่ 9–10 - อ่าน kind Quick Start หัวข้อ Loading an Image Into Your Cluster และ Pull an Image from a Private Registry เพื่อเปรียบเทียบการ
kind loadกับการใช้imagePullSecretsให้ node ดึง image แบบ private เอง (ข้อควรระวังจากเอกสาร: การพิมพ์ความลับในบรรทัดคำสั่งอาจถูกบันทึกในประวัติคำสั่ง) - ผู้ที่สนใจที่มาของ Kubernetes อ่านบทความ Borg, Omega, and Kubernetes (Burns et al., 2016)
- ทำแบบฝึกหัดท้ายบท ส่งงานข้อ 1–3 เพื่อรับ feedback และเขียน Learning Log ครั้งที่ 14
เอกสารอ้างอิง
แหล่งข้อมูลเพิ่มเติมสำหรับตัวอย่างและกรณีอธิบาย
ตรวจเอกสารออนไลน์เมื่อ 23 ตุลาคม 2569 ตัวเลขและเหตุการณ์ที่ระบุว่าสมมติใช้เพื่ออธิบายกลไก ไม่ใช่ผลทดลองหรือสถิติขององค์กรจริง
เอกสารอ้างอิงของเนื้อหาเดิม
- Burns, B., Grant, B., Oppenheimer, D., Brewer, E., & Wilkes, J. (2016). Borg, Omega, and Kubernetes. ACM Queue, 14(1), 70–93. https://doi.org/10.1145/2898442.2898444
- GitHub Docs. (n.d.). Working with the Container registry. https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
- Kubernetes Documentation. (n.d.). ConfigMaps. https://kubernetes.io/docs/concepts/configuration/configmap/
- Kubernetes Documentation. (n.d.). Deployments. https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
- Kubernetes Documentation. (n.d.). Install and set up kubectl on Linux. https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
- Kubernetes Documentation. (n.d.). kubectl port-forward. https://kubernetes.io/docs/reference/kubectl/generated/kubectl_port-forward/
- Kubernetes Documentation. (n.d.). kubectl quick reference. https://kubernetes.io/docs/reference/kubectl/quick-reference/
- Kubernetes Documentation. (n.d.). kubectl rollout restart. https://kubernetes.io/docs/reference/kubectl/generated/kubectl_rollout/kubectl_rollout_restart/
- Kubernetes Documentation. (n.d.). Kubernetes components. https://kubernetes.io/docs/concepts/overview/components/
- Kubernetes Documentation. (n.d.). Kubernetes self-healing. https://kubernetes.io/docs/concepts/architecture/self-healing/
- Kubernetes Documentation. (n.d.). Overview. https://kubernetes.io/docs/concepts/overview/
- Kubernetes Documentation. (n.d.). Pods. https://kubernetes.io/docs/concepts/workloads/pods/
- Kubernetes Documentation. (n.d.). Pull an image from a private registry. https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
- Kubernetes Documentation. (n.d.). Secrets. https://kubernetes.io/docs/concepts/configuration/secret/
- Kubernetes Documentation. (n.d.). Service. https://kubernetes.io/docs/concepts/services-networking/service/
- Kubernetes SIGs. (n.d.). kind. https://kind.sigs.k8s.io/
- Kubernetes SIGs. (n.d.). kind: Quick start. https://kind.sigs.k8s.io/docs/user/quick-start/