멀티 플랫폼 빌드

멀티 플랫폼 빌드 (Multi-platform builds)

한 번의 빌드 명령으로 서로 다른 OS나 CPU 아키텍처 조합을 위한 이미지를 동시에 만들 수 있으면 참 편하겠죠. '멀티 플랫폼 빌드'는 바로 그런 작업을 뜻해요. 이 페이지로 소개하는 방법을 따라 하면 linux/amd64용 이미지와 linux/arm64용 이미지를 따로 만들 필요 없이 하나의 이미지 안에 여러 변형을 담아 배포할 수 있답니다.

출처: https://docs.docker.com/build/building/multi-platform/

본문

'내 컴퓨터에선 잘 돌아가는데'라고요? 도커는 애플리케이션과 그 의존성을 컨테이너에 묶어 개발·테스트·운영 등 여러 환경에서 똑같이 실행되게 해주는 도구예요. 그런데 문제는 하드웨어까지 같으리란 보장이 없다는 점이에요. 개발자는 x86-64 노트북을 쓰고, 운영 서버는 ARM 계열 클라우드 인스턴스일 수 있거든요. 멀티 플랫폼 빌드는 이런 상황에서 애플리케이션의 여러 변형을 하나의 이미지로 묶어, 에뮬레이션 없이도 같은 이미지를 다양한 하드웨어에서 실행할 수 있게 해줘요.

단일 플랫폼 이미지와의 차이

단일 플랫폼 이미지를 레지스트리에 올리면 그 이미지 하나만 저장돼요. 반면 멀티 플랫폼 이미지를 push하면 레지스트리는 매니페스트 목록(manifest list) 과 개별 매니페스트들을 함께 저장해요. 나중에 이미지를 pull하면 레지스트리가 매니페스트 목록을 돌려주고, 도커가 호스트 아키텍처에 맞는 변형을 자동으로 골라요. 예를 들어 ARM 기반 라즈베리 파이에서 실행하면 linux/arm64 변형이, x86-64 노트북에서는 linux/amd64 변형이 선택되는 식이죠.

선행 조건

멀티 플랫폼 이미지를 쓰려면 매니페스트 목록을 지원하는 이미지 저장소가 필요해요. Docker Desktop과 Docker Engine 29.0 이상은 기본적으로 containerd 이미지 저장소를 사용하므로 별도 설정 없이 바로 쓸 수 있어요. 더 오래된 Docker Engine을 쓰거나 클래식 스토리지 드라이버로 업그레이드한 경우에는 두 가지 선택지가 있어요. daemon 설정 파일로 containerd 이미지 저장소를 켜거나, 아래처럼 docker-container 드라이버를 쓰는 커스텀 빌더를 만드는 방법이에요.

$ docker buildx create \
  --name container-builder \
  --driver docker-container \
  --bootstrap --use

참고로 docker-container 드라이버로 빌드한 결과물은 Docker Engine 이미지 저장소에 자동으로 로드되지 않아요. 그래서 이 경우엔 docker build --push로 바로 컨테이너 레지스트리에 올려야 해요.

멀티 플랫폼 이미지 빌드하기

빌드를 실행할 때 --platform 플래그로 대상 플랫폼을 지정하면 그 조합만큼 한 번에 만들어요.

$ docker buildx build --platform linux/amd64,linux/arm64 .

멀티 플랫폼 빌드를 할 수 있는 전략은 크게 세 가지예요. 상황에 따라 골라 쓰면 돼요.

  1. QEMU 에뮬레이션 — 시작하기 가장 쉬운 방법이에요. Dockerfile을 바꿀 필요가 없고, BuildKit이 에뮬레이션 가능한 아키텍처를 자동으로 감지해요. 다만 에뮬레이션은 컴파일처럼 계산이 많은 작업에서 네이티브 빌드보다 훨씬 느릴 수 있어요.
  2. 네이티브 노드 여러 개 — QEMU로 처리하기 어려운 복잡한 경우에 더 좋고 성능도 우수해요. 준비된 도커 컨텍스트를 --append로 빌더에 추가하는 방식이에요.
  3. 크로스 컴파일 — 멀티스테이지 빌드와 미리 정의된 빌드 인자를 조합해 각 플랫폼을 위한 바이너리를 직접 만드는 방법이에요.

QEMU를 수동으로 설치해야 할 때

대부분은 BuildKit이 QEMU 사용자 모드 에뮬레이터를 동봉하고 있어서 따로 설치할 필요가 없어요. 그런데 에뮬레이션이 실패하면 아래 명령으로 QEMU를 설치하고 호스트에 실행 가능한 파일 형식을 등록해요.

$ docker run --privileged --rm tonistiigi/binfmt --install all

이 명령은 QEMU 바이너리를 설치하고 binfmt_misc에 등록한 뒤, 호스트 OS에 등록된 실행 형식이 컨테이너 안에서도 투명하게 동작하게 해줘요. 등록이 잘 됐는지는 /proc/sys/fs/binfmt_misc/qemu-* 플래그 중 F가 있는지 확인하면 돼요.

다중 네이티브 노드 빌더

여러 네이티브 노드를 쓰면 QEMU가 처리하지 못하는 복잡한 케이스를 다루고 성능도 더 좋아져요. 이미 준비된 컨텍스트(node-amd64, node-arm64)를 빌더에 추가하는 예시를 볼게요.

$ docker buildx create --use --name mybuild node-amd64
$ docker buildx create --append --name mybuild node-arm64
$ docker buildx build --platform linux/amd64,linux/arm64 .

크로스 컴파일로 Go 애플리케이션 빌드하기

크로스 컴파일은 BuildKit이 미리 정의해 둔 빌드 인자(BUILDPLATFORM, TARGETOS, TARGETARCH)로 플랫폼 정보를 얻어 컴파일러에 넘기는 방식이에요. Go는 GOOSGOARCH 환경변수로 대상 플랫폼을 지정해요. 아래는 크로스 컴파일을 반영한 Dockerfile이에요.

# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
ADD https://github.com/dvdksn/buildme.git#eb6279e0ad8a10003718656c6867539bd9426ad8 .
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o server .

FROM alpine
COPY --from=build /app/server /server
ENTRYPOINT ["/server"]

핵심은 세 가지예요. 첫째, FROM--platform=$BUILDPLATFORM으로 빌더 플랫폼에 맞는 단계를 고정해요. 둘째, ARG TARGETOSARG TARGETARCH를 선언해 컴파일 단계에서 대상 플랫폼을 쓸 수 있게 해요. 셋째, GOOSGOARCH에 이 인자들을 넣어 크로스 컴파일을 수행해요. 빌드는 이렇게 실행해요.

$ docker build --platform linux/amd64,linux/arm64 -t go-server .

크로스 컴파일 방법은 프로그래밍 언어마다 조금씩 달라지니, 자신의 언어 문서에서 구체적인 방법을 확인하는 게 좋아요. 언어별 크로스 컴파일 헬퍼가 필요하면 tonistiigi/xx 이미지도 참고할 만해요.

더 알아보기