Docker Swarm nâng cao: Rolling Update, Placement Constraints và Deploy không Downtime

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

Lần đầu dùng Docker Compose cho dự án thực tế, mình mắc khá nhiều lỗi cơ bản mà bây giờ nghĩ lại thấy buồn cười — deploy xong thì site down cả tiếng đồng hồ vì quên drain traffic trước khi restart container. Team hỏi “sao down vậy?” mà mình không biết giải thích. Đó là điểm khởi đầu để mình đào sâu vào Docker Swarm nâng cao: Rolling Update có kiểm soát, Placement Constraints, và Docker Config.

Bài này dành cho bạn đã biết Swarm cơ bản và muốn cấu hình production thật sự — không phải demo lab, mà là hệ thống chạy 24/7 có user thật.

Bối cảnh: Tại sao cần thêm lớp cấu hình này?

Swarm mặc định đã khá ổn, nhưng khi đưa lên production bạn sẽ gặp ngay mấy vấn đề hay gặp:

  • Rolling update mặc định gây downtime: Swarm stop replica cũ trước khi start replica mới, tạo ra khoảng hở thời gian không có instance nào phục vụ — user thấy 502.
  • Container phân bổ không kiểm soát: Database nặng có thể bị schedule lên node RAM thấp, toàn bộ API replica có thể dồn vào 1 node rồi node đó down là xong.
  • Config và secret trong environment variable: Dễ bị leak qua docker inspect, log aggregation, hay ps aux trên host.
  • Không có rollback tự động: Deploy xong mới phát hiện lỗi, phải xử lý thủ công trong khi user đang thấy lỗi thật.

Ba tính năng — Placement Constraints, Docker Config/Secret, và Rolling Update với order: start-first — giải quyết đúng ba vấn đề trên.

Chuẩn bị: Gán label cho nodes

Label là nền tảng của Placement Constraints. Trước khi viết stack file, cần gán label cho từng node theo vai trò và đặc điểm phần cứng. Đây là bước nhiều tutorial bỏ qua, khiến người đọc loay hoay không hiểu tại sao constraint không hoạt động:

# Gán role label cho worker nodes
docker node update --label-add role=worker node-1
docker node update --label-add role=worker node-2

# Gán storage type — quan trọng cho database
docker node update --label-add storage=ssd node-1
docker node update --label-add storage=hdd node-2

# Gán availability zone nếu cluster multi-region
docker node update --label-add zone=az-1 node-1
docker node update --label-add zone=az-2 node-2

# Verify label đã được gán đúng
docker node inspect node-1 --format '{{json .Spec.Labels}}'
docker node ls --format 'table {{.Hostname}}\t{{.Status}}\t{{.ManagerStatus}}'

Sau khi gán label, kiểm tra lại bằng docker node ls -q | xargs docker node inspect --format '{{.Description.Hostname}}: {{.Spec.Labels}}' để chắc chắn tất cả nodes đều có label trước khi deploy stack.

Cấu hình chi tiết

Docker Config và Secret — quản lý cấu hình đúng cách

Thay vì truyền config qua environment variable, Docker Config lưu file cấu hình tĩnh (nginx.conf, app.yaml…) và Docker Secret lưu thông tin nhạy cảm (password, API key). Cả hai đều được mã hóa khi lưu trữ và chỉ decrypt trong RAM của container đang chạy:

# Tạo config từ file
docker config create nginx_conf ./nginx.conf
docker config create app_settings ./app.yaml

# Tạo secret từ file (khuyến nghị hơn stdin để tránh lưu vào shell history)
docker secret create db_password ./db_password.txt
docker secret create jwt_secret ./jwt_secret.txt

# Verify
docker config ls
docker secret ls

Khai báo và mount vào container trong stack file:

configs:
  nginx_conf:
    external: true
  app_settings:
    external: true

secrets:
  db_password:
    external: true

services:
  nginx:
    image: nginx:1.25-alpine
    configs:
      - source: nginx_conf
        target: /etc/nginx/nginx.conf
        mode: 0440         # Read-only cho owner và group, không cho others

  api:
    image: myapp/api:latest
    configs:
      - source: app_settings
        target: /app/config/settings.yaml
    secrets:
      - source: db_password
        target: db_password
        mode: 0400         # Chỉ owner đọc được — secret nên strict hơn config

Secret được mount tại /run/secrets/<secret_name> trong container. App đọc file này thay vì đọc environment variable — cách này an toàn hơn hẳn vì secret không xuất hiện trong environment của process, không bị lộ qua docker inspect.

Placement Constraints — workload phân bổ đúng node

Với label đã gán ở trên, giờ có thể kiểm soát chính xác container nào chạy trên node nào. constraints là hard rule (phải thỏa mãn), còn preferences là soft rule (cố gắng thỏa mãn khi có thể):

services:
  api:
    deploy:
      replicas: 4
      placement:
        constraints:
          - node.role == worker          # API không chạy trên manager node
          - node.labels.role == worker   # Double check qua custom label
        preferences:
          - spread: node.labels.zone     # Phân tán đều ra các AZ, không dồn vào 1 chỗ

  database:
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.labels.storage == ssd   # DB chỉ chạy trên node có SSD
          - node.role == worker          # Không chạy trên manager

  redis:
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.labels.zone == az-1     # Pin Redis vào zone cụ thể nếu cần

Rolling Update không downtime — cấu hình chi tiết

Phần này hay bị cấu hình sai nhất. Tham số mấu chốt là order: start-first — Swarm start replica mới, đợi healthcheck pass, rồi mới stop replica cũ. Ngược với mặc định stop-first gây downtime.

Nhưng order: start-first chỉ hoạt động đúng khi kết hợp với healthcheck được cấu hình hợp lý:

services:
  api:
    image: myapp/api:${VERSION:-latest}
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s    # Cho app 30s để startup trước khi bắt đầu đánh giá health
    deploy:
      replicas: 4
      update_config:
        parallelism: 1            # Update 1 replica mỗi lần — conservative nhưng an toàn
        delay: 15s                # Đợi 15s giữa các batch update
        order: start-first        # START replica mới TRƯỚC, STOP replica cũ SAU
        failure_action: rollback  # Tự động rollback toàn bộ nếu update fail
        monitor: 30s              # Monitor 30s sau mỗi update để phát hiện lỗi delayed
        max_failure_ratio: 0.3    # Cho phép tối đa 30% replica fail trước khi trigger rollback
      rollback_config:
        parallelism: 0            # 0 = rollback tất cả replicas đồng thời
        delay: 0s                 # Không delay khi rollback — cần nhanh
        failure_action: continue  # Tiếp tục rollback dù gặp lỗi
        order: stop-first         # Khi rollback: stop phiên bản mới trước, restore cũ sau

start_period trong healthcheck là thứ mình mất nhiều thời gian nhất để tune đúng. Nếu app cần 20 giây để connect database, load config và warmup cache — set start_period: 25s để có buffer. Thiếu thông số này, Swarm đánh giá container fail ngay lúc startup — container cứ restart mãi và rolling update không bao giờ hoàn thành.

Kiểm tra và Monitoring

Deploy stack và theo dõi rolling update

# Deploy stack lần đầu
docker stack deploy -c docker-stack.yml myapp

# Xem tất cả services trong stack
docker stack services myapp

# Update API lên version mới — rolling update tự động chạy
VERSION=v2.1.0 docker stack deploy -c docker-stack.yml myapp

# Theo dõi quá trình rolling update real-time
# Quan sát: replica mới Running TRƯỚC khi replica cũ Shutdown
watch -n2 'docker service ps myapp_api --format "table {{.Name}}\t{{.Node}}\t{{.CurrentState}}\t{{.DesiredState}}"'

Khi update diễn ra đúng, bạn sẽ thấy thời điểm có replicas+1 tasks đang tồn tại: replica mới ở trạng thái Running trong khi replica cũ chưa chuyển sang Shutdown. Đó là bằng chứng zero-downtime đang hoạt động.

Rollback và kiểm tra placement

# Rollback service về version trước (nếu cần can thiệp thủ công)
docker service rollback myapp_api

# Verify database đang chạy đúng trên SSD node
docker service ps myapp_database --format 'table {{.Name}}\t{{.Node}}\t{{.CurrentState}}'

# Xem distribution của API replicas qua các nodes
docker service ps myapp_api --filter 'desired-state=running'

# Aggregate logs từ tất cả replicas của service
docker service logs -f --tail 100 myapp_api

Giám sát resource usage

# Resource usage của tất cả containers trong service
docker stats $(docker ps --filter 'name=myapp_api' -q)

# Xem resource limits đã cấu hình
docker service inspect myapp_api --pretty | grep -A 8 Resources

# Health status của tất cả tasks
docker service ps myapp_api --format 'table {{.Name}}\t{{.Node}}\t{{.CurrentState}}\t{{.Error}}'

Sau khi thiết lập xong, thứ mình hay làm là chạy một “dry” rolling update: tag lại image cùng code với version number mới, trigger update, rồi ngồi xem watch docker service ps. Nếu replica mới sang Running trước khi replica cũ sang Shutdown — zero-downtime deployment đang chạy đúng.

Ba thứ này — Placement Constraints để workload đúng chỗ, Docker Config/Secret để bảo mật cấu hình, Rolling Update với order: start-first để deploy không gián đoạn — là những gì cần có từ ngày đầu khi đưa Swarm lên production. Không phức tạp, nhưng thiếu một trong ba là kiểu gì cũng gặp incident. Mình học bài này theo cách khó hơn.

Share: