- Published on
Docker 이미지 80% 줄이기 - distroless+SBOM+SLSA
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 컨테이너를 운영하다 보면 이미지가 커지는 순간이 반드시 옵니다. 빌드 도구, 패키지 매니저 캐시, 디버깅 유틸리티가 런타임 이미지에 섞이면서 수백 MB가 순식간에 붙습니다. 문제는 크기만이 아닙니다. 이미지가 클수록 배포가 느려지고, 스캔 대상이 늘어나며, 취약점 알림이 폭주합니다.
이 글은 아래 3가지를 한 번에 달성하는 방향으로 정리합니다.
- distroless 기반 런타임으로 이미지 크기와 공격면을 크게 줄이기
- SBOM을 생성하고 배포 파이프라인에 붙여 가시성과 대응 속도 높이기
- SLSA 관점에서 빌드 출처와 재현성, 서명을 강화해 공급망 리스크 낮추기
운영 관점에서 배포 안정성은 오토스케일링과도 연결됩니다. 예를 들어 이미지가 작아지면 노드 스케일아웃 시 풀링 시간이 줄어 워밍업이 빨라지고, 결과적으로 스파이크 대응이 좋아집니다. 관련해서는 EKS HPA 폭주를 KEDA 큐기반 오토스케일링으로 안정화 글도 함께 보면 맥락이 이어집니다.
왜 distroless가 이미지 크기를 줄이는가
일반적인 debian, ubuntu, alpine 기반 이미지는 편리하지만 런타임에 불필요한 것들이 함께 들어갑니다.
- 쉘과 코어 유틸리티
- 패키지 매니저와 메타데이터
- 문서, 로케일, 인증서 중복
- 디버깅 도구
distroless는 "애플리케이션 실행에 필요한 최소 런타임 파일만" 포함합니다. 보통 다음 효과가 같이 옵니다.
- 이미지 크기 감소
- CVE 표면 감소
- 컨테이너 내부에서 임의 명령 실행이 어려워져 침해 확산 난이도 증가
주의할 점도 있습니다.
- 기본적으로 쉘이 없어서
kubectl exec로 들어가sh를 치는 방식의 디버깅이 불가능 - 런타임에 필요한 CA 인증서, 타임존, 글리브c 호환성 등을 명확히 챙겨야 함
결론적으로 distroless는 "운영 성숙도가 있는 팀" 에 더 잘 맞습니다. 대신 한 번 표준화하면 장기적으로 비용이 크게 내려갑니다.
1단계: 멀티스테이지 빌드로 빌드 도구 분리
이미지 다이어트의 시작은 멀티스테이지입니다. 빌드 단계에는 컴파일러와 패키지 매니저를 마음껏 쓰고, 런타임 단계에는 결과물만 복사합니다.
아래는 Node.js 예시입니다. 핵심은 npm ci 와 빌드 산출물만 런타임으로 가져오는 것입니다.
# syntax=docker/dockerfile:1.7
FROM node:20-bookworm AS build
WORKDIR /app
# 의존성 메타데이터만 먼저 복사해 캐시 효율을 올립니다
COPY package.json package-lock.json ./
# BuildKit 캐시 마운트로 npm 캐시 재사용
RUN \
npm ci
COPY . .
RUN npm run build
# 런타임: distroless
FROM gcr.io/distroless/nodejs20-debian12 AS runtime
WORKDIR /app
# 실행에 필요한 파일만 복사
COPY /app/dist ./dist
COPY /app/node_modules ./node_modules
COPY /app/package.json ./package.json
ENV NODE_ENV=production
# distroless는 쉘이 없으므로 JSON 배열 형태로 CMD 지정
CMD ["dist/server.js"]
여기서 이미지 크기를 더 줄이려면 다음을 추가로 고려합니다.
devDependencies제거: 빌드 후npm prune --omit=dev를 빌드 스테이지에서 수행- 번들링 강화: 서버 코드도 번들러로 단일 산출물에 가깝게 만들면
node_modules를 크게 줄일 수 있음 COPY범위 최소화:.dockerignore로 테스트, 문서, 로컬 캐시 제외
.dockerignore 예시는 아래처럼 시작하면 안전합니다.
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
**/*.test.*
coverage
.env
2단계: distroless에서 자주 막히는 포인트
CA 인증서
외부 API 호출이나 TLS 통신이 있다면 CA 번들이 필요합니다. distroless 계열은 보통 CA가 포함된 변형이 준비되어 있지만, 선택한 이미지에 따라 다를 수 있습니다. 문제가 생기면 런타임 이미지에서 인증서 경로를 확인하고, 필요한 경우 CA를 명시적으로 포함합니다.
타임존
로그 타임존이 UTC로 고정되는 이슈가 자주 나옵니다. 컨테이너는 원칙적으로 UTC가 권장되지만, 비즈니스 요구로 로컬 타임존이 필요하면 런타임에 TZ 와 타임존 데이터 포함 여부를 점검합니다.
디버깅 전략
쉘이 없으니 "디버깅용 사이드카" 또는 "임시 디버그 이미지" 전략이 필요합니다.
- 동일한 볼륨이나 네임스페이스에
busybox같은 디버그 파드를 띄워 네트워크, DNS, TLS 확인 - 프로덕션 이미지는 distroless, 스테이징만 디버그 가능한 베이스를 쓰는 이원화
3단계: SBOM으로 무엇이 들어갔는지 증명하기
이미지를 줄이면 취약점이 줄어드는 경향은 있지만, "무엇이 들어갔는지" 를 문서화하지 않으면 운영에서 의미가 반감됩니다. SBOM은 소프트웨어 구성 목록을 표준 포맷으로 기록합니다.
대표 포맷은 SPDX, CycloneDX 입니다. 실무에서는 도구 호환이 좋은 CycloneDX를 많이 씁니다.
Syft로 SBOM 생성
아래는 이미지에서 SBOM을 뽑아 JSON으로 저장하는 예시입니다.
# 이미지 빌드
docker build -t myapp:1.0.0 .
# SBOM 생성 (CycloneDX JSON)
syft myapp:1.0.0 -o cyclonedx-json=sbom.json
SBOM을 만들면 다음이 가능해집니다.
- 보안팀, 감사 대응에서 "구성요소" 를 즉시 제출
- 특정 라이브러리 이슈가 터졌을 때 영향 범위 검색이 빨라짐
- 이미지 스캔 결과와 연결해 false positive를 줄이기 쉬움
SBOM을 CI 아티팩트로 남기기
SBOM은 컨테이너 레지스트리에 이미지와 함께 보관하는 편이 좋습니다.
- CI에서
sbom.json을 빌드 아티팩트로 업로드 - 또는 OCI 아티팩트로 레지스트리에 첨부
4단계: SLSA로 "어디서 어떻게" 빌드됐는지 보강하기
SLSA는 공급망 보안 성숙도를 단계적으로 정의합니다. 여기서 중요한 목표는 2가지입니다.
- 빌드가 변조되지 않았음을 보장
- 산출물이 어떤 소스와 어떤 빌드 절차에서 나왔는지 추적 가능
실무에서는 아래 조합이 현실적입니다.
- GitHub Actions 같은 호스티드 CI에서 빌드 provenance 생성
- 이미지 서명과 검증 체계 도입
- SBOM과 provenance를 함께 배포
cosign으로 이미지 서명
아래는 키리스 방식 예시입니다. OIDC 기반으로 서명하고, 배포 시 검증할 수 있습니다.
# 이미지 푸시
docker tag myapp:1.0.0 ghcr.io/myorg/myapp:1.0.0
docker push ghcr.io/myorg/myapp:1.0.0
# 서명 (키리스)
cosign sign --yes ghcr.io/myorg/myapp:1.0.0
# 검증
cosign verify ghcr.io/myorg/myapp:1.0.0 \
--certificate-identity-regexp ".*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
이 단계에서 중요한 운영 포인트는 "검증을 어디에서 강제할 것인가" 입니다.
- 쿠버네티스 admission controller로 서명된 이미지만 허용
- 특정 레포, 특정 워크플로에서 나온 provenance만 허용
5단계: GitHub Actions 파이프라인 예시
아래는 빌드, 푸시, SBOM 생성, 서명까지 한 번에 묶는 흐름 예시입니다. 실제로는 조직 정책에 맞게 권한과 조건을 더 강화하세요.
name: build-and-sign
on:
push:
branches: ["main"]
permissions:
contents: read
packages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/myorg/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Install syft
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- name: Generate SBOM
run: |
syft ghcr.io/myorg/myapp:${{ github.sha }} -o cyclonedx-json=sbom.json
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.json
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Sign image (keyless)
run: |
cosign sign --yes ghcr.io/myorg/myapp:${{ github.sha }}
여기까지 하면 최소한 다음이 갖춰집니다.
- 빌드 산출물과 런타임 분리로 이미지 슬림화
- SBOM 생성으로 구성요소 가시화
- 서명으로 무결성 강화
SLSA 레벨을 더 올리려면 provenance 생성과 정책 강제가 필요합니다. 예를 들어 GitHub의 provenance 기능 또는 별도 attestation을 활용해 "이 커밋에서 이 워크플로로 빌드됐다" 를 증명하고, 클러스터에서 그 조건을 만족하지 않으면 배포를 막는 식입니다.
6단계: 80퍼센트 감량을 만드는 체크리스트
아래 항목은 체감 효과가 큽니다.
- 멀티스테이지 빌드로 빌드 도구 제거
- distroless 런타임으로 전환
- 의존성 최소화: 번들링,
devDependencies제거 - 캐시 최적화: BuildKit 캐시 마운트, 레이어 순서 최적화
- 불필요 파일 제거:
.dockerignore강화 - 관측 가능성은 이미지에 넣지 말고 런타임에서 해결: 사이드카, 에이전트, 원격 로깅
운영 중 디스크 압박이 심할 때는 이미지 크기 자체가 노드 디스크를 빨리 잠식합니다. 이미지가 줄면 pull 캐시가 효율적으로 유지되고, 디스크 경보 빈도도 줄어듭니다. 디스크 관련 트러블슈팅은 리눅스 디스크 100%인데 용량이 안 줄 때 - deleted-but-open(lsof) 글도 실전에 도움이 됩니다.
distroless 도입 시 흔한 실패 패턴과 대응
패턴 1: 런타임에서만 모듈 로딩 실패
빌드 스테이지에서는 동작하지만 런타임에서 네이티브 모듈이 실패하는 경우가 있습니다. 원인은 대개 glibc, openssl, libc 호환성 차이입니다.
- 빌드와 런타임의 OS 계열을 맞추기
- 네이티브 모듈을 피하거나, 정적 링크 가능한 언어 선택 고려
패턴 2: 운영자가 컨테이너 내부에서 즉시 확인할 수 없음
distroless는 의도적으로 내부 접근성을 줄입니다.
- 진단은 애플리케이션 로그, 메트릭, 트레이싱 중심으로 전환
- 필요 시 디버그 파드로 네트워크, DNS, 인증서만 점검
패턴 3: 취약점 제로를 기대함
이미지가 작아져도 취약점은 남습니다. 중요한 것은 "취약점 수" 보다 "패치 가능성" 과 "영향 범위" 입니다. SBOM이 있으면 영향을 빠르게 판별할 수 있고, SLSA 체계가 있으면 안전한 재빌드와 배포가 빨라집니다.
결론: 크기 최적화는 보안과 배포 성능을 함께 올린다
이미지를 80퍼센트 줄이는 것은 단순한 다이어트가 아니라, 운영 비용과 보안 리스크를 함께 낮추는 구조 개선입니다.
- distroless로 런타임을 최소화해 공격면과 크기를 줄이고
- SBOM으로 구성요소를 투명하게 만들며
- SLSA 관점의 provenance와 서명으로 "신뢰 가능한 빌드" 를 구축하면
결국 배포 속도, 장애 대응, 감사 대응까지 한 번에 좋아집니다.
다음 액션으로는 현재 서비스의 대표 이미지 하나를 골라, distroless 멀티스테이지로 바꾼 뒤 syft 로 SBOM을 남기고 cosign 서명까지 붙여보는 것을 권합니다. 이 1회 전환 경험이 팀의 컨테이너 표준을 바꾸는 출발점이 됩니다.