rules_img 정리: Bazel에서 컨테이너 이미지를 빠르게 만드는 방식

목차

Tweag의 글(“Announcing rules_img: a faster path to container images in Bazel”)을 읽고 핵심을 정리했다. 인상 깊었던 건 속도가 빨라지는 이유가 “도구 최적화”보다는 “데이터 이동 방식을 바꾸는 설계”에 있다는 점이다. 요점은 간단하다. 이미지를 메타데이터로 먼저 다루고, 바이트(레이어)는 마지막에만 움직인다.

Bazel부터 짚고 가기

Bazel은 구글이 만든 빌드 시스템이다. 의존성 그래프를 기준으로 빌드·테스트·패키징을 관리하고, 캐시와 재현성이 강점이다. 원격 캐시나 원격 실행(RBE)을 붙이면 대규모 코드베이스에서도 빌드 시간을 줄일 수 있다. 이 글에서 다루는 rules_img는 Bazel에서 컨테이너 이미지를 만드는 규칙셋이다.

문제의 출발점: 왜 Bazel 이미지 빌드가 느려지는가

Bazel은 빌드 결과물을 잘 캐싱하고 재현성을 확보한다. 그런데 컨테이너 이미지를 붙이는 순간 병목이 생긴다. 기존 rules_oci 흐름에서는 이런 일이 일어난다.

즉, 빌드와는 무관한 대용량 바이트 이동이 반복된다. 속도 저하는 결국 “데이터 이동량”의 문제다.

2분 요약: OCI 이미지의 구성 요소

이미지는 크게 메타데이터와 레이어 바이트로 나뉜다.

정리하면 이미지는 “레이어의 실제 바이트”와 “그 바이트를 가리키는 JSON”으로 구성된다. rules_img는 이 구조를 그대로 성능 최적화에 활용한다.

rules_img의 핵심 아이디어

rules_img: 메타데이터는 빌드 그래프로, 레이어 바이트는 끝에서만

rules_img: 메타데이터는 빌드 그래프로, 레이어 바이트는 끝에서만

rules_img: 메타데이터는 빌드 그래프로, 레이어 바이트는 끝에서만pull은 레지스트리에서 manifest와 config JSON만 가져온다. image_layer 액션은 레이어 바이트를 CAS에 두고 메타데이터만 빌드 그래프로 보낸다. image_manifest 조립은 digest와 size만 참조하고, bazel run 시점에 레지스트리에 없는 레이어만 전송한다.메타데이터만메타데이터레이어 바이트메타데이터digest·size만 참조무엇을 푸시할지 명세없는 레이어만 전송레지스트리레이어 블롭은 여기에 둔다pullmanifest·config JSON만image_layer 액션tar와 메타데이터를 함께 만듦CAS레이어 바이트 저장빌드 그래프작은 메타데이터만 흐름image_manifest 조립작은 JSON 작업bazel runpush·load 실행
pull은 manifest와 config만 가져오고, image_layer가 만든 레이어 바이트는 CAS에 두며 빌드 그래프에는 메타데이터만 흐른다. 실제 레이어 전송은 bazel run 때 레지스트리에 없는 것만 일어난다.
그림 관계를 글로 읽기
  1. pull은 레지스트리에서 manifest와 config JSON만 가져온다. 레이어 블롭은 레지스트리에 둔다.
  2. 가져온 메타데이터가 빌드 그래프로 들어간다.
  3. image_layer 액션이 tar와 메타데이터를 함께 만들고, 레이어 바이트는 CAS에 저장된다.
  4. 같은 액션의 작은 메타데이터만 빌드 그래프로 흐른다.
  5. image_manifest 조립은 레이어의 digest와 size만 참조하는 작은 JSON 작업이다.
  6. 빌드 단계는 무엇을 푸시할지 명세만 만들고, 실제 바이트 이동은 bazel run 시점으로 미룬다.
  7. bazel run은 레지스트리에 없는 레이어만 전송한다.

1) 메타데이터만 가져오는 Pull

베이스 이미지를 받을 때 레이어를 내려받지 않는다. manifest와 config JSON만 가져와서 “무엇이 있는지”만 안다. 레이어 블롭은 레지스트리에 둔다.

2) 레이어 생성은 CAS에, 메타데이터는 그래프에

레이어는 한 번의 액션에서 tar와 메타데이터를 함께 만든다. 이때 레이어 바이트는 CAS에 저장되고, 빌드 그래프에는 작은 메타데이터만 흐른다.

3) Manifest 조립은 작은 JSON 작업으로 끝

이미지 조립 단계는 레이어의 digest와 size만 참조한다. 덕분에 원격 실행에서도 입력이 가벼워지고 캐시 효율이 높아진다.

4) Push/Load는 run 단계로 미룬다

빌드 단계에서는 “무엇을 푸시할지”에 대한 명세만 만든다. 실제 바이트 이동은 bazel run 시점에 실행되고, 레지스트리에 없는 레이어만 전송한다. 로컬 머신이 불필요한 중계 역할을 하지 않는다.

속도가 빨라지는 이유를 한 줄로 요약하면

빌드 그래프에는 메타데이터만 흐르게 하고, 바이트는 끝에서만 이동하게 만든다.

글에서 언급한 효과는 이렇다.

rules_oci와 rules_img 비교 관점

항목 rules_oci rules_img
베이스 이미지 풀 모든 레이어 다운로드 manifest/config만 다운로드
빌드 입력 전체 레이어 포함 메타데이터 중심
원격 실행 입력 스테이징 부담 큼 부담 작음
푸시 방식 전체 레이어 재전송 누락 레이어만 전송
로컬 로드 docker load 전체 재적재 containerd 기반 증분 로드

실무에서 체크해야 할 지점

간단한 시작 스니펫

bazel
# MODULE.bazel
bazel_dep(name = "rules_img", version = "<version>")

pull = use_repo_rule("@rules_img//img:pull.bzl", "pull")

pull(
    name = "ubuntu",
    registry = "index.docker.io",
    repository = "library/ubuntu",
    tag = "24.04",
)
bazel
# BUILD.bazel
load("@rules_img//img:layer.bzl", "image_layer")
load("@rules_img//img:image.bzl", "image_manifest")

image_layer(
    name = "app_layer",
    srcs = {
        "/app/bin/server": "//cmd/server",
    },
)

image_manifest(
    name = "app_image",
    base = "@ubuntu",
    layers = [":app_layer"],
)

마무리

rules_img는 “이미지는 결국 메타데이터와 바이트의 조합”이라는 구조를 그대로 성능 전략으로 연결한다. 빌드 중에는 메타데이터만 다루고, 바이트 이동은 마지막에만 한다. 이 단순한 원칙이 원격 실행 환경에서 큰 체감 속도 차이를 만든다. Bazel 기반 CI에서 이미지 빌드가 병목이라면, rules_img가 왜 빠른지 이해하는 것만으로도 설계 방향이 달라진다.

참고

도식

100%

끌어서 이동, 두 손가락이나 휠로 확대·축소. 키보드: 화살표 이동, + − 확대·축소, 0 맞춤, Esc 닫기. 같은 내용은 그림 아래 “그림 관계를 글로 읽기”에 있다.