Field Log · Entry

Production RAG Agent 배포: CI·관찰성·Canary·Rollback (10/10)

하나의 검증된 container digest가 CI gate와 release manifest를 거쳐 canary stable로 승격되고 telemetry와 rollback loop로 연결되는 Production RAG Agent release 구조

오늘의 결론

  • Release 대상은 source code만이 아닙니다. Image digest + DB schema + prompt + model + tool contract + retrieval index + policy를 하나의 manifest로 묶어야 같은 동작을 재현할 수 있습니다.
  • CI는 unit test 뒤 곧바로 배포하지 않습니다. Contract·fault test, local golden eval, schema check, container smoke, supply-chain artifact를 모두 같은 commit에 연결합니다.
  • Image는 한 번 build하고 digest로 승격합니다. Staging과 production에서 다시 build하면 서로 다른 artifact를 검증하고 배포할 수 있습니다.
  • Liveness는 process 재시작 여부, readiness는 새 traffic 수신 여부, startup은 느린 초기화 보호를 판단합니다. 세 probe를 같은 dependency ping으로 만들면 장애가 재시작 폭풍으로 바뀔 수 있습니다.
  • Rollback은 이전 tag를 다시 실행하는 명령이 아닙니다. 이전 image가 현재 schema·prompt·index와 호환되는지, in-flight run을 어떻게 drain하는지까지 사전에 test한 경로입니다.

앞 글에서는 외부 API 없이 failure와 contract를 재현하는 test harness를 만들었습니다. 이번 완결편에서는 81~89편의 application을 하나의 검증 가능하고 되돌릴 수 있는 release로 묶습니다.

검증된 하나의 container digest가 CI gate와 release manifest를 거쳐 canary에서 stable로 승격되고 telemetry가 rollback 판단으로 돌아오는 흐름

이 글의 대상과 학습 시간

  • 대상: Python RAG Agent를 실제 환경에 처음 release하려는 backend·ML platform 개발자
  • 선수 지식: Container, CI, HTTP service, 81~89편의 application boundary
  • 빠르게 읽기: 약 17분
  • Docker·CI·telemetry 초안까지 따라 하기: 약 90분
  • 산출물: immutable release manifest, CI gate, health probe, canary·rollback runbook

1. “코드가 같다”만으로 같은 Agent가 아니다

전통적인 web service도 config와 database에 영향을 받지만 RAG Agent는 동작을 바꾸는 축이 더 많습니다.

answer behavior =
  application code
  × model identifier / provider behavior
  × prompt template
  × tool schema / policy
  × retrieval index snapshot
  × embedding / reranker version
  × document ACL / freshness
  × runtime budget / retry policy

Git SHA만 기록하면 incident 당시의 답변 경로를 재현할 수 없습니다. “어제와 같은 container인데 citation이 달라졌다”는 상황은 index나 model alias가 바뀌었기 때문일 수 있습니다.

Release는 다음 질문에 답할 수 있어야 합니다.

  • 어떤 source commit으로 어떤 image를 만들었는가?
  • 어떤 test와 eval result가 그 image를 승인했는가?
  • 어떤 DB schema와 event schema를 기대하는가?
  • 어떤 prompt·model·index·policy version이 결합됐는가?
  • Canary에서 어떤 지표를 보고 승격했는가?
  • 문제가 생기면 어느 조합으로 되돌리는가?

2. 먼저 release unit을 정의한다

Version을 하나의 문자열에 억지로 합치기보다 manifest에 각 축을 명시합니다.

{
  "manifest_schema": 1,
  "release_id": "rag-agent-2026-07-16.1",
  "git_sha": "4f2c...",
  "image": {
    "ref": "registry.example/rag-agent",
    "digest": "sha256:abc123...",
    "sbom_digest": "sha256:def456...",
    "provenance_attestation": "attestation://build/8421"
  },
  "application": {
    "api_schema": "2026-07-01",
    "event_schema": 3,
    "database_revision": "20260716_01"
  },
  "behavior": {
    "model": "provider/model-snapshot-id",
    "prompt": "answer-v12",
    "tool_contracts": "sha256:tool789...",
    "policy": "agent-policy-v8",
    "retrieval_index": "kb-20260716T010000Z",
    "embedding": "embed-model-v4",
    "reranker": "reranker-v2"
  },
  "quality": {
    "dataset": "golden-rag-v7",
    "report_digest": "sha256:eval321..."
  },
  "telemetry": {
    "semantic_convention": "pinned-version",
    "content_capture": false
  }
}

실제 manifest에는 secret, raw prompt, user query를 넣지 않습니다. Mutable model alias만 제공하는 provider라면 요청 시 사용한 alias와 provider가 반환한 resolved identifier·request ID를 가능한 범위에서 함께 남깁니다.

Version 축을 따로 롤백할 수 있어야 한다

  • Code는 정상인데 prompt가 문제면 prompt version만 되돌림
  • Index freshness가 문제면 이전 snapshot으로 read alias 전환
  • Tool policy가 너무 넓으면 policy version 차단
  • DB schema가 irreversible하면 image만 되돌릴 수 없음

그래서 release manifest는 단순 build metadata가 아니라 rollback graph입니다.

3. Production image는 작고 재현 가능하게 만든다

Docker 공식 가이드는 multi-stage build, 불필요한 package 제외, USER를 통한 non-root 실행, base image digest pin을 권장합니다. Tag는 바뀔 수 있으므로 완전한 재현성이 필요하면 digest를 기록합니다.

# syntax=docker/dockerfile:1

# <PINNED_DIGEST>는 dependency update PR에서 검증한 실제 sha256으로 교체한다.
FROM python:3.13-slim@sha256:<PINNED_DIGEST> AS builder

ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
    PIP_NO_CACHE_DIR=1 \
    PYTHONDONTWRITEBYTECODE=1

RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

WORKDIR /build
COPY requirements.lock ./
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --require-hashes -r requirements.lock


FROM python:3.13-slim@sha256:<PINNED_DIGEST> AS runtime

ENV PATH="/opt/venv/bin:$PATH" \
    PYTHONPATH="/app/src" \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

RUN groupadd --system --gid 10001 app \
    && useradd --system --uid 10001 --gid app --home-dir /nonexistent app

COPY --from=builder /opt/venv /opt/venv
WORKDIR /app
COPY --chown=app:app src ./src

USER 10001:10001
EXPOSE 8080

CMD ["uvicorn", "rag_agent.api:create_app", "--factory", "--host", "0.0.0.0", "--port", "8080", "--no-access-log"]

requirements.lock의 모든 transitive dependency hash를 고정했다는 전제의 예시입니다. 조직 표준이 uv, Poetry, PDM이라면 해당 lock·sync 방식으로 바꾸되 CI에서 lockfile drift를 거부합니다.

Image에 넣지 않을 것

  • .env와 provider API key
  • Test cassette의 raw capture
  • 개발용 compiler·package manager cache
  • Git history와 local credential
  • Production document·prompt secret
  • 불필요한 shell·debug tool

.dockerignore로 context 자체를 줄이고 secret은 runtime secret manager에서 주입합니다. Build secret이 필요하면 BuildKit secret mount를 사용하고 layer와 build log에 남지 않는지 확인합니다.

Tag와 digest의 역할

human label: rag-agent:2026-07-16.1
immutable identity: rag-agent@sha256:abc123...

Tag는 찾기 편한 pointer이고 digest가 실제 승격·rollback identity입니다.

4. Local Compose는 production 복제가 아니라 contract lab이다

개발 환경에는 application, PostgreSQL, Redis, OpenTelemetry Collector를 함께 띄울 수 있습니다.

services:
  migrate:
    build: .
    command: ["python", "-m", "alembic", "upgrade", "head"]
    environment:
      DATABASE_URL: postgresql://app:app@postgres:5432/app
    depends_on:
      postgres:
        condition: service_healthy

  app:
    build: .
    ports: ["8080:8080"]
    environment:
      DATABASE_URL: postgresql://app:app@postgres:5432/app
      REDIS_URL: redis://redis:6379/0
      OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4317
    depends_on:
      migrate:
        condition: service_completed_successfully
      redis:
        condition: service_healthy

  postgres:
    image: postgres:<PINNED_VERSION_OR_DIGEST>
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 2s
      timeout: 2s
      retries: 20

  redis:
    image: redis:<PINNED_VERSION_OR_DIGEST>
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 2s
      timeout: 2s
      retries: 20

  otel-collector:
    image: otel/opentelemetry-collector:<PINNED_VERSION_OR_DIGEST>
    command: ["--config=/etc/otelcol/config.yaml"]
    volumes:
      - ./deploy/otel-collector.yaml:/etc/otelcol/config.yaml:ro

Docker Compose는 service_healthy dependency의 healthcheck를 기다릴 수 있습니다. 그러나 이것은 startup ordering을 돕는 기능입니다. Runtime에 DB가 재시작할 수 있으므로 application connection retry·pool recovery는 여전히 필요합니다.

Local Compose의 목적은 다음을 반복하는 것입니다.

  • Migration from empty DB and previous schema
  • Redis가 없어도 correctness가 유지되는지
  • OTLP exporter가 느리거나 꺼져도 request가 막히지 않는지
  • Container shutdown과 SSE drain
  • Non-root filesystem permission
  • Offline golden eval의 재현성

5. CI는 빠른 실패에서 비싼 gate 순서로 구성한다

static → unit → contract/fault → integration → offline eval
       → image build → image smoke → SBOM/scan/attest → publish digest

Gate 1. 정적 검증

  • Formatter check
  • Lint
  • Type check
  • Dependency lock consistency
  • Secret scan
  • API/event schema compatibility
  • Migration graph가 single head인지

Gate 2. Software correctness

  • Pure unit
  • Fake vertical slice
  • Shared adapter contract
  • HTTP transport fault matrix
  • Session idempotency race
  • SSE terminal·disconnect·replay

Gate 3. Local quality eval

  • Retrieval hit·forbidden result rate
  • Citation validity
  • No-answer/abstention behavior
  • Tool policy violation 0
  • Latency·cost envelope

Gate 4. Built artifact test

Source tree에서만 test하지 않고 실제 image를 실행합니다.

container_id="$(docker run --rm --detach --name rag-agent-smoke \
  --read-only --tmpfs /tmp \
  -p 18080:8080 \
  registry.example/rag-agent@sha256:abc123)"
trap 'docker stop "$container_id" >/dev/null 2>&1 || true' EXIT

for attempt in $(seq 1 30); do
  if curl --fail --silent http://127.0.0.1:18080/health/live; then
    break
  fi
  if [ "$attempt" -eq 30 ]; then
    docker logs "$container_id"
    exit 1
  fi
  sleep 1
done
curl --fail --silent http://127.0.0.1:18080/health/ready

CI에서는 container lifetime과 cleanup을 trap·job finalizer로 관리합니다. 위 command의 digest와 port는 설명용입니다.

Gate 5. Supply-chain evidence

  • Image vulnerability scan policy
  • SBOM 생성·보존
  • Build provenance attestation
  • Artifact digest와 workflow run·commit 연결
  • Registry write 권한 최소화

GitHub Actions artifact는 job 사이 결과와 test report를 보존할 수 있고, artifact attestation은 build provenance를 연결하는 데 사용할 수 있습니다. 어떤 CI를 쓰든 동일 원칙은 검증 결과와 배포 digest의 identity가 같아야 한다는 것입니다.

6. Build once, promote by digest

나쁜 pipeline은 환경마다 image를 다시 만듭니다.

PR build → staging rebuild → production rebuild

Base tag·package mirror·timestamp가 달라지면 test한 artifact와 production artifact가 달라집니다.

권장 흐름:

  1. Commit에서 image 한 번 build
  2. Test·scan·attestation 완료
  3. Registry digest 확정
  4. Staging은 그 digest 실행
  5. Canary도 같은 digest 실행
  6. Stable pointer만 그 digest로 이동

Environment별 차이는 secret·endpoint·resource·feature flag 같은 runtime config로 제한하고 manifest에 config revision을 남깁니다.

7. Migration은 rollback 가능성을 먼저 설계한다

Image rollback이 DB rollback을 뜻하지 않습니다. Destructive migration을 같은 release에 넣으면 이전 image가 새 schema에서 실행되지 않을 수 있습니다.

Expand–migrate–contract 예시

Release N     : new nullable column/table 추가, old code와 호환
Background    : backfill + 검증
Release N+1   : dual-write, new read를 점진 활성화
Release N+2   : old write 중단, compatibility 관찰
Release N+3   : old column/drop contract migration

원칙:

  • Additive migration과 destructive migration을 분리
  • Migration job을 application replica마다 실행하지 않음
  • Lock duration과 table rewrite를 사전 측정
  • Old image가 new schema를 읽을 수 있는 compatibility window 유지
  • Event payload에는 schema version과 upcaster 전략
  • Roll forward가 rollback보다 안전한 case를 runbook에 명시

Index schema도 비슷합니다. 기존 index를 in-place rewrite하기보다 new snapshot을 만들고 read alias를 전환하면 rollback이 쉬워집니다.

8. Probe 세 종류의 질문을 분리한다

Kubernetes 기준으로 startup probe가 성공하기 전에는 liveness와 readiness가 실행되지 않습니다. Liveness 실패는 container restart, readiness 실패는 traffic 대상 제거로 이어집니다.

/health/live: process를 재시작해야 하는가

  • Event loop가 최소 작업을 수행할 수 있음
  • Fatal internal state가 아님
  • DB·Redis·LLM provider의 순간 장애를 매번 포함하지 않음
  • 매우 빠르고 side effect 없음

/health/ready: 새 request를 받을 수 있는가

  • Startup config와 policy가 load됨
  • Required DB schema revision이 호환됨
  • Connection pool이 생성되고 drain mode가 아님
  • Queue/admission이 명백한 overload 상태면 실패 가능
  • 매 probe마다 LLM completion을 호출하지 않음

/health/startup: 초기화가 끝났는가

  • Large policy·model metadata·local artifact load 완료
  • Migration과 분리된 application startup 확인
  • 느린 시작 때문에 liveness가 조기 restart하지 않게 보호
from fastapi import APIRouter, Response, status

router = APIRouter()


@router.get("/health/live", include_in_schema=False)
async def live() -> dict[str, str]:
    return {"status": "live"}


@router.get("/health/ready", include_in_schema=False)
async def ready(response: Response) -> dict[str, object]:
    checks = await readiness.check(
        timeout_seconds=0.2,  # [예시값]
        include_external_provider=False,
    )
    if not checks.ready:
        response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
    return {"status": "ready" if checks.ready else "not_ready", "checks": checks.safe}

Probe response에는 connection string·host credential·exception detail을 넣지 않습니다. Kubernetes 공식 문서도 liveness 오구성이 cascading failure를 만들 수 있다고 경고합니다. Upstream outage 때 모든 replica를 restart하는 설계는 피합니다.

9. Graceful shutdown은 streaming과 durable run을 구분한다

Deploy가 시작되면 process가 즉시 종료될 수 있다고 가정하면 SSE와 tool call이 잘립니다.

종료 순서

  1. Readiness를 false로 바꿔 새 traffic 차단
  2. 새 run 생성 거부 또는 다른 replica로 routing
  3. Ephemeral SSE에 drain deadline 안내
  4. Durable run ownership lease를 갱신 중단하고 safe handoff
  5. In-flight DB transaction 완료 또는 rollback
  6. Telemetry batch flush에 짧은 timeout
  7. HTTP server 종료

Durable worker는 owner token·lease를 통해 다른 worker가 takeover할 수 있어야 합니다. Ephemeral stream은 client가 terminal을 받지 못할 수 있으므로 idempotency key로 재요청하거나 durable mode로 전환하는 복구 경로를 문서화합니다.

10. Trace는 stage boundary를 보여줘야 한다

OpenTelemetry Python은 trace와 metric API/SDK를 제공하며 library instrumentation과 manual span을 함께 사용할 수 있습니다. Auto instrumentation만 켜면 HTTP·DB call은 보이지만 “왜 이 Agent가 검색을 반복했는가”는 보이지 않을 수 있습니다.

HTTP POST /runs
└─ rag.run
   ├─ rag.route
   ├─ rag.retrieve
   │  ├─ search.sparse
   │  ├─ search.dense
   │  └─ rag.rerank
   ├─ rag.context.build
   ├─ rag.generate
   ├─ rag.tool.validate
   ├─ rag.tool.execute
   └─ rag.persist

Span에 남길 bounded metadata

  • service.version, release ID, image digest suffix
  • Run·session의 비식별 correlation token
  • Model·prompt·index·policy version
  • Retrieval candidate count와 selected count
  • Tool name, validation outcome, termination reason
  • Token usage, retry count, deadline remaining bucket
  • Typed error code와 provider request ID의 safe form

기본적으로 남기지 않을 content

  • Raw user query와 prompt
  • Retrieved document 전문
  • Tool argument·result 전문
  • Authorization·cookie·API key
  • Raw chain-of-thought

OpenTelemetry GenAI semantic convention은 이동·변경될 수 있는 영역입니다. Instrumentation package와 semantic convention version을 pin하고, upgrade 시 dashboard·alert·attribute migration을 contract test합니다. Content 관련 attribute는 민감할 수 있다는 경고를 따르고 기본 capture를 끕니다.

11. Metric은 low-cardinality로 SLO와 연결한다

Run ID·query·document ID를 metric label로 쓰면 cardinality가 폭발합니다. 세부 식별은 trace/log로 보내고 metric label은 제한된 enum으로 둡니다.

Service metric

  • Request rate·error rate·duration
  • Active run·subscriber 수
  • Queue depth·queue put wait
  • DB pool wait·Redis error
  • SSE disconnect·replay count

RAG quality proxy

  • Empty retrieval rate
  • ACL rejected result rate
  • Citation invalid rate
  • Abstention rate
  • Retrieval·rerank latency
  • Context token utilization

Agent control

  • Step count distribution
  • Tool validation deny rate
  • Max-step termination rate
  • Retry·breaker-open rate
  • Cancellation propagation latency

Cost

  • Input/output/cache token
  • Model call per run
  • Retrieval query per run
  • Tool execution per run

Canary 승격 기준은 평균만 보지 않습니다. p95/p99 latency, error budget, tenant·query class별 sample, quality regression을 함께 봅니다.

12. Trace·log·event를 같은 ID로 연결한다

{
  "timestamp": "2026-07-16T03:04:05Z",
  "level": "INFO",
  "message": "run terminated",
  "trace_id": "4bf92f...",
  "run_id_hash": "hmac:71a...",
  "release_id": "rag-agent-2026-07-16.1",
  "termination": "completed",
  "steps": 3,
  "prompt_version": "answer-v12",
  "index_version": "kb-20260716T010000Z"
}

Raw run ID를 모든 backend로 복제하기보다 keyed HMAC 같은 correlation policy를 쓸 수 있습니다. HMAC key rotation과 retention은 별도 관리합니다.

Audit event와 debug log도 구분합니다.

  • Audit: 누가 어떤 tool action을 승인·실행했는지, immutable retention
  • Application log: 운영 상태와 오류, 제한된 retention
  • Trace: 한 request의 latency와 dependency graph, sampling
  • Metric: 집계된 SLO와 alert

13. Canary는 traffic 비율보다 비교 가능한 release가 핵심이다

stable digest A ── 99% traffic
canary digest B ──  1% traffic   [예시값]

단순 random traffic은 query mix가 달라 비교가 어렵습니다. 가능한 방식:

  • Tenant·account allowlist
  • Stable hash로 일정 session을 canary에 고정
  • Synthetic shadow query를 side effect 없이 양쪽에 실행
  • 동일 golden set을 release 전후에 실행

Tool side effect가 있는 Agent request를 shadow로 복제하면 안 됩니다. Shadow는 retrieval·generation read-only mode 또는 synthetic request로 제한합니다.

승격 gate 예시

지표판단예시 기준
HTTP 5xxstable 대비 증가+0.2%p 이내
p95 first-event latencystable 대비+10% 이내
Citation invalid절대 비율0.5% 이하
Policy deny anomalystable 대비유의한 증가 없음
Max-step terminationstable 대비+1%p 이내
Cost/runstable 대비+8% 이내

모두 가상 예시값입니다. 실제 threshold는 baseline variance, traffic volume, product error budget으로 정합니다. Sample이 작으면 자동 승격하지 않고 관찰 시간을 늘립니다.

14. Rollback trigger와 명령을 release 전에 정한다

Rollback trigger 예시:

  • Crash loop·readiness 지속 실패
  • Error budget 급격 소진
  • Citation/ACL policy violation
  • Tool side effect 중복
  • Latency·cost ceiling 초과
  • Event schema consumer incompatibility

Rollback runbook

  1. Canary 승격 중단, new run admission 제한
  2. Incident release manifest와 trace sample 고정
  3. 이전 검증 digest·config bundle 선택
  4. DB schema compatibility 확인
  5. Traffic pointer를 이전 digest로 이동
  6. In-flight durable run의 owner handoff 확인
  7. Probe·smoke·golden sentinel query 실행
  8. Error·latency·quality 회복 확인
  9. 문제 release의 prompt/index/model pointer도 필요한 만큼 복원
  10. Evidence를 보존한 뒤 incident review

latest tag를 다시 pull하는 것은 rollback이 아닙니다. 이전에 검증된 exact digest가 필요합니다.

Rollback이 위험한 경우

  • Destructive DB migration이 이미 완료됨
  • External side effect schema가 바뀜
  • Old image가 new event payload를 읽지 못함
  • Index alias와 prompt가 독립적으로 전진함

이때는 compatible hotfix로 roll forward하는 편이 안전할 수 있습니다. Runbook에 판단 조건을 미리 둡니다.

15. Security는 runtime flag가 아니라 release gate다

최소 기준:

  • Non-root user와 read-only root filesystem
  • Writable path는 /tmp 등 명시된 volume만
  • Secret은 image·manifest·telemetry에 없음
  • Egress destination allowlist와 DNS/TLS policy
  • Tool별 최소 IAM·tenant scope
  • Dependency hash lock·SBOM·vulnerability policy
  • Signed/attested artifact 검증
  • Admin/debug endpoint production 비활성화
  • Prompt·document·tool result logging opt-in
  • Data retention·deletion이 event·trace·backup에 전파

Image scan이 통과해도 prompt injection과 tool authorization을 해결하지 않습니다. Software supply chain과 Agent behavior security는 서로 다른 gate입니다.

16. Release pipeline을 policy as code로 만든다

사람이 체크리스트를 기억하는 대신 machine-readable policy를 둡니다.

release_policy: 1
required:
  static: [lint, typecheck, lockfile, secret_scan]
  test: [unit, contract, fault, integration]
  eval: [retrieval, citation, abstention, policy]
  artifact: [image_smoke, sbom, vulnerability, provenance]
  operations: [manifest, rollback_candidate, migration_compatibility]

promotion:
  staging:
    requires: [all_required]
  canary:
    requires: [staging_smoke, synthetic_eval]
  stable:
    requires: [canary_window, slo_gate, quality_gate]

실제 CI syntax와 별개인 domain policy 예시입니다. Branch protection은 required check 이름을 고정하고 우회·수동 승인 권한을 audit합니다.

17. Release 실패를 빠르게 분류한다

증상먼저 볼 것흔한 원인
모든 pod not-readyreadiness·schema revisionmigration 미완료, config drift
Restart 반복liveness·OOM·startupdependency를 liveness에 포함, memory limit
First event만 느림retrieval·queue·proxy spancold connection, buffering
Citation만 악화index·prompt·reranker versionbehavior bundle drift
Cost만 증가step·retry·token metricloop termination, context 증가
일부 tenant만 실패ACL·routing·canary hashpolicy scope, data version
Disconnect 증가deploy drain·proxy timeouttermination grace 부족
Rollback 후에도 지속DB·index·model pointerimage 밖 release 축 미복원

Dashboard 첫 화면에 release marker와 stable/canary digest를 표시하면 변화 시점과 원인을 연결하기 쉬워집니다.

18. Capstone directory 예시

81편에서 만든 구조에 deployment·evaluation artifact를 더합니다.

rag-agent/
├── src/rag_agent/
│   ├── domain/
│   ├── application/
│   ├── adapters/
│   └── api/
├── tests/
│   ├── unit/
│   ├── contract/
│   ├── integration/
│   ├── replay/
│   └── live/
├── evals/
│   ├── datasets/golden-rag-v7.jsonl
│   ├── policies/release-gates.yaml
│   └── reports/
├── migrations/
├── deploy/
│   ├── compose.yaml
│   ├── otel-collector.yaml
│   ├── probes.yaml
│   └── runbooks/rollback.md
├── releases/
│   └── manifest.schema.json
├── Dockerfile
├── requirements.lock
└── pyproject.toml

Eval report와 image가 같은 workflow의 immutable artifact로 연결돼야 합니다. evals/reports/의 대용량·민감 결과는 Git에 넣지 않고 artifact storage에 digest로 보관할 수 있습니다.

19. 자주 실패하는 배포 전략

1. Production에서 image를 다시 build한다

검증한 artifact와 실행 artifact가 달라집니다. 한 digest를 승격합니다.

2. latest tag만 release record에 남긴다

나중에 같은 binary를 찾을 수 없습니다. Digest와 provenance를 기록합니다.

3. Liveness가 DB·Redis·LLM을 모두 호출한다

Dependency outage가 모든 replica restart로 번집니다. Process 생존과 traffic 준비를 분리합니다.

4. Migration과 destructive drop을 한 번에 한다

이전 image rollback 경로를 없앱니다. Expand–migrate–contract로 compatibility window를 둡니다.

5. Code canary만 보고 prompt·index는 즉시 전역 변경한다

어느 축이 품질을 바꿨는지 알 수 없습니다. Behavior bundle도 version·canary·rollback합니다.

6. Trace에 raw prompt와 document를 기본 저장한다

관찰성이 새로운 data exfiltration 경로가 됩니다. Metadata 우선, content opt-in입니다.

7. HTTP 성공률만 보고 승격한다

근거 없는 답변도 200입니다. Citation·policy·abstention·cost gate를 함께 봅니다.

8. SIGTERM 즉시 process를 종료한다

Stream·transaction·tool receipt가 잘립니다. Readiness drain과 bounded shutdown을 구현합니다.

직접 적용 체크리스트

  • Release manifest에 image·schema·prompt·model·index·policy version이 있다.
  • Base image와 dependency가 검토 가능한 version/digest로 고정돼 있다.
  • Multi-stage·non-root·read-only 실행이 smoke test를 통과한다.
  • CI가 unit·contract·fault·offline eval·image smoke를 순서대로 실행한다.
  • Build한 한 image digest를 staging·canary·stable에 승격한다.
  • SBOM·scan·provenance가 digest와 commit에 연결된다.
  • Migration이 old/new image compatibility window를 유지한다.
  • Liveness·readiness·startup probe의 질문이 다르다.
  • Graceful shutdown이 새 run 차단과 in-flight drain을 수행한다.
  • Trace에 stage·release version은 있고 raw content는 기본 제외된다.
  • Metric label은 low-cardinality이며 SLO·quality·cost를 포함한다.
  • Canary가 stable과 같은 traffic slice에서 비교 가능하다.
  • Rollback candidate digest와 schema compatibility를 release 전에 test한다.
  • Image 밖의 prompt·index·model rollback도 runbook에 있다.

스스로 확인하기

  1. 같은 Git SHA로 build한 두 image가 달라질 수 있는 이유는 무엇인가?
  2. Readiness와 liveness에 같은 dependency check를 넣으면 어떤 failure amplification이 생기는가?
  3. Image digest만 되돌려도 답변 behavior가 복원되지 않을 수 있는 이유는 무엇인가?
  4. Canary에서 HTTP error 외에 citation·policy·cost를 봐야 하는 이유는 무엇인가?
  5. Destructive migration이 rollback 가능 시간을 어떻게 줄이는가?

자주 묻는 질문

Q1. Kubernetes가 아니어도 이 구조가 필요한가요?

필요합니다. Probe 구현 방식과 traffic controller는 달라도 immutable artifact, health/readiness 분리, drain, canary metric, rollback manifest 원칙은 platform과 무관합니다.

Q2. Model provider가 snapshot ID를 주지 않으면 어떻게 하나요?

요청 alias, 호출 시각, provider request ID, 응답 metadata를 기록하고 live golden drift를 감시합니다. 재현 불가능성을 release risk로 명시하고 더 안정적인 model contract나 self-hosting을 검토할 수 있습니다.

Q3. OpenTelemetry trace를 100% 저장해야 하나요?

아닙니다. Error·high-latency·canary는 높은 sampling, 정상 traffic은 head/tail sampling을 적용할 수 있습니다. Audit event는 trace sampling에 의존하지 않는 별도 경로가 필요합니다.

Q4. Canary traffic이 적어 통계가 안 나오면 어떻게 하나요?

승격 시간을 늘리고 synthetic golden traffic과 tenant allowlist를 결합합니다. Sample이 없는데 “문제가 없었다”고 해석하지 않습니다.

Q5. 자동 rollback이 항상 좋은가요?

Crash·명확한 error spike에는 유용하지만 DB migration·side effect가 얽힌 경우 자동 rollback이 더 위험할 수 있습니다. 자동화 가능한 trigger와 사람 판단이 필요한 trigger를 분리합니다.

20. 81~90편 완주 지도

이 시리즈에서 만든 system boundary는 다음과 연결됩니다.

  1. 프로젝트 아키텍처 — domain·application·adapter 분리
  2. LLM API client — message·stream·typed error
  3. 복원력 정책 — deadline·retry·rate limit·breaker
  4. Retrieval adapter — ACL·provenance·budget contract
  5. Context와 citation — evidence-first prompt boundary
  6. Typed runtime — tool registry·bounded state loop
  7. Session 영속화 — PostgreSQL·Redis·idempotency
  8. FastAPI SSE — backpressure·disconnect·replay
  9. Test harness — fake·contract·fault·record/replay
  10. Release engineering — container·CI·telemetry·canary·rollback

처음부터 거대한 framework를 만든 것이 아닙니다. 각 글에서 하나의 failure boundary를 typed contract와 testable port로 바꾸고, 마지막에 같은 digest와 manifest로 묶었습니다.

시리즈 마무리

Production RAG Agent의 품질은 model 한 번의 좋은 답변이 아니라 다음 loop에서 나옵니다.

명시적 contract
→ bounded execution
→ durable evidence
→ deterministic test
→ immutable release
→ observable canary
→ safe rollback
→ 다음 개선

이것으로 Python으로 만드는 Production RAG Agent 10편을 완결합니다. 다음 단계는 기능을 더 붙이는 일이 아니라, 자신의 query·document·failure data로 golden set을 만들고 release마다 같은 gate를 반복하는 것입니다.

검증 정보

  • 글의 성격: Docker·Kubernetes·OpenTelemetry·GitHub 공식 문서 해설 + 작성자 release architecture 제안 + 가상 설정 예시
  • 마지막 검증일: 2026-07-16
  • 검증 기준: Docker multi-stage/digest/non-root, Compose dependency health, Kubernetes startup/liveness/readiness, OpenTelemetry Python·semantic convention, GitHub artifact provenance 개념
  • 실행 주의: Docker base digest, action commit SHA, PostgreSQL·Redis·Collector version은 placeholder를 실제 검증값으로 교체하고 lock update PR에서 갱신
  • 수치 표기: Canary 1%, queue·probe timeout·승격 threshold는 설명용이며 실제 baseline·SLO·traffic으로 결정
  • 경험 표기: Registry·release ID·metric·manifest 값은 실제 운영 정보가 아닌 synthetic 예시

참고자료