멱등 Dockerfiles: 바람직한 이상인가, 아니면 잘못된 목표인가?
Source: Dev.to
TL;DR
Idempotent(멱등) 혹은 완전 재현 가능한 Dockerfile이 최선의 실천법으로 자주 언급되지만, 실제 엔지니어링 현장에서는 올바른 목표가 아니다. 팀이 실제로 필요로 하는 것은: (1) 레지스트리에 저장된 불변이며 추적 가능한 아티팩트, 그리고 (2) 보안 패치와 업데이트된 의존성을 지속적으로 반영하는 정기적인 CI 재빌드이다. 원본 이미지 아티팩트가 다이제스트로 보존되고 접근 가능할 때, 멱등 재빌드는 운영적 가치를 거의 제공하지 않는다. 재현성은 규제된 환경, 연구, 고신뢰 시스템 등 특수 분야에서는 여전히 유용하지만, 일반적인 애플리케이션 개발에서는 비용과 복잡성만 늘리고 이득은 적다.
1. “Idempotent Dockerfile”이 실제 의미하는 바
이 용어는 여러 관련 개념을 혼합한다:
- 멱등 빌드:
docker build를 여러 번 실행해도 “같은” 이미지를 만든다. - 재현 가능한 빌드: 누구든지 어느 머신에서든 재빌드하면 비트‑대‑비트로 동일한 이미지를 얻는다.
- 기능적 동등성: 비트가 다르더라도(메타데이터, 타임스탬프) 런타임 동작은 사실상 동일하다.
실제 현장에서 사용되는 Dockerfile은 보통 기능적 동등성 수준에 머물며, 엄격한 멱등성이나 완전 재현성은 거의 달성되지 않는다.
비멱등 Dockerfile 예시
FROM debian:stable-slim
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
COPY app /usr/local/bin/app
ENTRYPOINT ["/usr/local/bin/app"]
몇 주 뒤에 다시 빌드하면 베이스 이미지의 패치 레벨이나 패키지 버전이 바뀔 수 있어 이미지 해시가 달라진다. 이는 흔한 상황이며 종종 허용된다.
2. 멱등·재현 가능한 빌드를 선호하는 이유
2.1. CI 드리프트와 “내 환경에서는 동작한다” 현상을 없앤다
가이드라인은 버전 고정, 네트워크 비결정성 회피, 부작용 방지를 강조한다. 같은 Dockerfile이 같은 이미지를 만든다면 디버깅이 쉬워지고 협업자가 일관된 결과를 볼 수 있다.
2.2. 과학적 재현성을 지원한다
연구 워크플로우에서는 정확히 같은 환경을 수년 후에도 재구성해야 할 때가 있다. 재현 연구용 Dockerfile을 위한 전용 가이드가 존재한다.
2.3. 공급망 보안 내러티브와 맞는다
재현 가능한 빌드는 이미지가 특정 소스와 정확히 일치함을 검증하고 변조를 탐지하는 데 도움을 준다. BuildKit 및 SBOM/attestation 기능이 이러한 사용 사례를 위해 제공된다.
3. 실무에서 멱등 Dockerfile이 드문 이유
3.1. 일반적인 Dockerfile 패턴은 본질적으로 비결정적이다
BuildKit 토론에서 다음과 같이 요약한다:
“Dockerfile로 도커 이미지를 빌드하는 것은 재현 가능하지 않다… 대부분의 실제 사례는 비결정적인 동작을 하는 패키지 매니저를 포함한다.”
드리프트의 원인
- 부동(base) 태그 사용
- 시간에 따라 변하는 패키지 저장소
- 고정된 다이제스트 없이 다운로드
- 레이어에 삽입되는 타임스탬프와 메타데이터
3.2. Docker 자체 가이드는 빈번한 재빌드를 강조한다
Docker는 보안 패치와 의존성 업데이트를 반영하기 위해 자주 재빌드할 것을 권장한다. 진정한 멱등성을 위해 버전을 고정해야 하지만, 지속적인 보안은 변화를 허용해야 한다.
3.3. 완전 재현성은 큰 운영 오버헤드를 초래한다
비트‑대‑비트 동일한 이미지를 만들려면 사설 미러, 전체 버전 고정, 스냅샷 저장소, 타임스탬프 정규화, 제어된 빌드 환경 등이 필요하다. 이는 유지비용을 크게 늘리며 일반적인 이득은 제한적이다.
4. 반대 입장: 멱등성을 주요 목표로 삼지 말아야 함
핵심 통찰은 컨테이너 워크플로우가 불변 아티팩트에 초점을 맞추며, 재빌드 가능성은 부수적인 요소라는 점이다.
4.1. 레지스트리가 이미 중요한 것을 보존한다
컨테이너 생태계는 다음과 같은 불변 이미지를 전제로 한다:
- 다이제스트로 저장·조회 가능
- 빌드 메타데이터, CI 커밋, SBOM과 연결된 추적성
- 재구성보다 빌드 시점에 버전 관리
레지스트리가 아티팩트를 보존하므로, 운영 차원에서 재빌드해 동일 이미지를 만들 필요는 거의 없다.
4.2. 현대 CI 파이프라인은 시간에 따라 변하는 빌드를 전제로 한다
flowchart LR
A[git commit] --> B[CI build]
B --> C[docker build -> digest]
C --> D[push to registry]
D --> E[deploy by digest]
올바른 불변 조건
- 각 커밋은 자체 이미지를 만든다.
- 각 이미지는 다이제스트로 불변하게 저장된다.
- 배포와 롤백은 저장된 다이제스트를 참조한다.
docker build가 다시 같은 다이제스트를 만들 수 있든 못 만들든, 원본 다이제스트가 보존되는 한 무관하다.
4.3. 정기적인 비멱등 재빌드는 보안 강점이다
자주 재빌드하면 OS 패키지, 베이스 이미지, 의존성이 최신 보안 패치를 반영한다. 엄격한 멱등성은 보안 모범 사례와 직접 충돌한다.
레지스트리 중심 워크플로우 예시
Dockerfile (시간에 따라 변하지만 운영상 문제 없음)
FROM python:3.12-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN pip install --no-cache-dir poetry && \
poetry install --no-interaction --no-ansi
COPY . .
CMD ["poetry", "run", "myapp"]
CI Build
COMMIT_SHA=$(git rev-parse --short HEAD)
docker build \
-t registry.example.com/myapp:${COMMIT_SHA} \
-t registry.example.com/myapp:main \
.
docker push registry.example.com/myapp:${COMMIT_SHA}
docker push registry.example.com/myapp:main
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \
registry.example.com/myapp:${COMMIT_SHA})
echo "Built image: ${DIGEST}"
운영상의 올바름은 다음으로 보장된다:
- 다이제스트로 기록·배포
- 모든 생성된 아티팩트를 보존
- 지속적인 최신성을 위해 정기적으로 재빌드
멱등 재빌드는 불필요하다.
5. 재현성이 필요한 경우
멱등성과 재현성은 다음 상황에서 중요하다:
- 규제 혹은 고신뢰 공급망
- 정확한 재현 환경이 요구되는 과학 워크플로우
- 재빌드가 유일한 허용 옵션인 에어갭 환경
이러한 경우에는 특화된 접근법이 필요하다:
- BuildKit 재현 빌드 설정,
SOURCE_DATE_EPOCH, 결정적 타임스탬프 - 결정적 의존성 그래프를 제공하는 Nix, Bazel 같은 기능 빌드 시스템
- 고정 버전 저장소 혹은 스냅샷 미러
이 경우는 예외이며, 일반적인 상황은 아니다.
6. 유용한 계층 모델
컨테이너 빌드 정확성을 세 계층으로 생각해 보자:
- 런타임 동작: 컨테이너가 실행될 때 예측 가능한 동작을 한다.
- 아티팩트 불변성: 레지스트리가 불변 다이제스트와 출처 정보를 저장한다.
- 빌드 재현성: Dockerfile을 다시 빌드해도 동일한 결과물이 나온다.
계층 1과 2는 보편적으로 중요하고, 계층 3은 앞서 언급한 특수 시나리오에서만 가치가 있다.