
자막 파이프라인에서 GPU 모델을 subprocess로 돌리는 구조를 만들었는데, worker 하나가 엉뚱한 모듈을 import하기 시작했다. ModuleNotFoundError가 뜨는데, 분명 worker venv에 해당 패키지가 설치되어 있었다. 원인은 생각보다 단순했다.
Python의 subprocess.Popen()은 env 파라미터를 안 넘기면 부모 프로세스의 환경변수를 통째로 상속한다. 대부분은 이걸 신경 쓰지 않는다. 그런데 worker마다 독립 venv를 쓰는 구조에서는 이게 함정이 된다.
메인 프로세스의 VIRTUAL_ENV가 worker에 그대로 넘어가면 패키지 import 경로가 꼬인다. PYTHONPATH도 마찬가지. worker가 자기 venv 패키지 대신 메인 프로세스 패키지를 import하는 상황이 벌어진다.
보안 문제도 있다. GPU_API_TOKEN이나 GPU_JUPYTER_PASSWORD 같은 자격증명이 worker subprocess에 고스란히 노출되고 있었다.
blocklist(차단 목록)보다 allowlist(허용 목록)가 안전하다. 새 환경변수가 추가돼도 명시적으로 허용하지 않는 한 자동 차단되니까.
구현은 간단하다. 허용할 키 목록과 prefix 목록을 정의하고, dict comprehension으로 필터링한다.
_WORKER_ENV_ALLOW_PREFIXES = ("CUDA_", "NVIDIA_", "NCCL_", "LC_", "XDG_")
_WORKER_ENV_ALLOW_KEYS = frozenset({
"PATH", "HOME", "USER", "LOGNAME",
"LANG", "TMPDIR", "TEMP", "TMP",
"SHELL", "TERM",
})
def worker_env() -> dict[str, str]:
return {
k: v for k, v in os.environ.items()
if k in _WORKER_ENV_ALLOW_KEYS or k.startswith(_WORKER_ENV_ALLOW_PREFIXES)
}GPU 관련 변수는 종류가 많다. CUDA_VISIBLE_DEVICES, NVIDIA_DRIVER_CAPABILITIES 등 일일이 나열하기보다 prefix 매칭으로 한 번에 허용했다.
사용법도 한 줄이면 된다.
proc = subprocess.Popen(
[str(venv_python), "-m", f"{worker_name}_worker"],
env=worker_env(), # 핵심: 환경변수 명시적 필터링
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)모든 worker에 worker_env()를 쓸 수 있는 건 아니다. 텍스트 교정 단계에서는 Claude CLI를 호출하는데, API 키 등의 환경변수가 필요하다. 이 경우에는 환경변수를 대부분 넘기되 CLAUDECODE만 빼는 별도 로직을 썼다. Claude Code가 자기 자신을 재귀 호출하는 걸 막기 위해서다.
환경변수 격리를 하면서 코드에 박혀 있던 자격증명도 정리했다.
# Before
GPU_API_URL = "http://100.90.74.57:9090"
GPU_API_TOKEN = "gpu-widget-2026"
# After
GPU_API_URL = os.environ.get("GPU_API_URL", "http://100.90.74.57:9090")
GPU_API_TOKEN = os.environ.get("GPU_API_TOKEN", "gpu-widget-2026")HTTP 클라이언트도 urllib에서 httpx로 바꿨다. 타임아웃 처리가 깔끔해지고 코드량도 줄었다.
subprocess, Docker, K8s 등 어떤 계층이든 환경변수 격리는 신경 써야 한다. 기본 동작이 "전부 상속"인 경우가 많고, 그게 보통 원하는 동작이 아니다.
blocklist는 새 변수가 추가될 때 놓치기 쉽다. allowlist를 쓰면 "명시적으로 허용한 것만 통과" 원칙이 지켜져서 실수를 방지할 수 있다. 코드 10줄이면 충분하다.