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

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

· 7 минуты на чтение

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

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

В индустрии происходят важные изменения: архитектура ARM набирает популярность. Все больше ноутбуков работают на ARM-процессорах, а устройства, такие как Raspberry Pi, становятся мощнее. Примером служит мой опыт использования Raspberry Pi 4 (8 ГБ), на котором мне удалось запустить TeamCity в Docker-контейнере

Также компания Apple с ее процессорами серии M также внесла существенный вклад в это изменение. Однако из пяти образов четыре мне пришлось пересобирать для работы на моем MacBook с процессором M1 Max.

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

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

Вот как выглядит образ в Docker Hub для различных архитектур.

Давайте рассмотрим, как можно создать такие образы двумя способами: с использованием Docker Manifest и docker buildx. Но сначала разберемся, что такое Docker Manifest.

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

Docker Manifest

Каждый Docker-образ сопровождается манифестом — это JSON-файл, который описывает все ключевые характеристики образа. Этот файл включает ссылки на слои образа, их размеры, контрольные суммы (хэши) и платформу, на которой образ должен запускаться.

Манифест помогает Docker правильно идентифицировать образ и позволяет ссылаться на него через тег, что делает поиск и использование образов более удобным.

Например, команда docker manifest inspect rustlang/rust:nightly-slim покажет манифест образа 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 list).

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

Начнем с более сложного подхода — создания Docker Manifest вручную. Сначала мы создадим образы для каждой архитектуры и отправим их в Docker Hub. Затем объединим эти образы в мульти-архитектурный манифест, который будет ссылаться на общий тег.

Для примера нам понадобится проект для сборки. Используем следующий 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

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

Теперь выполним команды для сборки и публикации образов для каждой архитектуры:

Сборка для AMD64:

$ docker build -t your-username/multiarch-example:manifest-amd64 --build-arg ARCH=amd64 .
$ docker push your-username/multiarch-example:manifest-amd64

Сборка для ARM32V7:

$ docker build -t your-username/multiarch-example:manifest-arm32v7 --build-arg ARCH=arm32v7 .
$ docker push your-username/multiarch-example:manifest-arm32v7

Сборка для ARM64V8:

$ docker build -t your-username/multiarch-example:manifest-arm64v8 --build-arg ARCH=arm64v8 .
$ docker push your-username/multiarch-example:manifest-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 автоматически выберет правильную архитектуру для конечной системы, на которой будет запущен образ.

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

Docker Buildix

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

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

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

Чтобы это заработало, необходимо зарегистрировать интересующие нас платформы в системе. Если вы используете 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 для мульти-архитектурных сборок, необходимо сначала создать и активировать экземпляр сборщика:

docker buildx create --use

Теперь можно выполнить сборку образов для нескольких платформ с помощью одной команды:

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

В команде сборки используется аргумент --platform для указания архитектур, на которые мы нацеливаемся. Это позволяет Docker скачивать соответствующие образы для каждой платформы. Например, если в нашем Dockerfile указан базовый образ FROM debian:buster, Docker автоматически выберет образы Debian для всех указанных платформ (arm/v7, arm64/v8 и amd64), что упрощает процесс мульти-архитектурной сборки.

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

Сборка в GitLab CI

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

Ниже приведен пример конфигурации .gitlab-ci.yml для сборки мульти-архитектурного Docker-образа:

docker-build:
  image: docker.struchkov.dev/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.

  • CI_REGISTRY_USER — имя пользователя для Docker Hub или другого регистра.
  • CI_REGISTRY_PASSWORD — пароль или токен доступа.
  • CI_REGISTRY_IMAGE — путь к образу в вашем реестре.
Настройка GitLab CI CD для Java приложения
На примере Java приложения рассматриваем сборку и деплой на сервер с помощью GitLab CI.

Подробнее о GitLab CI в этой статье ☝️

Заключение

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

Мы также рассмотрели, как автоматизировать этот процесс для репозиториев в GitLab CI, создавая мульти-архитектурные образы при пуше тегов. Однако аналогичный процесс можно реализовать и в других CI-системах, таких как Jenkins, CircleCI или GitHub Actions. Основные принципы остаются одинаковыми: использование buildx для сборки и публикации образов и настройка аутентификации для Docker Registry.

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