Skip to content

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

ContainerImagePort (host:container)APP_NOหน้าที่
vtrc-redisbuilt from ./docker-redis/6379:6379cache + session
vtrc-web-servernginx:latest80:80, 443:443reverse proxy + static host
vtrc-phpMyAdminphpmyadmin/phpmyadmin8093:80DB admin (กลางวง)
vtrc-api-nodejsvtrc-api_vtrc-api:latest8081:80811API replica (leader)
vtrc-api-nodejs-2เดียวกัน8082:80812API replica
vtrc-api-nodejs-3เดียวกัน8083:80813API replica
vtrc-api-nodejs-5เดียวกัน8085:80815API replica

ข้อสังเกตสำคัญเกี่ยวกับ topology

1. APP_NO = 4 หายไป

มี replica 1, 2, 3, 5 แต่ไม่มี 4 — เป็นจุดที่บอกว่าเคยมี replica 4 แต่ปัจจุบันเก็บไปแล้ว (อาจจะล่มบ่อย หรือถูกยุบ) แต่ใครจะไปรู้ — ไม่มี comment ไม่มี git blame อธิบาย

APP_NO ใช้ในที่เดียวคือ crons.jsvtrc-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 กว่า

ContainerImagePort (host:container)APP_NOหน้าที่
redis-dbbuilt locally6379:6379cache
rc-web-proxynginx:latest443:443, 25563:25563proxy + static
cu-cen-apigcr.io/vtrc-nonprod/cu-centralize-api:latest8000:80001leader (cron)
cu-cen-api_2เดียวกัน8002:80002worker
cu-cen-api_3เดียวกัน8003:80003worker

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 รวดเดียว


ขั้นตอนถัดไป

ไป บท 2.3 Request lifecycle