AI-Native Solutions Architect

Deploying AI Agents: Chạy Agent trên server (Docker/K8s) thay vì cục bộ

7/19/2026 · 14p đọc


title: "Deploying AI Agents: Chạy Agent trên server (Docker/K8s) thay vì cục bộ"
series: "AI-Native Solutions Architect: Từ Coder đến Kiến trúc sư AI"
season: "Season 3 — AI Agents & Platform Engineering"
order: 25
audience: "Software Engineer hướng tới Solutions Architect"
reading_time: "13 phút"
tags: ["ai-agents", "docker", "kubernetes", "deployment", "session-state", "resource-limits", "fargate", "solutions-architect", "ai-native"]

Deploying AI Agents: Chạy Agent trên server (Docker/K8s) thay vì cục bộ

Con agent bạn build ở Bài 21-24 chạy mượt trên laptop suốt hai tuần thử nghiệm — một tiến trình Python, một terminal mở sẵn, gọi LLM API, gọi vài tool, trả kết quả. Demo cho team xong, ai cũng gật gù. Rồi có người hỏi một câu tưởng đơn giản: "Vậy giờ mình deploy cái này cho cả team dùng thế nào?" Bạn khựng lại, vì toàn bộ logic agent đang nằm trong một script chạy bằng python agent.py, phụ thuộc vào đúng phiên bản Python cài trên máy bạn, đúng bộ biến môi trường bạn export tay trong .zshrc, và đúng con terminal đang mở — tắt terminal là agent chết.

Đây không phải vấn đề của riêng AI agent — đây là vấn đề kinh điển "chạy được trên máy tôi" mà bất kỳ ai làm backend cũng từng đối mặt khi đưa một service từ máy dev lên production. Nhưng với AI agent, vấn đề này có thêm hai lớp phức tạp riêng: agent thường cần giữ trạng thái hội thoại/phiên làm việc xuyên suốt nhiều lượt gọi (khác với một REST API stateless truyền thống), và agent có thể chạy vòng lặp reasoning-action nhiều bước, nghĩa là một request có thể tốn tài nguyên và thời gian nhiều hơn hẳn một API call thông thường.

Bài này không dạy lại Docker hay Kubernetes từ đầu — giả định bạn đã quen dùng chúng cho các service backend thông thường (nếu chưa, xem lại Bài 17 về Infrastructure as Code). Trọng tâm ở đây là: điều gì khác biệt khi đối tượng đóng gói và triển khai là một AI agent, chứ không phải một CRUD service bình thường, và hai cạm bẫy hay bị bỏ sót nhất khi lần đầu đưa agent lên môi trường điều phối container.

Vấn đề

Ba khoảng cách cụ thể giữa "agent chạy tốt trên laptop" và "agent sẵn sàng phục vụ nhiều người dùng 24/7":

1. Môi trường không nhất quán. Trên máy bạn, agent phụ thuộc vào phiên bản Python cụ thể, các package đã cài qua pip install thủ công có thể không khớp phiên bản với máy đồng nghiệp, và các biến môi trường (API key, config endpoint) đang nằm rải rác trong shell profile. Khi một người khác clone code về chạy, hoặc khi deploy lên một máy chủ khác, xác suất gặp lỗi "thiếu package X" hoặc "sai phiên bản Y" là rất cao — đây chính xác là bài toán mà container hóa giải quyết: đóng gói toàn bộ runtime, dependency, và code vào một image bất biến, chạy giống hệt nhau ở mọi nơi.

2. Không có cơ chế chạy liên tục, tự phục hồi, và mở rộng. Một script Python chạy trong terminal sẽ chết khi terminal đóng, khi máy tắt, hoặc khi bản thân process crash vì một exception không bắt được. Không có gì tự động khởi động lại nó. Khi nhiều người dùng cùng gọi agent một lúc, một process đơn lẻ trên một máy không thể phục vụ nổi — cần nhiều instance chạy song song, và cần thứ gì đó điều phối traffic tới các instance đó, phát hiện instance nào chết để thay thế.

3. Trạng thái phiên làm việc (session state) không có nơi lưu trú ổn định. Agent hội thoại thường cần nhớ ngữ cảnh của những lượt trao đổi trước đó trong cùng một phiên — lịch sử message, kết quả trung gian của các bước reasoning, trạng thái của một tác vụ nhiều bước chưa hoàn tất. Trên laptop, trạng thái này nằm gọn trong bộ nhớ của một process duy nhất, tồn tại xuyên suốt vì process đó không bao giờ bị thay thế hay nhân bản. Trên một cụm điều phối container, chuyện hoàn toàn khác.

Ba khoảng cách này không thể giải quyết bằng cách "chạy script trên EC2 rồi để đó" — đó chỉ là chuyển laptop cá nhân thành một máy chủ cá nhân, vẫn thiếu tính sẵn sàng cao và khả năng mở rộng. Cần đóng gói đúng cách (container hóa) và triển khai lên nền tảng có khả năng điều phối, tự phục hồi, và scale.

Kỹ thuật cốt lõi

Container hóa: tách "cái gì chạy" khỏi "chạy ở đâu"

Docker đóng gói agent cùng toàn bộ runtime và dependency của nó thành một image — một artifact bất biến, chạy giống hệt nhau dù trên laptop, trên CI, hay trên production. Đây là bước tối thiểu bắt buộc trước khi nói tới bất kỳ hình thức điều phối nào, vì Kubernetes (hay bất kỳ nền tảng điều phối container nào) không quan tâm bạn viết code bằng gì — nó chỉ điều phối container.

Điều phối: Kubernetes hoặc container serverless

Sau khi có image, cần một nền tảng chạy nó liên tục, tự khởi động lại khi chết, và scale theo tải. Có hai hướng chính, và lựa chọn phụ thuộc vào quy mô đội ngũ vận hành — giống logic đã bàn ở Bài 11 khi so sánh EC2 Auto Scaling Group với ECS Fargate:

  • Kubernetes (tự vận hành hoặc managed như EKS/GKE): kiểm soát chi tiết nhất — tự định nghĩa scaling rule, network policy, resource quota — nhưng đòi hỏi năng lực vận hành cluster.
  • Container serverless (ECS Fargate, Cloud Run, hoặc tương đương): giao phần lớn việc quản lý hạ tầng bên dưới cho nhà cung cấp, đổi lại ít quyền kiểm soát chi tiết hơn — phù hợp đội ngũ nhỏ, không có DevOps chuyên trách, giống ràng buộc đã nêu ở Bài 11.

Nguyên tắc chọn: nếu team đã có kinh nghiệm và hạ tầng Kubernetes cho các service khác, đưa agent vào cùng cluster đó là hợp lý để tận dụng tooling đã có (monitoring, CI/CD, ingress). Nếu agent là service độc lập đầu tiên cần triển khai và team không có ai chuyên trách vận hành cluster, container serverless giảm gánh nặng vận hành đáng kể — đánh đổi là ít quyền tùy biến network/scaling hơn.

Quản lý session state: tách trạng thái ra khỏi vòng đời container

Đây là phần khác biệt rõ nhất so với deploy một REST API stateless thông thường. Sơ đồ dưới minh họa vì sao không thể giữ session trong bộ nhớ của container:

flowchart TD
    U[User Request - session_id: abc123] --> LB[Load Balancer / Ingress]
    LB --> P1[Agent Pod 1]
    LB --> P2[Agent Pod 2]
    LB --> P3[Agent Pod 3 - mới được tạo do scale-out]

    P1 -.->|"Nếu lưu state trong RAM<br/>của Pod 1: request sau<br/>của session abc123 có thể<br/>rơi vào Pod 2 hoặc Pod 3<br/>→ mất ngữ cảnh"| X["❌ State thất lạc"]

    P1 --> ES[(External State Store<br/>Redis / DynamoDB)]
    P2 --> ES
    P3 --> ES
    ES -->|"session abc123:<br/>history, context,<br/>trạng thái tác vụ"| P1

    style ES fill:#5a5,stroke:#333,stroke-width:1px
    style X fill:#c55,stroke:#333,stroke-width:1px

Với nhiều replica chạy song song và load balancer phân phối request theo round-robin (không đảm bảo cùng một session luôn rơi vào cùng một pod), lưu session trong bộ nhớ của một container cụ thể là một giả định sai. Container đó có thể bị Kubernetes xóa và tạo lại bất kỳ lúc nào — khi deploy version mới, khi node gặp sự cố, khi scale-down do tải giảm. Giải pháp bắt buộc: lưu session state ra một external state store (Redis cho state ngắn hạn/tốc độ cao, hoặc DynamoDB/PostgreSQL cho state cần bền vững hơn) — mọi pod đọc/ghi vào cùng một nơi, và bản thân container trở thành stateless theo đúng nghĩa, có thể bị xóa và thay thế bất kỳ lúc nào mà không mất dữ liệu phiên.

Thực hành

Dockerfile tối giản cho một agent service viết bằng Python

Giả sử agent được expose qua một FastAPI service (nhận request, gọi LLM, gọi tool, trả kết quả):

# Dockerfile
FROM python:3.12-slim AS base

# Không chạy container bằng root — nguyên tắc bảo mật cơ bản
RUN useradd --create-home --shell /bin/bash agent-user

WORKDIR /app

# Copy riêng requirements trước để tận dụng Docker layer cache —
# chỉ rebuild lại bước cài dependency khi requirements.txt đổi
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy code sau cùng — code đổi thường xuyên hơn dependency
COPY ./src ./src

# Biến môi trường mặc định — giá trị thật inject qua ConfigMap/Secret ở K8s,
# KHÔNG hardcode API key vào image
ENV PYTHONUNBUFFERED=1 \
    PORT=8080

USER agent-user

EXPOSE 8080

# Healthcheck để Kubernetes/orchestrator biết container còn "sống" đúng nghĩa,
# không chỉ process còn chạy mà service thực sự trả lời được request.
# Dùng Python thay vì curl vì base image "slim" không cài sẵn curl —
# tránh phải thêm một layer apt-get chỉ để có healthcheck.
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8080/health', timeout=3).status == 200 else 1)" || exit 1

# uvicorn chạy FastAPI app — số worker giữ ở 1 vì scale ngang
# nên thực hiện bằng cách tăng số replica (pod), không phải tăng
# worker trong một container — dễ quản lý resource limit hơn
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8080", "--workers", "1"]

requirements.txt tối thiểu cho một agent service dạng này:

fastapi==0.115.0
uvicorn[standard]==0.32.0
redis==5.2.0
anthropic==0.40.0
pydantic==2.9.0

Build và chạy thử cục bộ trước khi đẩy lên registry:

docker build -t agent-service:0.1.0 .
docker run --rm -p 8080:8080 \
  --env ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
  --env REDIS_URL=redis://host.docker.internal:6379 \
  --cpus="1.0" --memory="512m" \
  agent-service:0.1.0

Lưu ý cờ --cpus--memory khi test cục bộ — đây chính là bước tập dượt cho resource limit sẽ khai báo ở Kubernetes bên dưới, giúp phát hiện sớm nếu agent cần nhiều tài nguyên hơn dự kiến trước khi lên production.

Kubernetes Deployment + Service (rút gọn)

# agent-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: agent-service
  labels:
    app: agent-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: agent-service
  template:
    metadata:
      labels:
        app: agent-service
    spec:
      containers:
        - name: agent-service
          image: your-registry/agent-service:0.1.0
          ports:
            - containerPort: 8080
          env:
            - name: REDIS_URL
              value: "redis://redis-service:6379"
          envFrom:
            - secretRef:
                name: agent-secrets   # chứa ANTHROPIC_API_KEY, không hardcode trong YAML
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "1000m"
              memory: "512Mi"
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 15
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: agent-service
spec:
  selector:
    app: agent-service
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: agent-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: agent-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Ba điểm đáng chú ý trong file trên, đúng trọng tâm của bài này:

  • resources.requestsresources.limits là bắt buộc, không phải tùy chọn — đây chính là cơ chế chặn một agent chạy vòng lặp lỗi khỏi việc chiếm hết tài nguyên node, chi tiết ở phần Cạm bẫy bên dưới.
  • livenessProbe/readinessProbe cho Kubernetes biết khi nào container còn sống và khi nào sẵn sàng nhận traffic — quan trọng hơn bình thường với agent, vì một tiến trình gọi LLM có thể "treo" (process còn chạy nhưng không phản hồi được nữa do một call ra ngoài bị timeout) mà không tự crash để Kubernetes phát hiện qua cơ chế restart mặc định.
  • envFrom.secretRef đưa API key vào qua Kubernetes Secret, không hardcode trong image hay trong YAML thường — image Docker build ra có thể bị đẩy nhầm lên registry công khai, và một API key lộ trong đó là sự cố bảo mật thực sự đã xảy ra nhiều lần trong ngành.

Prompt mẫu: nhờ AI review Dockerfile/K8s manifest trước khi deploy

Vai trò: Bạn là Solutions Architect có kinh nghiệm vận hành container trên Kubernetes.

Tôi có Dockerfile và Kubernetes Deployment cho một AI agent service (dán nội dung
Dockerfile và agent-deployment.yaml ở trên vào đây).

Agent này: nhận request qua HTTP, gọi LLM API (Anthropic), có thể gọi thêm 2-3 tool
ngoài (web search, database query), và cần giữ session state của một cuộc hội thoại
qua nhiều lượt gọi (session timeout 30 phút không hoạt động).

Yêu cầu review, trả lời theo đúng cấu trúc:

1. RESOURCE LIMITS: Với đặc điểm agent gọi LLM (I/O-bound, có thể có vòng lặp
   reasoning-action nhiều bước), giá trị cpu/memory requests-limits tôi đặt
   (250m/1000m CPU, 256Mi/512Mi memory) có hợp lý không? Rủi ro cụ thể nếu đặt
   quá thấp hoặc quá cao là gì?

2. SESSION STATE: Tôi định lưu session state vào Redis bên ngoài pod, key theo
   session_id, TTL 30 phút. Cách này có đủ để chịu được việc pod bị restart/scale
   giữa chừng một phiên hội thoại không? Có edge case nào tôi đang bỏ sót (ví dụ
   race condition khi 2 request cùng session_id tới gần như đồng thời nhưng
   route vào 2 pod khác nhau)?

3. HEALTH CHECK: Với agent có thể "treo" khi một lời gọi ra ngoài (LLM API,
   tool call) bị timeout mà không throw exception rõ ràng, thiết kế /health và
   /ready endpoint nên kiểm tra những gì để phản ánh đúng tình trạng, tránh
   Kubernetes tưởng pod còn khỏe trong khi nó thực chất đã treo?

4. FAILURE MODE: Liệt kê 2-3 kịch bản lỗi đặc thù của AI agent (khác với một
   REST API CRUD thông thường) mà cấu hình hiện tại của tôi chưa xử lý.

Không cần review lại toàn bộ YAML dòng theo dòng — tập trung vào 4 điểm trên.

Cạm bẫy thường gặp

1. Không đặt resource limit, để một agent chạy vòng lặp lỗi chiếm hết tài nguyên cụm. Đây là cạm bẫy đặc thù của AI agent hơn hẳn các service thông thường: một agent có bug logic khiến nó lặp lại bước reasoning-action liên tục (ví dụ tool trả về lỗi, agent cố gọi lại tool đó vô hạn lần thay vì dừng và báo lỗi) sẽ liên tục gọi LLM API và tiêu tốn CPU xử lý response, mỗi vòng lặp đều tốn tài nguyên thật. Nếu container không có resources.limits, Kubernetes cho phép nó dùng tài nguyên không giới hạn trên node — một pod lỗi có thể khiến các pod khác trên cùng node bị đói tài nguyên (CPU throttling, OOM kill lây lan), sập cả cụm chứ không chỉ sập một agent. Luôn đặt limits rõ ràng, và cân nhắc thêm giới hạn ở tầng logic (ví dụ max số vòng lặp reasoning cho mỗi request, timeout tổng cho một tác vụ) chứ không chỉ dựa vào giới hạn hạ tầng.

2. Lưu session state trong bộ nhớ container, tưởng rằng "pod sẽ không bị restart giữa chừng". Giả định này đúng trên laptop (một process, không ai đụng vào) nhưng sai hoàn toàn trên Kubernetes — pod bị xóa và tạo lại là chuyện bình thường: rolling update khi deploy version mới, node bị drain để bảo trì, Horizontal Pod Autoscaler scale-down khi tải giảm, hay đơn giản là pod bị OOM kill vì cạm bẫy số 1 ở trên. Nếu session (lịch sử hội thoại, trạng thái tác vụ nhiều bước chưa hoàn tất) chỉ nằm trong RAM của một pod cụ thể, người dùng sẽ đột ngột mất toàn bộ ngữ cảnh giữa chừng cuộc trò chuyện mà không có cảnh báo nào — trải nghiệm tệ hơn nhiều so với một lỗi 500 rõ ràng. Bắt buộc đẩy session ra external state store (Redis/DynamoDB) ngay từ thiết kế đầu tiên, không phải "để sau khi có vấn đề mới sửa".

3. Bỏ qua sự khác biệt giữa "process còn chạy" và "agent còn phản hồi được". Một health check mặc định kiểu chỉ kiểm tra "process có đang chạy không" dễ báo cáo sai với agent — vì tiến trình Python vẫn chạy bình thường ngay cả khi nó đang treo chờ một lời gọi LLM API timeout kéo dài, hoặc một tool call ra service bên thứ ba bị treo vô thời hạn vì thiếu timeout ở tầng code. Kubernetes sẽ tiếp tục route traffic vào pod "trông có vẻ khỏe" đó, khiến người dùng chờ vô ích. Cần thiết kế /health//ready endpoint kiểm tra thực chất khả năng phản hồi (ví dụ ping tới dependency quan trọng, hoặc theo dõi thời gian xử lý request gần nhất), đồng thời đặt timeout rõ ràng ở tầng code cho mọi lời gọi ra ngoài — đừng để một dependency bên ngoài treo vô hạn kéo cả agent theo.

🧭 Góc nhìn Solutions Architect
Nếu pod đang chạy agent bị Kubernetes xóa đột ngột ngay giữa một tác vụ nhiều bước (ví dụ agent đang ở bước 3/5 của một quy trình), thiết kế hiện tại của tôi có cho phép tác vụ đó tiếp tục đúng chỗ ở một pod khác, hay người dùng phải bắt đầu lại từ đầu? Resource limit tôi đặt cho agent có dựa trên số liệu đo thực tế (P95 CPU/memory khi agent chạy một tác vụ điển hình), hay chỉ là con số đoán theo cảm tính rồi hy vọng đủ?

🔗 Bài viết liên quan

  • [Bài 21] Introduction to AI Agents — nền tảng khái niệm agent trước khi bàn tới triển khai production (ans-21-introduction-to-ai-agents.md)
  • [Bài 17] Infrastructure as Code (IaC) — cách quản lý cấu hình Kubernetes/Terraform cho hạ tầng agent một cách review được, rollback được (ans-17-infrastructure-as-code-terraform-cdk.md)

Bài trước: Multi-Agent Systems · Bài tiếp theo: Evaluation & Monitoring

Deploying AI Agents: Chạy Agent trên server (Docker/K8s) thay vì cục bộ