buildKit
개요
컨테이너 이미지 빌드 툴 중 하나로, 도커에서 기본으로 활용하는 빌드 툴이다.
과거의 도커 빌드는 호스트 루트 유저의 권한을 필요로 했기에 보안적으로 취약하다는 문제가 있었다.
이것을 개선하면서 새롭게 나온 것이 바로 이 빌드킷이다.
그래서 이 문서는 도커에서 활용하는 이미지 빌드 방식에 대한 내용도 담는다.
구조
buildKit 자체는 이미지를 빌드하는 데몬 프로세스이다.
도커가 dockerd와 docker client 통신을 통해 컨테이너를 띄우듯이, 빌드킷은 서버 클라이언트 구조로 되어 있어 사용자는 앞단의 툴을 이용해 이 빌드킷과 통신하여 명령을 수행한다.
이때 도커를 활용하여 빌드를 진행할 때 사용하게 되는 툴은 buildx라고 부른다.
반면 도커와 독립적으로 이 빌드킷을 사용할 수 있도록 자체적으로 내장하고 있는 명령툴이 있는데 이것이 바로 buildctl이다.
간단하게 정리하면 도커를 쓰고 있다? docker buildx build
이런 식으로 빌드들 하면 된다.
도커와 무관하게 이 빌드 툴만 쓰고 싶다면 buildctl build
이런 식으로 사용하면 된다.
buildkitd \
--addr tcp://0.0.0.0:1234 \
--tlscacert /path/to/ca.pem \
--tlscert /path/to/cert.pem \
--tlskey /path/to/key.pem
참고로 빌드킷 데몬은 systemd 소켓으로 열릴 수도 있고[1], 위처럼 tcp 서비스로도 만들 수도 있다.
위의 경우에는 gRPC 통신을 하게 된다.
기능
비단 빌드킷에만 한정되는 특징까지는 아니지만 빌드킷은 이미지 빌드를 위한 다양한 기능들을 가지고 있다.
다양한 프론트엔드
여기에서 말하는 프론트엔드는 빌드를 진행하기 위한 양식을 이야기한다.
대표적으로는 Dockerfile이 있고, 이외에도 Gockerfile, Mockerfile(...) 등 다양한 파일이 있다.
빌드킷은 들어오는 양식을 읽어서 LLB(Low-Level Build) 포맷으로 변환한다.
이 LLB는 이미지 빌드를 최적화하기 위한 그래프 구조, 프로세스 실행 순서 등의 정보가 해석되어 같이 들어가 있기에, 빌드킷은 마치 컴파일이라도 한 것마냥 효과적으로 빌드 작업을 수행할 수 있다.
아무튼 그래서 실제 사용자는 다양한 양식의 파일을 이용하여 빌드킷을 이용하는 것이 가능하다.
buildctl build \
--frontend=dockerfile.v0 \
--local context=. \
--local dockerfile=.
여기 frontend 인자 부분이 프론트엔드를 지정하는 부분이며, 여기에 gateway.v0
를 지정하면 이미지를 받아서 처리하는 것도 가능해진다.
프론트엔드에 대한 설정으로 local 인자를 넣는다.
어떤 컨텍스트를 쓸 건지, 그래서 어떤 파일을 쓸 건지를 명시한다.
local 인자는 특수 인자이고, 조금 더 범용적으로 프론트엔드에 대한 설정을 넣을 때는 사실 opt라는 인자를 사용하면 된다.
buildctl build \
--frontend gateway.v0 \
--opt source=docker/dockerfile \
--local context=. \
--local dockerfile=.
이 설정은 위의 설정과 같은 방식이다.
opt에는 아래와 같은 설정들을 넣을 수 있다.
- target
- build-arg
- context - 이게 세팅되면 위의 local로 설정되는 컨텍스트가 오버라이드된다.
다양한 출력 형식
빌드킷은 이미지를 빌드하는 툴이지만, 빌드를 통해 나온 값이 이미지가 아니게도 설정하는 것이 가능하다.
buildctl build ...\
--output type=image,name=docker.io/username/image,push=true \
output 부분을 보면 type이 이미지라서 이미지가 나오게 된다.
buildctl build ... --output type=local,dest=path/to/output-dir
근데 이런 식으로 local이라 주면 이미지가 만들어지는 것이 아니라 빌드를 통해 만들어진 파일시스템 자체가 dest 경로에 저장된다!
FROM busybox AS build
ARG TARGETOS
ARG TARGETARCH
RUN mkdir /out && echo foo > /out/hello-$TARGETOS-$TARGETARCH
FROM scratch
COPY --from=build /out /
이 로컬 형식으로 나오는 것이 사실 멀티스테이지 빌드에서 이뤄지는 방식이다.
아래 COPY 구문에서는 from을 이용해 앞선 스테이지의 파일을 자신 쪽으로 베껴오는데, 이때 앞선 스테이지가 이미지로 빌드되는 것이 아니라 파일시스템 형태로 출력물을 가지고 있기 때문에 이게 가능한 것이다.
buildctl build ... --output type=oci,dest=path/to/output.tar
이런 식으로 주면 압축된 tarball 파일을 받아볼 수도 있다.
캐시
같은 호스트에서 이미지를 여러 번 빌드하면 이미지 레이어가 캐싱돼서 효율적으로 빌드를 하는 것이 가능해진다는 것은 익히 알고 있을 것이다.
다양한 환경에서 빌드를 해야 한다면 이 캐시를 외부에 저장하고 빌드를 진행할 때 가져오는 방식을 통해 어디에서는 빌드를 효율적으로 하는 것이 가능하다.
(물론 네트워크 환경을 타고 받아오기 때문에 로컬 캐시보다는 쵸금 느리긴 하겠지만, 순수하게 다시 빌드하는 것보다는 낫다.)
설정은 export-cache, import-cache 인자를 통해 할 수 있으며, 여러 가지 방식으로 캐싱하는 것이 지원된다.
inline 캐시
이미지 안 속에 캐싱 데이터를 임베딩한다!
구체적으로는 이미지의 메타데이터, 설정 정보에 캐싱 데이터를 우겨박는 방식이라 실제 이미지 자체의 크기가 증가하진 않는다.
푸시할 때도 그 상태 그대로 레지스트리에 푸시되며, 이미지를 풀 받아온다면, 이때 캐시 데이터도 같이 받아오는 방식이 될 것이다.
buildctl build ... \
--output type=image,name=docker.io/username/image,push=true \
--export-cache type=inline \
--import-cache type=registry,ref=docker.io/username/image
사용은 대충 이런 식으로 한다.
export-cache를 통해 빌드 후의 캐싱을 넣어주고, 빌드 이전에는 import-cache를 통해 캐싱 데이터를 받아온다.
인라인 방식은 딱 위와 같은 방식으로밖에 사용하지 못하니 참고.
registry 캐시
레지스트리에 캐싱 데이터를 올리긴 하는데, 이미지 자체와는 구분하여 따로 태그로 구분해서 올린다.
buildctl build ... \
--output type=image,name=localhost:5000/myrepo:image,push=true \
--export-cache type=registry,ref=localhost:5000/myrepo:buildcache \
--import-cache type=registry,ref=localhost:5000/myrepo:buildcache
보다시피 output에 담기는 이미지 태그와 캐시 태그가 다른 것을 볼 수 있다.
여기에 세부적으로 세팅할 수 있는 옵션은 대충 이런 것들이 있다.
- export-cache
- mode= min|max - min이면 최종 이미지 레이어 부분만 캐싱하고(기본값), max면 각 레이어를 전부 캐싱한다.
- ref - 캐싱된 경로!
- image-manifest=true|false - OCI 준수하는 이미지로 올릴지
- import-cache
- 위에 있는 게 전부임
local 캐시
buildctl build ... --export-cache type=local,dest=path/to/output-dir
말 그대로 그냥 로컬 캐싱인데, 사실 이거야 뭐 굳이 이렇게 인자 안 줘도 디폴트값이다.
굳이 캐싱 데이터 경로를 다른 곳으로 지정할 때 쓰면 유용하겠다.
gha 캐시
buildctl build ... \
--output type=image,name=docker.io/username/image,push=true \
--export-cache type=gha,url=주소,token=토큰 \
--import-cache type=gha
깃헙 액션의 캐시를 이용하는 방식이라고 한다!
근데 내가 깃헙 액션을 많이 안 다뤄봐서 구체적으로 어디에 저장된다는 건지는 잘 모르겠다.
s3 캐시
buildctl build ... \
--output type=image,name=docker.io/username/image,push=true \
--export-cache type=s3,region=eu-west-1,bucket=my_bucket,name=my_image \
--import-cache type=s3,region=eu-west-1,bucket=my_bucket,name=my_image
S3와 호환되는 오브젝트 스토리지를 캐시 저장소로 쓰는 것도 가능하다.
이때 ~/.aws/credentials
던 환경변수던 적절하게 빌드킷이 s3와 통신할 수 있을 신원이 세팅돼야 한다.
멀티 스테이지
멀티 스테이지야 흔하니 넘어간다.
위에서 말했듯, 도커파일에 명시된 방식으로 동작하며 각 스테이지는 기본 출력물을 파일시스템 형태로 가지기에 서로 값을 전달하는 게 가능하다.
추가적으로 말할 만한 건.. 빌드 시에 최종 스테이지를 타겟으로 지정해서 빌드도 가능하다는 것?
빌드킷의 경우 명확하게 최종 타겟을 위해 사용되는 스테이지를 분석하여 LLB를 구성하기에 여러 스테이지가 담긴 파일이더라도 최소한의 스테이지만 거칠 수 있도록 보장해준다.
이것 덕분에 하나의 도커 파일에 운영용, 개발용 레이어 설정을 해뒀더라도 타겟을 어떻게 설정하냐에 따라 다른 결과물을 내는 것이 가능하다.
멀티 플랫폼
buildctl build \
--frontend dockerfile.v0 \
--opt platform=linux/amd64,linux/arm64 \
--output type=image,name=docker.io/username/image,push=true \
...
opt 인자를 통해 다양한 아키텍쳐를 설정할 수 있다.
가능한 값은 이렇게 있다고 합니다.[2]
이 방식은 기본적으로는 로컬에 qemu로 해당 아키텍처 빌드가 가능하게 바이너리를 가져오는 식으로 진행된다.
근데 buildctl만으로는 이 기능을 활용하는 게 조금 힘들 수도 있다.
이 경우에는 buildx와 결합하여 더 상위 차원에서 다양한 세팅을 곁들여 사용하는 것이 가능하다.[3]
buildx에서는 각각 다른 노드에 빌드를 맡기거나, 크로스 컴파일이되는 언어에 대한 설정 등을 조금 더 광범위하게 지원해서 조금 더 편리할 것이다.
컨테이너 기반 실행
빌드 자체를 컨테이너 위에서 진행하는 것이 가능하다!
빌드킷은 루트 권한이 없는 상태로도 빌드가 가능하도록 설계됐다.
그래서 이런 이미지들 활용해서 빌드를 하는 것이 가능하다.
apiVersion: batch/v1
kind: Job
metadata:
name: buildkit
spec:
template:
spec:
restartPolicy: Never
initContainers:
- name: prepare
image: busybox
command:
- sh
- -c
- "echo -e 'FROM alpine\nRUN apk add gcc\n' > /workspace/Dockerfile"
securityContext:
runAsUser: 1000
runAsGroup: 1000
volumeMounts:
- name: workspace
mountPath: /workspace
containers:
- name: buildkit
image: moby/buildkit:master-rootless
env:
- name: BUILDKITD_FLAGS
value: --oci-worker-no-process-sandbox
command:
- buildctl-daemonless.sh
args:
- build
- --frontend
- dockerfile.v0
- --local
- context=/workspace
- --local
- dockerfile=/workspace
# To push the image to a registry, add
# `--output type=image,name=docker.io/username/image,push=true`
securityContext:
# Needs Kubernetes >= 1.19
seccompProfile:
type: Unconfined
# Needs Kubernetes >= 1.30
appArmorProfile:
type: Unconfined
# To change UID/GID, you need to rebuild the image
runAsUser: 1000
runAsGroup: 1000
volumeMounts:
- name: workspace
readOnly: true
mountPath: /workspace
# Dockerfile has `VOLUME /home/user/.local/share/buildkit` by default too,
# but the default VOLUME does not work with rootless on Google's Container-Optimized OS
# as it is mounted with `nosuid,nodev`.
# https://github.com/moby/buildkit/issues/879#issuecomment-1240347038
- mountPath: /home/user/.local/share/buildkit
name: buildkitd
# To push the image, you also need to create `~/.docker/config.json` secret
# and set $DOCKER_CONFIG to `/path/to/.docker` directory.
volumes:
- name: workspace
emptyDir: {}
- name: buildkitd
emptyDir: {}
쿠버네티스 환경에서 서버 클라 구조 없이(daemonless), 루트 권한 없이(rootless) 빌드를 하는 예시이다.
buildx 사용법
여기에서는 buildx의 사용법을 잠시 다룬다.
나중에 내용을 다른 곳으로 분화시킬 수도 있다.
https://docs.docker.com/build/builders/drivers/kubernetes/
https://medium.com/nttlabs/buildx-kubernetes-ad0fe59b0c64
buildx를 쓸 때, 이렇게 지원된다. https://docs.docker.com/build/builders/drivers/
https://github.com/docker/buildx?tab=readme-ov-file#building-multi-platform-images
일단 쿠버 환경 빌드 지원하는데, 이때 플랫폼 지정 가능은 한듯
docker buildx create \
--bootstrap \
--name=kube \
--driver=kubernetes \
--driver-opt=namespace=buildkit,qemu.install=true
요런 식으로 가능.
커맨드를 입력하는 환경에서 kubeconfig 설정이 돼있어야 한다.
쿠버환경에서 캐싱방법
https://overcast.blog/optimizing-layer-caching-in-kubernetes-424c23f39587
https://github.com/GoogleContainerTools/kaniko?tab=readme-ov-file#creating-multi-arch-container-manifests-using-kaniko-and-manifest-tool
카니코도 멀티 아크 가능인데, 이건 각 환경에서 실제로 돌아가야한다고 함.
아무래도 공식 지원도 아니라 불편할 것으로 보임
관련 문서
이름 | noteType | created |
---|---|---|
buildKit | knowledge | 2025-03-30 |
buildKit | knowledge | 2025-05-04 |
E-buildKit을 활용한 멀티 플랫폼, 캐싱 빌드 실습 | topic/explain | 2025-03-30 |