Сборка docker образа под различные платформы

Разбираемся, как собирать docker image под различные платформы: amd64(x86-64), arm/v7, arm64/v8.

· 6 минуты на чтение
Сборка docker образа под различные платформы

Docker образы стали стандартным инструментом для тестирования и развертывания ПО. Docker говорит нам: "Build once, deploy anywhere". Звучит отлично, но по умолчанию образы Docker, которые мы создаем, работают на платформе linux/amd64 (x86-64).

Это не учитывает пользователей других платформ. А это значительная аудитория, которая с каждым годом растет. В индустрии происходят важные изменения: ARM архитектура становится все более распространенной. Все больше ноутбуков использует ARM, а такие устройства как Raspberry Pi становятся мощнее. Не стоит забывать про Apple с их новыми процессорами серии M. Больше нельзя ожидать, что все программное обеспечение должно работать только на процессорах x86.

Как пример, на Raspberry Pi 4 (8 Gb) мне удалось запустить TeamCity в докере. А 4 из 5 образов мне приходится пересобирать под мой MacBook на M1 Max.

Если вы соберете образ на архитектуре x86, он не запустится на архитектуре ARM, потому что у этой архитектуры может не оказаться необходимых команд процессора.

Итак, как вы можете создавать образы для этих других платформ? Самый простой способ — просто запустить сборку на целевой платформе. Это сработает во многих случаях, но такие устройства как Raspberry Pi, обычно ограничены в мощности или вовсе неспособны создавать образы.

Чтобы решить эту проблему, Docker ввел принцип мульти-архитектурных сборок. При запуске образа с поддержкой нескольких архитектур клиенты контейнера автоматически выбирают вариант образа, соответствующий вашей операционной системе и архитектуре.

ℹ️
Мульти-архитектурный образ — это тип образа контейнера, который может сочетать варианты для различных архитектур, а иногда и для разных операционных систем.
Вот как выглядит образ в Docker Hub для различных архитектур.

Есть два способа использовать создания мультиархивного образа: с помощью манифеста докера или с помощью docker buildx. Рассмотрим оба этих варианта по порядку. Но сначала узнаем, что такое Docker Manifest.

Спонсор поста

Docker Manifest

Каждый образ Docker представлен манифестом. Манифест — это файл JSON, содержащий всю информацию об образе Docker. Он включает в себя ссылки на каждый из его слоев, их размеры, хэш, а также платформу, на которой он должен работать. Затем на этот манифест можно ссылаться с помощью тега, чтобы его было легко найти.

Например, если вы выполните следующую команду, вы получите манифест образа без нескольких архитектур в репозитории rustlang/rust с тегом nightly-slim:

$ docker manifest inspect --verbose rustlang/rust:nightly-slim
[
	{
		"Ref": "docker.io/rustlang/rust:nightly-slim@sha256:5acc1a41311e82d6ff877dac798f8283511d638883fa718a61dc396f4fd776b8",
		"Descriptor": {
			"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
			"digest": "sha256:5acc1a41311e82d6ff877dac798f8283511d638883fa718a61dc396f4fd776b8",
			"size": 742,
			"platform": {
				"architecture": "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:38f09f84d5e6b1c55bbe525d63d5734892c01207f7d67f1282bdb7ac9b9920fb",
				"size": 2492
			},
			"layers": [
				{
					"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
					"digest": "sha256:6552179c3509e3c4314b4065e0d2790563d01cd474e2fdd58be4d46acd48af6a",
					"size": 27153731
				},
				{
					"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
					"digest": "sha256:72f0c721929d948d14bcc5b4b383727a8242000ecb86f1c05b7facf0b735af9c",
					"size": 262458494
				}
			]
		}
	},
	{
		"Ref": "docker.io/rustlang/rust:nightly-slim@sha256:ff4487a78b71c04f384c8232e489388e78d4bafc2b8a2ee11ff72471263a0724",
		"Descriptor": {
			"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
			"digest": "sha256:ff4487a78b71c04f384c8232e489388e78d4bafc2b8a2ee11ff72471263a0724",
			"size": 742,
			"platform": {
				"architecture": "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:a44e240b23915e620ff22072223d082d4d7c90cd608eb09301879ff21cc54604",
				"size": 2491
			},
			"layers": [
				{
					"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
					"digest": "sha256:4c7c9f6f1115fd4f35807a2f6c1375759365a991748aee0111873e55255f150b",
					"size": 25923216
				},
				{
					"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
					"digest": "sha256:d05d169a780fb952804b80fa0472838866af0117b407b21fa3790eb4d291d77b",
					"size": 306612829
				}
			]
		}
	}
]

Теперь возникает вопрос, как мы можем поместить несколько образов, каждый из которых поддерживает свою архитектуру, за одним и тем же тегом?

Что, если бы этот файл манифеста содержал список манифестов, чтобы Docker мог выбрать тот, который соответствует ему во время выполнения. Именно так строится манифест для мульти-архитектурного образа. Этот тип манифеста называется списком манифестов.

Создаем Manifest руками

Мы начнем с трудного пути, с создания docker manifest руками, потому что это самый старый инструмент для создания мульти-архитектурных образов.

Нам понадобится проект для сборки. Используем следующий Dockerfile, который просто основан на Debian и добавляет в него curl.

ARG ARCH=
FROM ${ARCH}/debian:buster-slim

RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*

ENTRYPOINT [ "curl" ]
Наш Dockerfile

Первым делом мы соберем образы под каждую платформу и отправим их в Docker Hub. Затем мы объединим все эти образы в список манифеста, на который будет ссылается тег.

Обратите внимание на аргумент ARCH в Dockerfile. При сборке образа, мы будем указывать нужную нам базовую версию образа debian. Если бы мы этого не сделали, а оставили бы просто FROM debian:buster-slim, то докер автоматически скачал бы debian под архитектуру, на которой запущена сборка. Поэтому важно, чтобы базовый образ имел вверсию для нужной вам архитектуры. Если этого образа не будет, вам сначала придется собрать его самостоятельно.

Выполним три команды сборки и пуша для каждой архитектуры:

$ docker build -t your-username/multiarch-example:manifest-amd64 --build-arg ARCH=amd64 .
$ docker push your-username/multiarch-example:manifest-amd64
Сборка для AMD64
$ docker build -t your-username/multiarch-example:manifest-arm32v7 --build-arg ARCH=arm32v7 .
$ docker push your-username/multiarch-example:manifest-arm32v7
Сборка для ARM32V7
$ docker build -t your-username/multiarch-example:manifest-arm64v8 --build-arg ARCH=arm64v8 .
$ docker push your-username/multiarch-example:manifest-arm64v8
Сборка для ARM64V8
Наши собранные образы на Docker Hub

Теперь, когда мы создали наши образы и отправили их, мы можем использовать их при создании мулти-архитектурного образа:

docker manifest create \
your-username/multiarch-example:manifest-latest \
--amend your-username/multiarch-example:manifest-amd64 \
--amend your-username/multiarch-example:manifest-arm32v7 \
--amend your-username/multiarch-example:manifest-arm64v8 

После создания манифеста отправляем его в Docker Hub.

docker manifest push your-username/multiarch-example:manifest-latest

Если вы сейчас перейдете в Docker Hub, вы сможете увидеть новый тег:

Задавай вопросы

Docker Buildix

Есть фантастический проект под названием QEMU, который может эмулировать целую кучу платформ. Благодаря недавней работе buildx стало проще, чем когда-либо, использовать QEMU с Docker.

😺
Проект на GitHub: github.com/docker/buildx

Интеграция QEMU основана на функции ядра Linux с немного загадочным именем обработчика binfmt_misc. Когда Linux сталкивается с форматом исполняемого файла, который он не распознает (например, для другой архитектуры), он проверяет с помощью обработчика, есть ли какие-либо приложения, настроенные для работы с этим форматом (например, эмулятор или виртуальная машина). Если есть, он передаст исполняемый файл этому приложению.

Чтобы это работало, нам нужно зарегистрировать интересующие нас платформы в ядре. Если вы используете Docker Desktop, это уже сделано для наиболее распространенных платформ. Если вы используете Linux, вы можете зарегистрировать обработчики запустив последний образ docker/binfmt, например:

$ docker run --privileged --rm docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64

После этого вам может потребоваться перезапустить Docker. Если вы хотите выбрать какие платформы зарегистрировать, или хотите использовать более эзотерическую платформу (например, PowerPC), взгляните на проект qus.

Слегка изменим наш первоначальный Dockerfile:

FROM debian:buster-slim

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

ENTRYPOINT [ "curl" ]

Чтобы создавать мульти-архитектурные образы, нам также необходимо создать экземпляр сборщика.

docker buildx create --use

Сборка под различные архитектуры выполняется с помощью 1 команды:

$ docker buildx build \
--push \
--platform linux/arm/v7,linux/arm64/v8,linux/amd64 \ --tag your-username/multiarch-example:buildx-latest .
Процесс сборки. Видно что он выполняется паралельно для 3 указанных платформ.

В приведенном выше совете об эмуляции вы могли заметить, что мы использовали аргумент --platform для установки платформы сборки, но использовали образ, указанный в строке FROM, как debian:buster. Я уже говорил, что автоматически будет выбран образ платформы изходя из архитектуры машины, но в данном случае будут скачены 3 образа debian под указанные платформы.

Этот метод эффективен, но для более сложных сборок вы можете обнаружить, что он работает слишком медленно, или вы сталкиваетесь с ошибками в QEMU. В таких случаях стоит проверить, можете ли вы кросс-компилировать образ.

Подписывайся на Twitter

Сборка в GitLab CI

И напоследок, расскажу как собирать контейнеры для различных архитектур используя GitLab CI. Например, когда вы пушите тег формата v.*, GitLab CI собирает образ и пушит его в Docker Hub.

docker-build:
  image: upagge/docker-buildx:latest
  stage: deploy
  only:
    - /^v.*$/
  except:
    - branches
  services:
    - docker:dind
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin
  script:
    - docker buildx create --use
    - docker buildx build --push --platform linux/amd64,linux/arm64/v8 -t "$CI_REGISTRY_IMAGE:latest" -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_TAG" .

Для сборки используется мой образ buildx, который основан на официальном образе Docker, в который добавлен buildx. Не забудьте указать свои переменные GitLab CI.

Настройка GitLab CI CD для Java приложения
На примере Java приложения рассматриваем сборку и деплой на сервер с помощью GitLab CI.
Подробнее о GitLab CI в этой статье ☝️

Заключение

Итак, мы узнали, что не все пользователи образов Docker используют amd64. С помощью buildx и QEMU можно поддерживать этих пользователей не прилагая много усилий.

Также разобрались, как автоматизировать этот процесс для репозиториев git с помощью GitLab CI, но это можно сделать и из любой другой системы CI.

Struchkov Mark
Struchkov Mark
Задавайте вопросы, если что-то осталось не понятным👇