왜 환경변수가 배포를 무너뜨리는가
대부분의 배포 사고는 코드가 아니라 환경변수에서 시작한다. DATABASE_URL 오타, 스테이징 값이 프로덕션에 섞여 들어감, 필수 키 누락, 숫자여야 할 값에 빈 문자열. 이런 문제는 컴파일 단계에서 잡히지 않고 런타임에 터진다. 최악은 애플리케이션이 죽지도 않고 잘못된 값으로 조용히 뜨는 경우다.
근본 원인은 환경변수가 "타입 없는 전역 문자열"이라는 데 있다. CD 파이프라인은 이 값들을 검증 없이 컨테이너에 주입하고, 문제는 배포 이후에야 드러난다. 해결책은 배포 전에 스키마로 검증하고, 통과하지 못하면 파이프라인을 멈추는 스키마 게이트를 두는 것이다.
스키마로 계약을 정의한다
먼저 서비스가 요구하는 환경변수의 계약을 명시적으로 선언한다. Python이라면 pydantic이 실무에서 가장 편하다. 필수/선택, 타입, 형식 제약을 한곳에 모은다.
from pydantic import BaseModel, Field, PostgresDsn, field_validator
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
ENV: str = Field(pattern="^(dev|staging|prod)$")
DATABASE_URL: PostgresDsn
REDIS_URL: str
MAX_WORKERS: int = Field(ge=1, le=64)
FEATURE_NEW_CHECKOUT: bool = False
@field_validator("DATABASE_URL")
@classmethod
def no_localhost_in_prod(cls, v, info):
if info.data.get("ENV") == "prod" and "localhost" in str(v):
raise ValueError("prod에서 localhost DB 금지")
return v
if __name__ == "__main__":
Settings() # 검증 실패 시 비정상 종료
print("env schema OK")
이 스크립트 하나가 게이트의 핵심이다. 검증에 실패하면 예외를 던지며 종료 코드가 0이 아니게 되고, 그걸 CD가 감지한다.
CD 파이프라인에 게이트를 끼운다
배포(deploy) 잡보다 앞선 별도 잡으로 검증을 두는 것이 원칙이다. 검증이 실패하면 배포 잡은 실행조차 되지 않아야 한다.
jobs:
validate-env:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install pydantic pydantic-settings
- name: Validate env schema
env:
ENV: ${{ vars.ENV }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
REDIS_URL: ${{ secrets.REDIS_URL }}
MAX_WORKERS: ${{ vars.MAX_WORKERS }}
run: python scripts/validate_env.py
deploy:
needs: validate-env # 게이트 통과가 배포의 전제
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh
needs로 의존성을 걸면 검증 실패 시 배포가 자동으로 차단된다. 이것이 "게이트"라는 이름의 실질이다.
어디서 검증할 것인가: 단계별 비교
검증 위치에 따라 잡을 수 있는 사고의 범위와 실패 시점이 달라진다.
| 검증 위치 | 잡히는 문제 | 실패 시점 |
|---|---|---|
| CD 게이트 잡 | 누락·형식·환경 혼입 | 배포 전(가장 저렴) |
| 컨테이너 엔트리포인트 | 주입 후 최종 값 | 기동 직전 |
| 애플리케이션 부팅 | 런타임 의존성 결합 | 기동 시 |
가장 좋은 조합은 CD 게이트에서 1차로 막고, 컨테이너 기동 시 동일 스키마로 2차 검증(fail-fast)하는 이중 방어다. 스키마 정의를 공유하면 중복 없이 두 지점을 커버할 수 있다.
실무에서 자주 놓치는 주의점
- 시크릿 값 로깅 금지. 검증 실패 메시지에 값 자체를 찍지 말고 키 이름과 위반 규칙만 노출한다. 로그가 유출 경로가 된다.
- 기본값의 함정.
Field(default=...)는 편하지만 프로덕션 필수값에 기본값을 주면 누락을 은폐한다. prod에서는 기본값 없이 필수로 강제하라. - 불리언 파싱.
"false","0"은 문자열이라 그냥 쓰면 참으로 평가된다. 스키마 파서로 반드시 캐스팅한다. - 드리프트. 스키마와 실제 배포 값 목록이 시간이 지나며 어긋난다. 정의되지 않은 키가 주입되면 경고하도록
extra="forbid"같은 옵션으로 여분 키를 잡는다.
정리
환경변수는 타입 없는 문자열이기에 반드시 계약을 명시하고 검증해야 한다. 스키마를 코드로 선언하고, CD의 별도 잡으로 배포 전 검증한 뒤, needs로 배포와 연결해 게이트로 만든다. 여기에 컨테이너 기동 시 재검증을 더하면, 설정 오류가 프로덕션에 도달하기 전에 저렴하게 차단된다. 핵심은 "런타임에 발견하지 말고, 파이프라인에서 멈추게 하라"는 원칙이다.