1 본 글의 위치
11-0편 Config 의존성 추적이 환경변수·YAML·A/B override의 정적 전파 흐름을 추적했다면, 본 글은 운영에서 그 흐름을 어떻게 다루는지를 다룬다. 세 가지 주제로 나뉜다:
- Hot Reload 가능성과 한계 — 어디까지 재시작 없이 반영 가능한가
- Docker/K8s 시크릿 주입 패턴 — 시크릿을 이미지에 굽지 않는 방법
- 도커로 Config 고정 — Reproducible Build — 같은 이미지가 같은 동작을 보장하는 빌드
11-0편이 “Config가 어떻게 흘러가는가”라면 본 글은 “그 흐름을 어떻게 운영하는가”이다.
- 11-0편 Config 의존성 추적 — Layer 1~3 정적 전파
- 04편 FastAPI 서빙 — env 우선순위·warmup
- 07편 프로덕션 배포 — Docker 멀티스테이지·env 파일 우선순위
2 Hot Reload 가능성 분석
설정 변경 후 프로세스 재시작 없이 반영하려면 어떤 부분이 가능한지 정리한다.
2.1 가능한 부분
YAML 파일 다시 읽기: get_config(profile_name)을 다시 호출하면 디스크에서 YAML을 다시 읽는다. 캐시 없음.
실험 YAML 다시 읽기: load_experiments()도 매번 디스크에서 읽는다. 다음 요청부터 새 실험이 즉시 적용된다.
2.2 불가능한 부분
Agent 캐시 갱신: _agent_cache는 (exp_name, arm_id) 키로 인스턴스를 보관한다. 같은 키의 실험 override가 변경되어도 캐시된 agent는 이전 config를 그대로 가진다.
# 현재 동작
key = (exp_name, arm_id)
agent = _agent_cache.get(key)
if agent is not None:
return agent # ← 이전 config 그대로 사용됨Retriever 캐시 갱신: agent 내부 self._retrievers도 동일하게 인스턴스를 캐시한다. config 변경이 반영되려면 retriever 재생성이 필요하다.
환경변수 재읽기: field(default_factory=lambda: os.getenv(...)) 패턴은 인스턴스 생성 시점에만 evaluation된다. os.environ을 변경해도 기존 인스턴스는 변하지 않는다.
2.3 Hot Reload 패턴
설정을 hot-reload하려면 다음 변경이 필요하다.
# 권장: 파일 mtime 기반 무효화
import os
class ConfigCache:
def __init__(self):
self._cache = {}
self._mtimes = {}
def get_config(self, profile_name: str) -> RAGConfig:
path = CONFIG_DIR / f"{profile_name}.yaml"
current_mtime = os.path.getmtime(path)
if (profile_name not in self._cache
or self._mtimes[profile_name] != current_mtime):
self._cache[profile_name] = RAGConfig.from_yaml_file(str(path))
self._mtimes[profile_name] = current_mtime
return self._cache[profile_name]
# Agent 캐시도 mtime 기반 무효화
def _build_agent(user_id=None, force_arm=None):
config, exp_name, arm_id, overrides = resolve_config_for_user(...)
config_signature = hash(json.dumps(config.to_dict(), sort_keys=True))
key = (exp_name, arm_id, config_signature) # config 변경 시 자동 새 캐시
...trade-off: config_signature 계산 오버헤드(매 요청 ~1ms), 메모리 증가(이전 config의 agent도 GC 전까지 잔류).
3 Docker/K8s 시크릿 주입 패턴
운영 환경에서 권장되는 시크릿 관리 패턴을 정리한다.
3.1 Docker
# Dockerfile에는 시크릿을 굽지 않는다
COPY . /app
WORKDIR /app
CMD ["uvicorn", "services.api.main:app", "--host", "0.0.0.0", "--port", "8000"]# 실행 시 --env-file로 주입
docker run --env-file .env.production -p 8000:8000 minerva:latest
# 또는 Compose
# docker-compose.yml
services:
api:
image: minerva:latest
env_file:
- .env.production.env.production은 git에 커밋하지 않고 시크릿 매니저(Azure Key Vault, AWS Secrets Manager 등)에서 가져온다.
3.2 Kubernetes
apiVersion: v1
kind: Secret
metadata:
name: minerva-secrets
type: Opaque
stringData:
AZURE_OPENAI_API_KEY: "..."
AZURE_VECTOR_STORE_API_KEY: "..."
---
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: api
image: minerva:latest
envFrom:
- secretRef:
name: minerva-secrets
env:
- name: WARMUP_ON_STARTUP
value: "true"
- name: MINERVA_LOG_LEVEL
value: "INFO"K8s Secret은 os.environ에 그대로 노출되므로 04편의 _load_env_files() override=False 동작과 호환된다(.env 파일이 컨테이너에 없으면 시스템 env만 사용).
3.3 검증 패턴
기동 직후 핵심 환경변수가 채워졌는지 확인하는 헬스 체크:
@router.get("/health/config")
def health_config():
required = [
"AZURE_OPENAI_ENDPOINT",
"AZURE_OPENAI_API_KEY",
"AZURE_VECTOR_STORE_ENDPOINT",
]
missing = [v for v in required if not os.getenv(v)]
if missing:
raise HTTPException(503, detail={"missing_env": missing})
return {"status": "ok"}이 헬스 체크가 K8s readinessProbe에 연결되면 시크릿 누락 상태에서 트래픽이 흐르지 않는다.
4 도커로 Config 고정 — Reproducible Build
11-0편의 정적 Config 흐름과 본 글 앞 절의 시크릿 주입은 운영 시점의 Config를 다뤘다. 그러나 그 운영의 토대인 이미지 자체가 재현 가능한가는 별개 문제다. “어제 돈 이미지가 오늘 다시 빌드해도 같은 동작을 한다”는 보장이 없으면 디버깅·롤백·감사 모두 어려워진다.
4.1 비고정의 위험 — 같은 Dockerfile, 다른 결과
# 위험한 Dockerfile
FROM python:3.11-slim
COPY pyproject.toml .
RUN pip install poetry && poetry install이 Dockerfile은 같은 commit에서 빌드해도 다른 결과를 낳을 수 있다.
| 비고정 요소 | 영향 |
|---|---|
python:3.11-slim 태그 |
같은 태그도 patch 버전이 갱신됨 (3.11.5 → 3.11.6) — 동작 미세 변화 |
poetry install (lock 미반영) |
poetry가 의존성 그래프를 매번 재계산. 인덱스 변경 시 다른 버전 |
| pip 인덱스의 transitive deps | 직접 의존성은 같아도 trans 의존성이 갱신될 수 있음 |
RUN apt-get update |
OS 패키지가 매 빌드 다른 버전 |
| ALBERT weight·RAG 인덱스 | 외부에서 mount되면 이미지마다 다른 데이터 |
이 비고정성이 누적되면 “개발에서는 잘 되는데 운영에서 안 된다”는 현상의 근본 원인이 된다.
4.2 고정 5요소
| 요소 | 방법 |
|---|---|
| Base image digest | FROM python:3.11.10-slim-bookworm@sha256:abc123... (태그 + SHA digest) |
| Python 의존성 lock | poetry export -f requirements.txt --output requirements.txt 결과를 commit 후 이미지에 복사 |
| Node 의존성 lock | package-lock.json 또는 pnpm-lock.yaml 커밋, npm ci로 lock 그대로 설치 |
| 데이터 자산 version | ALBERT weight·RAG 인덱스에 version label, volume mount 시점에 검증 |
| 빌드 메타데이터 | LABEL org.opencontainers.image.revision=$COMMIT_SHA 등을 이미지에 baking |
4.3 고정한 Dockerfile 예시
# Stage 1 — 프론트엔드 빌드 (Node 22 alpine pinned)
FROM node:22.11.0-alpine@sha256:def456... AS frontend
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci # lock 그대로 설치
COPY frontend/ ./
RUN npm run build
# Stage 2 — Python 런타임 (3.11.10 slim pinned)
FROM python:3.11.10-slim-bookworm@sha256:abc123... AS runtime
# 빌드 메타데이터 baking
ARG COMMIT_SHA
ARG BUILD_DATE
LABEL org.opencontainers.image.revision="${COMMIT_SHA}" \
org.opencontainers.image.created="${BUILD_DATE}"
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential=12.* \
curl=7.88.* \
&& rm -rf /var/lib/apt/lists/*
# poetry export 결과를 commit해두고 그것을 사용
COPY requirements.lock.txt ./
RUN pip install --no-cache-dir --require-hashes -r requirements.lock.txt
COPY src/ ./src/
COPY data/configs/ ./data/configs/
COPY --from=frontend /app/frontend/dist ./static
# 빌드 시점에 commit hash를 환경변수로 주입
ENV BUILD_COMMIT_SHA="${COMMIT_SHA}"
ENV BUILD_DATE="${BUILD_DATE}"
CMD ["uvicorn", "services.api.main:app", "--host", "0.0.0.0", "--port", "8000"]requirements.lock.txt는 다음으로 생성한다:
poetry export -f requirements.txt --without-hashes --output requirements.txt
poetry export -f requirements.txt --output requirements.lock.txt # hash 포함--require-hashes 플래그가 transitive deps의 hash까지 검증한다. 한 번 만들어진 이미지는 같은 hash 셋의 패키지로만 만들어진다.
4.4 빌드 시점 메타데이터 — 운영에서 추적 가능
# CI/CD 파이프라인 (GitHub Actions 예시)
docker build \
--build-arg COMMIT_SHA=$(git rev-parse HEAD) \
--build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
-t minerva:${GITHUB_SHA::7} \
-t minerva:latest \
.런타임에서 어느 commit 빌드인지 확인:
@router.get("/health/build")
def health_build():
return {
"commit": os.getenv("BUILD_COMMIT_SHA", "unknown"),
"build_date": os.getenv("BUILD_DATE", "unknown"),
}이 엔드포인트가 monitoring/ab 라우터의 메트릭과 결합되면 “특정 commit 이후 응답 품질이 변화했다”는 회귀를 즉시 추적할 수 있다.
4.5 데이터 자산 — 이미지에 굽지 않되 version pin
ALBERT weight·RAG 인덱스는 이미지에 포함하지 않는다(이미지 거대화). 대신 volume mount + version 라벨로 고정한다:
# docker-compose.yml
services:
api:
image: minerva:${GIT_SHA}
volumes:
- type: volume
source: minerva-models-v2.3.1 # 버전 명시
target: /app/data/models:ro
- type: volume
source: minerva-faiss-index-2026q2 # 인덱스 snapshot 버전
target: /app/data/faiss_index:ro
environment:
EXPECTED_ALBERT_VERSION: "v2.3.1"
EXPECTED_INDEX_VERSION: "2026q2"기동 시 health_build 또는 별도 검증 스크립트가 mount된 데이터의 version 라벨을 읽어 환경변수와 비교, 불일치 시 기동 거부.
4.6 비고정 vs 고정 비교
| 측면 | 비고정 빌드 | 고정 빌드 |
|---|---|---|
| 같은 commit, 다른 시점 빌드 | 결과 다를 수 있음 | 결과 동일 보장 |
| 회귀 디버깅 | “이미지 다시 빌드해서 재현” 어려움 | 같은 이미지 다시 띄우면 재현 |
| 롤백 | tag 기반, 미세 차이 가능 | digest pinning으로 정확 |
| 감사 | “이 이미지에 뭐가 들어갔는지” 추적 어려움 | LABEL + lock 파일로 명시적 |
| CI 실행 시간 | trans deps 매번 resolve | lock 파일 그대로 설치, 더 빠름 |
| 보안 | 모든 trans deps의 새 취약점 즉시 반영 | lock 파일을 의도적으로 갱신해야 반영 (의식적 결정) |
마지막 행이 trade-off다 — 고정은 의식적 갱신이 필요하다. CVE 발생 시 lock 파일 자동 갱신 PR을 봇(예: Renovate, Dependabot)에 맡기는 것이 권장 운영 패턴이다.
4.7 MINERVA의 현실적 시작점
| 단계 | 작업 | 효과 |
|---|---|---|
| 1 | poetry export --output requirements.lock.txt 매 PR마다 갱신 |
의존성 lock 시작 |
| 2 | Dockerfile의 pip install을 pip install --require-hashes -r requirements.lock.txt로 변경 |
빌드 결정성 확보 |
| 3 | base image에 SHA digest 추가 | OS·Python 버전 고정 |
| 4 | LABEL + ENV로 commit/build_date baking | 운영 추적성 |
| 5 | /health/build 엔드포인트 추가, 모니터링에 commit 컬럼 |
회귀 분석에 활용 |
| 6 | 데이터 자산 version 라벨 + 기동 시 검증 | 모델·인덱스 정합성 |
| 7 | Renovate/Dependabot 자동 PR — lock 의식적 갱신 | 보안 패치 회수 |
11-0편이 “Config는 어떻게 흘러가는가”였다면, 본 절은 그 Config가 담긴 그릇(이미지)을 어떻게 변하지 않게 만드는가의 답이다.
5 정리
| 영역 | 핵심 |
|---|---|
| Hot Reload 가능 | YAML 파일 다시 읽기, 실험 YAML 다시 읽기 |
| Hot Reload 불가능 | agent 캐시, retriever 캐시, env 기본값 |
| Hot Reload 패턴 | 파일 mtime 기반 무효화 + config_signature를 캐시 키에 추가 |
| Docker 시크릿 | --env-file로 주입, Dockerfile에 굽지 않음 |
| K8s 시크릿 | Secret 리소스 + envFrom, readinessProbe로 누락 차단 |
| Reproducible Build | base image digest, lock 파일 + --require-hashes, 메타데이터 baking, 데이터 version pin |
| 운영 추적 | /health/build + monitoring 메트릭에 commit 컬럼 결합 |
11-0편의 정적 추적과 본 글의 운영 패턴을 종합하면 Config 흐름이 개발 → CI 빌드 → 컨테이너 → 런타임 → Hot Reload까지 일관되게 추적된다.
6 관련 주제
선행 학습
- Config 의존성 추적 (11-0) — Layer 1~3 정적 전파
- FastAPI 서빙 레이어 (04) — env 우선순위·warmup
- 프로덕션 배포 (07) — Docker 멀티스테이지
Phase C-1 후속
- 테스트 전략 분석 (12) — 본 운영 패턴의 테스트 가능성
Phase C-9 연결 (예정)
- C34 관측성 설계 —
/health/build엔드포인트가 분산 추적과 결합되는 패턴 - C36 보안과 접근 제어 — 시크릿 rotation, 이미지 보안 스캔
다른 카테고리 연결
- Docker 기초 — 멀티스테이지·Compose 일반