Dockerfile의 반복 비용

서비스가 늘어날수록 Dockerfile은 팀 전체의 유지보수 부채가 된다. 언어 런타임 버전, 베이스 이미지 CVE 패치, 레이어 캐시 순서, 비루트 사용자 설정을 서비스마다 복붙하다 보면 미묘하게 다른 수십 개의 Dockerfile이 생긴다. 보안팀이 "베이스 이미지를 전부 교체하라"고 하면 그 수십 개를 일일이 고쳐야 한다. 애플리케이션 개발자 입장에서 컨테이너 빌드는 본질적 관심사가 아닌데, 인프라 지식을 강요당하는 셈이다.

Buildpacks가 해결하는 것

Cloud Native Buildpacks(CNB)는 소스 코드를 입력받아 실행 가능한 OCI 이미지를 만든다. 개발자는 Dockerfile을 작성하지 않는다. 대신 detect 단계에서 언어와 프레임워크를 자동 감지하고, build 단계에서 의존성을 설치해 표준화된 레이어로 이미지를 구성한다. 베이스 이미지 교체, 런타임 패치 같은 공통 관심사는 플랫폼(빌더)이 책임진다.

기본 사용법

Paketo나 Google 빌더와 함께 pack CLI를 쓰는 것이 가장 단순하다. 소스 디렉터리에서 명령 한 줄이면 된다.

pack build myapp:1.0.0 \
  --builder paketobuildpacks/builder-jammy-base \
  --env BP_JVM_VERSION=21 \
  --publish   # 빌드 후 레지스트리에 바로 푸시

# 빌더 안에 어떤 buildpack이 들어있는지 확인
pack builder inspect paketobuildpacks/builder-jammy-base

Spring Boot나 Paketo가 만든 이미지는 기본적으로 비루트 사용자로 실행되고, SBOM과 메타데이터가 이미지에 포함된다.

CI 파이프라인 통합

CI에서는 pack을 쓰거나, Kubernetes 환경이라면 kpack으로 선언적으로 관리한다. GitHub Actions 예시는 다음과 같다.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install pack
        run: |
          curl -sSL -o pack.tgz \
            https://github.com/buildpacks/pack/releases/latest/download/pack-linux.tgz
          sudo tar -C /usr/local/bin -xzf pack.tgz pack
      - name: Build & push
        run: |
          echo "${{ secrets.REG_PASSWORD }}" | \
            pack build $REG/myapp:${{ github.sha }} \
              --builder paketobuildpacks/builder-jammy-base \
              --cache-image $REG/myapp-cache \
              --publish
        env:
          REG: registry.example.com/team

--cache-image를 지정하면 의존성 레이어를 레지스트리에 캐시해, 러너가 매번 초기화되는 CI에서도 빌드 시간을 크게 줄일 수 있다.

Dockerfile과의 비교

항목DockerfileBuildpacks
빌드 정의서비스마다 직접 작성자동 감지, 파일 불필요
베이스 이미지 패치개별 수정 후 재빌드빌더 갱신 후 rebase
커스터마이징 자유도매우 높음제한적(정해진 관례)
비루트/SBOM수동 설정기본 제공

rebase — 재빌드 없는 패치

Buildpacks의 강점 중 하나는 rebase다. 애플리케이션 레이어와 OS 런타임 레이어가 분리돼 있어, 베이스 이미지에 CVE 패치가 나오면 소스 재빌드 없이 런타임 레이어만 교체할 수 있다.

# 실행 이미지만 최신 패치본으로 교체 (앱 레이어는 그대로)
pack rebase myapp:1.0.0 \
  --run-image paketobuildpacks/run-jammy-base:latest \
  --publish

수백 개 서비스가 같은 run image를 공유한다면, rebase 한 번으로 전체 취약점 대응이 끝난다.

주의점

모든 상황에 맞는 도구는 아니다. 시스템 패키지(예: 특정 apt 라이브러리)나 세밀한 레이어 제어가 필요하면 관례를 벗어나기 어렵다. 이럴 땐 커스텀 buildpack을 작성하거나 project.toml로 환경을 조정하되, 그래도 부족하면 Dockerfile이 낫다. 또한 빌더 이미지 자체가 크기 때문에 첫 빌드는 느리고, 빌더/런 이미지의 버전을 팀 차원에서 고정·관리하지 않으면 재현성이 흔들린다. 빌더 태그는 latest가 아니라 다이제스트로 핀 고정하는 것을 권한다.

# project.toml — 빌드 환경을 코드로 관리
[[build.env]]
name = "BP_JVM_VERSION"
value = "21"

[[io.buildpacks.build.env]]
name = "BP_MAVEN_BUILD_ARGUMENTS"
value = "-Dmaven.test.skip=true package"

정리하면, 표준화된 다수의 서비스에는 Buildpacks가 유지보수 비용을 크게 낮춰준다. 특수한 요구가 있는 소수 서비스만 Dockerfile로 남기는 혼합 전략이 현실적이다.