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 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
.
Первым делом мы соберем образы под каждую платформу и отправим их в Docker Hub. Затем мы объединим все эти образы в список манифеста, на который будет ссылается тег.
Обратите внимание на аргумент ARCH
в Dockerfile. При сборке образа, мы будем указывать нужную нам базовую версию образа debian
. Если бы мы этого не сделали, а оставили бы просто FROM debian:buster-slim
, то докер автоматически скачал бы debian под архитектуру, на которой запущена сборка. Поэтому важно, чтобы базовый образ имел вверсию для нужной вам архитектуры. Если этого образа не будет, вам сначала придется собрать его самостоятельно.
Выполним три команды сборки и пуша для каждой архитектуры:
Теперь, когда мы создали наши образы и отправили их, мы можем использовать их при создании мулти-архитектурного образа:
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.
Интеграция 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 .
В приведенном выше совете об эмуляции вы могли заметить, что мы использовали аргумент --platform
для установки платформы сборки, но использовали образ, указанный в строке FROM
, как debian:buster
. Я уже говорил, что автоматически будет выбран образ платформы изходя из архитектуры машины, но в данном случае будут скачены 3 образа debian под указанные платформы.
Этот метод эффективен, но для более сложных сборок вы можете обнаружить, что он работает слишком медленно, или вы сталкиваетесь с ошибками в QEMU. В таких случаях стоит проверить, можете ли вы кросс-компилировать образ.
Сборка в 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.
Заключение
Итак, мы узнали, что не все пользователи образов Docker используют amd64. С помощью buildx и QEMU можно поддерживать этих пользователей не прилагая много усилий.
Также разобрались, как автоматизировать этот процесс для репозиториев git с помощью GitLab CI, но это можно сделать и из любой другой системы CI.