[Docker] Buildx로 cross-platform 이미지 빌드하기

bluewhale·2022년 2월 21일
0

Docker

목록 보기
8/8
post-thumbnail

m1 맥북을 사용하다보면, 맥북에서 빌드한 이미지가, 서로 다른 CPU 아키텍처로 인해 서버 에러에서 실행할 수 없는 경우가 발생합니다. 그래서, 이번 포스트에서는 다양한 플랫폼에서 지원되는 이미지를 빌드하는 방법에 대해 다루어 보았습니다.

Buildx

Docker는 multi-architecture 빌드 등, 다양한 빌드 옵션을 지원하는 CLI 플러그인을 제공합니다. Buildx는 19.03 이후 버전부터 사용이 가능하다고 하니 버전 확인이 필요합니다. 공식 문서에 따르면, Docker Desktop을 사용하는 Windows나 MacOS 사용자 혹은 DEB, RPM 패키지로 도커를 설치한 사용자들은 자동으로 Buildx 플러그인이 포함되어 있습니다. docker buildx 명령어를 터미널에 입력했을 때, 다음과 같은 화면이 출력된다면 buildx를 사용할 수 있습니다.

$ docker buildx

Usage:  docker buildx [OPTIONS] COMMAND

Extended build capabilities with BuildKit

Options:
      --builder string   Override the configured builder instance

Management Commands:
  imagetools  Commands to work on images in registry

Commands:
  bake        Build from a file
  build       Start a build
  create      Create a new builder instance
  du          Disk usage
  inspect     Inspect current builder instance
  ls          List builder instances
  prune       Remove build cache
  rm          Remove a builder instance
  stop        Stop builder instance
  use         Set the current builder instance
  version     Show buildx version information

Run 'docker buildx COMMAND --help' for more information on a command

일부 하위 버전에서는 buildx 플러그인을 사용하기 위해서는 ~/.docker/config.json에 아래의 옵션을 추가해주어야 합니다. 아래 예제에서 사용할 docker manifest와 같은 커맨드들을 사용하기 위해서도 아래의 옵션을 추가해야 합니다.

# ~/.docker/config.json

{
    ...
    "experimental": "enabled"
}

QEMU

Docker는 내부적으로 QEMU를 사용하여 빌드 환경을 에뮬레이트(emulate) 합니다. QEMU는 리눅스에서 사용하는 Hypervisor의 일종으로, 서로 다른 아키텍처 기반의 명령어를 번역(binary translation)하여 커널에 전달하는 등, 가상의 하드웨어 환경이 존재하는 것처럼 꾸며내는 역할을 수행하는 에뮬레이터입니다.

QEMU는 내부적으로 binfmt_misc handler라 불리는 리눅스 커널 기능을 사용합니다. 리눅스 커널은 현재 호스트 환경에서 해석할 수 없는 명령어(ex, 다른 아키텍처)를 만났을 때, 유저 스페이스에 해당 명령어를 처리할 수 있는 어플리케이션이 존재하는지 찾아보고, 만약 존재한다면 해당 어플리케이션이 해당 명령어를 처리하도록 합니다.

저는 m1 맥북에서, Docker Desktop을 사용하고 있는데, Docker Desktop에서는 이미 자주 사용되는 플랫폼의 환경설정이 되어 있습니다. 터미널에서 docker buildx ls 명령어를 실행하면, Docker Desktop에서 이미 제공하는 플랫폼들의 종류를 확인할 수 있습니다.

$ docker buildx ls

desktop-linux                docker
  desktop-linux              desktop-linux               running  linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
default                      docker
  default                    default                     running  linux/arm64, linux/amd64, linux/riscv64, linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6

리눅스 OS 사용자는 추가적으로 binfmt을 사용하여 binfmt_misc handler에 핸들러를 추가로 설치할 수도 있습니다.

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

{
  "supported": [
    "linux/arm64",
    "linux/amd64",
    "linux/riscv64",
    "linux/ppc64le",
    "linux/s390x",
    "linux/386",
    "linux/mips64le",
    "linux/mips64",
    "linux/arm/v7",
    "linux/arm/v6"
  ],
  "emulators": [
    "qemu-arm",
    "qemu-i386",
    "qemu-mips64",
    "qemu-mips64el",
    "qemu-ppc64le",
    "qemu-riscv64",
    "qemu-s390x",
    "qemu-x86_64"
  ]
}

Cross-Platform Build

이제 Buildx를 사용하여 실제로 서로 다른 플랫폼을 위한 이미지를 빌드해보도록 하겠습니다.

먼저, 빌드된 이미지가 런타임 환경의 시스템 아키텍처를 출력하도록 다음과 같이 Dockerfile을 작성합니다.

FROM debian:buster
 
CMD uname -m

베이스 이미지를 선택할 때에는 해당 이미지의 manifest를 확인하여 빌드하고 싶은 플랫폼을 지원하는지 여부를 확인해야 합니다. Docker는 특정 플랫폼의 이미지를 빌드할 때에, 자동으로 베이스 이미지에서 동일한 플랫폼으로 빌드된 이미지를 가져와서 사용하기 때문입니다. 예를 들어, x86 아키텍처를 지원하지 않는 베이스 이미지를 사용하여 동일한 아키텍처로 이미지를 빌드할 수 없습니다.

예제에서 사용한 debian:buster 이미지를 비롯하여 베이스 이미지로 널리 사용되는 이미지들은 대부분, 다양한 플랫폼을 지원합니다.

$ docker buildx imagetools inspect debian:buster

Name:      docker.io/library/debian:buster
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest:    sha256:fde7a280413ec0122bd3a14dc76ba152f89cae999f3b8efe8784100df3640763

Manifests:
  Name:      docker.io/library/debian:buster@sha256:5bb6e8c4f738d2da8662393543ddacdc8be9e421c46ce5316d2877c73c1fde16
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/amd64

  Name:      docker.io/library/debian:buster@sha256:59aca2b78d3884d0aed0aa32af61195dd0b96da1697fd4dfc64c5f504580a32c
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/arm/v5

  Name:      docker.io/library/debian:buster@sha256:705be950aa01d4efc68285cd6bdad9eb2ce1e64f67f95f6af3f5e0c12ebfc334
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/arm/v7

  Name:      docker.io/library/debian:buster@sha256:b9c2eac754faff5c52adbdadaef1ddef8c941c3e15060938e9d32db00472f82d
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/arm64/v8

  Name:      docker.io/library/debian:buster@sha256:77c49f5cbd69af85d855cae68e4975e6df56af852f766b3337a89cd5b0fc2654
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/386

  Name:      docker.io/library/debian:buster@sha256:97558357837f017e74d36a32e6977024ecf4e77a475ea487c8efa88a88928191
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/mips64le

  Name:      docker.io/library/debian:buster@sha256:e0737f57e6d1b26d1ff8a5a31dc08c453c2a918e029baab2be377459bc063446
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/ppc64le

  Name:      docker.io/library/debian:buster@sha256:0c4c4fd046c7eddcc3132f50dda341945d1e269a0556c0c8c03160e35aac04bb
  MediaType: application/vnd.docker.distribution.manifest.v2+json
  Platform:  linux/s390x

다음으로, 별도의 설정없이 이미지를 빌드하고 컨테이너를 생성하였습니다. 저는 m1 맥북을 사용하고 있기 때문에 호스트 환경과 동일한 aarch64가 출력되었습니다.

$ docker buildx build -t print-host-arch --load .
$ docker run --rm print-host-arch

aarch64

이번에는 platform=linux/amd64 옵션을 추가하여 이미지를 빌드한 후, 동일하게 실행하였습니다. 출력 결과를 살펴보면, 이전과 다르게 x86_64(AMD64의 실제 이름)를 출력하는 것을 확인할 수 있습니다.

$ docker buildx build --platform=linux/amd64 -t print-different-arch --load .
$ docker run print-different-arch

x86_64

마지막으로, platform=linux/amd64,linux/arm64 옵션을 추가하여 amd64arm64를 모두 지원하는 이미지를 빌드하였습니다. 이번에는 이미지를 빌드하고 푸시한 뒤 docker manifest inspect 명려어를 사용하여 이미지를 검사하였습니다. 출력 결과를 보면, 해당 이미지는 amd64arm64로 각각 빌드된 것을 확인할 수 있습니다.

$ docker buildx build --platform=linux/amd64,linux/arm64 -t <user-name>/print-arch --push .
$ docker manifest inspect --verbose <user-name>/print-host-arch

[
	{
		"Ref": "docker.io/<user-name>/print-arch:latest@sha256:4edf8cb577eb5ff3f2db876a947c6c50afdda11c33162441a3c4371721c0dbba",
		"Descriptor": {
			"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
			"digest": "sha256:4edf8cb577eb5ff3f2db876a947c6c50afdda11c33162441a3c4371721c0dbba",
			"size": 528,
			"platform": {
				"architecture": "amd64", # !! amd64 !!
				"os": "linux"
			}
		},
		"SchemaV2Manifest": {
			"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
			"schemaVersion": 2,
			"config": {
				"mediaType": "application/vnd.docker.container.image.v1+json",
				"digest": "sha256:45ae9aad12b8a18933969e1fb418841ac14969f8635c3fa019bc19df5afcc398",
				"size": 784
			},
			"layers": [
				{
					"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
					"digest": "sha256:a024302f8a017855dd20a107ace079dd543c4bdfa8e7c11472771babbe298d2b",
					"size": 50437057
				}
			]
		}
	},
	{
		"Ref": "docker.io/<user-name>/print-arch:latest@sha256:9cc943ec4351ce2e7a11e0ecbab4ec19ed45b7d67f9941228f361b4755688eb0",
		"Descriptor": {
			"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
			"digest": "sha256:9cc943ec4351ce2e7a11e0ecbab4ec19ed45b7d67f9941228f361b4755688eb0",
			"size": 528,
			"platform": {
				"architecture": "arm64",  # !! arm64 !!
				"os": "linux"
			}
		},
		"SchemaV2Manifest": {
			"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
			"schemaVersion": 2,
			"config": {
				"mediaType": "application/vnd.docker.container.image.v1+json",
				"digest": "sha256:68294e75b3f9ecbb98d4a7c50f8d59cfb13b0a4a09504afabbfd0e773a300a89",
				"size": 780
			},
			"layers": [
				{
					"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
					"digest": "sha256:ccd458f933f7966e412773ee1551aaf2433a5bf9adaae519e2ac7c9c3f8b5f89",
					"size": 49223041
				}
			]
		}
	}
]

마무리

이번 포스팅에서는 Docker를 활용하여 다양한 플랫폼을 지원하는 이미지를 빌드하는 방법에 대해 다뤄보았습니다. 빌드된 이미지가 호스트 환경에 영향을 받는 것을 방지하기 위해, 이미지를 빌드하는 경우에는 --platform 옵션을 추가하여 사용하면 유용할 것 같습니다.

Reference

profile
안녕하세요

0개의 댓글