Service per Container
คุณมีหลาย service ที่ต้อง deploy แต่ละตัวมี runtime ของตัวเอง ชุด library ของตัวเอง และเวอร์ชันของ library เหล่านั้นของตัวเอง service หนึ่งต้องการ OpenSSL เวอร์ชันเก่า อีกตัวต้องการ Java เวอร์ชันเฉพาะ ตัวที่สามรวมเอา native image-processing library มาด้วย ช่วงแรกคุณรันทุกตัวตรง ๆ บน host ที่แชร์กัน ติดตั้ง runtime แต่ละตัวเคียงข้างกันไป
แบบนั้นพอไหวตอนมีสอง service แต่พอขึ้นเป็นสิบ host ก็กลายเป็นทุ่งระเบิด upgrade library ที่แชร์กันให้ service หนึ่ง อีกตัวพังทันที “มันรันได้บนเครื่องผม” กลายเป็นวลีติดปากประจำวัน และไม่มีใครจำลอง production ได้เป๊ะ ๆ สิ่งที่คุณต้องการคือวิธีส่งมอบ service พร้อมทุกอย่างที่ต้องใช้รัน แล้ววางหลาย service แบบนี้บนเครื่องเดียวได้โดยไม่กวนกัน
คุณต้องการให้แต่ละ service ถูก deploy เป็นหน่วยที่ครบในตัวเอง ไม่เปลี่ยนแปลง และรวม dependency ของตัวเองไว้ เพื่อให้สิ่งที่คุณทดสอบเป็นสิ่งที่รันเป๊ะ ๆ คุณต้องการให้หน่วยนั้นเริ่มทำงานภายในไม่กี่วินาที ไม่ใช่หลายนาที เพื่อให้การ deploy และ autoscaling เร็ว คุณต้องการ isolation ที่แข็งแกร่งเพื่อให้ library, file system และการใช้ resource ของ service หนึ่งไม่รั่วไปยังอีกตัว และคุณต้องการสิ่งเหล่านี้ทั้งหมดโดยไม่ต้องจ่ายค่า virtual machine เต็ม ๆ ต่อหนึ่ง service
ดังนั้นแรงที่ขัดแย้งกันคือ: isolation และ reproducibility ที่แข็งแกร่ง แต่ต้องเริ่มทำงานเร็วและมีความหนาแน่นสูงบนแต่ละเครื่อง และ artifact ชิ้นเดียวที่รันเหมือนกันทุกที่
วิธีแก้
หัวข้อที่มีชื่อว่า “วิธีแก้”package แต่ละ service เป็น container image: snapshot ของ filesystem แบบ layered และไม่เปลี่ยนแปลง ที่รวมทั้ง binary ของ service, runtime และ dependency ไว้ด้วยกัน build ครั้งเดียวจากสูตรเชิงประกาศ (declarative recipe) พอถึงตอนรัน image นี้จะกลายเป็น container คือ process ที่แยกส่วน มี filesystem, network namespace และ resource limit ของตัวเอง โดยแชร์ host kernel แทนที่จะ boot guest OS เต็มตัว
กฎที่นิยาม pattern นี้คือ หนึ่ง service ต่อหนึ่ง container คุณไม่ยัดหลาย service เข้าไปใน image เดียว แต่ละ service ได้ image และ container ของตัวเอง จะได้ build, ทำเวอร์ชัน, deploy, scale และ roll back แยกจาก service ตัวอื่นได้ทั้งหมด
เพราะ container เบา คุณจึงรันได้หลายตัวต่อเครื่อง และไม่ต้องวางเองด้วยมือ orchestrator อย่าง Kubernetes จะรับ desired state ที่คุณประกาศไว้ เช่น “รันสาม replica ของ order service” แล้วจัดสรร container ลงเครื่องต่าง ๆ เอง ตัวไหนตายก็เริ่มใหม่ node ไหนล่มก็ย้ายไปที่อื่นให้
flowchart TB
subgraph Build[Build once]
Dockerfile[Dockerfile] --> Image[order-service:1.4.2 image]
end
Image --> Reg[(Image registry)]
subgraph Cluster[Orchestrated cluster]
subgraph N1[Node A]
C1[order-service container]
C2[order-service container]
end
subgraph N2[Node B]
C3[order-service container]
C4[inventory-service container]
end
end
Reg --> C1
Reg --> C2
Reg --> C3
Sched[Scheduler / control plane] --> N1
Sched --> N2 ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”สูตรที่ build image ขึ้นมาคือ Dockerfile การ build แบบ multi-stage จะ compile service ใน build image ที่หนัก แล้วคัดลอกเฉพาะ binary ที่เสร็จแล้วเข้าไปใน runtime image จิ๋ว ๆ เพื่อให้ artifact ที่ส่งมอบยังคงเล็กและมีจุดให้โจมตีน้อย
# Stage 1: buildFROM golang:1.22 AS buildWORKDIR /srcCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -o /out/order-service ./cmd/order
# Stage 2: minimal runtime — one service, nothing elseFROM gcr.io/distroless/static:nonrootCOPY --from=build /out/order-service /order-serviceUSER nonrootEXPOSE 8080ENTRYPOINT ["/order-service"]จากนั้นประกาศ image นี้ต่อ orchestrator ในรูปของ Deployment สังเกตว่ามีการขอสาม replica ตั้ง resource limit ไว้กัน container ตัวหนึ่งแย่ง resource เพื่อนบ้าน และต่อสาย health check ที่ orchestrator ใช้ตัดสินว่า container พร้อมรับงานเมื่อไร
apiVersion: apps/v1kind: Deploymentmetadata: name: order-servicespec: replicas: 3 selector: matchLabels: app: order-service template: metadata: labels: app: order-service spec: containers: - name: order-service image: registry.example.com/order-service:1.4.2 ports: - containerPort: 8080 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 3 periodSeconds: 5ผลลัพธ์ที่ตามมา
หัวข้อที่มีชื่อว่า “ผลลัพธ์ที่ตามมา”สิ่งที่คุณได้:
- Isolation และ reproducibility แต่ละ service ส่งมอบพร้อม dependency ของตัวเองใน image ที่ไม่เปลี่ยนแปลง artifact ที่คุณทดสอบจึงเหมือนกับที่รันจริงแบบ byte-for-byte ปัญหา library ชนกันระหว่าง service หายไป และ “มันรันได้บนเครื่องผม” ก็แทบหมดไป
- เร็ว หนาแน่น พกพาได้ container เริ่มทำงานในไม่กี่วินาทีและอัดได้หลายตัวต่อ host เพราะแชร์ kernel แทนที่จะ boot VM image เดียวกันรันได้ทั้งบน laptop ใน CI และใน production โดยไม่ต้องแก้อะไร
- lifecycle แยกอิสระ หนึ่ง service ต่อหนึ่ง container แปลว่าคุณ build, ทำเวอร์ชัน, scale และ roll back แต่ละ service ตามจังหวะของตัวเองได้
สิ่งที่คุณต้องเสีย:
- ความซับซ้อนของ orchestration การรัน container ในระดับใหญ่หมายถึงการรับเอา orchestrator เข้ามา ที่เป็นระบบขนาดใหญ่ในตัวเอง — scheduling, networking, storage และ security ทั้งหมดกลายเป็นสิ่งที่ทีมคุณต้องเข้าใจและ operate
- สุขอนามัยของ image และ supply chain image อาจพ่วง base layer ที่มีช่องโหว่และบวมขึ้นได้ คุณต้องมีวินัยในการทำ image ที่เล็ก rebuild เป็นประจำ และ scan
- isolation อ่อนกว่า VM container แชร์ host kernel ดังนั้น exploit ระดับ kernel จึงมี blast radius กว้างกว่า virtualization เต็มรูปแบบ workload ที่ไม่น่าเชื่อถือหรือความเสี่ยงสูงอาจยังต้องการขอบเขตที่แข็งแกร่งกว่า
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”- Externalized Configuration — เก็บการตั้งค่าเฉพาะสภาพแวดล้อมไว้นอก image เพื่อให้ image เดียวรันได้ทุกที่
- Service Mesh — เพิ่ม sidecar container ข้าง ๆ แต่ละ service container เพื่อ networking ที่สอดคล้องกัน
- Serverless Deployment — โมเดล runtime ทางเลือกเมื่อคุณไม่อยากรัน container ด้วยตัวเอง
| ข้อดี | ข้อแลกเปลี่ยน |
|---|---|
| isolate dependency — service A และ B ใช้ library version ต่างกันได้ | ต้องมี container orchestration (Kubernetes) จัดการ |
| deploy และ scale แต่ละ service แยก | container image ต้องดูแล — security patch, base image update |
| environment เหมือนกันตั้งแต่ dev จนถึง production | networking ระหว่าง container ซับซ้อนกว่า process ใน server เดียว |
| rollback ง่าย — revert image tag เดิม | resource overhead จาก container runtime |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”Container = Virtual Machine — ใส่หลาย process ลงใน container เดียว อาการ:
- container มี web server + worker + cron job รันพร้อมกัน
- scale ไม่ได้แยก — ต้อง scale ทุก process พร้อมกัน
- ไม่ได้ประโยชน์จาก container isolation
Running as Root — container รันด้วย root user อาการ:
- security scan fail
- ถ้า container ถูก compromise ผู้โจมตีได้ root ของ host ทันที
💡 ตัวอย่างจากของจริง
Google:
- รัน container มาตั้งแต่ปี 2003 — ก่อน Docker จะเกิด
- Borg (internal container orchestration) เป็นต้นแบบของ Kubernetes
- production ทุก service ที่ Google รันใน container
Spotify:
- ย้ายจาก bare metal servers มาใช้ Docker + Kubernetes
- deploy velocity เพิ่มขึ้นมากเพราะ environment consistency