MINERVA Config 운영 패턴 — Hot Reload·시크릿 주입·컨테이너 고정

정적 추적을 끝낸 뒤, 운영에서 Config를 어떻게 다루는가

11-0편이 .env → YAML → A/B override의 정적 전파 흐름을 추적했다면, 본 글은 그 위에 얹히는 운영 패턴 3가지를 다룬다. Hot Reload 가능성과 한계, Docker/K8s 시크릿 주입 패턴, 그리고 사용자 제안으로 추가된 “도커로 Config를 고정시켜 재현 가능한 빌드”까지 정리한다.

Agent
저자

Kwangmin Kim

공개

2026년 05월 06일

1 본 글의 위치

11-0편 Config 의존성 추적이 환경변수·YAML·A/B override의 정적 전파 흐름을 추적했다면, 본 글은 운영에서 그 흐름을 어떻게 다루는지를 다룬다. 세 가지 주제로 나뉜다:

  1. Hot Reload 가능성과 한계 — 어디까지 재시작 없이 반영 가능한가
  2. Docker/K8s 시크릿 주입 패턴 — 시크릿을 이미지에 굽지 않는 방법
  3. 도커로 Config 고정 — Reproducible Build — 같은 이미지가 같은 동작을 보장하는 빌드

11-0편이 “Config가 어떻게 흘러가는가”라면 본 글은 “그 흐름을 어떻게 운영하는가”이다.

선행 학습

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.53.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 installpip 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 관련 주제

선행 학습

Phase C-1 후속

Phase C-9 연결 (예정)

  • C34 관측성 설계 — /health/build 엔드포인트가 분산 추적과 결합되는 패턴
  • C36 보안과 접근 제어 — 시크릿 rotation, 이미지 보안 스캔

다른 카테고리 연결

Subscribe

Enjoy this blog? Get notified of new posts by email: