บทที่ 9 Nginx และค่าตั้งระบบ: reverse proxy, 12-factor config และความลับใน Git
แผนบริหารการสอนประจำบทที่ 9
หัวข้อเนื้อหา
- 9.1 web server กับ reverse proxy
- 9.2 โครงสร้างค่าตั้งของ Nginx
- 9.3 แยกค่าตั้งออกจากโค้ดตามแนวคิด 12-factor
- 9.4 ความลับไม่อยู่ใน Git
- 9.5 ติดตาม request ทีละชั้นก่อนสรุปสาเหตุ
- 9.6 เครื่องหมาย slash ที่เปลี่ยนเส้นทาง upstream
- 9.7 ค่าตั้งที่จำเป็น ค่าปริยาย และการเริ่มระบบผิดโหมด
- 9.8 ความลับที่เคย commit ต้องจัดการที่ตัวความลับ
- ปฏิบัติการที่ 9 วาง Nginx หน้าแอปและตรวจความลับใน repository (Lab A–D)
วัตถุประสงค์เชิงพฤติกรรม
เมื่อศึกษาบทนี้และทำปฏิบัติการครบถ้วนแล้ว ผู้เรียนสามารถปฏิบัติได้ตามผลลัพธ์การเรียนรู้ต่อไปนี้ โดยแต่ละข้อมีงานที่ใช้วัดผลกำกับไว้
| รหัส | ผลลัพธ์การเรียนรู้ | วัดจาก | CLO ของรายวิชา |
|---|---|---|---|
| 9.1 | กำหนดค่า Nginx reverse proxy ตามแม่แบบให้เข้าถึงแอปผ่านพอร์ต 80 | งานข้อ 1 | CLO2 |
| 9.2 | แยกค่าตั้งออกจากโค้ด และตรวจว่าไม่มีรหัสผ่านอยู่ใน repository หรือไฟล์ Compose | งานข้อ 2 | CLO4 (จุดตรวจที่ 2) |
CLO ที่เกี่ยวข้อง (ข้อความจากหลักสูตร):
- CLO2 สามารถกำหนดค่าและใช้งาน DevOps workflow ที่กำหนดมา เพื่อให้กระบวนการพัฒนา–ทดสอบ–ส่งมอบซอฟต์แวร์เป็นไปอย่างถูกต้องและมีประสิทธิภาพ
- CLO4 ผู้เรียนแสดงออกถึงความรับผิดชอบและความใฝ่รู้ในการเรียนรู้เทคโนโลยีและเครื่องมือใหม่ที่เกี่ยวข้องกับ DevOps โดยศึกษาค้นคว้าด้วยตนเองจากแหล่งข้อมูลที่เชื่อถือได้ และปฏิบัติตามแนวปฏิบัติด้านความปลอดภัยและคุณภาพของระบบ
บทนี้เป็นจุดตรวจด้านความปลอดภัยครั้งที่ 2 ของ CLO4 ต่อจากจุดตรวจครั้งที่ 1 ในบทที่ 1 ก่อนจุดตรวจครั้งที่ 3 ในบทที่ 10 และจุดตรวจครั้งที่ 4 ในบทที่ 12 จุดตรวจครั้งนี้ประเมินทั้งผลการค้นหาความลับและพฤติกรรมเมื่อพบปัญหา
วิธีสอนและกิจกรรมการเรียนการสอน
- บรรยายแนวคิดตามหัวข้อ 9.1–9.4 โดยเริ่มจากคำถามว่า "ถ้าระบบมีหลายบริการ ผู้ใช้ควรเข้าทางประตูใด" และใช้คำถามตรวจความเข้าใจท้ายหัวข้อเป็นกิจกรรมอภิปรายเป็นคู่
- สาธิตการวาง Nginx หน้าแอป แล้วหยุดบริการแอปให้เห็นว่า Nginx ตอบ 502 หรือ 504 แทน เพื่อแยกปัญหาของ proxy ออกจากปัญหาของแอป
- สาธิตการค้นหารหัสผ่านในประวัติ commit ของ repository ตัวอย่างที่เคย commit ไฟล์
.envแล้วลบออก ให้เห็นว่าการลบใน commit ถัดไปไม่ได้ลบความลับออกจากประวัติ - ปฏิบัติการ Lab A–D บน repository ทีม แต่ละ Lab ประกอบด้วยส่วนฝึกตามตัวอย่างและส่วนที่ผู้เรียนต้องตัดสินใจเอง
- มอบหมายแบบฝึกหัดท้ายบท Learning Log ครั้งที่ 9 และการศึกษาค้นคว้าเพิ่มเติม
สื่อการเรียนการสอน
- เอกสารประกอบการสอนบทที่ 9 (บทความนี้) และหน้ารวมรายวิชา 10306343
- repository ทีม
devops-appที่ผ่าน M7 จากบทที่ 8 และเครื่องปลายทางของทีมที่รันระบบชุดdevops-prod - เอกสารทางการ nginx.org หัวข้อ Module ngx_http_proxy_module, The Twelve-Factor App หัวข้อ III. Config และ Docker Docs หัวข้อ Set environment variables within your container's environment
- บทความประกอบ สร้างเว็บด้วย Nginx และพัฒนา REST API ด้วย Node.js บน Docker
การวัดผลและการประเมินผล
กำหนดส่ง: ก่อนเริ่มเรียนบทที่ 11 เพื่อเว้นระยะหนึ่งบท
| ข้อ | งาน | หลักฐาน | ผลลัพธ์ | CLO |
|---|---|---|---|---|
| 1 | M8 เข้าถึงแอปผ่านพอร์ต 80 | ลิงก์ pull request ของตนเองที่ merge แล้ว (Lab B, C หรือ D) · ไฟล์ nginx/default.conf บน main · ผล curl -i http://<เครื่องปลายทาง>/ ที่ได้ HTTP 200 พร้อม header Server: nginx และเวอร์ชัน 0.4.0 · ผล docker compose -p devops-prod ps ที่มีเพียงบริการ proxy เปิดพอร์ตสู่เครื่อง | 9.1 | CLO2 |
| 2 | จุดตรวจความปลอดภัยครั้งที่ 2 | ผลรัน scripts/check-secrets.sh บน main · ผล git check-ignore -v .env · บันทึกรายบุคคลไม่เกิน 1 หน้าว่าพบอะไร ทำอะไร และอ้างแหล่งข้อมูลทางการใด | 9.2 | CLO4 |
| 3 | Learning Log ครั้งที่ 9 | ตอบ 4 หัวข้อ: สิ่งที่เรียนในบทนี้ · คำสั่งหรือข้อความ error ที่พบและแก้อย่างไร · แหล่งที่ใช้ค้นหาคำตอบ (ระบุลิงก์เอกสารทางการอย่างน้อย 1 แหล่ง) · ส่วนใดของค่าตั้ง Nginx ที่ยังไม่เข้าใจ | — | บันทึกสะท้อนการเรียนรู้ ไม่มีหมวดคะแนน portfolio แยก |
ประเมินงานแต่ละข้อจากหลักฐานของข้อนั้นโดยตรง ไม่ตัดสิทธิ์รับคะแนนของอีกข้อเพราะ milestone หรือจุดตรวจอื่นยังไม่ผ่าน งานที่ยังทำไม่ได้ให้บันทึกปัญหาและหลักฐานที่มีตามจริง
เกณฑ์ให้คะแนน
| ข้อ | ผ่าน | ยังไม่ผ่าน |
|---|---|---|
| 1 | compose.yaml บน main มีบริการ proxy และบริการ app ไม่เปิดพอร์ตสู่เครื่อง · nginx/default.conf ส่งต่อไปที่ app:3000 พร้อม proxy_set_header Host $host · curl ผ่านพอร์ต 80 ได้ HTTP 200 | ไม่มี pull request ของตนเอง หรือเข้าถึงแอปได้เฉพาะพอร์ตของแอปโดยตรง หรือ proxy ตอบ 502/504 |
| 3 | ตอบครบ 4 หัวข้อ และมีลิงก์เอกสารทางการ | ขาดหัวข้อ หรือไม่มีแหล่งอ้างอิง |
ข้อ 2 ใช้ rubric CLO4 3 ระดับ
| ระดับ | พฤติกรรมที่สังเกตได้ |
|---|---|
| 3 ดี | พบและรายงานปัญหาด้านความลับหรือค่าตั้งก่อนถูกตรวจ (เช่น เสนอปรับรูปแบบการค้นในสคริปต์เมื่อพบกรณีที่สคริปต์ตรวจไม่พบ หรือแจ้งทีมให้เปลี่ยนรหัสผ่านทันทีที่พบ) และอ้างแหล่งข้อมูลทางการประกอบการตัดสินใจ |
| 2 ผ่าน | ทำตามขั้นตอนตรวจครบทุกข้อ ผลการตรวจบน main ไม่พบรหัสผ่านจริง และอ้างแหล่งข้อมูลทางการอย่างน้อย 1 แหล่ง |
| 1 ยังไม่ผ่าน | ทำเฉพาะเมื่อถูกสั่ง หรือพบรหัสผ่านจริงใน repository หรือประวัติ commit โดยไม่มีการดำเนินการแก้ไข |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
การคิดคะแนนฉบับปัจจุบัน
มติผู้สอน 28 ต.ค. 2569: เข้าเรียน 10% · ใบงานบทที่ 1–13 รวม 40% · FP 20% · FW 20% · โครงงาน 10% ส่วนบทที่ 14–15 ฝึกเพื่อรับข้อเสนอแนะ ไม่นับคะแนน
ใบงาน C09-WS01 มีคะแนนดิบเต็ม 20 คะแนน แยกตามผลลัพธ์ด้านล่าง ระบบนำไปรวมในหมวดใบงาน 40% โดยไม่เพิ่มคะแนน milestone, portfolio หรือจุดตรวจเป็นหมวดแยก
| ผลลัพธ์ | หลักฐานตามงานส่ง | คะแนนดิบเต็ม |
|---|---|---|
| LLO9.1 | งานข้อ 1 | 10 |
| LLO9.2 | งานข้อ 2 | 10 |
ประเมินงานแต่ละข้อจากหลักฐานของข้อนั้นโดยตรง ไม่ตัดสิทธิ์รับคะแนนของอีกข้อเพราะ milestone หรือจุดตรวจอื่นยังไม่ผ่าน งานที่ยังทำไม่ได้ให้บันทึกปัญหาและหลักฐานที่มีตามจริง
ใช้เกณฑ์ความถูกต้อง เหตุผล และหลักฐานที่ระบุในงานแต่ละข้อประกอบการประเมินราย LLO งานเตรียมพื้นและ Learning Log ที่ไม่มีรหัส LLO ใช้ให้ข้อเสนอแนะ ไม่เพิ่มคะแนนดิบให้ใบงาน
บทนำ
ระบบของทีมหลังบทที่ 8 เปิดพอร์ตของแอปให้ผู้ใช้เข้าถึงโดยตรง และค่าที่ต่างกันระหว่างเครื่อง เช่น ชื่อที่แสดงบนหน้าแรก ยังเขียนไว้ในโค้ด บทนี้วาง Nginx เป็น reverse proxy หน้าแอป ให้ผู้ใช้เข้าทางพอร์ต 80 ทางเดียว ซ่อนบริการภายในไว้หลัง proxy แล้วย้ายค่าที่ต่างกันระหว่างเครื่องออกจากโค้ดไปไว้ในตัวแปรสภาพแวดล้อมตามแนวคิด 12-factor และปิดท้ายด้วยการตรวจว่าไม่มีรหัสผ่านอยู่ใน repository หรือในประวัติ commit
ความสำคัญ: reverse proxy เป็นองค์ประกอบมาตรฐานของระบบเว็บแทบทุกระบบ เพราะเป็นจุดเดียวที่ควบคุมการเข้าถึง เพิ่ม HTTPS และกระจายการเชื่อมต่อได้ ส่วนการแยกค่าตั้งออกจากโค้ดทำให้ image เดียวกันใช้ได้ทุกสภาพแวดล้อม และการตรวจความลับในประวัติ commit เป็นนิสัยที่ป้องกันความเสียหายร้ายแรงที่สุดประเภทหนึ่งในงานซอฟต์แวร์
ความเชื่อมโยงกับบทอื่น: บทนี้ใช้ compose.yaml, network และ healthcheck จากบทที่ 7 workflow CD และ scripts/deploy.sh จากบทที่ 8 .gitignore จากบทที่ 2 และคำสั่ง grep จากบทที่ 1 ความรู้เรื่องพอร์ตและ HTTP มาจากรายวิชา 10306242 ผลผลิตของบทนี้ถูกใช้ต่อตามตารางต่อไปนี้
| สิ่งที่ได้จากบทนี้ | ใช้ต่อในบท |
|---|---|
บริการ proxy และ nginx/default.conf | 11 (deploy gate ตรวจผ่าน proxy) · 12 (pipeline ครบสาย) |
APP_TITLE และค่าตั้งในตัวแปรสภาพแวดล้อม | 12 (สถานการณ์จำลองเวอร์ชันพัง) · 14 (ConfigMap ใน Kubernetes) |
scripts/check-secrets.sh | 10 (ก่อนย้ายความลับไป GitHub Secrets) · 12 (จุดตรวจความปลอดภัยครั้งที่ 4) |
ความรู้พื้นฐาน เครื่องมือ และรุ่นซอฟต์แวร์
ความรู้พื้นฐาน: พอร์ต, HTTP request และ status code จากรายวิชา 10306242 network และการเรียกกันด้วยชื่อบริการจากบทที่ 7 และการ deploy ด้วย tag เวอร์ชันจากบทที่ 8
เครื่องมือ: เครื่องที่มี Docker Engine และ Docker Compose plugin repository ~/devops-app ที่ตรงกับ main ล่าสุด และเครื่องปลายทางของทีมจากบทที่ 8 พอร์ต 80 ต้องว่างทั้งบนเครื่องพัฒนาและเครื่องปลายทาง
รุ่นซอฟต์แวร์ที่ใช้ทดสอบ:
| ซอฟต์แวร์ | รุ่นที่ทดสอบ |
|---|---|
| Docker Engine และ Docker Compose | 29.8.2 และ v5.5.1 ทดสอบเมื่อ 19 ต.ค. 2569 |
| Nginx | image จำลองชื่อ nginx:1.28 ที่ผู้เขียนสร้างจาก Nginx 1.24.0 ของ Ubuntu 24.04 ไม่ใช่ image ทางการจาก Docker Hub |
| image อื่น | image จำลอง node:22-slim และ postgres:16 เดียวกับบทที่ 6–7 |
| แอป | devops-app 0.3.0 → 0.4.0 |
สภาพแวดล้อมที่ใช้เขียนบทความดึง image จาก Docker Hub ไม่ได้ ผู้เขียนจึงสร้าง image จำลองที่มี tag nginx:1.28 จากโปรแกรม Nginx รุ่น 1.24.0 ของ Ubuntu โดยจัดตำแหน่งไฟล์ค่าตั้งให้เหมือนที่ใช้ในบทความ (/etc/nginx/conf.d/default.conf) และส่ง log ออกทาง stdout ค่าตั้ง proxy_pass และ proxy_set_header ในบทนี้ทำงานเหมือนกันในทั้งสองรุ่น แต่ header Server ในผลลัพธ์จะแสดง nginx/1.24.0 (Ubuntu) ขณะที่ image ทางการจะแสดงรุ่นของตน และหน้าเริ่มต้นของ image จำลองเป็นหน้าที่ผู้เขียนสร้างเอง ไม่ใช่หน้าของ image ทางการ
ค่าใช้จ่ายและข้อมูลลับ: Nginx เป็นซอฟต์แวร์โอเพนซอร์สที่ใช้ได้โดยไม่มีค่าใช้จ่าย ข้อมูลลับของบทนี้คือรหัสผ่านฐานข้อมูลทั้งของเครื่องพัฒนา (.env) และของเครื่องปลายทาง (~/devops-deploy/.env) ระหว่าง Lab D อาจพบรหัสผ่านจริงในประวัติ commit ห้ามคัดลอกรหัสผ่านที่พบลงในบันทึก issue หรือ pull request ให้บันทึกเพียงชื่อไฟล์และรหัส commit แล้วแจ้งทีมให้เปลี่ยนรหัสผ่านนั้นทันที
9.1 web server กับ reverse proxy
web server คือโปรแกรมที่รับ HTTP request แล้วตอบกลับด้วยไฟล์หรือผลการประมวลผล Nginx ทำหน้าที่นี้ได้ทั้งในฐานะ web server ที่ส่งไฟล์ HTML, CSS และรูปภาพโดยตรง และในฐานะ reverse proxy ที่รับ request จากผู้ใช้แล้วส่งต่อไปยังบริการอื่นที่อยู่ด้านหลัง จากนั้นนำคำตอบของบริการนั้นกลับไปให้ผู้ใช้ ผู้ใช้เห็นเพียง Nginx และไม่รู้ว่าด้านหลังมีบริการกี่ตัวอยู่ที่ใด
| บทบาท | Nginx ทำอะไร | ตัวอย่าง |
|---|---|---|
| web server | อ่านไฟล์จากดิสก์แล้วตอบ | หน้าเริ่มต้นของ Nginx ใน Lab A |
| reverse proxy | ส่ง request ต่อไปยังบริการด้านหลังแล้วส่งคำตอบกลับ | ส่งต่อไปยังบริการ app พอร์ต 3000 ใน Lab B |
ระดับที่ 1: หลัง Lab B ผู้ใช้เข้าถึงแอปผ่านพอร์ต 80 ของ Nginx ส่วนบริการ app ไม่เปิดพอร์ตสู่เครื่องอีกต่อไป header เช่น Server: nginx/... และ X-Powered-By: Express ใช้เป็นเบาะแสได้ แต่ปรับหรือสร้างเพิ่มได้ จึงต้องยืนยันเส้นทางด้วยเนื้อหาตอบกลับและ log ของคำขอที่สัมพันธ์กันด้วย
ระดับที่ 2: เมื่อ proxy เชื่อมต่อบริการด้านหลังไม่ได้ Nginx จะตอบด้วย status code กลุ่ม 5xx ที่บอกปัญหาของการส่งต่อ ผลการทดสอบในบทนี้พบสองแบบ คือ 502 Bad Gateway เมื่อบริการปลายทางปฏิเสธการเชื่อมต่อ (connect() failed (111: Connection refused)) และ 504 Gateway Time-out เมื่อรอการเชื่อมต่อจนหมดเวลา (upstream timed out (110: Connection timed out)) ข้อความเหล่านี้ช่วยระบุว่าปัญหาเกี่ยวกับการเชื่อมต่อหรือการรอ upstream แต่ยังต้องแยกสาเหตุจากค่าตั้ง Nginx ชื่อบริการ พอร์ต เครือข่าย และตัวแอปตามหัวข้อ 9.5
ระดับที่ 3: การมี proxy เป็นประตูเดียวทำให้เพิ่มความสามารถได้โดยไม่ต้องแก้แอป เช่น เข้ารหัสด้วย HTTPS ที่ proxy, จำกัดอัตรา request, หรือกระจาย request ไปยังแอปหลายสำเนา แต่ก็ทำให้ proxy เป็นจุดที่ถ้าล้มแล้วทั้งระบบเข้าถึงไม่ได้ (single point of failure) และเป็นอีกหนึ่งชั้นที่ต้องตรวจเมื่อเกิดปัญหา บทที่ 11 จึงตรวจสุขภาพของระบบผ่าน proxy ไม่ใช่ตรวจที่แอปโดยตรง
คำถามตรวจความเข้าใจ 9.1: ผู้ใช้แจ้งว่าเปิดหน้าเว็บแล้วได้ 502 Bad Gateway ระบุว่าส่วนใดของระบบยังทำงานอยู่ ส่วนใดน่าจะมีปัญหา และคำสั่งแรกที่ควรใช้ตรวจ
9.2 โครงสร้างค่าตั้งของ Nginx
ค่าตั้งของ Nginx ประกอบด้วย directive ที่จบด้วยเครื่องหมาย ; และ block ที่อยู่ในวงเล็บปีกกา ไฟล์หลัก /etc/nginx/nginx.conf รวมไฟล์ทุกไฟล์ใน /etc/nginx/conf.d/ ที่ลงท้ายด้วย .conf เข้ามา ทีมจึงเขียนเฉพาะไฟล์ nginx/default.conf ใน repository แล้วต่อเข้าไปแทนไฟล์เริ่มต้นใน container
server {
listen 80;
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}| ส่วน | ความหมาย |
|---|---|
server { ... } | ค่าตั้งของเว็บหนึ่งชุด |
listen 80; | รับการเชื่อมต่อที่พอร์ต 80 ภายใน container |
location / { ... } | ค่าตั้งสำหรับ request ที่ path ขึ้นต้นด้วย / (ทุก request) |
proxy_pass http://app:3000; | ส่งต่อไปยังบริการ app พอร์ต 3000 ผ่าน network ของ Compose |
proxy_set_header Host $host; | ส่งชื่อ host ที่ผู้ใช้ร้องขอไปให้แอป |
proxy_set_header X-Real-IP $remote_addr; | ส่งที่อยู่ IP ของผู้ใช้ไปให้แอป |
เอกสารของโมดูล ngx_http_proxy_module ใช้ตัวอย่างเดียวกันนี้ (proxy_pass พร้อม proxy_set_header Host $host และ X-Real-IP $remote_addr) และอธิบายว่าโดยปริยาย Nginx ไม่ส่ง header Host ของผู้ใช้ไปยังบริการปลายทาง แต่ตั้งเป็น $proxy_host ซึ่งคือชื่อและพอร์ตใน proxy_pass แทน (nginx.org: Module ngx_http_proxy_module)
ระดับที่ 1: คำสั่ง nginx -t ตรวจไวยากรณ์ของค่าตั้งโดยไม่เริ่มบริการ ใช้ใน container ได้ด้วย docker compose exec proxy nginx -t ซึ่งตอบ syntax is ok และ test is successful เมื่อค่าตั้งถูกต้อง ควรตรวจทุกครั้งก่อนสั่งให้ Nginx โหลดค่าตั้งใหม่
ระดับที่ 2: ผลการทดสอบด้วยบริการจำลองที่พิมพ์ header ที่ได้รับแสดงความต่างของสองกรณีอย่างชัดเจน location ที่ตั้ง proxy_set_header Host $host ทำให้บริการปลายทางเห็น host=localhost และ xreal=172.20.0.1 ส่วน location ที่ไม่ได้ตั้งทำให้บริการปลายทางเห็น host=api:4000 และไม่มี X-Real-IP ซึ่งตรงกับพฤติกรรมที่เอกสารอธิบาย แอปที่สร้างลิงก์หรือบันทึก log จากค่า Host หรือ IP ของผู้ใช้จะได้ค่าผิดถ้าไม่ตั้ง header เหล่านี้
ระดับที่ 3: Nginx แปลงชื่อ app ใน proxy_pass เป็นที่อยู่ IP ตอนเริ่มทำงานหรือตอนโหลดค่าตั้ง แล้วใช้ IP นั้นต่อไป ผลการทดสอบพบว่าเมื่อ container ของแอปถูกสร้างใหม่และได้ IP ใหม่ (172.19.0.3 → 172.19.0.6) Nginx ยังส่งไปที่ IP เดิมจนได้ 502 Bad Gateway และกลับมาตอบ 200 หลังสั่ง nginx -s reload หรือ restart บริการ proxy ปัญหานี้เกิดได้ทุกครั้งที่ deploy เวอร์ชันใหม่ซึ่งสร้าง container ของแอปใหม่ สคริปต์ deploy ของทีมจึงต้องสั่ง reload หลังสร้าง container ใหม่ (Lab B.4)
คำถามตรวจความเข้าใจ 9.2: แอปของทีมหนึ่งสร้างลิงก์ "เปิดในหน้าต่างใหม่" จาก header Host ของ request หลังวาง Nginx หน้าแอปแล้ว ลิงก์ทั้งหมดกลายเป็น http://app:3000/... อธิบายสาเหตุโดยอ้างค่าเริ่มต้นของ Nginx และระบุบรรทัดที่ต้องเพิ่ม
9.3 แยกค่าตั้งออกจากโค้ดตามแนวคิด 12-factor
The Twelve-Factor App นิยามค่าตั้ง (config) ว่าเป็น "everything that is likely to vary between deploys" คือทุกสิ่งที่มีแนวโน้มจะต่างกันระหว่างการ deploy แต่ละที่ และแนะนำให้เก็บค่าตั้งไว้ในตัวแปรสภาพแวดล้อม (environment variables) แยกจากโค้ด เอกสารเสนอบททดสอบอย่างง่ายว่า ถ้าเปิดเผย codebase เป็นโอเพนซอร์สได้ทันทีโดยไม่เปิดเผยข้อมูลยืนยันตัวตนใด ๆ แสดงว่าค่าตั้งถูกแยกออกจากโค้ดถูกต้องแล้ว (12factor.net: III. Config)
| ค่า | ต่างกันระหว่าง deploy หรือไม่ | ควรอยู่ที่ใด |
|---|---|---|
เส้นทาง /api/items และรายการ items | ไม่ | โค้ด |
พอร์ตที่แอปฟังใน container (PORT) | อาจต่าง | ตัวแปรสภาพแวดล้อม (แอปอ่าน PORT อยู่แล้วตั้งแต่บทที่ 2) |
ชื่อที่แสดงบนหน้าแรก (APP_TITLE) | ต่าง (เครื่องพัฒนา, เครื่องปลายทาง, staging) | ตัวแปรสภาพแวดล้อม (Lab C) |
| รหัสผ่านฐานข้อมูล | ต่างและเป็นความลับ | นอก repository (หัวข้อ 9.4) |
Compose ส่งค่าตั้งให้ container ได้หลายทาง เอกสารทางการระบุทั้งคีย์ environment ใน compose.yaml, คีย์ env_file ที่อ่านจากไฟล์ (path นับจากตำแหน่งของ compose.yaml) และการแทนค่าจากไฟล์ .env (Docker Docs: Set environment variables within your container's environment) ทีมใช้คีย์ environment ร่วมกับการแทนค่าจาก .env ตามแบบที่ทำในบทที่ 7
ระดับที่ 1: ในบทที่ 8 Lab D ชื่อทีมถูกเขียนลงใน src/app.js โดยตรง การเปลี่ยนชื่อที่แสดงจึงต้องแก้โค้ด build image ใหม่ และออกเวอร์ชันใหม่ หลังย้ายไปอ่านจาก process.env.APP_TITLE ใน Lab C การเปลี่ยนชื่อทำได้ด้วยการแก้ค่าในไฟล์ค่าตั้งแล้วสร้าง container ใหม่ โดยใช้ image เดิม
ระดับที่ 2: ผลการทดสอบใน Lab C แสดงว่า image 0.4.0 ตัวเดียวกันแสดงหน้าแรกเป็น DevOps App (team 01) v0.4.0 เมื่อ .env ตั้งค่าไว้ และเป็น Staging v0.4.0 เมื่อสั่ง APP_TITLE="Staging" docker compose up -d นี่คือเหตุผลที่หลัก build, release, run ในบทที่ 4 แยก release ออกจาก build คือ release หนึ่งชุดเท่ากับ build หนึ่งชุดรวมกับค่าตั้งของสภาพแวดล้อมนั้น
ระดับที่ 3: ค่าตั้งที่ย้ายออกจากโค้ดแล้วต้องมีค่าเริ่มต้นที่ปลอดภัยและมีเอกสารกำกับ แอปของทีมใช้ process.env.APP_TITLE || 'DevOps App' เพื่อให้ทำงานได้แม้ไม่ได้ตั้งค่า และ .env.example บอกชื่อตัวแปรทั้งหมดที่ระบบใช้ ค่าตั้งที่จำเป็นจริงและไม่มีค่าเริ่มต้นที่สมเหตุสมผล เช่น รหัสผ่าน ควรทำให้ระบบหยุดพร้อมข้อความชัดเจนเมื่อไม่ได้ตั้ง (รูปแบบ ${VAR:?...} ในบทที่ 7) แทนการใช้ค่าเริ่มต้นที่ไม่ปลอดภัย
คำถามตรวจความเข้าใจ 9.3: ใช้บททดสอบของ 12-factor ตรวจ repository ของทีมหลังจบ Lab C ระบุว่า repository ผ่านบททดสอบหรือไม่ และยกตัวอย่างค่าหนึ่งค่าที่ยังควรย้ายออกจากโค้ดหรือไฟล์ที่ commit (ถ้ามี)
9.4 ความลับไม่อยู่ใน Git
ความลับ (secret) เช่น รหัสผ่าน token และ private key เป็นค่าตั้งชนิดพิเศษที่การรั่วไหลก่อความเสียหายทันที Git เก็บทุก commit ไว้ถาวร ความลับที่เคยถูก commit จึงยังอ่านได้จากประวัติแม้ commit ถัดไปจะลบไฟล์นั้นแล้ว และทุกคนที่เคย clone repository มีสำเนาของความลับนั้นอยู่ในเครื่อง
ผลการทดสอบใน repository ตัวอย่างที่สมาชิกเผลอ commit ไฟล์ .env แล้วลบใน commit ถัดไป
$ git grep -n "Sup3r"
$ git log --all --oneline -- .env
89db2bb remove .env
54f8c2c update config
$ git show HEAD~1:.env
POSTGRES_PASSWORD=Sup3rS3cret!git grep ค้นใน working tree ปัจจุบันจึงไม่พบอะไร แต่ git log -- .env แสดงว่าไฟล์นี้เคยอยู่ใน commit 54f8c2c และ git show อ่านเนื้อหาของไฟล์จาก commit นั้นได้ครบ
ระดับที่ 1: การป้องกันที่ดีที่สุดคือไม่ให้ความลับเข้า Git ตั้งแต่แรก ทีมมีสามชั้นป้องกันแล้ว คือ .gitignore กัน .env (บทที่ 2), .env.example ที่มีเพียงค่าตัวอย่าง (บทที่ 7) และ compose.yaml ที่อ้างรหัสผ่านผ่าน ${POSTGRES_PASSWORD:?...} แทนการเขียนค่าจริง
ระดับที่ 2: เมื่อพบว่าความลับเคยถูก commit สิ่งแรกที่ต้องทำคือถือว่าความลับนั้นรั่วแล้วและเปลี่ยนรหัสผ่านหรือเพิกถอน token ทันที การลบออกจากประวัติด้วยการเขียนประวัติใหม่เป็นขั้นตอนเสริมที่ทำทีหลัง เพราะไม่สามารถลบสำเนาที่ผู้อื่น clone หรือ fork ไปแล้วได้
ระดับที่ 3: การย้ายรหัสผ่านไปไว้ในตัวแปรสภาพแวดล้อมแก้ปัญหาความลับใน Git แต่ยังไม่ใช่วิธีที่ปลอดภัยที่สุดสำหรับ container เอกสารของ Docker เตือนไว้ว่า "Don't use environment variables to pass sensitive information, such as passwords" และแนะนำให้ใช้ Compose secrets แทน (Docker Docs: Set environment variables within your container's environment) ซึ่งต่างจากคำแนะนำของ 12-factor ที่ให้เก็บค่าตั้งทุกชนิดในตัวแปรสภาพแวดล้อม ความต่างนี้มาจากบริบท 12-factor เขียนเพื่อแยกค่าตั้งออกจากโค้ด ส่วน Docker เพิ่มข้อกังวลว่าตัวแปรสภาพแวดล้อมของ container อ่านได้ด้วย docker inspect รายวิชาจึงใช้ตัวแปรสภาพแวดล้อมสำหรับค่าตั้งทั่วไป และระบุ Compose secrets เป็นทางที่ดีกว่าสำหรับรหัสผ่านซึ่งเลือกทำเป็นงานต่อยอดได้
คำถามตรวจความเข้าใจ 9.4: สมาชิกคนหนึ่งพบว่าเคย commit token ของ GitHub ไว้ในไฟล์ notes.txt เมื่อสามสัปดาห์ก่อน และเสนอให้ "ลบไฟล์แล้ว commit ใหม่ก็พอ" อธิบายว่าเหตุใดจึงไม่พอ และเรียงลำดับสิ่งที่ต้องทำ
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
9.5 ติดตาม request ทีละชั้นก่อนสรุปสาเหตุ
เส้นทางของระบบในบทนี้ประกอบด้วยผู้ใช้ → พอร์ตที่เปิดให้เข้าถึง → Nginx → ชื่อบริการและพอร์ตของแอป → dependency ของแอป การตรวจแต่ละจุดตอบคำถามต่างกัน การที่แอปตอบจากภายใน container ไม่ได้พิสูจน์ว่า Nginx ใช้ค่าตั้งถูกต้อง และการที่ Nginx ส่งหน้าเริ่มต้นได้ไม่ได้พิสูจน์ว่า request ไปถึงแอปแล้ว
ข้อมูลสมมติชุดแรกคือผู้ใช้ได้หน้าเริ่มต้น Nginx ด้วยสถานะ 200 แต่ไม่ใช่เนื้อหา devops-app ข้อสรุปว่า “ได้ 200 จึง deploy สำเร็จ” ไม่ถูกต้อง เพราะคำขออาจตกใน server block เริ่มต้น วิธีตรวจควรเปรียบเทียบ host header, path, เนื้อหาที่คาดหวัง และตัวระบุ release รวมถึง access log ของแอป หากไม่มีคำขอที่สัมพันธ์กันต้องตรวจ routing ก่อนแก้โค้ดแอป
ข้อมูลชุดที่สองคือผู้ใช้ได้ 502 และ error log มี connection refused อาการชี้ว่าการเชื่อม upstream ไม่สำเร็จ แต่สาเหตุอาจเป็นแอปหยุด พอร์ตผิด ชื่อบริการผิด หรือค่าตั้ง proxy ผิด ไม่ควรสรุปทันทีว่า “แอปมี bug” การแยกต้นเหตุเริ่มจากตรวจบริการว่ารันหรือไม่ ตรวจพอร์ตที่ฟัง และทดลองเชื่อมจากบริบทเครือข่ายเดียวกับ proxy
ข้อมูลชุดที่สามคือผู้ใช้ได้ 504 หลังรอ แต่แอปยังมี log รับคำขอ ต้องตรวจเวลาที่ใช้ในแอปและ dependency รวมทั้ง timeout ของ proxy การเพิ่ม timeout อาจจำเป็นในบางงาน แต่ถ้าแอปค้างจากการรอฐานข้อมูล การเพิ่มเวลารออย่างเดียวเพียงยืดช่วงที่ผู้ใช้รอ การแก้ควรสัมพันธ์กับสัญญาเวลาตอบและการจัดการคำขอที่ไม่สำเร็จ
การเห็น header เช่น Server หรือ X-Powered-By ใช้เป็นเบาะแสได้ แต่ header ปรับ ลบ หรือสร้างเพิ่มได้ จึงไม่ใช่หลักฐานเดี่ยวที่รับรองเส้นทาง request การยืนยันที่หนักแน่นกว่าคือเชื่อมคำขอเดียวกันกับ log ตามเวลาและรหัสอ้างอิงที่ไม่ใช่ความลับ แล้วเทียบ response กับพฤติกรรมที่แอปกำหนด
9.6 เครื่องหมาย slash ที่เปลี่ยนเส้นทาง upstream
proxy_pass ที่มี URI กับไม่มี URI ส่งผลต่างกัน ใน location แบบ prefix ทั่วไป ถ้าระบุ URI หลัง upstream ส่วนที่ตรง location จะถูกแทนด้วย URI นั้น แต่ถ้าไม่มี URI จะส่งเส้นทางตามกติกาที่เอกสารระบุ กรณี rewrite และตัวแปรมีรายละเอียดเพิ่มเติม จึงต้องอ่านบริบททั้งหมด (Nginx: ngx_http_proxy_module)
ตัวอย่างต่อไปนี้เป็นค่าตั้งสองทางเลือกสำหรับเปรียบเทียบ เลือกใช้ทีละแบบ ไม่ใส่ location ที่ซ้ำกันทั้งสองแบบใน server เดียว โดยสมมติว่าไม่มี rewrite และใช้ prefix location ตามที่แสดง
# แบบ ก: ส่ง /api/health ต่อเป็น /api/health
location /api/ {
proxy_pass http://app:3000;
}
# แบบ ข: ส่ง /api/health ต่อเป็น /health
location /api/ {
proxy_pass http://app:3000/;
}ทั้งสองแบบอาจถูกต้องได้ขึ้นกับ route ของแอป ถ้าแอปมีเพียง /health แบบ ข สอดคล้องกับเป้าหมาย แต่ถ้าแอปประกาศ /api/health แบบ ก อาจเป็นสิ่งที่ต้องการ ความผิดจึงไม่ใช่การมีหรือไม่มี slash โดยตัวมันเอง แต่เป็นความไม่ตรงกันของการแปลง path กับสัญญาของแอป
ตัวอย่างที่ไม่ถูกต้องคือแก้ค่าตั้งจากแบบหนึ่งเป็นอีกแบบโดยไม่ได้ทดสอบ path ย่อย แล้วตรวจเฉพาะ / หน้าแรกอาจผ่านแต่ /api/health หรือไฟล์ทรัพยากรกลับ 404 การตรวจควรใช้ตาราง path ต้นทาง → path upstream → status และเนื้อหาที่คาดอย่างน้อยทั้งกรณีปกติและ path ที่ไม่รองรับ เพื่อแยก routing ผิดจาก resource ไม่มีจริง
การตรวจ nginx -t ช่วยตรวจความถูกต้องของค่าตั้งในบริบทที่เรียก แต่ไม่พิสูจน์ว่าทุก path ตอบถูกทางธุรกิจ จึงต้องตรวจทั้ง syntax และ request จริงหลังใช้ค่าตั้ง การตรวจไฟล์บน host ยังต้องแน่ใจว่าเป็นไฟล์เดียวกับที่ container mount และกระบวนการใช้อยู่ มิฉะนั้นอาจทดสอบไฟล์ที่ไม่ได้มีผลต่อระบบเลย
9.7 ค่าตั้งที่จำเป็น ค่าปริยาย และการเริ่มระบบผิดโหมด
การแยก config ออกจากโค้ดทำให้ใช้ผลผลิตชุดเดียวกับหลายสภาพแวดล้อมได้ แนวทาง Twelve-Factor อธิบายการเก็บค่าที่ต่างกันระหว่าง deploy ไว้ใน environment โดยแยกจาก code (The Twelve-Factor App: Config) อย่างไรก็ตาม environment ไม่ใช่ตัวรับประกันความลับ ต้องพิจารณาว่ากระบวนการใดอ่านได้และค่าจะปรากฏใน log หรือเครื่องมือใด
ตัวอย่างสมมติคือแอปต้องมี DATABASE_URL สำหรับใช้งานฐานข้อมูล แต่โค้ดเลือกใช้ฐานข้อมูลทดลองเงียบ ๆ เมื่อค่านี้ว่าง ระบบอาจเริ่มได้และตอบ health ผ่าน แต่ผู้ใช้เขียนข้อมูลไปผิดที่ ตัวอย่างนี้แสดงว่า default ที่สะดวกในเครื่องพัฒนาอาจอันตรายในสภาพใช้งานจริง ค่าที่จำเป็นควรตรวจตั้งแต่เริ่มและรายงานชื่อ key ที่ขาดโดยไม่แสดงค่าลับ
อีกตัวอย่างคือ PORT ได้ข้อความที่แปลงเป็นตัวเลขไม่ได้ หากปล่อยให้ runtime ล้มด้วย error ที่ไม่ชัด การสืบเหตุจะยากขึ้น ควรแปลงและตรวจช่วงที่ยอมรับ พร้อมข้อความที่บอกตำแหน่งปัญหา ส่วนค่าที่มี default ได้ต้องมีเหตุผล เช่น ระดับ log ปริยายที่เหมาะสมและไม่เปิดข้อมูลละเอียดเกินจำเป็น
แนวทางที่ผิดคือสร้าง image คนละชุดเพียงเพื่อเปลี่ยนชื่อ host ของฐานข้อมูล ทั้งที่ค่านั้นควรเปลี่ยนตอน release หรือ run วิธีนี้ทำให้ผลผลิตแตกแขนงโดยไม่จำเป็น แต่ค่าบางประเภทที่ถูกฝังตอน build ต้องระบุให้ชัด ไม่ควรสัญญาว่าค่า environment ทุกตัวแก้ภายหลังได้ การจัดประเภทจึงต้องอิงพฤติกรรมของเครื่องมือและโค้ดที่ใช้จริง
9.8 ความลับที่เคย commit ต้องจัดการที่ตัวความลับ
หาก token เคยถูก push แล้ว การลบไฟล์หรือเพิ่ม .gitignore ไม่ทำให้ token ใช้งานไม่ได้ สำเนาอาจยังอยู่ในประวัติ clone, fork, cache หรือ log แนวทาง GitHub ให้เพิกถอนหรือหมุนเวียนความลับเป็นขั้นแรก แล้วจึงพิจารณาการล้างข้อมูลอ่อนไหวจากประวัติร่วมกับผู้เกี่ยวข้อง (GitHub Docs: Removing sensitive data)
ตัวอย่างที่ไม่ถูกต้องคือพบรหัสผ่านใน commit เก่าแล้วสร้าง commit ใหม่ลบออก จากนั้นประกาศว่าเหตุจบแล้ว สิ่งที่เปลี่ยนคือ snapshot ปัจจุบัน ส่วนข้อมูลรับรองเดิมอาจยังใช้ได้ การแก้ต้องระบุเจ้าของความลับ ยกเลิกค่าเดิม ออกค่าใหม่ ปรับระบบที่ต้องใช้ ตรวจผล และทบทวนสำเนาที่อาจเผยแพร่ พร้อมหลีกเลี่ยงการคัดลอกค่าลับลงรายงานเหตุอีกครั้ง
ตัวอย่างที่ถูกต้องสำหรับงานฝึกคือใช้ค่าปลอมที่ใช้ยืนยันตัวตนกับระบบจริงไม่ได้ เช่น EXAMPLE_NOT_A_REAL_TOKEN เพื่อเรียนรู้การค้นประวัติ ห้ามใช้ token จริงเพียงเพื่อทดสอบว่าเครื่องมือค้นเจอหรือไม่ เมื่อเขียนรายงานให้ใช้ชื่อ secret และรหัสอ้างอิงเหตุ ไม่ใส่ค่า แม้ตั้งใจจะลบรายงานภายหลังก็ตาม
สิ่งที่ควรส่งต่อไปบท DevSecOps คือรายการว่าค่าลับแต่ละประเภทถูกสร้างที่ใด ใครหรือระบบใดต้องใช้ สิทธิ์ต่ำสุดคืออะไร หมุนเวียนอย่างไร และตรวจการรั่วที่จุดใด รายการนี้ช่วยให้การย้ายค่าไป GitHub Secrets เป็นการออกแบบการใช้ความลับครบวงจร ไม่ใช่เพียงย้ายข้อความไปเก็บอีกหน้าเว็บหนึ่ง
กรณีอธิบายต่อเนื่อง: ผ่าน Nginx ได้ 200 แต่เป็นระบบคนละตัว
กำหนดโจทย์ว่าผู้ใช้ต้องเข้าถึง devops-app ผ่าน host และ path ที่กำหนด ข้อมูลสมมติแสดงว่าคำขอได้ 200 และ Server: nginx แต่เนื้อหาเป็น welcome page ขณะที่ log แอปไม่มีคำขอในเวลาเดียวกัน ผลนี้ไม่ผ่านข้อกำหนด แม้ตรวจเพียง status code จะดูปกติ ต้องตรวจว่าคำขอเลือก server block และ location ที่ตั้งใจหรือไม่
วิธีแก้ที่ผิดคือเพิ่ม header ชื่อแอปลง response ของ Nginx แล้วใช้ header นั้นเป็นหลักฐานว่า request ไปถึงแอป การสร้างข้อความยืนยันเองไม่ได้เปลี่ยนเส้นทางจริง ควรแก้ routing ตรวจ config ที่กระบวนการใช้อยู่ แล้วส่งคำขอที่มีรหัสอ้างอิงไม่ลับเพื่อจับคู่ access log ของ proxy กับ log ของแอป
หากทดสอบด้วย URL ภายในโดยไม่มี host header เดียวกับผู้ใช้ อาจเลือก server block ต่างกัน จึงต้องระบุ host, path, scheme และจุดที่เรียก ไม่เขียนเพียงว่า “curl ผ่าน” การทดสอบที่จำลองเส้นทางจริงควรใช้ข้อมูลเหล่านี้สอดคล้องกับการเข้าถึงที่ประกาศไว้
เมื่อแก้แล้วต้องตรวจทั้ง path ที่รองรับและ path ที่ไม่รองรับ หากทุก path ถูกส่งกลับหน้าแรกด้วย 200 ก็อาจซ่อน 404 ที่ควรเกิด สถานะถูกต้องสัมพันธ์กับสัญญาของแต่ละเส้นทาง ไม่ใช่การทำให้ทุกคำขอสำเร็จ การเปรียบเทียบนี้ช่วยแยก reverse proxy ที่ถูกต้องจากระบบที่เพียงตอบบางอย่างได้
ปฏิบัติการที่ 9 วาง Nginx หน้าแอปและตรวจความลับใน repository
ปฏิบัติการนี้ประกอบด้วย Lab A–D ทุกคนรันระบบบนเครื่องของตนเอง ส่วนการเปลี่ยนแปลงที่เข้า repository แบ่งผู้รับผิดชอบดังนี้ เพื่อให้ทุกคนมี pull request ของตนเองใน milestone M8
| pull request | branch | Lab | ผู้รับผิดชอบ | ผู้ตรวจทาน |
|---|---|---|---|---|
เพิ่มบริการ proxy และ nginx/default.conf | proxy/nginx | A และ B | สมาชิกคนที่ 1 | สมาชิกคนที่ 2 |
อ่าน APP_TITLE จากตัวแปรสภาพแวดล้อมและปรับเวอร์ชันเป็น 0.4.0 | config/app-title | C | สมาชิกคนที่ 2 | สมาชิกคนที่ 3 |
เพิ่ม scripts/check-secrets.sh | chore/check-secrets | D | สมาชิกคนที่ 3 | สมาชิกคนที่ 1 |
pull request ทุกรายการต้องผ่าน CI จากบทที่ 5 ก่อน merge เมื่อ merge ครบแล้วหัวหน้าทีมออกเวอร์ชัน v0.4.0 ตามบทที่ 8 แต่ละ Lab แบ่งเป็นส่วน ฝึกตามตัวอย่าง และส่วน ตัดสินใจเอง (A.3, B.4, C.4 และ D.3)
ผลลัพธ์ในปฏิบัติการนี้ได้จากการรันจริงบน Docker Engine 29.8.2 และ Docker Compose v5.5.1 โดยใช้ image จำลองตามที่ระบุในบทนำ header Server จะแสดงรุ่นของ Nginx ที่ต่างไป และที่อยู่ IP, เวลา และค่า ETag จะต่างจากตัวอย่าง
Lab A: Nginx ใน Compose
สถานการณ์: ทีมต้องการเพิ่มประตูทางเข้าหนึ่งเดียวให้ระบบ ก่อนตั้งค่าให้ส่งต่อไปยังแอป ต้องรัน Nginx ให้ได้และเข้าใจว่าค่าตั้งเริ่มต้นทำอะไร
เป้าหมาย: เพิ่มบริการ proxy ที่รัน Nginx ด้วยค่าตั้งเริ่มต้นและตอบที่พอร์ต 80 (พื้นฐานของ LLO 9.1)
ฝึกตามตัวอย่าง
A.1 เพิ่มบริการ proxy (สมาชิกคนที่ 1)
cd ~/devops-app
git switch main
git pull
git switch -c proxy/nginx
nano compose.yamlเพิ่มบริการ proxy ไว้ระหว่าง app กับ db โดยย่อหน้าระดับเดียวกับบริการอื่น
proxy:
image: nginx:1.28
ports:
- "${PROXY_PORT:-80}:80"A.2 รันและตรวจ (สมาชิกทุกคน)
docker compose config -q
docker compose up -d --wait
curl -sI http://localhost/
curl -sI http://localhost:8080/
docker compose ps --format 'table {{.Service}}\t{{.Ports}}'HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
...
HTTP/1.1 200 OK
...
SERVICE PORTS
app 0.0.0.0:8080->3000/tcp
db 5432/tcp
proxy 0.0.0.0:80->80/tcpตัวเลือก -I ขอเฉพาะ header ที่พอร์ต 80 Nginx ตอบหน้าเริ่มต้นของตนเอง ส่วนพอร์ต 8080 ยังเป็นแอปที่เปิดตรงจากบทที่ 7–8 ขณะนี้ระบบจึงมีสองประตู ซึ่ง Lab B จะปิดประตูของแอป
ตัดสินใจเอง A.3: ใช้คำสั่ง docker compose exec proxy ร่วมกับคำสั่งอ่านไฟล์ (เช่น cat) เปิดไฟล์ /etc/nginx/nginx.conf และ /etc/nginx/conf.d/default.conf ใน container แล้วตอบสองข้อใน Learning Log (1) บรรทัดใดของ nginx.conf ทำให้ไฟล์ใน conf.d/ ถูกอ่าน (2) ไฟล์ HTML ของหน้าเริ่มต้นอยู่ที่ path ใดใน container โดยอ้างบรรทัดในค่าตั้ง
ผลลัพธ์ที่คาดหวัง A.3: ระบุบรรทัด include /etc/nginx/conf.d/*.conf; และ path จาก directive root ใน default.conf ได้
จุดตรวจ A: สมาชิกทุกคนได้ HTTP 200 ที่พอร์ต 80 และอธิบายได้ว่าเหตุใดระบบตอนนี้มีสองประตู
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
Bind for 0.0.0.0:80 failed: port is already allocated | มี web server อื่นหรือระบบชุดอื่นใช้พอร์ต 80 | หยุดบริการนั้น หรือตั้ง PROXY_PORT=8000 ใน .env แล้วใช้พอร์ตนั้นแทน |
curl: (7) Failed to connect to localhost port 80 | container ของ proxy ไม่ได้ทำงาน | docker compose ps และ docker compose logs proxy |
cat: not found ใน container | image ที่ใช้ไม่มีโปรแกรมนั้น | คัดลอกไฟล์ออกมาอ่านบนเครื่องด้วย docker compose cp proxy:/etc/nginx/nginx.conf ./nginx.conf.copy แล้วลบไฟล์สำเนาเมื่ออ่านเสร็จ |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
Lab B: ตั้ง proxy ไปที่แอป
สถานการณ์: ทีมต้องการให้ผู้ใช้เข้าถึงแอปผ่าน Nginx ทางเดียว และปิดพอร์ตของแอปที่เปิดตรงสู่เครื่อง
เป้าหมาย: เขียนค่าตั้ง reverse proxy ตามแม่แบบ ต่อเข้า container ปิดพอร์ตของแอป และตรวจว่าได้ HTTP 200 ผ่าน proxy (LLO 9.1)
ฝึกตามตัวอย่าง
B.1 เขียน nginx/default.conf (สมาชิกคนที่ 1 ต่อใน branch เดิม)
mkdir -p nginx
nano nginx/default.confserver {
listen 80;
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}B.2 ต่อค่าตั้งเข้า container และปิดพอร์ตของแอป
ใน compose.yaml ลบสองบรรทัด ports: และ - "${APP_PORT:-8080}:3000" ออกจากบริการ app แล้วเพิ่ม volumes และ depends_on ให้บริการ proxy
proxy:
image: nginx:1.28
ports:
- "${PROXY_PORT:-80}:80"
volumes:
- ./nginx:/etc/nginx/conf.d:ro
depends_on:
app:
condition: service_healthyบรรทัด ./nginx:/etc/nginx/conf.d:ro ต่อทั้งโฟลเดอร์ nginx/ ใน repository เข้าไปแทนโฟลเดอร์ conf.d ใน container แบบอ่านอย่างเดียว (:ro) ไฟล์เริ่มต้นของ image ในโฟลเดอร์นั้นจึงถูกแทนด้วยไฟล์ของทีมทั้งหมด เหตุที่ต่อทั้งโฟลเดอร์แทนการต่อไฟล์ default.conf ไฟล์เดียว คือเมื่อแก้ไฟล์ในโฟลเดอร์แล้วสั่ง nginx -s reload Nginx จะเห็นเนื้อหาใหม่ทันที ผลการทดสอบพบว่าถ้าต่อไฟล์เดี่ยวแล้วแก้ไฟล์ด้วยเครื่องมือที่เขียนไฟล์ใหม่มาแทน เช่น sed -i container ยังเห็นไฟล์เดิม (รายละเอียดในบทที่ 11 Lab D) ส่วน depends_on ทำให้ Nginx เริ่มหลังแอปพร้อม เพราะ Nginx ต้องแปลงชื่อ app เป็น IP ตอนเริ่มทำงาน
B.3 ตรวจผ่าน proxy (สมาชิกทุกคน)
docker compose up -d --wait
docker compose exec proxy nginx -t
curl -si http://localhost/health
curl -s http://localhost/api/items/2
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/
docker compose ps --format 'table {{.Service}}\t{{.Status}}\t{{.Ports}}'
docker compose logs proxy | grep GET | tail -n 2nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Content-Type: application/json; charset=utf-8
X-Powered-By: Express
...
{"status":"ok","version":"0.3.0"}
{"id":2,"name":"Git"}
000
SERVICE STATUS PORTS
app Up 6 seconds (healthy) 3000/tcp
db Up 25 seconds (healthy) 5432/tcp
proxy Up Less than a second 0.0.0.0:80->80/tcp
proxy-1 | 172.19.0.1 - - [19/Oct/2026:00:14:55 +0000] "GET /health HTTP/1.1" 200 33 "-" "curl/8.5.0" "-"
proxy-1 | 172.19.0.1 - - [19/Oct/2026:00:14:55 +0000] "GET /api/items/2 HTTP/1.1" 200 21 "-" "curl/8.5.0" "-"คำตอบมีทั้ง Server: nginx และ X-Powered-By: Express พอร์ต 8080 ไม่ตอบแล้ว (000 คือ curl เชื่อมต่อไม่ได้) และ log ของ proxy บันทึกทุก request ที่ผ่าน
ตัดสินใจเอง B.4: สมาชิกคนหนึ่งพบว่าหลัง deploy เวอร์ชันใหม่บนเครื่องปลายทาง บางครั้งหน้าเว็บได้ 502 Bad Gateway ทั้งที่ docker compose -p devops-prod ps แสดงแอปเป็น (healthy) ให้จำลองเหตุการณ์บนเครื่องตนเองตามขั้นตอนต่อไปนี้ แล้วอธิบายสาเหตุโดยอ้างหัวข้อ 9.2 และแก้ scripts/deploy.sh ให้ปัญหานี้ไม่เกิดหลัง deploy
docker compose rm -sf app
docker run -d --rm --name filler --network devops-app_default node:22-slim node -e "setTimeout(() => {}, 600000)"
docker compose up -d --no-deps --wait app
curl -s -o /dev/null -w "%{http_code}\n" http://localhost/
docker compose logs proxy | grep error | tail -n 1
docker rm -f fillercontainer filler ทำหน้าที่จองที่อยู่ IP เดิมของแอปไว้ เพื่อให้ container ใหม่ของแอปได้ IP ใหม่
ผลลัพธ์ที่คาดหวัง B.4: ได้ 502 และ log มี connect() failed (111: Connection refused) ... upstream: "http://<IP เดิม>:3000/" อธิบายได้ว่า Nginx ยังใช้ IP เดิม และ scripts/deploy.sh มีขั้นตอนที่ทำให้ Nginx แปลงชื่อใหม่หลัง up
จุดตรวจ B: curl ผ่านพอร์ต 80 ได้ HTTP 200 บนเครื่องผู้ตรวจทาน บริการ app ไม่มี 0.0.0.0: ในคอลัมน์ PORTS และ scripts/deploy.sh มีการแก้ตาม B.4
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
502 Bad Gateway และ log มี connect() failed (111: Connection refused) | แอปหยุดอยู่ หรือ Nginx ใช้ IP เดิมของแอป | ตรวจ docker compose ps แล้ว docker compose exec proxy nginx -s reload |
504 Gateway Time-out และ log มี upstream timed out | IP ปลายทางไม่ตอบเลย เช่น container ของแอปหยุดไปแล้ว | เริ่มแอปใหม่ แล้ว reload Nginx |
nginx: [emerg] host not found in upstream "app" แล้ว proxy หยุด | Nginx เริ่มก่อนแอปหรือพิมพ์ชื่อบริการผิด | ตรวจชื่อใน proxy_pass และ depends_on |
nginx: [emerg] unexpected "}" | ลืม ; ท้าย directive ก่อนวงเล็บปิด | แก้ไฟล์แล้วตรวจด้วย nginx -t |
| หน้าเว็บยังแสดงหน้าเริ่มต้นของ Nginx | path ใน volumes ผิด โฟลเดอร์จึงไม่ถูกต่อเข้า | ตรวจว่าไฟล์อยู่ที่ nginx/default.conf และ volumes เป็น ./nginx:/etc/nginx/conf.d:ro แล้วสร้าง container ใหม่ |
แก้ nginx/default.conf แล้ว reload แต่ Nginx ยังใช้ค่าเดิม | ต่อไฟล์เดี่ยว (./nginx/default.conf:/etc/nginx/conf.d/default.conf) แทนการต่อโฟลเดอร์ | เปลี่ยนเป็นต่อโฟลเดอร์ตาม B.2 แล้ว docker compose up -d หนึ่งครั้ง |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
Lab C: ย้ายค่าตั้งเป็นตัวแปรสภาพแวดล้อม
สถานการณ์: ชื่อทีมบนหน้าแรกถูกเขียนลงในโค้ดตั้งแต่บทที่ 8 ทีมต้องการใช้ image เดียวกันกับหลายสภาพแวดล้อมที่แสดงชื่อต่างกัน
เป้าหมาย: ให้แอปอ่านชื่อที่แสดงจากตัวแปร APP_TITLE พร้อม test และส่งค่าผ่าน Compose (LLO 9.2 ส่วนแยกค่าตั้งออกจากโค้ด)
ฝึกตามตัวอย่าง
C.1 แก้แอป (สมาชิกคนที่ 2)
git switch main
git pull
git switch -c config/app-title
nano src/app.jsแก้ route / ให้อ่านค่าจากตัวแปรสภาพแวดล้อมทุกครั้งที่มี request
app.get('/', (req, res) => {
const title = process.env.APP_TITLE || 'DevOps App';
res.type('text').send(`${title} v${version}\n`);
});C.2 ส่งค่าผ่าน Compose
เพิ่ม environment ให้บริการ app ใน compose.yaml ต่อจาก init: true และเพิ่มตัวแปรใน .env.example และ .env ของตน
environment:
APP_TITLE: ${APP_TITLE:-DevOps App}APP_TITLE=DevOps App (team 01)C.3 ตรวจผลและปรับเวอร์ชัน
npm version minor --no-git-tag-version
docker compose up -d --build --wait
docker compose exec -T proxy nginx -s reload
curl -s http://localhost/
APP_TITLE="Staging" docker compose up -d --wait
docker compose exec -T proxy nginx -s reload
curl -s http://localhost/
docker compose up -d --wait
docker compose exec -T proxy nginx -s reload
curl -s http://localhost/DevOps App (team 01) v0.4.0
Staging v0.4.0
DevOps App (team 01) v0.4.0image เดียวกันแสดงชื่อต่างกันตามค่าตั้ง โดยไม่ต้อง build ใหม่ระหว่างคำสั่งที่สองและสาม การ reload Nginx หลังแต่ละ up คือผลจาก Lab B.4
ตัดสินใจเอง C.4: test เดิมของทีมไม่ได้ตรวจว่าแอปอ่าน APP_TITLE จริง ให้เขียน test ใหม่ในไฟล์ test/title.test.js ที่ตั้งค่า process.env.APP_TITLE ก่อนเรียก / และตรวจว่าข้อความขึ้นต้นด้วยค่านั้น โดยต้องคืนค่าตัวแปรหลัง test จบเพื่อไม่ให้กระทบ test อื่น ใช้รูปแบบเดียวกับ test/app.test.js และเอกสาร Node.js test runner เป็นแนวทาง แล้ว commit พร้อมการแก้ใน C.1–C.3
ผลลัพธ์ที่คาดหวัง C.4: npm test ผ่านทุกรายการ จำนวน test เพิ่มขึ้นหนึ่งรายการ และ test ใหม่ล้มเมื่อจงใจลบการอ่าน process.env.APP_TITLE ออกจาก src/app.js
จุดตรวจ C: curl ผ่านพอร์ต 80 แสดงชื่อตาม APP_TITLE ใน .env บนเครื่องผู้ตรวจทาน src/app.js ไม่มีชื่อทีมเขียนตายตัว และ .env.example มี APP_TITLE
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
หน้าแรกยังแสดงชื่อเดิมหลังแก้ .env | container ยังไม่ถูกสร้างใหม่ | docker compose up -d ซึ่งสร้าง container ใหม่เมื่อค่าตั้งเปลี่ยน |
หน้าแรกแสดง DevOps App ทั้งที่ตั้งค่าแล้ว | พิมพ์ชื่อตัวแปรไม่ตรงกันระหว่าง .env, compose.yaml และโค้ด | ตรวจด้วย docker compose config ส่วน environment ของ app |
| test อื่นล้มหลังเพิ่ม test ใหม่ | ไม่ได้คืนค่า process.env.APP_TITLE หลัง test | ลบตัวแปรใน t.after |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
Lab D: ค้นหารหัสผ่านใน repository และประวัติ commit
สถานการณ์: ก่อนจุดตรวจความปลอดภัยครั้งที่ 2 ทีมต้องยืนยันว่าไม่มีรหัสผ่านอยู่ทั้งในไฟล์ปัจจุบันและในประวัติ commit ทุก branch และต้องการให้การตรวจนี้ทำซ้ำได้ด้วยคำสั่งเดียว
เป้าหมาย: ค้นหาความลับด้วยคำสั่งของ Git ในสามมุม แล้วรวมเป็นสคริปต์ที่ทีมใช้ซ้ำในบทที่ 10 และ 12 (LLO 9.2)
ฝึกตามตัวอย่าง
D.1 ค้นด้วยมือสามมุม (สมาชิกทุกคน บน main ล่าสุด)
git grep -n -i -E "password|passwd|secret|token" -- . ':!package-lock.json'
git log --all --format='' --name-only | grep -E '(^|/)\.env$' | sort -u
git log -p --all --format='' | grep -i -E "^\+.*(password|passwd|secret|token)" | sort -u.env.example:2:POSTGRES_PASSWORD=change-me
.github/workflows/cd.yml:43: password: ${{ secrets.GITHUB_TOKEN }}
.github/workflows/cd.yml:74: password: ${{ secrets.GITHUB_TOKEN }}
compose.yaml:32: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is not set}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is not set}
+POSTGRES_PASSWORD=change-me| คำสั่ง | ค้นอะไร |
|---|---|
git grep -n -i -E ... | คำที่เข้าข่ายในไฟล์ทุกไฟล์ที่ Git ติดตามอยู่ตอนนี้ (ยกเว้น lock file ที่มีคำว่า token ในชื่อแพ็กเกจได้) |
git log --all --format='' --name-only | ชื่อไฟล์ทุกไฟล์ที่เคยปรากฏใน commit ใด ๆ ของทุก branch แล้วกรองหา .env |
git log -p --all --format='' | เนื้อหาที่เคยถูกเพิ่ม (บรรทัดขึ้นต้นด้วย +) ในทุก commit |
ผลของ repository ตัวอย่างพบเพียงค่าตัวอย่าง (change-me) และการอ้างถึงตัวแปรหรือ secret (${...}) ซึ่งไม่ใช่รหัสผ่านจริง คำสั่งที่สองไม่มีผลลัพธ์ หมายความว่าไม่เคยมีไฟล์ .env ถูก commit
D.2 รวมเป็นสคริปต์ (สมาชิกคนที่ 3)
git switch -c chore/check-secrets
nano scripts/check-secrets.sh#!/usr/bin/env bash
# check-secrets.sh : ค้นหาร่องรอยความลับในประวัติ commit ทุก branch
set -euo pipefail
PATTERN='password|passwd|secret|token'
ALLOW='\$\{|change-me|secrets\.'
found=0
echo "== ไฟล์ .env ที่เคยถูก commit =="
if git log --all --format='' --name-only | grep -E '(^|/)\.env$' | sort -u | grep .; then
found=1
fi
echo "== บรรทัดที่เคยถูกเพิ่มและเข้าข่ายความลับ =="
if git log -p --all --format='' -- . ':(exclude)scripts/check-secrets.sh' | grep -i -E "^\+.*($PATTERN)" | grep -v -E "$ALLOW" | sort -u | grep .; then
found=1
fi
if [ "$found" -eq 1 ]; then
echo "ผล: พบร่องรอยที่ต้องตรวจสอบ"
exit 1
fi
echo "ผล: ไม่พบร่องรอยความลับตามรูปแบบที่ค้น"chmod +x scripts/check-secrets.sh
shellcheck scripts/check-secrets.sh && echo "shellcheck ok"
bash scripts/check-secrets.sh; echo "exit=$?"shellcheck ok
== ไฟล์ .env ที่เคยถูก commit ==
== บรรทัดที่เคยถูกเพิ่มและเข้าข่ายความลับ ==
ผล: ไม่พบร่องรอยความลับตามรูปแบบที่ค้น
exit=0ตัวแปร ALLOW กรองบรรทัดที่เป็นการอ้างถึงตัวแปร (${), ค่าตัวอย่าง (change-me) และ GitHub secrets (secrets.) ออก ส่วน -- . ':(exclude)scripts/check-secrets.sh' ตัดประวัติของไฟล์สคริปต์เองออก เพราะบรรทัด PATTERN= ในสคริปต์มีคำที่ค้นหาอยู่ ผลการทดสอบพบว่าถ้าไม่ตัดออก สคริปต์จะรายงานตัวเองทันทีที่ถูก commit สคริปต์คืน exit code 1 เมื่อพบร่องรอย จึงใช้เป็นขั้นตอนหนึ่งของ workflow ได้ ผลการทดสอบกับ repository ตัวอย่างที่เคย commit .env แล้วลบออกแสดงว่าสคริปต์พบทั้งไฟล์และบรรทัดรหัสผ่าน
== ไฟล์ .env ที่เคยถูก commit ==
.env
== บรรทัดที่เคยถูกเพิ่มและเข้าข่ายความลับ ==
+POSTGRES_PASSWORD=Sup3rS3cret!
ผล: พบร่องรอยที่ต้องตรวจสอบ
exit=1ตัดสินใจเอง D.3: สคริปต์ค้นจากคำสำคัญจึงตรวจไม่พบความลับบางรูปแบบ ให้คิดตัวอย่างบรรทัดที่มีรหัสผ่านจริงแต่สคริปต์ตรวจไม่พบอย่างน้อย 1 รูปแบบ (ทดลองใน repository ทดลองที่ clone แยกออกมา ห้ามทดลองใน repository ทีม) แล้วเลือกทางใดทางหนึ่ง (ก) ปรับ PATTERN ให้ตรวจพบโดยไม่ทำให้บรรทัดปกติของทีมถูกรายงานผิด หรือ (ข) ค้นเครื่องมือตรวจความลับเฉพาะทางจากแหล่งที่เชื่อถือได้ แล้วเปรียบเทียบข้อดีข้อจำกัดกับสคริปต์ของทีม บันทึกตัวอย่าง การตัดสินใจ และแหล่งอ้างอิงลงในคำอธิบาย pull request
ผลลัพธ์ที่คาดหวัง D.3: ตัวอย่างที่สคริปต์เดิมตรวจไม่พบ ผลการทดลองก่อนและหลังปรับ (ทาง ก) หรือตารางเปรียบเทียบพร้อมลิงก์เอกสาร (ทาง ข) และ scripts/check-secrets.sh บน main ยังคืน exit code 0
จุดตรวจ D: สคริปต์บน main ผ่าน shellcheck และคืน exit code 0 สมาชิกทุกคนส่งผลรันเป็นหลักฐานของงานข้อ 2
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| สคริปต์รายงานบรรทัดที่ไม่ใช่ความลับ | รูปแบบกว้างเกินไป เช่น คำว่า token ในข้อความอธิบาย | เพิ่มรูปแบบใน ALLOW อย่างระมัดระวัง และบันทึกเหตุผล |
สคริปต์พบ .env ในประวัติ | เคยมีคน commit .env | แจ้งทีมและผู้สอน เปลี่ยนรหัสผ่านทุกค่าในไฟล์นั้นทันที แล้วจึงพิจารณาเขียนประวัติใหม่ |
fatal: not a git repository | รันสคริปต์นอก repository | cd ~/devops-app ก่อน |
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
เก็บกวาดเมื่อจบปฏิบัติการ
หลัง merge ครบทั้งสาม pull request หัวหน้าทีมออกเวอร์ชัน v0.4.0 ตามบทที่ 8 ก่อนออกเวอร์ชันต้องแก้ไฟล์ ~/devops-deploy/.env บนเครื่องปลายทาง ให้สอดคล้องกับ compose.yaml ใหม่ คือลบ APP_PORT เพิ่ม PROXY_PORT=80 และ APP_TITLE=<ชื่อที่ต้องการแสดง> ถ้าบนเครื่องปลายทางมีระบบพัฒนา (project devops-app) ใช้พอร์ต 80 อยู่ ให้หยุดด้วย docker compose down ก่อน บนเครื่องของตนเองให้หยุดระบบด้วย docker compose down (ไม่มี -v) และลบ container filler ที่อาจค้างจาก Lab B.4 ด้วย docker rm -f filler
สรุป
Nginx ทำหน้าที่ได้ทั้ง web server ที่ส่งไฟล์โดยตรงและ reverse proxy ที่ส่ง request ต่อไปยังบริการด้านหลัง การวาง Nginx หน้าแอปทำให้ระบบมีประตูเดียวที่พอร์ต 80 และบริการภายในไม่ต้องเปิดพอร์ตสู่เครื่อง เมื่อการส่งต่อล้ม Nginx ตอบ 502 หรือ 504 พร้อมบันทึกสาเหตุใน log ซึ่งช่วยแยกปัญหาของ proxy ออกจากปัญหาของแอป
ค่าตั้งของ Nginx ประกอบด้วย block server และ location คำสั่ง proxy_pass กำหนดปลายทาง และต้องตั้ง proxy_set_header Host $host เอง เพราะโดยปริยาย Nginx ส่ง Host เป็นชื่อในค่า proxy_pass Nginx แปลงชื่อบริการเป็น IP ตอนเริ่มทำงาน การ deploy ที่สร้าง container ของแอปใหม่จึงต้องสั่ง nginx -s reload ตามด้วย มิฉะนั้นอาจได้ 502
แนวคิด 12-factor ให้แยกทุกค่าที่ต่างกันระหว่างการ deploy ออกจากโค้ดไปไว้ในตัวแปรสภาพแวดล้อม image เดียวจึงใช้ได้หลายสภาพแวดล้อม ความลับต้องไม่เข้า Git เลย เพราะ Git เก็บทุก commit ไว้ถาวร เมื่อพบว่าความลับรั่วต้องเปลี่ยนรหัสผ่านก่อนอย่างอื่น และสำหรับรหัสผ่านของ container เอกสารของ Docker แนะนำ Compose secrets แทนตัวแปรสภาพแวดล้อม
สิ่งที่ส่งต่อไปยังโครงงานและ Capstone
| สิ่งที่ได้ | นำไปใช้ที่ |
|---|---|
บริการ proxy, nginx/default.conf และ M8 | บทที่ 11 deploy gate ตรวจผ่านพอร์ต 80 · บทที่ 12 pipeline ครบสาย |
scripts/deploy.sh ที่ reload Nginx | บทที่ 11–12 และ Capstone บทที่ 13 |
APP_TITLE | บทที่ 12 สถานการณ์จำลอง · บทที่ 14 ConfigMap |
scripts/check-secrets.sh และผลจุดตรวจครั้งที่ 2 | บทที่ 10 ก่อนย้ายความลับ (จุดตรวจครั้งที่ 3) · บทที่ 12 จุดตรวจความปลอดภัยครั้งที่ 4 |
| Learning Log ครั้งที่ 9 | portfolio ที่ส่งในบทที่ 13 |
สรุปคำสั่งในบทนี้
| คำสั่งหรือค่าตั้ง | หน้าที่ |
|---|---|
curl -sI URL | ขอเฉพาะ header ของคำตอบ |
docker compose exec proxy nginx -t | ตรวจไวยากรณ์ค่าตั้ง Nginx ใน container |
docker compose exec -T proxy nginx -s reload | ให้ Nginx โหลดค่าตั้งใหม่และแปลงชื่อบริการใหม่ |
docker compose logs proxy | ดู access log และ error log ของ Nginx |
listen, location, proxy_pass, proxy_set_header | รับพอร์ต เลือก path ส่งต่อ และตั้ง header ที่ส่งต่อ |
volumes: - ./nginx:/etc/nginx/conf.d:ro | ต่อโฟลเดอร์ค่าตั้งจาก repository เข้า container แบบอ่านอย่างเดียว |
environment: VAR: ${VAR:-ค่าเริ่มต้น} | ส่งค่าตั้งให้ container พร้อมค่าเริ่มต้น |
git grep -n -i -E "รูปแบบ" | ค้นในไฟล์ปัจจุบันที่ Git ติดตาม |
git log --all --format='' --name-only | รายชื่อไฟล์ที่เคยปรากฏในทุก commit |
git log -p --all | เนื้อหาการเปลี่ยนแปลงทุก commit ทุก branch |
git log --all --oneline -- ไฟล์, git show COMMIT:ไฟล์ | หา commit ที่แตะไฟล์ และอ่านไฟล์จาก commit นั้น |
แบบฝึกหัดท้ายบท
ตอนที่ 1 ฝึกทบทวน
ชุด FP10306343-P09 จำนวน 12 ข้อ ข้อละ 1 คะแนน รวม 12 คะแนน ครอบคลุมบทบาท reverse proxy, การจัดเส้นทางใน Nginx, หลัก 12-factor config, การแยกค่าตั้งออกจากโค้ด และการจัดการความลับที่เคย commit
เข้าสู่ระบบห้องเรียนด้วยบัญชีที่ลงทะเบียนในรายวิชา 10306343 แล้วตอบคำถามให้ครบ ระบบตรวจคำตอบและแสดงคำอธิบายเพื่อทบทวน เมื่อทำครบและส่งผล ระบบจะบันทึกคะแนนไว้กับบัญชีของผู้เรียน สามารถทำซ้ำเพื่อทบทวนได้ ส่วนคะแนนที่นำไปรวมในรายวิชาเป็นไปตามเกณฑ์และกำหนดส่งในสมุดคะแนน
แบบฝึกหัดเพิ่มเติมสำหรับอภิปรายและเขียนตอบ
แบบฝึกหัดแบ่งเป็นสามกลุ่ม ได้แก่ ความเข้าใจ การประยุกต์ และการวิเคราะห์ปัญหา แต่ละข้อระบุผลลัพธ์การเรียนรู้ที่ฝึก
ก. ความเข้าใจ
- ก1 (LLO 9.1) อธิบายความต่างระหว่าง web server กับ reverse proxy และระบุว่า Nginx ในระบบของทีมทำหน้าที่ใด
- ก2 (LLO 9.1) อธิบายความต่างระหว่าง
502 Bad Gatewayกับ504 Gateway Time-outจากข้อความใน error log ของ Nginx - ก3 (LLO 9.2) ยกตัวอย่างค่าตั้ง 2 ค่าที่ควรอยู่ในตัวแปรสภาพแวดล้อม และ 2 ค่าที่ควรอยู่ในโค้ด พร้อมเหตุผลตามนิยามของ 12-factor
ข. การประยุกต์
- ข1 (LLO 9.1) อ่านค่าตั้งต่อไปนี้ แล้วระบุว่า request แต่ละรายการไปที่บริการใด และบริการปลายทางเห็น header Host เป็นค่าใด: (1)
GET /(2)GET /health(3)GET /api/items(4)GET /apiv2/x(5)GET /api
server {
listen 80;
location /api/ {
proxy_pass http://api:4000;
}
location / {
proxy_pass http://web:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}- ข2 (LLO 9.1) เขียนส่วนของ
compose.yamlที่ทำให้บริการproxyรับที่พอร์ต 8000 ของเครื่องเมื่อตั้งPROXY_PORT=8000ใน.envและรับที่พอร์ต 80 เมื่อไม่ได้ตั้ง - ข3 (LLO 9.2) เขียนคำสั่ง Git ที่ตอบว่า "ไฟล์
config/secret.jsonเคยถูก commit ใน commit ใดบ้าง" และคำสั่งที่แสดงเนื้อหาของไฟล์นั้นใน commit แรกที่พบ
ค. การวิเคราะห์ปัญหา
- ค1 (LLO 9.1) หลังออกเวอร์ชัน 0.4.1 workflow ผ่านทุก job แต่หน้าเว็บบนเครื่องปลายทางได้ 502 เมื่อสมาชิกคนหนึ่งสั่ง
docker compose -p devops-prod restart proxyหน้าเว็บกลับมาปกติ วิเคราะห์สาเหตุและเสนอการแก้ที่ทำให้ไม่ต้องแก้ด้วยมืออีก - ค2 (LLO 9.2) ทีมหนึ่งผ่าน
scripts/check-secrets.shแต่ผู้สอนพบDATABASE_URL=postgres://app:Pa55word@db:5432/appใน commit เก่า วิเคราะห์ว่าเหตุใดสคริปต์ตรวจไม่พบ และสิ่งที่ทีมต้องทำตามลำดับ - ค3 (LLO 9.2) สมาชิกคนหนึ่งเสนอให้ใส่รหัสผ่านฐานข้อมูลของเครื่องปลายทางลงใน
compose.yamlโดยตรงเพราะ "repository เป็น private อยู่แล้ว" วิเคราะห์ข้อเสนอนี้โดยอ้างบททดสอบของ 12-factor และอย่างน้อยหนึ่งความเสี่ยงที่ repository private ไม่ได้ป้องกัน
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
การศึกษาค้นคว้าเพิ่มเติม
- อ่าน nginx.org: Module ngx_http_proxy_module ส่วน
proxy_passและproxy_set_headerแล้วบันทึกค่าเริ่มต้นของ header ที่ Nginx ส่งต่อใน Learning Log - อ่าน The Twelve-Factor App: III. Config แล้วใช้บททดสอบในเอกสารตรวจ repository ของทีม เพื่อตอบคำถาม 9.3
- อ่าน Docker Docs: Set environment variables within your container's environment และบันทึกความต่างของคำแนะนำเรื่องรหัสผ่านเมื่อเทียบกับ 12-factor
- อ่านบทความ สร้างเว็บด้วย Nginx และพัฒนา REST API ด้วย Node.js บน Docker เพื่อดูตัวอย่าง Nginx ที่ส่งทั้งไฟล์และส่งต่อไปยัง API ในค่าตั้งเดียว
- ทำแบบฝึกหัดท้ายบทให้ครบ เขียนบันทึกจุดตรวจความปลอดภัยครั้งที่ 2 (งานข้อ 2) และเขียน Learning Log ครั้งที่ 9
เอกสารอ้างอิง
แหล่งข้อมูลเพิ่มเติมสำหรับตัวอย่างและกรณีอธิบาย
ตรวจเอกสารออนไลน์เมื่อ 23 ตุลาคม 2569 ตัวเลขและเหตุการณ์ที่ระบุว่าสมมติใช้เพื่ออธิบายกลไก ไม่ใช่ผลทดลองหรือสถิติขององค์กรจริง
เอกสารอ้างอิงของเนื้อหาเดิม
- 12factor.net. (n.d.). The twelve-factor app: III. Config. https://12factor.net/config
- Docker Inc. (n.d.). Set environment variables within your container's environment. Docker Docs. https://docs.docker.com/compose/how-tos/environment-variables/set-environment-variables/
- nginx.org. (n.d.). Module ngx_http_proxy_module. https://nginx.org/en/docs/http/ngx_http_proxy_module.html
- sarachai.com. (n.d.). สร้างเว็บด้วย Nginx และพัฒนา REST API ด้วย Node.js บน Docker. /blog/lab-04-nginx-nodejs-docker