Docker Build: Arch Linux bit-for-bit 재현 가능 이미지 분석

서론

“어제 통과했던 CI 파이프라인이 오늘은 실패했다.” 혹은 “로컬에서 빌드한 이미지와 CI 환경에서 빌드한 이미지의 해시(SHA256)가 달라서 캐시가 정상적으로 동작하지 않는다.” 경험해보신 DevOps 엔지니어라면 이 상황이 얼마나 답답한지 잘 아실 겁니다. 이는 단순히 효율성의 문제가 아닙니다. 우리가 배포하는 아티팩트가 정말로 우리가 빌드한 소스 코드로부터 유래했는지 증명할 수 없다는 보안상의 치명적인 결함이기도 합니다.

최근 Arch Linux가 Docker 이미지 빌드 과정에서 **Bit-for-bit 재현 가능성(Reproducibility)**을 확보했다는 소식은 이러한 DevOps의 근원적인 문제를 해결하는 중요한 이정표입니다. 동일한 소스 코드라면 빌드를 수행하는 시점과 장소에 상관없이 바이트 수준까지 완전히 동일한 이미지가 생성된다는 것입니다. 이 기술적 성취는 단순한 기술 개선을 넘어, 소프트웨어 공급망 보안(Supply Chain Security)과 CI/CD 파이프라인의 신뢰성을 획기적으로 높이는 핵심 동력입니다.

본문에서는 Arch Linux가 이룬 재현 가능한 빌드의 기술적 원리를 깊이 있게 분석하고, 실제 운영 환경에서 Docker 이미지의 재현 가능성을 확보하기 위한 실행 가능한 가이드를 제공합니다.

본론

비결정론적 요소의 제거: 기술적 원리

Docker 이미지 빌드가 기본적으로 비결정론적(Non-deterministic)인 이유는 파일 시스템 레이어를 생성하는 과정에서 다양한 변수가 개입하기 때문입니다. 대표적인 예로 파일의 메타데이터(Metadata)가 있습니다. tar 아카이브를 생성할 때 파일의 순서나 권한, 소유자 정보가 포함되는데, 이 과정에서 빌드를 수행하는 시스템의 상태, 타임스탬프, 파일 시스템의 특성에 따라 결과물이 달라질 수 있습니다.

Arch Linux 팀은 이 문제를 해결하기 위해 빌드 과정에서 발생하는 모든 ‘노이즈’를 제거하는 작업을 수행했습니다. 핵심은 빌드 환경을 표준화하고, 파일 시스템을 패키징하는 단계에서 항상 동일한 순서와 포맷을 유지하도록 강제하는 것입니다.

다음은 일반적인 빌드 프로세스와 재현 가능한 빌드 프로세스의 차이를 보여주는 다이어그램입니다.

1
2
3
4
5
6
7
graph LR
    A[Source Code] --> B[Build Process]
    B -->|Include Time/Random| C[Non-Deterministic Artifact A<br/>SHA: A1B2]
    B -->|Include Time/Random| D[Non-Deterministic Artifact B<br/>SHA: C3D4]
    A --> E[Reproducible Build]
    E -->|Normalize Metadata<br/>Sort Files| F[Identical Artifact A<br/>SHA: E5F6]
    E -->|Normalize Metadata<br/>Sort Files| G[Identical Artifact B<br/>SHA: E5F6]

이 다이어그램에서 볼 수 있듯이, 재현 가능한 빌드(Reproducible Build)는 외부 변수(Time, Random 등)를 차단하고 메타데이터를 정규화함으로써, 빌드 횟수와 상관없이 항상 동일한 해시 값을 가진 아티팩트를 생성해 냅니다.

비교 분석: 기존 빌드 vs 재현 가능 빌드

이러한 변화가 실제 운영 환경에서 어떤 이점을 가져오는지 명확히 이해하기 위해, 두 방식을 비교해 보겠습니다.

| 비교 항목 | 기존 빌드 (Conventional) | 재현 가능 빌드 (Reproducible) | | :— | :— | :— | | 출력 해시값 | 빌드 시마다 상이함 (SHA256 변경) | 소스 기반 항상 동일 (SHA256 고정) | | 캐시 효율성 | 낮음 (레이어 캐시 Miss 빈번) | 높음 (이미지 풀(Pull) 최적화) | | 보안 감사 | 어려움 (바이너리 비검증 가능성) | 용이함 (소스와 바이너리 1:1 검증) | | 빌드 환경 의존성 | 높음 (OS, 파일시스템 영향) | 낮음 (격리된 환경에서도 동일 결과) | | 디버깅 난이도 | 환경 차이로 인한 이슈 디버깅 곤란 | 환경 무관, 소스 코드 중심 디버깅 |

실전 가이드: Dockerfile을 통한 재현 가능성 확보

Arch Linux의 노력은 베이스 이미지 레벨에서 시작되지만, 우리가 서비스를 위해 작성하는 애플리케이션 Docker 이미지도 동일한 원칙을 적용해야 합니다. Docker BuildKit을 활용하여 재현 가능한 빌드를 구현하는 단계별 가이드를 제공합니다.

Step 1: BuildKit 및 환경 변수 설정

Docker BuildKit은 빌드 캐싱과 병렬 처리를 최적화해 주지만, 재현 가능성을 위해서도 필수적입니다. 특히 SOURCE_DATE_EPOCH 환경 변수는 빌드 시간을 고정하여 타임스탬프 문제를 해결하는 핵심 키입니다.

1
2
3
4
5
6
7
8
# Git 커밋 타임스탬프를 빌드 시간으로 사용 (커밋 시점 고정)
export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)

# BuildKit 사용하여 빌드
DOCKER_BUILDKIT=1 docker build \
  --build-arg SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH \
  -t myapp:reproducible \
  .

Step 2: Dockerfile 작성 시 주의사항

Dockerfile 내에서도 비결정론적 요소를 피해야 합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# syntax=docker/dockerfile:1

# 베이스 이미지 태그 고정 (latest 사용 금지)
FROM archlinux:base-20240101.0.123456 AS builder

# 빌드 인자로 전달된 시간 사용
ARG SOURCE_DATE_EPOCH
ENV SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH

# 패키지 설치 시 캐시 최소화 및 버전 고정
RUN pacman -Syu --noconfirm --needed base-devel && \
    pacman -Scc --noconfirm

# 소스 코드 복사 전에 파일 순서 정렬 보장 (Docker는 기본적으로 알파벳 순으로 복사하지만 명시적 관리 필요)
WORKDIR /app
COPY . .

# 재현 가능한 빌드 스크립트 실행
# 예: Go 언어의 경우 -ldflags "-buildid=" 등을 사용하여 빌드 ID 고정
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-buildid= -extldflags=-static" \
    -a -installsuffix cgo -o main .

# 최종 이미지 생성
FROM scratch
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]

Step 3: 검증 (Verification)

실제로 두 번의 빌드를 수행하여 결과물이 일치하는지 확인해야 합니다. 이 단계가 없다면 재현 가능성을 보장할 수 없습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 첫 번째 빌드
docker build -t test:v1 .
docker save test:v1 | sha256sum > hash1.txt

# 이미지 삭제 후 캐시 없는 두 번째 빌드 (혹은 다른 머신에서 빌드)
docker rmi test:v1
docker build -t test:v1 .
docker save test:v1 | sha256sum > hash2.txt

# 해시값 비교
diff hash1.txt hash2.txt
# 결과가 아무것도 출력되지 않으면(bit-for-bit) 성공입니다.

운영 관점에서의 고려사항

Kubernetes와 같은 컨테이너 오케스트레이션 환경에서는 이미지의 무결성 검증이 매우 중요합니다. 재현 가능한 빌드를 통해 생성된 이미지는 sha256 다이제스트를 통해 버전을 관리할 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: apps/v1
kind: Deployment
metadata:
  name: arch-app
spec:
  template:
    spec:
      containers:
      - name: app
        # 태그 대신 구체적인 다이제스트를 사용하여 배포 안정성 확보
        image: myrepo/arch-app@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

이 방식을 사용하면 누군가가 악의적으로 동일한 태그의 이미지를 덮어쓰더라도, Kubernetes는 다이제스트 불일치로 인해 배포를 거부


출처: https://news.hada.io/topic?id=28854

Hugo로 만듦
JimmyStack 테마 사용 중