에이전트는 API 서버가 아니다
앞 장까지 우리는 LLM 추론 서버를 어떻게 배포하고, 어떻게 오토스케일링하며, 어떻게 비용을 줄일지 살펴보았다. 그 관점에서 보면 LLM은 비교적 단순한 서버처럼 보인다. 사용자가 프롬프트를 보낸다. 모델 서버가 토큰을 생성한다. 응답을 돌려준다. 요청은 끝난다.
하지만 AI 에이전트는 여기서 한 단계 더 나아간다. 에이전트는 단순히 "답변을 생성하는 함수"가 아니라 "목표를 달성하기 위해 생각하고, 도구를 호출하고, 결과를 확인하고, 다시 계획하는 실행 주체"에 가깝다. 사용자가 "지난달 매출 데이터를 분석해서 이상치를 찾고, 그래프로 정리한 다음, 슬랙에 보고해 줘"라고 요청했다고 해보자. 일반 LLM 호출이라면 이 문장을 보고 그럴듯한 분석 방법을 설명하는 데서 끝날 수 있다. 에이전트는 다르다. 데이터베이스에 접속하고, 쿼리를 실행하고, 결과를 파일로 저장하고, 파이썬 코드를 만들어 그래프를 그리고, 슬랙 API를 호출할 수 있다.
이 차이가 쿠버네티스 운영 관점에서는 매우 크다. 웹 API 서버는 보통 요청 하나가 짧고, 상태를 외부 데이터베이스에 맡기며, 같은 Pod를 여러 개 띄워 수평 확장한다. 반면 에이전트는 하나의 작업이 몇 초가 아니라 몇 분, 길게는 몇 시간 이어질 수 있다. 중간에 LLM을 여러 번 호출하고, 외부 도구를 실행하고, 사람이 승인할 때까지 멈춰 있어야 할 수도 있다. Kubernetes SIG Apps에서 진행 중인 Agent Sandbox 프로젝트도 이런 변화를 "짧은 stateless 함수 호출에서 장시간 실행되는 상태ful singleton 워크로드로의 이동"으로 설명한다1.
따라서 에이전트를 일반 API 서버처럼 Deployment replicas: 3으로 띄우고 Service 뒤에 붙이는 것만으로는 부족하다. 처음에는 동작한다. 데모도 잘 된다. 문제는 프로덕션에서 시작된다. 사용자가 늘어나고, 작업 시간이 길어지고, 도구 호출이 실패하고, 재시도가 겹치고, 한 에이전트가 메모리를 과도하게 사용하기 시작하면 그제야 깨닫게 된다. 에이전트는 웹 서버가 아니라 작은 운영체제에 가깝다는 사실을 말이다.

단일 추론과 에이전트 실행의 결정적 차이
LLM 추론 서버의 기본 형태는 request-response다. 클라이언트가 /v1/chat/completions로 요청을 보낸다. vLLM이나 SGLang 같은 런타임이 토큰을 생성한다. 응답 스트림이 끝나면 서버 입장에서는 할 일이 끝난다. 물론 내부에서는 KV 캐시, continuous batching, GPU 메모리 관리처럼 복잡한 일이 일어나지만, 애플리케이션 관점의 생명주기는 비교적 명확하다.
에이전트 실행은 생명주기가 다르다. 하나의 사용자 요청이 내부적으로 여러 단계로 쪼개진다. 먼저 목표를 해석한다. 필요한 도구를 고른다. 도구를 호출한다. 결과가 충분한지 판단한다. 부족하면 다른 도구를 호출한다. 오류가 나면 재시도하거나 우회한다. 위험한 작업이면 사람에게 승인을 요청한다. 마지막으로 결과를 정리하고, 필요하면 장기 메모리에 저장한다.
간단히 표현하면 다음과 같다.
사용자 요청
↓
목표 해석
↓
계획 수립
↓
LLM 추론
↓
도구 호출
↓
결과 관찰
↓
추가 추론 또는 재시도
↓
필요 시 사람 승인
↓
최종 응답 및 상태 저장이런 구조를 흔히 reasoning loop라고 부른다. 말 그대로 에이전트가 "생각하고 행동하고 관찰하는 과정"을 반복하는 루프다. ReAct 패턴, LangGraph, AutoGen, LangChain 기반 에이전트가 내부 구현은 달라도 대체로 이 흐름을 따른다. Azure AKS의 KAITO 문서도 tool calling을 "LLM이 텍스트 생성에 머무르지 않고 외부 함수, API, 서비스와 상호작용하는 방식"으로 설명한다2.
운영자가 봐야 할 핵심은 이것이다. 단일 추론의 실패 단위는 보통 요청 하나다. 에이전트의 실패 단위는 세션, 작업, 도구 호출, 중간 상태, 외부 시스템 부작용까지 포함한다. 날씨 API를 잘못 호출하는 정도라면 큰 문제가 아닐 수 있다. 하지만 결제 취소 API, 데이터 삭제 API, 배포 자동화 API를 호출하는 에이전트라면 이야기가 달라진다. 에이전트 운영은 단순히 "토큰을 빠르게 생성하는 문제"가 아니라 "불확실한 실행 흐름을 안전하게 끝까지 관리하는 문제"다.
에이전트 실행을 구성하는 네 가지 시간
에이전트를 쿠버네티스에서 운영하려면 먼저 시간 감각을 바꿔야 한다. 웹 서버는 대개 요청 처리 시간, Pod 시작 시간, 롤링 업데이트 시간 정도를 신경 쓴다. 에이전트는 여기에 몇 가지 시간이 더해진다.
첫째는 추론 시간이다. LLM이 다음 행동을 결정하는 데 걸리는 시간이다. 작은 모델을 호출하면 짧고, 큰 모델이나 외부 API를 호출하면 길어진다. 둘째는 도구 실행 시간이다. 데이터베이스 조회, 웹 검색, 브라우저 자동화, 코드 실행, 파일 변환, 외부 SaaS API 호출이 여기에 들어간다. 셋째는 대기 시간이다. 사람의 승인을 기다리거나, 외부 시스템의 rate limit이 풀리기를 기다리거나, 큐에서 실행 순서를 기다리는 시간이다. 넷째는 회복 시간이다. 실패한 단계를 재시도하거나, 체크포인트에서 복원하거나, 중단된 세션을 다시 이어가는 시간이다.
이 네 가지 시간이 섞이면 "요청 하나당 평균 300ms" 같은 전통적인 API 서버 지표만으로는 시스템을 이해할 수 없다. 어떤 에이전트 작업은 5초 만에 끝난다. 어떤 작업은 40분 동안 30번의 도구 호출을 수행한다. 어떤 작업은 2분 실행 후 사람의 승인을 기다리며 6시간 동안 멈춰 있다가 다시 이어진다.
이때 Pod를 계속 붙잡아 둘 것인지, 상태만 저장하고 실행 환경은 잠시 내려도 되는지 판단해야 한다. Agent Sandbox가 idle 상태의 에이전트 환경을 중지하거나 다시 시작하는 lifecycle을 중요한 기능으로 보는 이유도 여기에 있다. 에이전트는 대부분의 시간 동안 바쁘게 CPU를 태우지 않는다. 잠깐 몰아서 일하고, 오래 기다리고, 다시 잠깐 일한다3.
durable execution: 왜 체크포인트가 필요한가
에이전트가 1초 안에 끝나는 함수라면 중간 상태 저장은 크게 중요하지 않다. 실패하면 처음부터 다시 실행하면 된다. 하지만 에이전트가 20분 동안 여러 도구를 호출하고, 그중 일부가 외부 시스템에 실제 변경을 남긴다면 이야기가 달라진다. 중간에 Pod가 재시작되었을 때 처음부터 다시 실행하면 같은 이메일을 두 번 보내거나, 같은 이슈를 두 번 만들거나, 이미 완료한 결제 취소를 다시 시도할 수 있다.
그래서 상태 기반 에이전트 프레임워크는 durable execution을 요구한다. durable execution은 실행 중인 작업의 상태를 안전하게 저장해 두고, 장애가 나도 이어서 실행할 수 있게 하는 방식이다. 한국어로는 "내구성 있는 실행" 정도로 번역할 수 있지만, 실무에서는 durable execution이라는 표현을 그대로 많이 쓴다.
LangGraph 같은 프레임워크가 상태 그래프와 체크포인트를 강조하는 이유가 여기에 있다. 에이전트의 각 단계는 노드가 되고, 다음에 어디로 갈지는 상태에 따라 결정된다. 중요한 것은 단순히 함수 호출 순서를 기록하는 것이 아니다. 현재 메시지 히스토리, 선택한 도구, 도구 호출 결과, 재시도 횟수, 승인 대기 여부, 다음 실행 위치를 함께 저장해야 한다. CNCF의 AI 플랫폼 관련 글에서도 에이전트 워크로드를 "LLM 호출 체인, 대화 상태, 외부 도구 접근, 장시간 실행 reasoning loop"로 설명하며, LangGraph 같은 상태 기반 오케스트레이션과 durable execution을 핵심 패턴으로 언급한다4.
쿠버네티스에서는 이 체크포인트 저장 위치를 명확히 설계해야 한다. Pod 내부 메모리에만 상태를 두면 안 된다. Pod는 언제든 죽을 수 있다. 노드가 교체될 수 있고, 이미지가 업데이트될 수 있고, 오토스케일링 과정에서 종료될 수 있다. 에이전트 상태는 Pod 밖에 있어야 한다.
상태를 어디에 저장할 것인가
에이전트 상태는 하나가 아니다. "상태"라는 단어 하나로 부르지만 실제로는 성격이 다른 여러 데이터가 섞여 있다. 대화 메시지, 실행 단계, 도구 호출 결과, 임시 파일, 장기 메모리, 사용자 설정, 승인 기록은 각각 저장소 선택 기준이 다르다.
짧은 세션 상태는 Redis에 잘 맞는다. Redis는 빠르고, TTL을 걸기 쉽고, 큐나 lock 구현에도 자주 사용된다. 사용자의 현재 대화 흐름, 실행 중인 작업의 lease, 임시 토큰, 짧은 캐시를 저장하기 좋다. 다만 Redis만 믿고 장기 상태를 저장하면 곤란하다. 장애 복구 정책과 persistence 설정을 제대로 하지 않으면 중요한 상태가 날아갈 수 있다.
작업 이력과 체크포인트는 Postgres나 MongoDB 같은 데이터베이스에 저장하는 편이 안정적이다. Postgres는 트랜잭션과 정합성이 강하고, 상태 전이를 명확히 관리하기 좋다. MongoDB는 JSON 형태의 유연한 실행 상태를 저장하기 편하다. TILNOTE처럼 MongoDB를 이미 핵심 저장소로 쓰는 서비스라면 에이전트 실행 로그와 세션 상태를 문서 구조로 저장하는 접근도 자연스럽다.
파일 기반 작업 공간은 PVC가 어울린다. 에이전트가 코드를 생성하고, 파일을 수정하고, 리포트를 만들고, 브라우저 다운로드 결과를 저장해야 한다면 데이터베이스만으로는 불편하다. 이때 PersistentVolumeClaim을 붙여 에이전트별 workspace를 제공할 수 있다. OpenClaw 사례도 에이전트의 workspace와 memory 파일을 PVC에 저장하여 Pod가 재시작되어도 지식과 작업 산출물이 유지되도록 구성한다5.
간단한 매핑은 다음과 같다.
| 상태 종류 | 예시 | 적합한 저장소 |
|---|---|---|
| 세션 캐시 | 현재 대화 단계, 임시 컨텍스트, 짧은 TTL 데이터 | Redis |
| 실행 체크포인트 | 현재 노드, 재시도 횟수, 도구 결과, 승인 대기 상태 | Postgres, MongoDB |
| 작업 공간 | 생성 코드, CSV, 이미지, 리포트, 임시 산출물 | PVC, 오브젝트 스토리지 |
| 장기 메모리 | 사용자 선호, 과거 요약, 의미 검색용 문서 | 벡터 DB, 문서 DB |
| 감사 로그 | 도구 호출, 권한 사용, 외부 API 요청 | 로그 저장소, Postgres, OpenTelemetry 백엔드 |
이 매핑에서 중요한 원칙은 "Pod는 실행 환경이지 기억 장소가 아니다"라는 것이다. Pod 안의 /tmp에 저장한 파일, 프로세스 메모리에 들고 있는 상태, 로컬 sqlite 파일은 모두 언제든 사라질 수 있다. 개발 환경에서는 괜찮아 보이지만, 운영 환경에서는 장애를 부르는 지름길이다.
StatefulSet, Job, Deployment 중 무엇을 쓸 것인가
에이전트를 쿠버네티스에 올릴 때 가장 먼저 부딪히는 질문은 이것이다. "이걸 Deployment로 띄워야 할까, Job으로 실행해야 할까, StatefulSet으로 관리해야 할까?" 정답은 에이전트의 성격에 따라 다르다.
항상 켜져 있는 에이전트 API 서버는 Deployment가 적합하다. 사용자의 요청을 받아 작업을 생성하고, 인증을 처리하고, 상태 저장소에 기록한 뒤, 실제 실행을 큐에 넣는 역할이라면 일반적인 stateless API 서버처럼 운영할 수 있다. 이 계층은 가능한 한 상태를 갖지 않게 만들어야 한다. 그래야 여러 replica로 쉽게 확장할 수 있다.
개별 작업 단위 에이전트는 Job이 적합하다. 예를 들어 "장애 원인 분석 한 건", "보고서 생성 한 건", "문서 100개 요약 작업 한 건"처럼 시작과 끝이 분명한 실행은 Kubernetes Job으로 분리하면 좋다. InfoQ의 autonomous diagnostic agent 사례에서도 각 조사를 별도의 Kubernetes Job으로 실행해 리소스 격리, 실패 격리, 깨끗한 상태, 감사 추적을 얻었다고 설명한다6. Job은 activeDeadlineSeconds, backoffLimit, ttlSecondsAfterFinished 같은 필드로 시간 제한, 재시도, 정리를 쿠버네티스 레벨에서 표현할 수 있다.
반면 장기적으로 살아 있는 개인 비서형 에이전트, 개발 환경형 에이전트, 사용자별 작업 공간을 유지하는 에이전트는 StatefulSet이나 Agent Sandbox류의 추상화가 더 어울린다. StatefulSet은 안정적인 네트워크 이름과 PVC를 제공한다. agent-0, agent-1처럼 번호가 붙은 Pod가 유지되고, 각 Pod에 고유한 볼륨을 연결할 수 있다. 다만 "사용자 한 명당 singleton 에이전트 하나" 같은 구조를 수천 개 운영하려면 StatefulSet만으로는 관리가 번거로워진다. 이 간극을 메우기 위해 Kubernetes SIG Apps의 Agent Sandbox는 안정적인 identity, persistent storage, pause/resume lifecycle을 가진 단일 상태ful workload용 CRD를 제안한다7.
요약하면 다음과 같이 선택할 수 있다.
| 실행 패턴 | 권장 리소스 | 설명 |
|---|---|---|
| 에이전트 요청을 받는 프론트 API | Deployment | stateless로 만들고 수평 확장한다. |
| 시작과 끝이 있는 작업 실행 | Job | timeout, retry, cleanup, audit 단위를 작업별로 분리한다. |
| 주기적으로 실행되는 에이전트 작업 | CronJob | 리포트 생성, 동기화, 정기 점검에 적합하다. |
| 사용자별 장기 workspace | StatefulSet, Agent Sandbox | 안정적인 identity와 PVC가 필요하다. |
| 도구 실행용 격리 환경 | Agent Sandbox, gVisor, Kata Containers | 신뢰할 수 없는 코드 실행을 격리한다. |
중요한 것은 모든 것을 하나의 Deployment에 넣지 않는 것이다. 웹 요청 처리, 에이전트 오케스트레이션, 도구 실행, 코드 실행, 장기 메모리 저장을 한 컨테이너에 몰아넣으면 처음에는 단순해 보인다. 그러나 장애가 나면 어디서 문제가 생겼는지 알기 어렵고, 확장 단위도 꼬인다. 에이전트 플랫폼은 처음부터 control plane과 execution plane을 나누는 편이 좋다.
기본 아키텍처: API, 큐, 워커, 상태 저장소
소규모 팀이 시작하기 좋은 에이전트 아키텍처는 다음과 같다.
사용자 / 외부 이벤트
↓
Agent API Deployment
↓
작업 생성 및 상태 저장
↓
Queue
↓
Agent Worker Job 또는 Deployment
↓
LLM Gateway / vLLM / 외부 LLM API
↓
Tool Services / MCP Servers / External APIs
↓
Postgres · MongoDB · Redis · PVC · Vector DBAgent API는 사용자 요청을 받는다. 인증과 권한을 확인하고, 작업 ID를 만든다. 이때 작업 상태를 데이터베이스에 queued로 저장한 뒤 큐에 메시지를 넣는다. 워커는 큐에서 작업을 가져와 lease를 획득한다. lease는 "이 작업은 지금 이 워커가 처리 중"이라는 임시 소유권이다. 워커는 에이전트 루프를 실행하면서 중간 상태를 계속 체크포인트로 저장한다. 작업이 끝나면 succeeded로 바꾸고, 실패하면 실패 원인과 재시도 가능 여부를 기록한다.
여기서 큐는 선택 사항이 아니라 거의 필수에 가깝다. 에이전트 작업은 실행 시간이 길고 변동이 크다. 사용자가 동시에 100개의 작업을 요청했을 때 이를 즉시 100개의 Pod로 만들면 클러스터가 흔들릴 수 있다. 큐는 요청을 흡수하는 완충 장치다. KEDA와 연결하면 큐 깊이에 따라 워커 수를 늘리고, 작업이 없을 때 0까지 줄일 수도 있다. 앞 장에서 다룬 event-driven autoscaling이 에이전트 워크로드에서도 그대로 중요해지는 이유다.
작업 상태 테이블은 대략 다음 필드를 갖는다.
agent_tasks
- id
- user_id
- status # queued, running, waiting_approval, succeeded, failed, cancelled
- input
- current_step
- checkpoint
- lease_owner
- lease_expires_at
- retry_count
- max_retries
- deadline_at
- created_at
- updated_at이 구조에서 status는 사용자에게 보여줄 상태이기도 하고, 운영자가 시스템을 제어하는 기준이기도 하다. lease_expires_at은 워커가 죽었는지 판단하는 기준이다. deadline_at은 작업이 무한히 실행되는 것을 막는다. checkpoint는 재시작 시 이어서 실행하기 위한 저장 지점이다.
StatefulSet과 PVC로 사용자별 workspace 만들기
사용자별로 장기 실행 에이전트를 운영한다고 가정해보자. 예를 들어 개발자를 돕는 코딩 에이전트가 있고, 각 사용자는 자기 프로젝트 파일, 설정, 실행 로그, 메모리를 가진다. 이때 모든 사용자의 파일을 하나의 공유 볼륨에 넣는 것은 권장하기 어렵다. 권한 경계가 흐려지고, 한 사용자의 파일 폭증이 전체에 영향을 줄 수 있다.
StatefulSet을 사용하면 각 replica에 고유한 PVC를 붙일 수 있다. 아래 예시는 개념을 보여주기 위한 단순화된 형태다.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: agent-workspace
namespace: agents
spec:
serviceName: agent-workspace
replicas: 3
selector:
matchLabels:
app: agent-workspace
template:
metadata:
labels:
app: agent-workspace
spec:
containers:
- name: agent
image: registry.example.com/agent-runtime:1.0.0
ports:
- containerPort: 8080
env:
- name: WORKSPACE_DIR
value: /workspace
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
volumeMounts:
- name: workspace
mountPath: /workspace
volumeClaimTemplates:
- metadata:
name: workspace
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 20Gi이 방식은 "정해진 수의 상태ful 에이전트 replica"에는 적합하다. 하지만 사용자 수에 따라 에이전트가 동적으로 생기고 사라지는 구조라면 StatefulSet을 직접 많이 만드는 것보다 별도의 컨트롤러나 Sandbox CRD를 사용하는 편이 낫다. Agent Sandbox는 바로 이 지점, 즉 안정적인 hostname과 persistent storage가 필요하지만 StatefulSet 조합만으로는 lifecycle 관리가 번거로운 singleton workload를 겨냥한다8.
실무에서는 다음과 같이 나누면 좋다. 개인별 장기 workspace는 PVC나 오브젝트 스토리지에 저장한다. 에이전트 실행 상태는 Postgres나 MongoDB에 저장한다. 빠른 락과 큐 상태는 Redis에 둔다. 의미 검색용 장기 기억은 벡터 DB에 둔다. 하나의 저장소로 모든 것을 해결하려고 하면 나중에 확장과 장애 복구가 어려워진다.
에이전트 메모리와 벡터 DB
에이전트에서 "메모리"라는 말은 조금 조심해서 써야 한다. 사람처럼 모든 것을 자연스럽게 기억한다는 뜻이 아니다. 실제 구현에서는 대개 과거 대화, 문서, 작업 결과, 사용자 선호를 저장해 두고 필요할 때 검색해서 프롬프트에 다시 넣는 구조다. 이때 핵심이 벡터 DB다.
벡터 DB는 텍스트나 이미지 같은 데이터를 embedding이라는 숫자 배열로 바꿔 저장하고, 의미적으로 비슷한 항목을 빠르게 찾는 데이터베이스다. RAG에서 사용했던 그 구조가 에이전트 메모리에도 그대로 들어온다. 사용자가 "지난번에 정리한 마케팅 아이디어를 바탕으로 새 캠페인 문구를 만들어 줘"라고 말하면, 에이전트는 과거 대화 전체를 프롬프트에 넣지 않는다. 먼저 벡터 DB에서 관련 기억을 찾고, 그 결과를 현재 컨텍스트에 넣는다.
Pinecone, Weaviate, Milvus, Qdrant, pgvector 같은 선택지가 있다. Pinecone은 관리형 서비스로 시작하기 쉽고, Weaviate와 Milvus는 자체 운영이나 쿠버네티스 배포를 고려할 때 많이 검토된다. CNCF의 AI 플랫폼 글에서도 에이전트 워크로드에서 vector database가 semantic memory를 담당한다고 설명한다9.
에이전트 메모리 파이프라인은 보통 다음 흐름을 따른다.
대화 또는 작업 결과
↓
요약 및 정제
↓
embedding 생성
↓
벡터 DB 저장
↓
다음 요청에서 유사 기억 검색
↓
프롬프트 컨텍스트에 삽입여기서 운영자가 주의할 점은 두 가지다. 첫째, 모든 것을 무조건 저장하면 비용과 품질이 동시에 나빠진다. 쓸모없는 중간 로그, 실패한 도구 결과, 민감한 개인정보까지 벡터 DB에 넣으면 검색 품질이 떨어지고 보안 위험이 커진다. 둘째, 검색된 기억을 그대로 믿으면 안 된다. 오래된 정보일 수 있고, 사용자가 삭제를 요청한 정보일 수도 있으며, 다른 테넌트의 데이터가 섞이면 심각한 사고가 된다.
따라서 에이전트 메모리는 최소한 tenant_id, user_id, source, created_at, expires_at, sensitivity, embedding_model_version 같은 메타데이터를 함께 저장해야 한다. 벡터 검색을 할 때도 단순 유사도 검색만 하지 말고 tenant와 user 필터를 반드시 걸어야 한다.
예를 들어 Pinecone이나 Weaviate를 쓰든, Milvus를 쓰든 개념적으로는 다음과 같은 필터가 필요하다.
{
"tenant_id": "tilnote",
"user_id": "user_123",
"memory_type": "project_note",
"expires_at": { "$gt": "2026-05-08T00:00:00Z" }
}에이전트 메모리에서 가장 무서운 장애는 "검색이 안 되는 것"이 아니다. "검색되면 안 되는 것이 검색되는 것"이다. 이 문제는 다음 장의 보안과 격리에서 더 깊게 다룰 것이다.
도구 호출은 내부 API 호출보다 위험하다
에이전트의 매력은 도구를 쓸 수 있다는 점이다. 동시에 가장 큰 위험도 도구 호출에서 나온다. 일반적인 백엔드 서비스는 코드에 정해진 API를 호출한다. 호출 경로가 비교적 예측 가능하다. 반면 에이전트는 런타임에 어떤 도구를 쓸지 결정한다. 같은 사용자 요청도 모델 응답, 검색 결과, 중간 오류에 따라 전혀 다른 호출 경로를 만들 수 있다.
도구 호출에는 여러 종류가 있다. 계산기나 날짜 변환처럼 안전한 로컬 함수도 있고, 사내 데이터베이스 조회처럼 읽기 권한이 필요한 도구도 있다. GitHub 이슈 생성, 슬랙 메시지 발송, 결제 환불, 쿠버네티스 리소스 변경처럼 외부 시스템에 부작용을 남기는 도구도 있다. 브라우저 자동화나 셸 실행처럼 더 넓은 권한을 요구하는 도구도 있다.
운영 관점에서는 도구를 신뢰 수준별로 나눠야 한다. 모든 도구를 같은 권한으로 취급하면 안 된다. 예를 들어 다음과 같은 구분이 가능하다.
| 도구 등급 | 예시 | 운영 정책 |
|---|---|---|
| 안전한 읽기 도구 | 검색, 문서 조회, 계산기 | 자동 실행 가능 |
| 제한된 읽기 도구 | 사내 로그, 고객 정보 조회 | 감사 로그와 권한 확인 필요 |
| 제한된 쓰기 도구 | 이슈 생성, 슬랙 발송 | rate limit과 승인 정책 필요 |
| 고위험 쓰기 도구 | 결제, 삭제, 배포, 권한 변경 | human-in-the-loop 권장 |
| 코드 실행 도구 | 셸, 브라우저, Python 실행 | Sandbox 격리 필수 |
human-in-the-loop는 사람이 중간에 승인하는 구조다. 에이전트가 "이 PR을 머지하겠습니다", "이 고객에게 환불을 실행하겠습니다", "프로덕션 배포를 시작하겠습니다" 같은 결정을 내렸을 때 바로 실행하지 않고 승인을 기다리게 만드는 것이다. 이때 에이전트 상태는 waiting_approval로 저장되어야 하며, Pod가 계속 실행 중일 필요는 없다. 승인 이벤트가 들어오면 체크포인트에서 다시 이어가면 된다.
이 구조를 제대로 만들지 않으면 두 가지 문제가 생긴다. 하나는 에이전트가 사용자의 의도보다 더 큰 행동을 해버리는 문제다. 다른 하나는 승인 대기 중인 작업이 Pod와 리소스를 계속 점유하는 문제다. 승인을 기다리는 6시간 동안 CPU와 메모리를 붙잡고 있는 것은 낭비다. 상태를 저장하고 실행을 멈출 수 있어야 한다.
큐, lease, timeout, cancellation이 없을 때 생기는 일
에이전트 시스템은 동시 실행 수가 늘어나는 순간 전혀 다른 얼굴을 보인다. 한 명이 테스트할 때는 아무 문제가 없다. 열 명이 쓰면 느려진다. 백 명이 동시에 쓰면 이상한 현상이 나타난다. 같은 작업이 두 번 실행되고, 이미 취소한 작업이 계속 진행되고, 실패한 도구 호출이 무한 재시도되고, LLM API 비용이 갑자기 치솟는다.
이 문제를 막기 위해서는 큐, lease, timeout, cancellation을 처음부터 설계해야 한다.
큐는 작업을 순서대로 받아들이는 완충 장치다. 모든 요청을 즉시 실행하지 않고, 현재 처리 가능한 만큼만 워커가 가져가게 한다. 큐가 없으면 트래픽 스파이크가 곧바로 Pod 폭증, 외부 API 폭증, 비용 폭증으로 이어진다.
lease는 작업 소유권이다. 워커 A가 작업을 가져갔다면 일정 시간 동안 그 작업은 A의 것이다. 워커 A는 주기적으로 lease를 갱신한다. 만약 워커가 죽어 lease가 만료되면 다른 워커가 작업을 이어받을 수 있다. lease가 없으면 장애 상황에서 같은 작업이 여러 워커에서 동시에 실행될 수 있다.
timeout은 작업의 최대 실행 시간이다. 에이전트는 reasoning loop를 돌기 때문에 잘못하면 끝나지 않을 수 있다. "조금 더 검색해 보자", "다른 관점에서 다시 분석해 보자", "오류가 났으니 다시 시도하자"가 반복되면 비용과 시간이 계속 늘어난다. 작업 전체 timeout, 단계별 timeout, 도구 호출 timeout을 따로 두는 것이 좋다.
cancellation은 사용자가 작업을 취소했을 때 실제 실행을 멈추는 능력이다. UI에서 "취소됨"으로 표시만 바꾸고 워커가 계속 실행되면 의미가 없다. 워커는 각 단계 사이에서 취소 여부를 확인해야 한다. 외부 API 호출이나 긴 코드 실행도 가능한 한 취소 신호를 받을 수 있어야 한다.
아래는 Job 기반 에이전트 실행에서 timeout과 정리를 표현하는 예시다.
apiVersion: batch/v1
kind: Job
metadata:
name: agent-task-8f3a2
namespace: agents
labels:
app: agent-worker
task-id: "8f3a2"
spec:
backoffLimit: 0
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: Never
serviceAccountName: agent-worker
containers:
- name: worker
image: registry.example.com/agent-worker:1.0.0
env:
- name: TASK_ID
value: "8f3a2"
- name: CHECKPOINT_STORE
value: "postgres"
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"activeDeadlineSeconds는 Job 전체가 30분을 넘기지 못하게 한다. backoffLimit: 0은 실패 시 쿠버네티스가 자동으로 같은 Job을 재시도하지 않게 한다. 에이전트 재시도는 무조건 나쁜 것은 아니지만, 어디서 재시도할지 명확해야 한다. 쿠버네티스 Job 레벨에서 재시도할지, 애플리케이션 단계에서 체크포인트 기반으로 재시도할지 섞이면 중복 실행이 생기기 쉽다.
동시 에이전트 수가 늘어날 때의 병목
에이전트의 동시성은 일반 웹 서버의 동시성과 다르다. 웹 서버에서는 replica를 늘리면 대체로 처리량이 증가한다. 에이전트에서는 replica를 늘리는 순간 다른 병목이 먼저 터질 수 있다.
첫 번째 병목은 LLM 호출 한도다. 외부 LLM API를 쓰면 provider의 TPM(tokens per minute), RPM(requests per minute) 제한에 걸린다. 자체 vLLM을 운영하더라도 GPU 메모리, KV 캐시, queue depth가 한계가 된다. 에이전트 하나가 내부적으로 LLM을 10번 호출한다면, 사용자 요청 100개는 추론 요청 1,000개가 될 수 있다.
두 번째 병목은 도구 서버다. 에이전트가 모두 같은 검색 API, 같은 사내 DB, 같은 MCP 서버를 호출하면 그쪽이 먼저 무너진다. 특히 브라우저 자동화, 파일 변환, 코드 실행 도구는 CPU와 메모리를 많이 쓴다.
세 번째 병목은 상태 저장소다. 모든 단계마다 체크포인트를 저장하고 로그를 남기면 Postgres나 MongoDB write 부하가 커진다. 벡터 DB도 embedding 생성과 upsert가 몰리면 지연이 생긴다.
네 번째 병목은 관찰 가능성 비용이다. 에이전트는 로그가 많다. 모든 LLM 프롬프트, 응답, 도구 호출, 중간 추론을 그대로 저장하면 저장 비용이 빠르게 증가하고 개인정보 위험도 커진다. 운영에 필요한 구조화 로그와 민감정보 마스킹 정책이 필요하다.
그래서 에이전트 플랫폼은 단순히 Pod 수를 늘리는 방식이 아니라 "동시 에이전트 수", "에이전트당 최대 LLM 호출 수", "도구별 동시 호출 수", "사용자별 예산", "작업별 deadline"을 함께 제어해야 한다. kubeswarm 같은 에이전트 운영 도구가 budget, tool allow/deny, audit trail, queue-depth scaling 같은 기능을 인프라 레벨에서 제공하려는 이유도 이 문제의식과 맞닿아 있다10.
에이전트 상태 머신 설계하기
에이전트를 안정적으로 운영하려면 상태 머신을 명확히 정의해야 한다. 상태 머신은 작업이 어떤 상태를 거쳐 이동할 수 있는지 정한 규칙이다. 이것이 없으면 작업 상태가 running인데 실제로는 죽어 있거나, failed인데 워커가 계속 실행 중이거나, cancelled인데 외부 API 호출이 진행되는 이상한 상황이 생긴다.
실무에서 시작하기 좋은 상태는 다음과 같다.
queued
↓
running
↓
waiting_approval
↓
running
↓
succeeded
running → retrying → running
running → failed
queued/running/waiting_approval → cancelling → cancelled
running → timed_outqueued는 아직 실행되지 않은 상태다. running은 워커가 lease를 가지고 처리 중인 상태다. waiting_approval은 사람의 결정을 기다리는 상태다. retrying은 실패했지만 재시도 가능한 상태다. cancelling은 취소 신호를 보냈지만 아직 정리가 끝나지 않은 상태다. cancelled, failed, succeeded, timed_out은 종료 상태다.
이 상태 전이는 데이터베이스 트랜잭션으로 보호하는 것이 좋다. 예를 들어 두 워커가 동시에 같은 작업을 가져가지 못하게 하려면 다음과 같은 조건부 업데이트가 필요하다.
UPDATE agent_tasks
SET status = 'running',
lease_owner = 'worker-17',
lease_expires_at = now() + interval '60 seconds'
WHERE id = 'task-123'
AND status = 'queued';업데이트된 row 수가 1이면 lease 획득에 성공한 것이다. 0이면 누군가 이미 가져간 작업이다. 이런 작은 설계가 중복 실행을 막는다.
에이전트 시스템에서 중복 실행은 단순한 버그가 아니다. 같은 이메일이 두 번 발송되고, 같은 결제가 두 번 처리되고, 같은 슬랙 메시지가 여러 번 올라갈 수 있다. 따라서 외부 부작용이 있는 도구는 idempotency key를 가져야 한다. idempotency는 같은 요청이 여러 번 들어와도 결과가 한 번만 적용되도록 만드는 성질이다. 예를 들어 task_id + tool_call_id를 idempotency key로 쓰면 같은 도구 호출이 재시도되어도 외부 시스템에서 중복 처리를 막을 수 있다.
작은 팀을 위한 현실적인 시작점
모든 팀이 처음부터 Agent Sandbox, LangGraph, KEDA, 벡터 DB, OpenTelemetry, MCP 서버, gVisor 격리를 완벽하게 갖출 필요는 없다. 중요한 것은 에이전트를 stateless API 서버로 착각하지 않는 것이다. 소규모 팀이라면 다음 정도로 시작해도 충분하다.
먼저 에이전트 API와 워커를 분리한다. API는 Deployment로 띄우고, 워커는 큐 기반 Deployment나 Job으로 실행한다. 처음에는 Redis Queue, BullMQ, Celery, RabbitMQ, SQS 중 익숙한 것을 선택하면 된다.
다음으로 작업 상태를 데이터베이스에 저장한다. MongoDB를 쓰고 있다면 agent_tasks 컬렉션을 만들고, Postgres를 쓰고 있다면 agent_tasks 테이블을 만든다. 상태, 체크포인트, 재시도 횟수, deadline, lease 정보를 반드시 포함한다.
그다음 도구 호출을 allowlist로 제한한다. 에이전트가 호출할 수 있는 도구를 코드나 설정으로 명확히 제한하고, 쓰기 작업에는 승인 단계를 둔다. 처음부터 모든 외부 API를 열어두면 운영자가 감당하기 어렵다.
마지막으로 로그를 구조화한다. 적어도 task_id, user_id, step_id, tool_name, latency_ms, status, error_code는 남겨야 한다. 에이전트 장애는 "왜 이런 결정을 했지?"를 추적해야 하는 경우가 많다. 일반적인 HTTP access log만으로는 부족하다.
이 정도만 해도 데모 수준의 에이전트와 운영 가능한 에이전트 사이에 큰 차이가 생긴다. 이후 트래픽이 늘어나면 KEDA로 큐 기반 오토스케일링을 붙이고, 장기 workspace가 필요해지면 PVC나 Agent Sandbox를 검토하고, 의미 기억이 필요해지면 벡터 DB를 붙이면 된다.
에이전트 운영의 핵심 원칙
에이전트는 쿠버네티스 위에서 충분히 운영할 수 있다. 오히려 쿠버네티스는 에이전트 운영에 필요한 많은 재료를 이미 가지고 있다. Pod는 실행 환경을 제공하고, Job은 작업 단위 격리를 제공하고, StatefulSet과 PVC는 안정적인 상태 저장을 제공하고, Service와 NetworkPolicy는 연결과 통제를 제공하고, KEDA는 이벤트 기반 확장을 제공한다.
하지만 조립 방식은 웹 서비스와 달라야 한다. 에이전트는 짧은 request-response가 아니다. reasoning loop, tool call, retry, human-in-the-loop, checkpoint, memory를 가진다. 그래서 다음 원칙을 기억해야 한다.
에이전트의 상태는 Pod 밖에 있어야 한다. Pod는 죽을 수 있고, 노드는 교체될 수 있다. 체크포인트, 세션, 메모리, 작업 산출물은 Redis, Postgres, MongoDB, PVC, 벡터 DB처럼 목적에 맞는 외부 저장소에 둬야 한다.
에이전트의 실행은 제한되어야 한다. 무한 루프를 막는 timeout, 중복 실행을 막는 lease, 사용자가 멈출 수 있는 cancellation, 실패를 제어하는 retry budget이 필요하다. 이것이 없으면 에이전트는 언젠가 비용 폭탄이나 장애 폭탄이 된다.
에이전트의 도구 사용은 통제되어야 한다. 읽기 도구와 쓰기 도구를 구분하고, 고위험 도구에는 승인 단계를 넣고, 코드 실행은 격리 환경에서 수행해야 한다. 이 주제는 다음 장에서 보안과 격리라는 이름으로 더 깊게 다룰 것이다.
AI 시대의 쿠버네티스에서 에이전트는 단순한 애플리케이션이 아니다. 사용자의 의도를 받아 외부 세계에 행동을 남기는 실행 주체다. 그래서 에이전트를 잘 운영한다는 것은 컨테이너를 잘 띄우는 것을 넘어, 상태와 시간과 권한과 실패를 설계하는 일이다. 이 관점을 잡는 순간, 쿠버네티스는 에이전트에게 꽤 자연스러운 무대가 된다.
참고
1Running Agents on Kubernetes with Agent Sandbox
3Running Agents on Kubernetes with Agent Sandbox
4The great migration: Why every AI platform is converging on Kubernetes
5Running AI Agents on Kubernetes: How OpenClaw Uses K8s for Autonomous Agent Infrastructure
7GitHub - kubernetes-sigs/agent-sandbox
8GitHub - kubernetes-sigs/agent-sandbox
9The great migration: Why every AI platform is converging on Kubernetes
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.