무색
기술블로그
에세이
연구
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
subprocess 환경변수, 기본값이 함정이다 — isometric 스타일 키 비주얼
자막 파이프라인
2026. 1. 15.

subprocess 환경변수, 기본값이 함정이다

pythonsubprocesssecurityenvironment-variables

자막 파이프라인에서 GPU 모델을 subprocess로 돌리는 구조를 만들었는데, worker 하나가 엉뚱한 모듈을 import하기 시작했다. ModuleNotFoundError가 뜨는데, 분명 worker venv에 해당 패키지가 설치되어 있었다. 원인은 생각보다 단순했다.

VIRTUAL_ENV가 새어나간다

Python의 subprocess.Popen()은 env 파라미터를 안 넘기면 부모 프로세스의 환경변수를 통째로 상속한다. 대부분은 이걸 신경 쓰지 않는다. 그런데 worker마다 독립 venv를 쓰는 구조에서는 이게 함정이 된다.

메인 프로세스의 VIRTUAL_ENV가 worker에 그대로 넘어가면 패키지 import 경로가 꼬인다. PYTHONPATH도 마찬가지. worker가 자기 venv 패키지 대신 메인 프로세스 패키지를 import하는 상황이 벌어진다.

보안 문제도 있다. GPU_API_TOKEN이나 GPU_JUPYTER_PASSWORD 같은 자격증명이 worker subprocess에 고스란히 노출되고 있었다.

allowlist로 필터링하기

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줄이면 충분하다.

자주 묻는 질문

subprocess에 환경변수를 전부 넘기면 안 되나요?
PYTHONPATH나 VIRTUAL_ENV가 전달되면 부모 프로세스의 Python 환경이 자식에게 오염됩니다. 독립 venv로 격리한 의미가 사라지고, 패키지 충돌이나 import 에러가 발생합니다.
allowlist 방식과 blocklist 방식의 차이는?
blocklist는 새 환경변수가 추가될 때마다 차단 목록을 갱신해야 합니다. allowlist는 명시적으로 허용한 것만 통과시켜, 예상치 못한 변수가 유출되는 것을 원천 차단합니다.
CUDA 관련 환경변수는 왜 통과시키나요?
GPU 워커가 CUDA 라이브러리와 드라이버에 접근하려면 CUDA_HOME, NVIDIA_VISIBLE_DEVICES 등이 필요합니다. 이들을 차단하면 GPU 연산 자체가 불가능해집니다.
자막 파이프라인(4/10)
Prev

Claude Code headless 모드의 JSON 파싱 함정 두 가지

Next

Forced Aligner 토큰에서 원문 복원하기: 한국어 자막의 자연스러운 줄바꿈