1 왜 환경변수를 알아야 하는가
MINERVA 시리즈 여러 편이 환경변수 운영에 의존한다.
| 글 | 환경변수 사용 |
|---|---|
| 04 FastAPI 서빙 | _load_env_files() 우선순위 (.env > .env.cloud > .env.local), WARMUP_ON_STARTUP, MINERVA_LOG_LEVEL |
| 07-0 프로덕션 배포 | 환경별 .env 파일, Docker --env-file |
| 11-0 Config 의존성 | field(default_factory=lambda: os.getenv(...)), YAML ${VAR:default} 치환 |
| 11-1 Config 운영 패턴 | Docker/K8s 시크릿 주입, /health/config 검증 |
환경변수는 코드와 설정을 분리하고 시크릿을 코드에서 격리하는 표준 메커니즘이다. 12-factor app(클라우드 친화 앱 12원칙)의 3번 원칙이 “Config는 환경변수에 저장”인 이유.
2 12-factor의 핵심 — 코드와 설정의 분리
운영체제 또는 프로세스가 보유하는 key=value 쌍의 집합. 프로세스 시작 시 부모로부터 상속되며, 자식 프로세스에 전달된다. Python에서는 os.environ dict로 접근한다.
같은 코드를 다른 환경(개발·스테이징·운영)에서 다르게 동작시키려면 다음 셋 중 하나다.
| 방법 | 장점 | 단점 |
|---|---|---|
| 코드 안에 if/else | 단순 | 환경 추가 시 코드 수정, 운영에 개발 자격증명 노출 위험 |
| 설정 파일 (YAML/JSON) | 구조화 | 시크릿이 파일에 들어가면 git 노출 위험 |
| 환경변수 | 코드 무수정, 시크릿 격리, OS·컨테이너·CI 모두 표준 지원 | 타입이 모두 string, 구조 표현 어려움 |
환경변수 단독으로는 한계가 있어 (string 타입, 구조 부족), MINERVA에서는 환경변수 + YAML 파일 결합을 사용한다 — 시크릿은 환경변수, 구조화된 설정은 YAML.
3 os.environ — 표준 라이브러리
import os
# 1. 읽기 — getenv (없으면 None 또는 default)
api_key = os.getenv("AZURE_OPENAI_API_KEY")
log_level = os.getenv("LOG_LEVEL", "INFO") # default 지정
# 2. 읽기 — environ dict (없으면 KeyError)
api_key = os.environ["AZURE_OPENAI_API_KEY"] # 필수 시 명시적 KeyError로 빠른 실패
# 3. 쓰기 (현재 프로세스만, 부모 영향 X)
os.environ["TEMP_VAR"] = "value"주의: 모든 값이 string이다. boolean·int·list 표현은 직접 파싱.
warmup = os.getenv("WARMUP_ON_STARTUP", "true").lower() in ("true", "1", "yes")
port = int(os.getenv("PORT", "8000"))
allowed_origins = os.getenv("CORS_ORIGINS", "").split(",")타입 변환을 매번 직접 작성하면 실수가 누적된다 → Pydantic BaseSettings로 한 곳에 모은다 (아래).
4 .env 파일 — python-dotenv
로컬 개발에서 환경변수를 매번 셸에 export하기 번거롭다. .env 파일에 모아두고 프로세스 시작 시 자동 로드한다.
# .env (프로젝트 루트)
AZURE_OPENAI_API_KEY=sk-abc123...
AZURE_OPENAI_ENDPOINT=https://my-resource.openai.azure.com/
LOG_LEVEL=DEBUG
WARMUP_ON_STARTUP=falsefrom dotenv import load_dotenv
load_dotenv() # 현재 디렉토리·상위에서 .env 자동 탐색
# 이제 os.getenv("AZURE_OPENAI_API_KEY")가 작동4.1 환경별 .env 파일 — 분리 운영
같은 프로젝트에 여러 환경이 있으면 파일을 분리한다.
프로젝트/
├── .env.local # 로컬 개발 (Ollama, 디버그)
├── .env.cloud # 클라우드 개발/스테이징 (Azure)
├── .env.production # 운영 (시크릿 매니저에서 주입)
└── .env.example # 템플릿 — git에 커밋, 실제 값은 비움
5 MINERVA의 우선순위 패턴 — _load_env_files()
MINERVA 04편이 다음 패턴을 사용한다.
from pathlib import Path
from dotenv import load_dotenv
def _load_env_files() -> None:
repo_root = Path(__file__).resolve().parent.parent.parent.parent
for env_name in [".env", ".env.cloud", ".env.local"]:
env_path = repo_root / env_name
if env_path.exists():
load_dotenv(env_path, override=False) # 시스템 env가 우선
return # 첫 번째 파일만 로드핵심 설계:
- 첫 번째 존재하는 파일만 로드 —
.env > .env.cloud > .env.local순서. merge 아님 override=False— Docker--env-file이나 K8s Secret으로 주입된 시스템 env가.env파일보다 우선. 운영에서는.env파일이 없거나 무시되도록
이 패턴이 운영·개발 둘 다 한 코드로 처리되게 한다.
| 환경 | 사용 파일 | 시스템 env | 결과 |
|---|---|---|---|
| 로컬 개발 | .env.local 존재 |
비활성 | .env.local에서 모든 값 |
| Docker | .env* 없음 |
--env-file로 주입 |
시스템 env만 |
| K8s | .env* 없음 |
Secret으로 주입 | 시스템 env만 |
| 클라우드 VM | .env.cloud 존재 |
일부 override | 시스템 env가 있으면 시스템, 없으면 .env.cloud |
6 Pydantic BaseSettings — 타입 안전 환경변수
os.getenv + 수동 파싱이 누적되면 한 곳에 모으는 게 좋다. pydantic-settings(Pydantic 2.x)가 이를 자동화한다.
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False, # AZURE_X와 azure_x 동일 인식
)
# Azure OpenAI
azure_openai_api_key: str
azure_openai_endpoint: str
azure_openai_api_version: str = "2024-12-01-preview"
# 운영 토글
log_level: str = "INFO"
warmup_on_startup: bool = True # 자동 boolean 변환
port: int = 8000 # 자동 int 변환
cors_origins: list[str] = ["http://localhost:5173"] # 자동 list 변환
settings = Settings() # 환경변수에서 자동 로드
print(settings.azure_openai_api_key) # 타입 안전 접근자동 처리되는 것:
- 타입 변환:
WARMUP_ON_STARTUP=true→boolTrue - 검증: 필수 필드 누락 시
ValidationError로 즉시 실패 (운영에서 시크릿 누락 catch) - default 값: 환경변수 없으면 dataclass default 사용
- case insensitive: 대소문자 자유
이 패턴이 MINERVA 11-0편 Config 의존성의 RAGConfig와 동일한 철학이다.
7 시크릿 관리 — 절대 git에 commit하지 않는다
새 개발자 온보딩:
git history에 한 번 들어간 시크릿은 force-push로 지워도 추적이 어렵다. 즉시 시크릿을 rotation(새 값으로 교체)하고 영향 범위를 점검해야 한다. 처음부터 commit하지 않는 게 유일한 안전책이다.
GitHub의 secret scanning이 일부 패턴(AWS key, Stripe key 등)을 자동 감지하지만 모든 시크릿을 catch하지 못한다. pre-commit hook으로 git-secrets나 detect-secrets 도구를 추가하는 것이 권장된다.
8 운영 환경 — 시크릿 매니저
운영에서는 .env 파일을 두지 않는다. 시크릿 매니저에서 가져와 컨테이너 시작 시 환경변수로 주입한다.
| 매니저 | 통합 |
|---|---|
| Azure Key Vault | Container Apps secretRef, AKS CSI Driver |
| AWS Secrets Manager | ECS Task Definition, EKS External Secrets |
| GCP Secret Manager | Cloud Run env, GKE External Secrets |
| HashiCorp Vault | Kubernetes Vault Agent Injector |
MINERVA 11-1편의 K8s 예시:
# K8s Secret 리소스 생성
apiVersion: v1
kind: Secret
metadata:
name: minerva-secrets
type: Opaque
stringData:
AZURE_OPENAI_API_KEY: "sk-..." # 실제 운영에서는 외부 매니저에서 동기화
---
# Deployment에서 envFrom으로 한 번에 주입
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: api
envFrom:
- secretRef:
name: minerva-secrets9 CI/CD에서 시크릿 — GitHub Secrets
MINERVA 07-1편 GitHub Actions의 패턴.
# .github/workflows/integration.yml
jobs:
integration:
runs-on: ubuntu-22.04
env:
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} # GitHub Secrets
AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
steps:
- run: pytest -m integrationGitHub Secrets는:
- 저장소 또는 organization 단위 설정
- 워크플로 로그에 출력 시 자동 마스킹 (
***) - pull request from fork에는 노출 안 됨 (보안 기본값)
그러나 echo $SECRET 같은 의도적 노출은 막지 못한다. 시크릿을 사용하는 step에서 의도치 않게 stdout에 출력하지 않도록 주의.
10 Docker에서 환경변수 주입
# 1. --env-file
docker run --env-file .env.production -p 8000:8000 minerva:latest
# 2. -e 개별
docker run -e AZURE_OPENAI_API_KEY="sk-..." minerva:latest
# 3. docker-compose# docker-compose.yml
services:
api:
image: minerva:latest
env_file:
- .env.production
environment:
WARMUP_ON_STARTUP: "true" # env_file 위에 추가/overrideENV 지시자는 이미지 레이어에 영구 baking된다. 이미지를 공유하는 순간 시크릿 노출. 환경변수는 항상 런타임에 주입한다.
MINERVA 11-1편 Reproducible Build의 원칙: 이미지에는 commit hash·build_date 같은 변경 빈도 낮은 메타만 baking, 시크릿은 runtime injection.
11 YAML에서 환경변수 참조 — ${VAR:default} 치환
MINERVA 11-0편이 RAGConfig YAML에서 환경변수를 참조하는 패턴.
# data/configs/default.yaml
embedding:
model: ${AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME:text-embedding-ada-002}
api_version: ${AZURE_OPENAI_API_VERSION:2024-08-01-preview}import re, os, yaml
def _replace_env(content: str) -> str:
def replace(match):
var_expr = match.group(1)
if ":" in var_expr:
name, default = var_expr.split(":", 1)
else:
name, default = var_expr, ""
return os.getenv(name, default)
return re.sub(r"\$\{([^}]+)\}", replace, content)
with open("config.yaml") as f:
content = _replace_env(f.read())
config = yaml.safe_load(content)${VAR:default} 패턴은 docker-compose, ansible, GitHub Actions 등 여러 도구에서 표준에 가깝다.
12 자주 발생하는 오류 패턴
CORRECT:
시크릿을 코드에 넣으면 git history에 영구 노출. os.environ으로 환경변수에서 받고, .env 파일은 .gitignore에 등록.
from services.api.routers import qna_chatbot # router import 시 모듈 코드 실행됨
# → os.getenv("AZURE_KEY") 호출됨
# → 환경변수 아직 로드 안 됨
from dotenv import load_dotenv
load_dotenv() # 너무 늦음CORRECT:
from dotenv import load_dotenv
load_dotenv() # 모든 import 전에 로드
from services.api.routers import qna_chatbot모듈 import 시점에 os.getenv()가 호출되면 그 시점에 환경변수가 있어야 한다. load_dotenv()는 모든 router import 전에.
CORRECT:
debug = os.getenv("DEBUG", "").lower() in ("true", "1", "yes")
# 또는 Pydantic BaseSettings 사용 — 자동 변환환경변수는 모두 string. "false"도 비어있지 않은 문자열이라 bool()은 True. 명시적 비교 또는 Pydantic 사용.
CORRECT:
override=True는 .env 파일이 시스템 환경변수를 덮어쓴다. 운영 컨테이너에서 K8s Secret으로 주입한 값을 .env 파일이 덮어쓰는 사고가 발생할 수 있다. False가 안전한 기본값.
13 정리
| 도구 | 사용처 | 비고 |
|---|---|---|
os.environ / os.getenv |
표준 라이브러리, 모든 환경에서 작동 | 모든 값 string |
python-dotenv |
로컬 개발 .env 파일 자동 로드 |
운영에서는 시스템 env가 우선 |
pydantic-settings |
타입 안전 환경변수 + 검증 + default | 한 곳에 모아 관리 |
.env.example |
템플릿 (git 커밋), 실제 값은 비움 | 새 개발자 온보딩 |
.gitignore |
.env, .env.* 등록 필수 |
시크릿 commit 방지 |
| GitHub Secrets | CI/CD에서 시크릿 주입 | 워크플로 로그 자동 마스킹 |
Docker --env-file |
컨테이너 환경변수 주입 | Dockerfile ENV에 굽지 말 것 |
| K8s Secret + envFrom | 운영 클러스터 시크릿 | 외부 매니저(Vault, Key Vault)와 동기화 |
YAML ${VAR:default} |
설정 파일에서 환경변수 참조 | 환경별 일관 패턴 |
핵심 원칙 5가지:
- 시크릿은 코드·이미지·git에 절대 넣지 않는다
- 로컬은
.env.local, 운영은 시스템 env (시크릿 매니저에서) load_dotenv(override=False)— 시스템 env가 우선- 타입·검증은 Pydantic BaseSettings에 위임
.env.example로 새 개발자 온보딩
14 응용 분야
| MINERVA 시리즈 사용처 | 본 글 절 |
|---|---|
04 _load_env_files() 우선순위 |
MINERVA 우선순위 패턴 + override=False |
11-0 field(default_factory=lambda: os.getenv(...)) |
os.environ + Pydantic 패턴 |
11-0 YAML ${VAR:default} 치환 |
YAML 환경변수 참조 |
11-1 Docker --env-file + K8s Secret |
Docker·K8s 환경변수 주입 |
| 07-1 GitHub Secrets | CI/CD 시크릿 |
15 관련 주제
선행 학습
- Python typing 심화 — Pydantic BaseSettings의 타입 시스템 토대
- Pydantic — BaseSettings의 부모
Tier 1 완료
본 글로 Tier 1(async/await, pytest, typing 심화, 환경변수) 완성. 다음은 Tier 2 (GitHub Actions·YAML·logging·Docker Compose).
MINERVA 시리즈 응용
- MINERVA FastAPI 서빙 (04) —
_load_env_files()우선순위 - MINERVA Config 의존성 (11-0) — 환경변수 의존 필드
- MINERVA Config 운영 패턴 (11-1) — Docker/K8s 시크릿
- MINERVA CI/CD GitHub Actions (07-1) — GitHub Secrets