CPU 아키텍처에 따른 Docker Multi Architecture 빌드 구성하기
IT 회사에 입사하면 기본적으로 맥북을 주는 회사들이 많아졌다. 물론 안 주는 회사도 있겠지만 개인용으로도 맥북을 사용하는 것이 많이 일반화되었을만큼 개발자들에게는 필수품이 아닌 필수품이 되었다.
하지만 로컬에서 맥북 M1 (ARM 기반 CPU)에서 개발해서 정상동작을 하였으나 개발 (AMD 기반 CPU) 서버에 배포하면 기능이 정상적으로 동작을 하지 않거나 호환성 오류가 발생하는 경우를 경험해 보았을 것이다.
여러가지 호환성 오류가 많은데 그 중 일부분인 Docker 환경에서의 호환성 해결을 위한 Multi Architechture 빌드 전략에 대해서 소개해보고자 한다.
🚀 개요
로컬 (ARM 기반 CPU) 개발해서 배포는 Ubuntu (AMD 기반 CPU)에 Docker로 배포하는 빌드 전략에 대해 알아보도록 하자.
- 로컬: 맥북 M1 (ARM 기반 CPU)
- 서버: Ubuntu 서버 (AMD 기반 CPU)
📘 배경지식
일단 Docker 빌드 방법을 알아보기 전 쌩기초의 배경지식인 CPU 제조사와 아키텍처 종류부터 알아야한다.
아래 링크를 타고 들어가면 설명을 초보도 잘 알도록 설명해놨으며 일부 발췌하여 이 블로그에 기재를 하였다.
참고로 저는 이 분과는 전혀 어떠한 관련이 없다.
CPU 제조사
Intel
PC에서 가장 많이 사용하는 안정적인 CPU를 만드는 제조사로 x86 아키텍처의 CPU를 생산한다.
AMD
Intel의 x86 아키텍처 호환 CPU를 만드는 회사로 성능은 빠르고 가성비 좋은 CPU를 내놓았던 회사로 유명하다.
ARM
스마트폰 시대에 접어들면서 모바일용을 많이 사용하다보니 저전력고효율의 수요가 많아져서 나오게 된 제품이다.
ARM CPU로 만든 서버인 AWS Graviton이라는 인스턴스를 내놓으면서 시장을 뒤흔들었으며 M1 CPU 맥북에도 탑재하면서 사람들 입방에 오르내리게 했다.
ARM 회사는 존재하지만 누구나 ARM 기반으로 커스터마이징한 칩을 직접 생산 판매해도 된다고한다.
CPU 아키텍처 종류
x86
- Intel 기반 32bit CPU 아키텍처이다.
- window, linux, mac까지 OS를 지원하고 있다.
x86_64 (amd64)
- Intel 기반 64bit CPU 이며, x86과 호환된다.
- AMD 회사에서 만들었으며 Intel에서도 크로스 라이센스로 둘 다 사용하고 있다.
arm
- arm 기반 32bit CPU이며 Intel 기반과 달라서 x86, x86_64는 호환이 되지 않는다.
- Linux, Mac, Android, iOS 등 조그만 기기에서 성능 내야하는 경우 많이 사용된다.
arm64 (arm64/v8)
- arm 기반의 64bit CPU이며 32bit arm 과 호환된다.
- Linux, Mac, Android, iOS 등 조그만 기기에서 성능 내야하는 경우 많이 사용된다.
🚩 빌드 전략
컴퓨터의 CPU는 기본 아키텍처에 대한 바이너리만 실행할 수 있다. x86 CPU는 ARM 바이너리를 실행할 수 없으며 그 반대의 경우도 마찬가지이다. 그렇다면 Intel 시스템에서 실행할 때 ARM용 바이너리를 어떻게 실행할 수 있을까?
직접 실행하는 대신 QEMU 에뮬레이터를 통해 바이너리를 실행할 수 있다.
지금부터 QEMU 에뮬레이터를 사용하는 전략과 Cross-Compile 전략 두 가지를 소개하고자한다.
QEMU 에뮬레이터 전략
QEMU 에뮬레이터를 사용하여 다양한 아키텍처에 빌드하는 전략이다.
- 장점: Dockerfile을 전혀 수정할 필요가 없으며 여러 플랫폼용으로 자동으로 빌드할 수 있다.
- 단점: 이러한 방식으로 실행되는 바이너리는 아키텍처 간에 지침을 지속적으로 변환해야 하므로 기본 속도로 실행되지 않으며 때로는 에뮬레이션 레이어에서 버그를 유발하는 사례를 찾을 수도 있다.
Cross-Compile 전략
Cross-Compile을 사용하여 특정 아키텍처용 이미지를 미리 빌드하는 전략이다.
에뮬레이션과 크로스 컴파일의 차이점은 전자에서는 소프트웨어에서 다른 아키텍처의 전체 시스템을 에뮬레이션하는 반면에 Cross-Compile에서는 새로운 바이너리를 생성하는 특수 구성 옵션을 사용하여 기본 아키텍처용으로 구축된 바이너리만 사용한다는 것이다.
차이점은 다음과 같은 구조로 이해할 수 있다.
💻 QEMU 에뮬레이터
호스트 OS에 등록된 QEMU 바이너리가 컨테이너 내에서 투명하게 작동하려면 정적으로 컴파일되고 fix_binary플래그로 등록되어야한다. 이를 위해서는 커널 버전 4.8 이상 및 binfmt-support버전 2.1.7 이상이 필요하다.
1. 현재 Docker host 아키텍처 확인
$ uname -m
x86_64
2. docker로 arm64/v8용 이미지로부터 컨테이너 생성
호스트는 x86_64지만 Docker Image가 arm64일 경우 다음과 같이 빌드가 되지않아 오류가 발생하는 것을 볼 수 있다.
$ docker run --rm -t arm64v8/ubuntu uname -m
WARNING: The requested image's platform (linux/arm64/v8) does not match the detected host platform (linux/amd64/v2) and no specific platform was requested
exec /usr/bin/uname: exec format error
3. QEMU 애뮬레이터를 host 파일에 설치해보자
--privileged 옵션을 주어 docker에 특별 권한을 주고 호스트에 qemu-xxx 파일을 복사하도록 한 다음 삭제하는 옵션이다.
$ docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
4. docker로 arm64/v8용 이미지로부터 컨테이너 생성
Ubuntu 기준
다음과 같이 QEMU 파일이 잘 복사가 되었는지 확인해보자
$ ls -al /proc/sys/fs/binfmt_misc/qemu-*
-rw-r--r-- 1 root root 0 2월 22 15:24 /proc/sys/fs/binfmt_misc/qemu-aarch64
-rw-r--r-- 1 root root 0 2월 22 15:24 /proc/sys/fs/binfmt_misc/qemu-aarch64_be
-rw-r--r-- 1 root root 0 2월 22 15:24 /proc/sys/fs/binfmt_misc/qemu-alpha
-rw-r--r-- 1 root root 0 2월 22 15:24 /proc/sys/fs/binfmt_misc/qemu-arm
...
<생략>
...
ARM 기반 Docker 이미지를 실행해보자. 다음과 같이 잘 실행되는 것을 볼 수 있다.
$ docker run --rm -t arm64v8/ubuntu uname -m
aarch64
CentOS 기준
안타깝게도 CentOS는 -static 에 해당하는 바이너리 버전이 없다.
$ ls /usr/bin/qemu-aarch64*
/usr/bin/qemu-aarch64
컨테이너에는 이미지에 필요한 바이너리가 포함되어 있으므로 다음과 같이 우회하는 방법으로 호스트에 복사할 수 있다.
$ docker run --rm --entrypoint tar multiarch/qemu-user-static \
-C /usr/bin -cf- . | tar -C /usr/bin -xf-
ARM 기반 Docker 이미지를 실행해보자. 다음과 같이 잘 실행되는 것을 볼 수 있다.
하지만 ubuntu와 다른점은 QEMU 바이너리에 대한 볼륨을 연결하는 방법으로 실행을 해야한다.
$ docker run --platform linux/arm64 --rm -v /usr/bin/qemu-aarch64-static:/usr/bin/qemu-aarch64-static oraclelinux:8.5 uname -a
Linux 2d7e2e0ce6d7 3.10.0-1160.66.1.el7.x86_64 #1 SMP Wed May 18 16:02:34 UTC 2022 aarch64 aarch64 aarch64 GNU/Linux
💻 Cross-Compile
다중 플랫폼 이미지 빌드를 위해서는 Docker Buildx가 필요하다. Docker Engine 19.03 이상부터 필요하며 최신버전부터 기본적으로 탑재되어 있다. 위에서 언급한 QEMU 에뮬레이터가 설치되어 있어야한다.
1. docker buildx 확인
docker buildx ls 각 빌더에 어떤 에뮬레이터가 설치되어 있는지 보여준다.
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default docker
default default running v0.12.4+3b6880d2a00f linux/amd64, linux/amd64/v2, linux/386, linux/arm64, linux/riscv64, linux/ppc64, linux/ppc64le, linux/s390x, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
docker buildx 생성
default는 기본적으로 생성되어 있으며 platform에 arch가 포함되어 있지 않을 경우 다음과 같이 builder를 생성할 수 있다.
$ docker buildx create --name custom_builder --bootstrap --use
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
custom_builder * docker-container
custom_builder0 unix:///var/run/docker.sock running v0.12.5 linux/amd64, linux/amd64/v2, linux/arm64, linux/riscv64, linux/ppc64, linux/ppc64le, linux/s390x, linux/386, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
default docker
default default running v0.12.4+3b6880d2a00f linux/amd64, linux/amd64/v2, linux/386, linux/arm64, linux/riscv64, linux/ppc64, linux/ppc64le, linux/s390x, linux/mips64le, linux/mips64, linux/arm/v7, linux/arm/v6
2. arm64/v8용 이미지 빌드해보기
Docker을 작성한다.
$ cat > Dockerfile
FROM arm64v8/ubuntu
CMD uname -m
Docker Image를 이제 빌드해보자.
$ docker buildx build -t test-arm64v8 . --load
Docker Container 실행
$ docker run --rm test-arm64v8
aarch64