ข้ามไปยังเนื้อหา

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
image เดียว build ครั้งเดียว ดึงจาก registry รันแบบหนึ่ง service ต่อหนึ่ง container และถูกจัดสรรไปตาม node โดย orchestrator

สูตรที่ build image ขึ้นมาคือ Dockerfile การ build แบบ multi-stage จะ compile service ใน build image ที่หนัก แล้วคัดลอกเฉพาะ binary ที่เสร็จแล้วเข้าไปใน runtime image จิ๋ว ๆ เพื่อให้ artifact ที่ส่งมอบยังคงเล็กและมีจุดให้โจมตีน้อย

# Stage 1: build
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/order-service ./cmd/order
# Stage 2: minimal runtime — one service, nothing else
FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/order-service /order-service
USER nonroot
EXPOSE 8080
ENTRYPOINT ["/order-service"]

จากนั้นประกาศ image นี้ต่อ orchestrator ในรูปของ Deployment สังเกตว่ามีการขอสาม replica ตั้ง resource limit ไว้กัน container ตัวหนึ่งแย่ง resource เพื่อนบ้าน และต่อสาย health check ที่ orchestrator ใช้ตัดสินว่า container พร้อมรับงานเมื่อไร

apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
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 จนถึง productionnetworking ระหว่าง 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
อะไรคือกฎที่นิยาม pattern Service per Container?
ทำไม container จึงเริ่มทำงานเร็วและอัดแน่นกว่า virtual machine?
orchestrator เช่น Kubernetes ให้อะไร?
ข้อใดคือต้นทุนจริงของการรับเอา container มาใช้ในระดับใหญ่?