
에이전트 배포: 컨테이너, 오케스트레이션(Orchestration), 그리고 루프의 확장(Scaling)
요약
AI 에이전트를 실제 프로덕션 환경에 배포할 때 발생하는 인프라 및 확장성 문제를 다룹니다. 에이전트의 실행 시간에 따른 적절한 배포 패턴(HTTP, WebSocket, Queue/Worker)과 패키징 전략을 제시합니다.
핵심 포인트
- 에이전트 배포 형태는 평균 응답 시간이 아닌 가장 긴 단계에 맞춰 결정해야 함
- 실행 시간에 따라 Stateless HTTP, WebSocket/SSE, Queue/Worker 패턴을 구분하여 적용
- HTTP 요청을 장시간 유지하지 말고 비동기 워크플로우를 활용할 것
- 보안과 안정성을 위해 Python 버전 고정 및 Non-root 사용자 실행 권장
- 도서: Agents in Production — Building, Tracing, and Shipping Multi-Step AI You Can Trust
- 저자의 다른 저서: Observability for LLM Applications — The AI Engineer's Library (2권 시리즈)의 동반 도서
- 내 프로젝트: Hermes IDE | GitHub — Claude Code 및 기타 AI 코딩 도구를 사용하여 작업하는 개발자를 위한 IDE
- 자기소개: xgabriel.com | GitHub
에이전트가 당신의 노트북에서는 잘 작동합니다. 평가(evals)도 통과합니다. 매니저가 언제 출시되냐고 물으면, 모델링이 끝났으니 월요일이라고 대답합니다. 그러고 나서 로드 밸런서(load balancer) 뒤에 배치하려고 시도하면, 마치 웹 서비스(web service)처럼 배포했기 때문에 시스템이 무너져 버립니다.
에이전트는 웹 서비스가 아닙니다. 웹 서비스는 밀리초(milliseconds) 단위로 응답하고 잊어버립니다. 에이전트는 몇 분 동안 생각하고, 두세 개의 제공업체(providers)에 걸쳐 토큰(tokens)을 소모하며, 브라우저로 부분적인 출력을 스트리밍(streaming)하고, 때로는 여덟 번째 턴에서 delete_invoice를 호출하기로 결정하기도 합니다. 당신이 내리는 모든 배포 결정은 하나의 질문에서 비롯됩니다: 이 장치가 실행되는 동안 당신의 인프라(infrastructure)에 어떤 영향을 미치는가?
여기 에이전트를 패키징하는 방법, 상태(state)를 어디에 유지할지, 그리고 병목 현상(bottleneck)이 당신이 제어할 수 없는 모델 호출(model call)인 워크로드(workload)를 어떻게 확장(scale)할지에 대한 방법이 있습니다.
형태는 가장 긴 단계에 의해 결정됩니다
당신의 고통을 가장 많이 줄여줄 단 하나의 규칙: 에이전트의 배포 형태는 평균 단계가 아니라, 가장 긴 단계에 의해 결정됩니다.
고객 지원 챗봇은 2초 안에 응답합니다. 코드 리뷰 에이전트는 6분 동안 생각합니다. 리서치 에이전트는 40분 동안 실행됩니다. 이 세 가지를 모두 동일한 HTTP 엔드포인트(endpoint) 뒤에 두고 그중 어느 하나라도 살아남기를 기대할 수는 없습니다. 가장 긴 단계에 맞는 패턴을 선택한 다음, 나머지는 타임아웃(timeouts)으로 제한하십시오.
- 30초 미만 → 상태가 없는 (stateless) HTTP 엔드포인트 (Cloud Run, Fly.io).
- 사용자가 지켜보고 있는 상태에서 30초~5분 → WebSocket 또는 SSE를 통한 스트리밍.
- 5분~1시간, 비동기 (async) → 큐(queue)와 워커(worker) 조합 (Temporal, Inngest, 또는 Redis).
- 1시간 이상 → 선택의 여지 없이 여전히 큐와 워커 조합.
HTTP 요청을 40분 동안 열어두지 마십시오. 당신이 존재조차 몰랐던 무언가(프록시, CDN, 로드 밸런서의 유휴 타임아웃 등)가 최악의 순간에 그 연결을 끊어버릴 것입니다.
패키징하기: 모든 것을 고정하고, 루트(root) 권한을 버려라
베이스 이미지(base image)는 모든 패턴에서 동일합니다. Python 버전을 고정하고, SDK를 고정하며, 루트가 아닌 사용자(non-root user)로 실행하고, 필요하지 않은 것은 아무것도 설치하지 마십시오.
# Dockerfile
FROM python:3.13-slim-bookworm AS builder
ENV PIP_NO_CACHE_DIR=1
...
두 가지가 제 역할을 다합니다. 멀티 스테이지 빌드(multi-stage build)는 첫 번째 스테이지에서 휠(wheels)을 컴파일하고 두 번째 스테이지로 런타임(runtime)만 복사하므로, 빌드 도구 체인(build toolchain)이 프로덕션 환경으로 넘어가지 않습니다. 그리고 slim-bookworm은 기본 이미지의 1.1GB에 비해 130MB에 불과합니다. 대략적인 추정치로, 부하가 걸려 스케일 업(scale up)할 때 이 더 작은 이미지를 가져오는 것이 콜드 파드 시작(cold pod start) 시간을 몇 초 단축해 줍니다.
API 키를 이미지에 절대 구워 넣지 마십시오. 런타임에서 주입해야 합니다. Kubernetes에서는 마운트된 Secret이며, GCP에서는 Workload Identity를 사용하는 Secret Manager이고, Fly에서는 fly secrets set입니다. 이미지에는 자격 증명(credentials)이 없어야 하며, 에이전트는 부팅 시 이를 읽고 절대 로그에 남기지 않아야 합니다.
requirements.txt에 정확한 버전을 고정하십시오. SDK의 필드 이름은 변할 수 있으며, 태그가 유동적이라면 2월에 작동하던 빌드가 7월에는 깨질 수 있습니다:
anthropic==0.94.1
langgraph==1.1.6
litellm==1.75.1
...
상태가 없는 방식 우선: 상태는 프로세스가 아닌 Redis에 유지하라
그렇지 않을 구체적인 이유가 없다면 상태가 없는(stateless) 방식을 택하십시오. 각 요청은 새로운 에이전트를 시작합니다. 대화 기록, 도구(tool) 결과, 스크래치패드(scratchpad) 등 모든 것은 클라이언트가 전송하는 세션 ID를 키로 하여 Redis 또는 Postgres에 저장됩니다. 프로세스는 요청 간에 아무것도 보유하지 않으므로, 어떤 파드(pod)라도 어떤 요청이든 처리할 수 있으며 롤링 배포(rolling deploy) 시에도 메모리를 잃지 않습니다.
상태(state)는 서버가 아니라 세션입니다. 이것이 핵심적인 요령입니다. 대신 내구성이 있고 오래 실행되는 에이전트(실행 도중 워커의 재시작에서도 살아남아야 하는 에이전트)가 필요한 경우에는 큐(queue)와 워크플로 엔진(workflow engine)으로 이동해야 하며, 상태는 Redis가 아닌 워크플로 저장소(workflow store)에 저장되어야 합니다. 프로세스 메모리(process memory)에 상태를 유지함으로써 내구성(durability)을 흉내 내지 마십시오.
상태 확인(Health checks): 저렴한 활성 상태 확인(liveness), 비용이 드는 준비 상태 확인(readiness)
Kubernetes 프로브(probes)는 에이전트 배포가 조용히 실패하는 지점입니다. 왜냐하면 기본 설정은 서비스가 매우 빠르다고 가정하기 때문입니다.
두 프로브를 분리하십시오. /healthz는 저렴합니다: 프로세스가 시작되었는지, 이벤트 루프(event loop)가 살아있는지를 확인합니다. /readyz는 비용이 많이 듭니다: 에이전트가 실제로 프로바이더(provider)에 도달할 수 있는지를 확인합니다. 잘못된 API 키로 부팅된 파드(pod)는 절대 트래픽을 받아서는 안 됩니다.
# app/probes.py
from fastapi import APIRouter, Response
from anthropic import AsyncAnthropic
...
활성 상태 확인(Liveness)은 거의 항상 파드를 종료해서는 안 됩니다. 정당한 긴 에이전트 턴(agent turn)은 이벤트 루프가 멈춘 것처럼 보이게 할 수 있으며, '3번 실패 후 종료'라는 표준 권장 사항은 정상적으로 작동 중이던 실행(run)을 죽여버릴 것입니다. 활성 상태 확인(liveness)은 넉넉한 실패 임계값(failure threshold)과 함께 /healthz를 가리키도록 설정하고, terminationGracePeriodSeconds를 최악의 경우의 턴 시간보다 크게 설정하여 롤링 배포(rolling deploy) 시 진행 중인 실행이 끊기지 않고 완료될 수 있도록 하십시오.
확장(Scaling): 병목 지점은 당신이 소유하지 않은 잠금(lock)입니다
이것이 에이전트를 일반적인 백엔드와 다르게 만드는 점입니다. 워커(worker)를 추가한다고 해서 특정 지점 이상의 처리량(throughput)이 늘어나지는 않습니다. 모든 프로바이더는 키(key)당 분당 요청 수(RPM)와 분당 토큰 수(TPM)에 제한을 두기 때문입니다. 풀(pool)을 10개에서 100개로 확장해도 그 제한은 변하지 않습니다. 단지 더 많은 429(Too Many Requests) 오류를 생성할 뿐입니다.
따라서 가장 먼저 연결해야 할 것은 오토스케일링(autoscaling)이 아닙니다. 실제 예산에 맞춘 (provider, model) 쌍당 세마포어(semaphore)를 구현하고, 여기에 통과하는 노이즈를 처리하기 위한 지수 백오프(backoff) 및 지터(jitter)를 포함한 재시도(retry) 로직을 추가하는 것입니다.
import asyncio
from tenacity import (
retry, stop_after_attempt,
...
워크로드는 I/O에서 블로킹(blocking)됩니다. 모델 호출은 네트워크 대기(network wait)이므로, 런타임(runtime) 수준에서의 동시성(concurrency)은 거의 비용이 들지 않지만, 제공자(provider) 수준에서는 비용이 많이 듭니다. 이러한 역전 현상이 확장(scaling)에 관한 이야기의 핵심입니다. 수백 개의 코루틴(coroutines)이 Claude를 기다리는 동안 당신의 CPU는 유휴(idle) 상태로 있게 됩니다. 따라서 CPU만으로 오토스케일링(autoscale)을 하지 마세요. 시스템이 완전히 포화된 상태에서도 CPU 사용량은 거의 0에 가깝게 읽힐 것입니다. 대신 진행 중인 요청 수(in-flight request count)를 기준으로 확장하고, 포드(pod)당 진행 중인 실행 수를 제한하세요 (30초 주기의 1-CPU 포드라면 4개가 합리적인 시작점입니다). 그래야 단일 서버가 수천 개의 동시 호출을 퍼뜨려 전체 속도 제한(rate limit)을 다 써버리는 일을 방지할 수 있습니다.
TypeScript 에이전트의 경우, 동일한 게이트(gate) 역할을 하는 것은 호출 주변에 작은 카운팅 세마포어(counting semaphore)를 두는 것입니다:
// gate.ts — 동시 모델 호출 제한
let active = 0;
const queue: Array<() => void> = [];
...
풀(pool)이 포화되면 큐(queue)가 늘어납니다. 그것이 당신의 신호입니다. 에이전트가 제공자(provider)를 상대로 허우적거리게 두지 말고, 인그레스(ingress) 단계에서 부하를 차단하세요 (429 에러를 반환하거나 작업을 내구성이 있는 큐(durable queue)에 보관하십시오). 작업을 보유하는 큐는 제공자 장애(provider outage) 상황에서도 버틸 수 있게 해줍니다. 즉, 실행이 실패하는 대신 모델이 복구될 때까지 기다릴 수 있게 됩니다.
제공자에게 직접 연결하지 말고 게이트웨이(gateway)를 통해 라우팅하세요
에이전트를 제공자 SDK에 직접 연결하지 말고, 폴백(fallback)을 관리하는 하나의 게이트웨이로 지정하세요. Claude Sonnet이 속도 제한(rate-limit)에 걸리면, 게이트웨이는 체인의 다음 모델로 재시도하며 에이전트 코드는 이를 전혀 인지하지 못합니다. LiteLLM Proxy는 셀프 호스팅(self-hosted) 방식의 기본 옵션이며, OpenRouter와 Portkey는 관리형(managed) 옵션입니다.
주의할 점: 한 번도 실행해 보지 않은 폴백 체인은 없는 것이나 마찬가지입니다. 페일오버(failover) 대상은 기본 모델과 동일한 속도 제한 여유분(headroom)을 가지고 있어야 하며, 모델이 달라지면 토크나이저(tokenizer)와 도구 호출(tool-call) 스키마도 달라집니다. 고정된 평가 세트(eval set)를 사용하여 체인을 테스트하고, 별도의 점수로 보고하며, 이를 반복적으로 훈련시키세요. 한 달에 한 번은 트래픽의 10%를 강제로 폴백으로 통과시켜 보십시오. 조용한 화요일에 10%를 감당하지 못한다면, 새벽 3시에는 100%를 감당하지 못할 것입니다.
배포는 쉬운 부분입니다
다섯 가지 패턴, Dockerfile, 세마포어(semaphore), 그리고 폴백 체인(fallback chain)을 갖추면 월요일에 출시하여 화요일에 무너지지 않는 무언가를 얻을 수 있습니다. 컨테이너가 실행되고, 게이트웨이(gateway)가 장애를 우회하며, 큐(queue)가 다른 수단이 없을 때 작업을 보관합니다. 그 부분은 기계적인 영역입니다.
어려운 부분은 새벽 3시에 에이전트(agent)가 아무도 묻지 않은 질문에 확신에 찬 답변을 내놓기 시작하고, 끝나지 않는 루프(loop)에서 토큰(token)을 태우기 시작할 때 발생하는 일입니다. 이것이 바로 배포가 트레이싱(tracing)과 평가(evals) 위에서 이루어져야만 가치가 있는 이유입니다. _Agents in Production_은 다섯 가지 패턴, 확장 한계(scaling limits), 그리고 성능 저하 단계(degradation ladder)를 처음부터 끝까지 다룹니다. _The AI Engineer's Library_의 동반서인 _Observability for LLM Applications_는 사용자가 문제를 인지하기 전에 루프에 문제가 생겼음을 알려주는 트레이싱, 평가, 그리고 비용 회계(cost-accounting) 계층을 다룹니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기