2.2 · Topology จริงใน production
บทที่แล้วเล่าภาพใหญ่ — บทนี้เล่ารายละเอียดว่าแต่ละ container อยู่ที่ไหน เปิด port อะไร ใครคุยกับใครผ่าน network อย่างไร
ถ้า DevOps เรียกเราตอน 3 ทุ่มว่า "production ล่ม" — บทนี้คือแผนที่ที่จะช่วยให้รู้ว่าจะดูที่ไหน
ภาพรวม topology
Internet
│
│ HTTPS :443
▼
┌────────────────────────────────┐
│ Production host (VM เดียว) │
│ │
│ ┌──────────────────────────┐ │
│ │ nginx (network_mode:host) │ │ ← host network!
│ │ • :80 → redirect :443 │ │
│ │ • :443 → upstream │ │
│ │ • static vtrc-web build │ │
│ └────────────┬─────────────┘ │
│ │ │
│ ┌───────┼───────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ vtrc-api vtrc-api vtrc-api ... │
│ :8081 :8082 :8083 :8085 │ ← APP_NO = 1, 2, 3, 5
│ │ │ │ │
│ └───────┼───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ • Redis :6379 │ │
│ │ • phpMyAdmin :8093 │ │ ← เปิดสู่สาธารณะ
│ └──────────────────────────┘ │
└────────────────────────────────┘
│
│ internal network
▼
┌────────────────────────────────┐
│ Satellite services (host อื่น) │
│ │
│ • centralize-api :3000 │
│ • cu-central-api :8080 │
│ • MSSQL (HRMI) │
│ • LDAP/AD (จุฬา) │
│ • GCP (Drive + FCM) │
└────────────────────────────────┘Container ทั้งหมดใน vtrc-api/docker-compose.yml
ที่มา — vtrc-api/docker-compose.yml:1-100
| Container | Image | Port (host:container) | APP_NO | หน้าที่ |
|---|---|---|---|---|
vtrc-redis | built from ./docker-redis/ | 6379:6379 | — | cache + session |
vtrc-web-server | nginx:latest | 80:80, 443:443 | — | reverse proxy + static host |
vtrc-phpMyAdmin | phpmyadmin/phpmyadmin | 8093:80 | — | DB admin (กลางวง) |
vtrc-api-nodejs | vtrc-api_vtrc-api:latest | 8081:8081 | 1 | API replica (leader) |
vtrc-api-nodejs-2 | เดียวกัน | 8082:8081 | 2 | API replica |
vtrc-api-nodejs-3 | เดียวกัน | 8083:8081 | 3 | API replica |
vtrc-api-nodejs-5 | เดียวกัน | 8085:8081 | 5 | API replica |
ข้อสังเกตสำคัญเกี่ยวกับ topology
1. APP_NO = 4 หายไป
มี replica 1, 2, 3, 5 แต่ไม่มี 4 — เป็นจุดที่บอกว่าเคยมี replica 4 แต่ปัจจุบันเก็บไปแล้ว (อาจจะล่มบ่อย หรือถูกยุบ) แต่ใครจะไปรู้ — ไม่มี comment ไม่มี git blame อธิบาย
APP_NO ใช้ในที่เดียวคือ crons.js — vtrc-api/api/src/crons.js:14 บังคับว่า replica #1 เท่านั้นที่รัน cron fund change rate (เป็น crude leader election ผ่าน env var)
2. Nginx ใช้ network_mode: host
vtrc-api/docker-compose.yml:35 — Nginx container แชร์ network stack กับ host โดยตรง หมายความว่า
- ไม่มี port isolation
- ถ้า host เปิด port อะไร → container เข้าถึงได้
- การเปลี่ยน nginx config มีผลทันทีต่อ production ไม่มี safety net
3. phpMyAdmin เปิดสู่สาธารณะ
vtrc-api/docker-compose.yml:35-46 — phpMyAdmin เปิดที่ port 8093 และ mount ที่ vtrcapi.redcross.or.th/db-management/ — เป็น DB admin surface ที่ใครก็เข้าถึง URL ได้ (แค่มี login gate ของ phpMyAdmin เอง)
ถ้าทำ security audit — นี่คือจุดที่ต้องดูเป็นอันดับต้นๆ
4. ทุก container restart: always แต่ไม่มี healthcheck
restart: always ใน compose (docker-compose.yml:13, 33, 45, 60, 73, 86, 99) — ถ้า container crash Docker จะ restart ให้ แต่
- ถ้า app ค้าง (hang) แต่ process ยังเปิดอยู่ → Docker ไม่รู้ ไม่ restart
- ถ้า DB connection หลุด → app ไม่มี logic reconnect อัตโนมัติ
5. เก็บ state ใน disk volume ไม่ใช่ volume ที่จัดการ
vtrc-api/api-storage และ vtrc-api/api-logs mount เข้า container ทุกตัวเหมือนกัน — docker-compose.yml:59, 72, 84, 98
ผลคือ
- 4 replica เขียน log ลง directory เดียวกัน (ไม่มี replica tag ในชื่อไฟล์) → อ่าน log ยาก
- PDF ที่ generate แล้วลบ delay 5 วินาที (
vtrc-api/api/src/routes.js:79-82) — ถ้า container ตายในช่วงนั้น PDF จะตกค้างใน disk ตลอดไป
Network topology ของ satellite services
centralize-api และ cu-central-api อยู่กับ Docker network เดียวกัน (เห็นได้จาก centralize-api/docker-compose.yml:29-31 — ใช้ external network cu-centralize-api_default)
หมายความว่า
- 2 services นี้ deploy ใน host เดียวกัน (หรือ cluster เดียวกัน)
- เรียกหากันผ่าน internal Docker DNS ได้ ไม่ผ่าน internet
แต่ vtrc-api (อีก host) → cu-central-api ผ่าน env var END_POINT_CENTRALIZE — ใน config.js:28 ค่า default เป็นสตริงว่าง (''); URL จริงมาจาก env เช่น vtrc-api/api/.env.example:36 (http://localhost:8082/api/graphql = CLIENT_TARGET ตัวอย่าง) ในขณะที่ CODE_DEFAULT ของ cu-central คือ APP_PORT=8080 — ปลายทางคือ cu-central-api GraphQL ไม่ใช่ NestJS centralize-api
ข้อควรทราบ — caller ที่แท้จริงของ NestJS
centralize-api(port 3000) ยังไม่ยืนยัน จาก code ใน workspace นี้vtrc-apiไม่ได้เรียก NestJS centralize-api โดยตรง ดูรายละเอียดใน บท 5.9
Frontend — แยกจาก backend
Frontend (vtrc-web, vtrc-rc-backoffice) deploy เป็น static files ใน container nginx ของตัวเอง
vtrc-web/Dockerfile:1-20
Stage 1: node:14.20 build → webpack → build/
Stage 2: nginx:1.24.0
• copy build/ → /usr/share/nginx/html/build/
• copy nginx-config/default.conf → /etc/nginx/conf.d/
vtrc-rc-backoffice/Dockerfile:1-17
Stage 1: node:16 build → react-scripts → build/
Stage 2: nginx:1.24.0 (same pattern)ทั้งคู่ใช้ nginx config เหมือนกันทุกประการ (vtrc-rc-backoffice/nginx/default.conf byte-identical กับ vtrc-web/nginx-config/default.conf)
Frontend container ทำแค่ serve static — try_files $uri $uri/ /index.html (SPA fallback) ไม่มี proxy ไม่มี auth
cu-central-api topology
cu-central-api/docker-compose.yml:1-101 — แยกออกมาเพราะ complex กว่า
| Container | Image | Port (host:container) | APP_NO | หน้าที่ |
|---|---|---|---|---|
redis-db | built locally | 6379:6379 | — | cache |
rc-web-proxy | nginx:latest | 443:443, 25563:25563 | — | proxy + static |
cu-cen-api | gcr.io/vtrc-nonprod/cu-centralize-api:latest | 8000:8000 | 1 | leader (cron) |
cu-cen-api_2 | เดียวกัน | 8002:8000 | 2 | worker |
cu-cen-api_3 | เดียวกัน | 8003:8000 | 3 | worker |
Port 25563 ที่ nginx เปิดไว้ — ไม่มี documentation อธิบาย น่าจะเป็น admin หรือ SSH
Hardcoded SSO DNS pin
cu-central-api/docker-compose.yml:54-55 แฮ็ก extra_hosts ไว้
extra_hosts:
- "sso.redcross.or.th ssoapi.redcross.or.th:10.188.128.213"หมายความว่า container จะ resolve SSO domain ไปที่ IP นี้เสมอ ถ้า IP เปลี่ยน → SSO จะ silently fail โดยไม่มีใครรู้ตัว
เคล็ดลับตอนทำงานจริง
ตอน debug ให้รู้ว่า "request ไปตกที่ container ไหน"
เพราะมี 4 replica ของ vtrc-api ถ้า user แจ้งว่า "บางครั้งได้ บางครั้งไม่ได้" — อาจจะเป็นเพราะ state ใน memory ไม่ sync ระหว่าง replica (เช่น cache ที่ฝั่ง process)
วิธีตรวจ — ดูว่า request ไปตกที่ replica ไหน โดยดู APP_NO ใน log header (vtrc-api/api/src/lib/wlog.js:9-20) แต่เนื่องจากทุก replica เขียน log ลงไฟล์เดียวกัน ต้อง grep เนื้อหาใน log เอง
Port ของ cu-central-api ไม่ตรงกัน 3 ที่
| ที่ | Port |
|---|---|
cu-central-api/main-api/src/config.js:20 (default) | 8080 |
cu-central-api/main-api/Dockerfile:18 (EXPOSE) | 9000 |
cu-central-api/docker-compose.yml:51-53 (host port) | 8000 |
อย่าไปเชื่อ Dockerfile EXPOSE — EXPOSE ใน Docker เป็นแค่ documentation ไม่มีผลจริง ใช้ compose เป็น source of truth
ถ้าทำ migration หรืออัปเดต schema
ต้องระวังเรื่อง timing — เพราะมีหลาย replica ที่ share database เดียวกัน ถ้า deploy schema ใหม่ที่ไม่ backward-compatible แบบทีละ replica → อาจจะมีบาง replica ใช้ schema เก่าอยู่
ปัจจุบัน deploy แบบ rolling ไม่มี จึงต้อง downtime รวดเดียว