О проекте

Сборщик OCI-образов, который использует altflow/altflow как backend, а repoteka в качестве инструмента проверки обновлений бинарных пакетов. Концепция работы altflow описана в документации altflow.

Проект во многом вдохновлён cloud/image-forge. Цели:

  • автоматизировать процесс сборки OCI-образов для целевого Registry;
  • дать широкому кругу контрибьютеров понятный алгоритм, чтобы они могли участвовать в добавлении новых восстребованных образов в автоматизированный пайплайн flowforge.

Полная документация проекта flowforge

Периодическая пересборка образов

Flowforge CI с заданной периодичностью сравнивает зафиксированные в репозитории версии бинарных пакетов с актуальной информацией от repoteka.mskdc.altlinux.org. Образы, бинарные пакеты которых обнововились, отправляются на пересборку. Образы, чьи source-образы обновились, также отправляются на пересборку.

Состояние последних успешно обработанных сборок хранится в ветках *-artefacts. Там же хранится служебное состояние пересборок, вызванных обновлением source-образов. Подробнее об этом написано в org/README.md.

Участие в добавлении новых образов в flowforge

Вы имеете право предложить новые образы в автоматизированный пайплайн flowforge. В первую очередь заведите новую задачу в Issues (Задачах) репозитория с обоснованием необходимости добавления новых образов. После появления задачи либо один из контрибьютеров flowforge может согласиться её выполнить, либо вы можете выполнить задачу самостоятельно. В последнем случае следуйте инструкции:

  1. Сделайте форк текущего репозитория.
  2. Дополните одну или несколько организаций каталога org/ спецификациями новых образов, которые вы предлагаете добавить. Спецификация образа содержит:
  • Dockerfile.template
  • altflow-test
  • info.yaml *
  • README.md (желательно)
  1. Сделайте Pull Request (PR) в ветку main данного основного репозитория, запустятся базовые тесты.
  2. После выполенения первичных тестов мейнтейнер flowforge вручную должен запустить тестовую сборку образов, если вы вносили изменения в org. Если запущенная тестовая сборка завершится красными индикаторами, изучите логи сборки в PR и поправьте ошибки в файлах спецификации.
  3. Когда PR успешно прошёл полный prepare-deploy цикл для ревью подключаются мейнтейнеры репозитория. Поправьте ваши коммиты по их замечаниям.
  4. При выполнении всех условий мейнтейнер отправляет ваши коммиты в ветку main.
  5. CI flowforge автоматически запустит сборку образов и отправит их в целевой Registry.

Примечание: Рекомендуем изучить корректное заполнении info.yaml и правила создания distroless-образов на базе flowforge в полной документации проекта.

Поддержка архитектур

В настоящее время поддерживаются следующие архитектуры: x86_64, aarch64, i586, loongarch64, riscv64

Данное ограничение связано с возможностями сборочницы OCI-образов altflow.

Локальная разработка

Основные положения

PR с функциональными изменениями скорее всего не будут приняты. Исключения возможны.

  • Если есть предложение по улучшению, создайте Issue и опишите его.
  • Если есть багрепорт, создайте Issue с описанием проблемы. Будет полезно, если вы опишете способ воспроизведения.
  • [В БУДУЩЕМ] Если PR не выглядит poorly vibecoded, изменения с использованием AI могут быть приняты. AI можно использовать для PR, но будьте готовы обсуждать и защищать каждое предложенное изменение.

Ожидаемые рабочие конфигурации

Организации: alt, base, k8s-core, k8s-extra, kubevirt
Архитектуры: amd64, arm64
Ветки: sisyphus

Зависимости

  • python3-module-aiohttp
  • python3-module-pydantic
  • python3-module-pydantic-settings
  • python3-module-loguru
  • python3-module-jinja2
  • python3-module-yaml

Использование

Переменные среды, которые использует скрипт:

  • DRY -> включает dry-run режим
  • TEST -> отправляет задания в altflow без тегов. Финальный push в registry становится фиктивным
  • EMAIL_NOTIFICATION -> включает email-уведомления об ошибках сборки
  • EMAIL_TO -> список получателей email-уведомлений. Можно перечислять через запятую или пробел
  • LOG_LEVEL -> уровень логирования
  • ALTFLOW_URL -> URL экземпляра altflow
  • ALTFLOW_AUTH -> токен авторизации для altflow
  • SOT_REPO -> Source Of Truth репозиторий. Из этого репозитория altflow забирает спецификацию образа
  • SOT_BRANCH -> Source Of Truth ветка. В неё рендерятся Dockerfile перед тем, как altflow их прочитает
  • SOT_FORCE_PUSH -> разрешает force push в SOT_BRANCH
  • REGISTRY_URL -> адрес целевого registry
  • REGISTRY_AUTH -> авторизация для REGISTRY_URL
  • SOURCE_REGISTRY -> registry, из которого берутся базовые образы
  • REPOTEKA_URL -> URL экземпляра repoteka
  • REPOTEKA_COMPONENT -> компонент repoteka
  • REPOTEKA_SSL_VERIFY -> включает SSL verify для запросов в repoteka
  • REPOTEKA_CONCURRENCY -> максимальное количество одновременных запросов к repoteka
  • SMTP_SERVER -> SMTP-сервер для email-уведомлений
  • SMTP_PORT -> SMTP-порт для email-уведомлений
  • SMTP_FROM -> адрес отправителя email-уведомлений
  • SMTP_USERNAME -> SMTP-пользователь для email-уведомлений
  • SMTP_PASSWORD -> SMTP-пароль для email-уведомлений
  • SMTP_SSL -> включает implicit TLS для SMTP. Для порта 465 включается автоматически

Подробнее в config.py

Доступные CLI-аргументы:

$ ./flowforge.py --help

Или можно посмотреть их в flowforge.py

Собрать один образ:

$ ./flowforge.py -i alt/redis

Собрать группу образов организации:

$ ./flowforge.py -o alt

Принудительно пересобрать образ, даже если сохранённые артефакты показывают, что он актуален:

$ ./flowforge.py -f -i alt/redis

Флаг -f не делает reset ветки *-artefacts, но весь выбранный набор образов проходит обычный цикл сборки с учётом уровней source_images. Dockerfile.<branch> заново рендерится и пушится в SOT_BRANCH, а после успешной сборки обновляется соответствующий info.*.yaml. Если в выбранном наборе есть source-образы и зависимые от них образы, artefacts-состояние обновится для обеих групп.

Отправить тестовую сборку без публикации тегов в registry:

$ TEST=1 ./flowforge.py -i alt/redis

Все примеры предполагают, что нужные переменные среды уже экспортированы.

Source Of Truth ветка

flowforge не отправляет локальное содержимое Dockerfile напрямую в altflow. Он рендерит Dockerfile.<branch> в SOT_BRANCH, пушит эту ветку, а затем отправляет в altflow SOT_REPO, SOT_BRANCH и путь к Dockerfile.

Если SOT_BRANCH не задана, она вычисляется как <current-branch>-artefacts.

Подробнее о роли *-artefacts, info.*.yaml и PR-веток ci/pr-<number>-artefacts написано в org/README.md.

CI secrets

CI jobs используют secret FLOWFORGE_CI_TOKEN, чтобы пушить PR artefacts-ветки в altlinux.space. Токену нужны права repository read/write. Для cron rebuild используется тот же FLOWFORGE_CI_TOKEN. Repository задаётся обязательной CI variable FLOWFORGE_CI_REPO, например altlinux.space/cloud/flowforge.git без схемы. Доступ к registry передаётся отдельно через REGISTRY_AUTH.

Если включены email-уведомления, SMTP может работать в трёх режимах:

  • plain SMTP без STARTTLS;
  • SMTP с STARTTLS, если сервер объявляет такую возможность;
  • implicit TLS через SMTP_SSL=1 или порт 465.

Если EMAIL_TO не задан, уведомления никому не отправляются. Если SMTP_FROM не задан, используется SMTP_USERNAME; если не задано и оно, email-уведомления отключаются.

Формат спецификаций в org

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

  • Dockerfile.template
  • info.yaml
  • altflow-test

Файлы, подобные Dockerfile.sisyphus и info.sisyphus.yaml, являются автоматически сгенерированными артефактами и не должны попадать в основную ветку репозитория.

Рабочие ветки и *-artefacts

Рабочая ветка содержит только исходную спецификацию образа:

  • Dockerfile.template
  • info.yaml
  • altflow-test
  • дополнительные файлы, которые нужны Dockerfile

Во время сборки flowforge переключается в SOT_BRANCH, обычно <branch>-artefacts, и создаёт там сгенерированные файлы:

  • Dockerfile.<branch>
  • info.<branch>.yaml
  • для versioned k8s-образов: info.<branch>.<version>.yaml

Скрипт формирует и добавляет файлы Dockerfile.<branch> в ветку <branch>-artefacts до отправки заданий на сборку в altflow, так как altflow ориентируется на данные файлы при сборке. Общая схема работы altflow описана в документации altflow.

В artefacts-ветке info.*.yaml хранит зафиксированные версии бинарных пакетов, полученные из repoteka после успешной сборки. При следующем запуске flowforge сравнивает эти версии с актуальными версиями пакетов и решает, какие образы устарели.

Из этого следует правило:

  • в рабочую ветку нельзя добавлять Dockerfile.<branch> и info.*.yaml;
  • в artefacts-ветке эти файлы должны сохраняться, потому что это source of truth для rebuild-логики;
  • если меняется info.yaml, cron rebuild подтягивает рабочую ветку в соответствующую artefacts-ветку merge'ом, чтобы сохранить старые слепки версий и обновить шаблоны.

Важно различать два сценария:

  • cron rebuild должен сохранять существующие info.*.yaml, потому что они нужны для сравнения версий пакетов;
  • PR-проверка новых образов постоянно пересоздаёт ci/pr-<number>-artefacts, потому что проверяет только новые спецификации из diff.

is_versioned

Бинарная спец опция для k8s-core, которая позволяет обозначить, что есть несколько версий пакета который нужно собрать.

Версии указываются в cfg/k8s-versions.json

Возможно в дальнейшем, опция будет расширена по функционалу, например для подержки сборки нескольких версий одного пакета вне k8s-core

target_package

Обязательная опция в конфигурации всех образов, кроме перечисленных в org/base. Может быть null или названием бинарного пакета.

Предназначение

От этого пакета будет унаследована версия в тэге образа. При null будет выставлен тэг соответсвующий дате сборки.

Для образов, которые собираются через DistrolessImage, версия target_package сейчас не используется как тег. Такие образы получают теги latest и дату запуска в формате YYYYMMDD.

binary_packages

Опция которая содержит список бинарных пакетов, зависимость от которых есть у данного образа.

Предназначение

  1. Слепки версий при сборке будут храниться в репозитории, для обнаружения обновлений, и устранения пересборки не устаревших образов.
  2. Если в Dockerfile.template используется {{ install_packages() }} без аргументов, будут установлены пакеты из binary_packages.

Если install_packages() вызван с явными аргументами, устанавливаются именно эти аргументы, а не перечисленные в binary_packages.

Если binary_packages указан в info.yaml, но Dockerfile.template не вызывает install_packages(), а builder.reinstall_packages и rootfs.packages не используют макрос @binary_packages для отсылки к списку binary_packages, список используется только для проверки версий и обновления artefacts. Это справедливо для обычных образов из org/alt, org/k8s-extra, org/kubevirt, для K8sImage из org/k8s-core, а также для недистролессных образов из org/base.

source_images

Локальные source-образы, которые flowforge должен учитывать как зависимости сборки.

Предназначение

  1. Проверка, что зависимость от source-образа известна flowforge
  2. Расчёт уровней сборки по графу зависимостей
  3. Пересборка зависимого образа, если его source-образ был собран в текущем запуске

В info.*.yaml flowforge может добавить служебный флаг source_images_updated. Он означает, что образ уже был поставлен в очередь из-за обновления source-образа, но ещё не был успешно собран. Вручную задавать этот флаг в info.yaml не нужно.

Если образ наследуется от обычного {{ registry }}{{ branch }}/{{ alt_image }}:latest, указывается:

source_images:
  - alt

Если образ наследуется от другого образа из org и этот образ тоже собирается flowforge, указывается canonical name:

source_images:
  - base/distroless-python3

Неизвестный в рамках flowforge source image считается ошибкой.

exclude_archs

Опциональный список архитектур, для которых образ не нужно отправлять в altflow. Возможные исключения: x86_64, aarch64, i586, loongarch64, riscv64. Для org/k8s-core рядом с архитектурой можно через запятую указать версии Kubernetes, для которых действует исключение.

Пример:

exclude_archs:
  - riscv64
  - loongarch64,1.33,1.34

exclude_branches

Опциональный список веток пакетной базы, для которых образ не нужно собирать. Исключение применяется до запросов к repoteka и до отправки задач в altflow.

Пример:

exclude_branches:
  - p10
  - c10f1

annotations

Опциональный словарь OCI annotations, который отправляется в altflow вместе с build payload.

Пример:

annotations:
  org.opencontainers.image.source: 'https://github.com/kubevirt/kubevirt'
  org.opencontainers.image.licenses: Apache-2.0
  org.opencontainers.image.vendor: 'ALT Linux Team'

altflow_context

Опциональный раздел для образов, где контекст для сборки образа (build context или buildContextPath) лежит в другом git-репозитории. Dockerfile при этом всё равно генерируется автоматически алгоритмом flowforge на основе конфигурационных файлов и не зависит от базового репозитория с основной кодовой базой.

Пример:

altflow_context:
  buildContextPath: "."
  buildGitBranch: "alt"
  git_url: "https://altlinux.space/cloud/cilium-envoy.git"

distroless

Опциональный раздел для образов, которые должны собираться через DistrolessImage. В org/k8s-core этот renderer пока не используется: там один каталог разворачивается в несколько Kubernetes-версий через K8sImage.

Основные секции:

  • from -> базовый образ финальной стадии. По умолчанию scratch
  • builder.reinstall_packages -> пакеты, которые должны быть установлены в builder-стадии
  • rootfs.files -> файлы, которые надо положить в rootfs без поиска библиотек
  • rootfs.library_files -> файлы, для которых надо добавить runtime libraries
  • rootfs.packages -> пакеты, файлы которых надо добавить в rootfs
  • rootfs.library_packages -> пакеты, для файлов которых надо добавить runtime libraries
  • rootfs.timezone -> timezone, для которой будет создан /etc/localtime
  • env -> ENV в финальном образе
  • user -> USER в финальном образе
  • workdir или workingdir -> WORKDIR в финальном образе
  • entrypoint -> JSON ENTRYPOINT
  • cmd -> JSON CMD

В builder.reinstall_packages и rootfs.packages указываются пакеты, которые нужны distroless-сборке:

distroless:
  builder:
    reinstall_packages:
      - python3
      - python3-base
  rootfs:
    packages:
      - python3
      - python3-base

altflow-test

Файл altflow-test обязателен в каждом каталоге образа. Файл не должен быть пустым, а его содержимое должно быть JSON-списком списков.

Минимально допустимый формат:

[]

Для образов, где необходимо протестировать один etrtypoint, допустим формат:

[
  "/usr/bin/python3",
  "--version"
]

Для тестирования нескольких etrypoints, используйте:

[
  [
    "/usr/bin/python3",
    "--version"
  ],
  [
    "/usr/bin/pytest3",
    "--version"
  ]
]

В случаях, когда образ собирается для нескольких архитектур и для каждой требуется уникальное тестирование, используйте формат:

{
  "amd64": [
    [
      "/usr/lib64/ld-linux-x86-64.so.2",
      "--help"
    ]
  ],
  "arm64": [
    [
      "/usr/lib64/ld-linux-aarch64.so.1",
      "--help"
    ]
  ],
  "all": [
    [
      "/usr/lib/ld-linux.so.2",
      "--help"
    ]
  ]
}

Маркер all здесь буквально говорит алгоритму протестируй следующие entrypoints для всех оставшихся не указанных явно архитектур.

Каждый вложенный список является командой для altflow test entrypoint: первый элемент - executable, остальные элементы - аргументы.

Для distroless и controller-like образов лучше проверять существование entrypoint или использовать безопасные --version/--help, которые не требуют kubernetes cluster и не запускают бесконечный процесс.

Как создать distroless-образ в flowforge

Этот раздел описывает, как создавать distroless-образы в flowforge на базе существующих спецификаций из org/base.

Distroless-образ в flowforge - это OCI-образ, в котором финальный rootfs собирается не через обычную установку пакетов в финальный слой, а через промежуточный builder-образ и tar-архив с минимальным набором файлов. В финальный образ попадает только то, что явно указано в info.yaml и Dockerfile.template.

Общий формат спецификации образа описан в разделе Формат спецификаций в org. Distroless добавляет к нему секцию distroless в info.yaml и несколько helper'ов для Dockerfile.template.

В flowforge есть два способа получить distroless-подобный образ:

  • использовать renderer DistrolessImage; он включается, если в info.yaml есть секция distroless и организация не является k8s-core;
  • написать обычный Dockerfile.template, который наследуется от уже собранного distroless-образа, например {{ registry }}{{ branch }}/distroless-base:latest.

В первом случае главным становится info.yaml: секция distroless передаёт renderer'у описание rootfs, окружения, entrypoint и базового образа. Во втором случае образ остаётся обычным AltImage или K8sImage, а distroless-поведение задаётся самим Dockerfile.

Где смотреть примеры

Актуальные примеры находятся в org/base:

  • distroless-static - минимальный rootfs от scratch;
  • distroless-base - динамический runtime с glibc, timezone и базовыми библиотеками;
  • distroless-cc - C/C++ runtime поверх distroless-base;
  • distroless-python3 - Python runtime поверх distroless-cc;
  • distroless-true, distroless-toybox, distroless-gotop - небольшие прикладные примеры;
  • distroless-devel - отладочный образ с shell и набором инструментов;
  • distroless-busybox - пример custom Dockerfile поверх distroless-base.

Вне org/base тоже есть образы, которые используют distroless как runtime base. Например org/k8s-extra/kube-rbac-proxy, org/k8s-extra/hubble-ui-backend, org/k8s-extra/spire-agent и org/alt/bldr наследуются от distroless-base или distroless-static. Если в их info.yaml добавить секцию distroless, flowforge будет рендерить их через DistrolessImage.

Если поведение документации расходится с кодом, точкой истины является core/image/distroless.py и текущие спецификации в org.

Простейший distroless-образ

Начинать лучше с маленького образа, который запускает один бинарник. В flowforge такой образ удобно делать в org/base, потому что там уже лежит базовая distroless-цепочка. Сам DistrolessImage выбирается не по организации, а по секции distroless в info.yaml.

Реальный минимальный пример уже есть в репозитории: org/base/distroless-true. Он кладёт в rootfs /bin/true из пакета coreutils и запускает его.

Каталог

org/base/distroless-true/
  Dockerfile.template
  altflow-test
  info.yaml

info.yaml

---
is_versioned: false
binary_packages:
  - coreutils
source_images:
  - base/distroless-builder
  - base/distroless-static
distroless:
  from: "{{ registry }}{{ branch }}/distroless-static:latest"
  builder:
    reinstall_packages:
      - coreutils
  rootfs:
    files:
      - /bin/true
  cmd:
    - /bin/true
annotations:
  org.opencontainers.image.revision: ""
  org.opencontainers.image.source: ""
  org.opencontainers.image.url: ""
  org.opencontainers.image.version: ""
  org.opencontainers.image.title: "distroless-true"
  org.opencontainers.image.description: "True-command image for zero status returning"
  org.opencontainers.image.licenses: GPLv2
  org.opencontainers.image.vendor: "ALT Linux Team"
...

Разбор по смыслу:

  • is_versioned: false, binary_packages и source_images - общие поля спецификации. Их смысл описан в формате спецификаций. Для distroless-true важная практическая связь такая: coreutils отслеживается как пакет-зависимость, а base/distroless-builder и base/distroless-static задают порядок сборки.
  • distroless.from задаёт финальный FROM через функцию distroless_from(). Если не указано иное, функция строит итоговый образ от scratch. В этом примере итоговый образ строится поверх distroless-static, что указано явно.
  • distroless.builder.reinstall_packages перечисляет пакеты, которые надо поставить или переустановить в builder-стадии перед извлечением файлов. Здесь снова указан coreutils, но смысл другой: это не проверка актуальности, а подготовка файла /bin/true для rootfs.
  • distroless.rootfs.files говорит distroless-builder.py, какие конкретные файлы забрать из builder-стадии. Для true дополнительные библиотеки искать не нужно.
  • distroless.cmd задаёт CMD финального образа.
  • annotations задаёт OCI metadata. Это общее поле, но его удобно видеть рядом с примером.

coreutils повторяется намеренно. В binary_packages он описывает зависимость образа. В distroless.builder.reinstall_packages он уже управляет сборкой: пакет должен быть установлен в builder-стадии, иначе distroless-builder.py не сможет положить /bin/true в rootfs. В минимальном примере это один и тот же пакет, но в более сложных образах список зависимостей образа и список пакетов для подготовки rootfs могут отличаться.

Dockerfile.template

Минимальный шаблон обычно повторяет схему существующих distroless-образов:

FROM {{ registry }}{{ branch }}/distroless-builder:latest AS builder

WORKDIR /usr/src/distroless

{{ distroless_base_rootfs() }}

{{ distroless_reinstall() }}

{{ distroless_add() }}

FROM {{ distroless_from() }}

COPY --from=builder /rootfs/ /

{{ distroless_config() }}

LABEL org.opencontainers.image.title="distroless-true" \
    org.opencontainers.image.description="True-command image for zero status returning" \
    org.opencontainers.image.licenses="GPLv2" \
    org.opencontainers.image.vendor="ALT Linux Team"

Шаблон короткий, потому что основная логика спрятана в helper'ах renderer'а DistrolessImage. Строки читаются так:

  • первый FROM запускает builder-стадию из distroless-builder;
  • WORKDIR выбирает каталог, где лежит distroless-builder.py;
  • distroless_base_rootfs() распаковывает базовый rootfs в /basefs, чтобы потом не добавлять в новый слой файлы, которые уже есть в итоговом указанном в distroless.from образе;
  • distroless_reinstall() ставит или переустанавливает пакеты из distroless.builder.reinstall_packages;
  • distroless_add() вызывает distroless-builder.py add, создаёт distroless.tar, распаковывает его в /rootfs и удаляет дубли относительно /basefs;
  • второй FROM берёт финальный базовый образ из distroless.from;
  • COPY --from=builder /rootfs/ / переносит подготовленный минимальный rootfs в финальный образ;
  • distroless_config() добавляет ENV, USER, WORKDIR, ENTRYPOINT, CMD из секции distroless;
  • LABEL добавляет OCI metadata в сам образ.

distroless.tar

distroless.tar - временный tar-архив rootfs, который создаётся только в builder-стадии. Его создаёт distroless-builder.py:

  1. distroless-builder.py add ... получает правила из distroless.rootfs: конкретные файлы, файлы из rpm-пакетов, библиотеки бинарников, списки файлов.
  2. Скрипт сохраняет список найденных путей во внутреннем рабочем состоянии.
  3. distroless-builder.py tar -o distroless.tar упаковывает эти пути в tar-архив.
  4. Dockerfile распаковывает архив командой tar -C /rootfs -xf distroless.tar.
  5. Финальная стадия копирует /rootfs в образ через COPY --from=builder /rootfs/ /.

В distroless-true архив содержит файл /bin/true, нормализованный под ALT usrmerge. На практике это означает, что основной файл оказывается в /usr/bin/true, а путь /bin в итоговом rootfs остаётся совместимым symlink'ом.

altflow-test

Общий формат файла описан в спецификации образов. Для distroless-образов тест должен быть безопасным: команда должна быстро завершаться и не требовать shell, сети или Kubernetes API, если shell не добавлен в образ явно.

[
  [
    "/bin/true"
  ]
]

Если в образе нет /bin/true, не используйте его в тесте. Проверяйте реальный entrypoint или другой файл, который действительно попадает в rootfs.

Pipeline сборки distroless

Distroless pipeline в flowforge состоит из двух уровней:

Как flowforge выбирает DistrolessImage

Для большинства организаций используется selector BaseImage. Он смотрит на info.yaml: если там есть секция distroless, образ создаётся как DistrolessImage; иначе используется обычный AltImage.

Из этого следует правило: для нового distroless-образа нужна секция:

distroless:
  ...

Одного имени каталога distroless-* недостаточно.

Исключение сейчас одно: org/k8s-core. Там используется K8sImage, потому что один каталог разворачивается в несколько Kubernetes-версий из cfg/k8s-versions.json.

В config.py сейчас указано:

ORG_STATE = {
    "alt": BaseImage,
    "base": BaseImage,
    "k8s-core": K8sImage,
    "k8s-extra": BaseImage,
    "kubevirt": BaseImage,
}

Если секции distroless нет, образ остаётся обычным AltImage. Он всё равно может наследоваться от distroless runtime через custom Dockerfile.template:

FROM {{ registry }}{{ branch }}/{{ alt_image }}:latest AS prepare

{{ install_packages("kube-rbac-proxy") }}

RUN mkdir -p /rootfs/usr/bin && \
    cp -aL /usr/bin/kube-rbac-proxy /rootfs/usr/bin/kube-rbac-proxy

FROM {{ registry }}{{ branch }}/distroless-base:latest

COPY --from=prepare /rootfs /
ENTRYPOINT ["/usr/bin/kube-rbac-proxy"]

При этом source_images всё равно должен указывать distroless base:

source_images:
  - alt
  - base/distroless-base

Так flowforge понимает порядок сборки и пересобирает зависимый образ после обновления base/distroless-base.

Если промежуточный образ приходит из registry и не собирается flowforge, в source_images его не добавляют. Например, cilium может делать FROM {{ registry }}{{ branch }}/cilium-envoy:latest AS envoy, но если cilium-envoy собирается отдельно вручную, зависимость фиксируется только в Dockerfile.template, а не в source_images.

Базовая цепочка

Текущая практическая цепочка в org/base такая:

alt
  -> base/distroless-builder
       -> base/distroless-static
            -> base/distroless-base
                 -> base/distroless-cc
                      -> base/distroless-python3

Параллельно существуют прикладные ветки:

base/distroless-static
  -> base/distroless-true
  -> base/distroless-toybox

base/distroless-base
  -> base/distroless-gotop
  -> base/distroless-devel
  -> base/distroless-busybox

source_images фиксирует эту зависимость для flowforge. Общий алгоритм очереди сборки описан в спецификации образов.

distroless-builder

base/distroless-builder - служебный образ. Он основан на обычном alt и содержит:

  • python3;
  • glibc-utils;
  • apt-repo;
  • скрипт distroless-builder.py.

Именно из него строятся остальные distroless-образы. Финальный distroless rootfs создаётся в builder-стадии, а затем копируется в финальный образ.

distroless-builder.py

Скрипт distroless-builder.py ведёт список файлов и собирает tar-архив rootfs.

Поддерживаемый CLI:

./distroless-builder.py add ...
./distroless-builder.py tar -o distroless.tar
./distroless-builder.py clean

distroless.tar появляется на шаге tar: это временный архив с файлами, которые были выбраны предыдущими вызовами add. Сам по себе он не является базовым образом и не хранится в репозитории. Dockerfile распаковывает его в /rootfs, а затем копирует /rootfs в финальную стадию образа.

add умеет принимать:

  • -f / --files - конкретные файлы;
  • -p / --packages - все обычные файлы из rpm-пакетов;
  • --library-files - библиотеки, найденные через ldd для указанных бинарников;
  • --library-packages - rpm-пакеты, которым принадлежат библиотеки указанных бинарников;
  • --clean - очистить старый список перед добавлением.

При упаковке distroless-builder.py учитывает ALT usrmerge:

  • /bin/... попадает в архив как usr/bin/...;
  • /lib/... попадает как usr/lib/...;
  • /lib64/... попадает как usr/lib64/...;
  • /sbin/... попадает как usr/sbin/...;
  • при необходимости добавляются symlink'и bin -> usr/bin, lib -> usr/lib, lib64 -> usr/lib64, sbin -> usr/sbin.

Это важно для distroless-образов ALT Linux: основные файлы должны жить в /usr, а legacy пути в корне должны быть symlink'ами.

Что делает renderer DistrolessImage

DistrolessImage читает distroless из info.yaml и готовит helper'ы для Jinja-шаблона:

  • distroless_from();
  • distroless_base_rootfs();
  • distroless_reinstall();
  • distroless_add();
  • distroless_config().

Типовой Dockerfile сначала строит /rootfs в builder-стадии:

FROM {{ registry }}{{ branch }}/distroless-builder:latest AS builder

WORKDIR /usr/src/distroless

{{ distroless_base_rootfs() }}
{{ distroless_reinstall() }}
{{ distroless_add() }}

Потом копирует /rootfs в финальный образ:

FROM {{ distroless_from() }}

COPY --from=builder /rootfs/ /

{{ distroless_config() }}

Если distroless.from не scratch, renderer копирует базовый rootfs в /basefs и после создания нового /rootfs удаляет из него файлы, которые совпадают с базовым образом. Это уменьшает повторное включение одинаковых файлов в новый слой.

Если distroless.from равен scratch или не задан, distroless_base_rootfs() не нужен для создания /basefs, так как впоследствии дедупликация не будет выполняться.

Связь с distroless.tar такая: этот архив создаёт новый rootfs-добавок, а /basefs используется только как эталон для сравнения. Если файл из распакованного distroless.tar уже есть в /basefs и совпадает по содержимому, renderer удаляет его из /rootfs; если файла нет или он отличается, файл остаётся и попадёт в новый слой.

Секция distroless в info.yaml

Общие поля info.yaml описаны в разделе Формат спецификаций в org. Здесь перечислены только поля секции distroless, которые управляют сборкой минимального rootfs.

Наличие секции distroless включает DistrolessImage во всех организациях, кроме org/k8s-core.

from

Финальный базовый образ:

distroless:
  from: "{{ registry }}{{ branch }}/distroless-base:latest"

Если from не задан, используется scratch.

Примеры:

  • scratch в distroless-static;
  • distroless-static в distroless-base;
  • distroless-base в distroless-cc, distroless-gotop, distroless-devel;
  • distroless-cc в distroless-python3.

builder.reinstall_packages

Пакеты, которые надо установить в builder-стадии перед сборкой rootfs:

distroless:
  builder:
    reinstall_packages:
      - gotop

Renderer превращает это в apt-get install -y ....

Несмотря на имя reinstall_packages, текущий renderer вызывает apt-get install, а не apt-get reinstall. Название поля сохраняет исторический смысл: обеспечить наличие свежих файлов пакетов в builder-стадии.

builder.install_packages

Поле встречается в distroless-builder и distroless-busybox, но стандартный helper distroless_reinstall() его не использует. Эти образы имеют custom Dockerfile.template, который сам вызывает обычный helper install_packages(...).

Для нового стандартного distroless-образа обычно используйте builder.reinstall_packages.

rootfs.files

Добавляет конкретные файлы в rootfs:

rootfs:
  files:
    - /bin/true

Symlink'и по умолчанию разворачиваются с учётом назначения. Для библиотек бинарника это поле не подходит: используйте library_files или full_files.

rootfs.library_files

Добавляет указанные файлы и библиотеки, найденные через ldd:

rootfs:
  library_files:
    - /usr/bin/vim

Если бинарник статический или ldd не может его обработать, сборка упадёт. Для таких файлов используйте files.

rootfs.full_files

Удобное сокращение: файл попадёт и в files, и в library_files.

rootfs:
  full_files:
    - /usr/bin/python3

Это нужно для динамически связанных исполняемых файлов: сам бинарник попадёт в rootfs, а его runtime libraries будут найдены через ldd.

rootfs.packages

Добавляет обычные файлы из rpm-пакетов:

rootfs:
  packages:
    - glibc-core
    - tzdata

rootfs.packages нужен, когда нужно забрать из rpm-пакета обычные runtime-файлы целиком, а не перечислять их вручную. Это удобно для пакетов, где важны не один-два бинарника, а набор данных, конфигов, сертификатов, Python-модулей, timezone-файлов, metadata и т.п.

Разница такая:

  • rootfs.files - забрать конкретные файлы. Пример: /bin/true, /etc/localtime.
  • rootfs.library_files - забрать указанный бинарник/файл и библиотеки, которые нужны ему по ldd. Пример: /usr/bin/gotop.
  • rootfs.packages - забрать обычные файлы из перечисленных rpm-пакетов. Это шире и грубее, зато надёжнее для runtime-пакетов со множеством связанных файлов.
  • rootfs.full_files - короткая запись для динамического исполняемого файла: указанный путь одновременно попадает в rootfs.files и в rootfs.library_files. В результате в rootfs попадает сам файл и библиотеки, которые нужны ему по ldd.

Например, для distroless-python3 rootfs.packages оправдан: Python runtime это не только /usr/bin/python3 и .so из ldd. Нужны стандартная библиотека, Python modules, CA/cert files и runtime data. Перечислять это вручную через files было бы хрупко.

Для distroless-true наоборот rootfs.packages не нужен: достаточно одного /bin/true, и библиотеки не нужны.

Практическое правило:

  • один бинарник или один конфиг -> rootfs.files;
  • бинарник с динамическими библиотеками -> rootfs.library_files;
  • runtime-пакет с деревом файлов -> rootfs.packages;
  • distroless-слой, где пакет целиком и есть смысл слоя -> rootfs.packages.

rootfs.library_packages

Добавляет пакеты, которым принадлежат библиотеки указанных бинарников:

rootfs:
  library_packages:
    - /usr/bin/my-binary

Это специализированный механизм. Он вызывает ldd, затем rpm -qf для найденных библиотек.

rootfs.timezone

Создаёт /etc/localtime в builder-стадии и добавляет его в rootfs:

rootfs:
  timezone: Europe/Moscow

Так сделан distroless-base.

env

Рендерится в ENV:

env:
  LANG: C.UTF-8
  SSL_CERT_FILE: /etc/pki/tls/certs/ca-bundle.crt

user

Рендерится в USER:

user: nonroot

Пользователь должен существовать в rootfs или базовом образе. В цепочке flowforge пользователь nonroot создаётся в distroless-builder и попадает в distroless-static.

workdir и workingdir

Оба имени поддерживаются. Рендерятся в WORKDIR:

workdir: /home/nonroot

entrypoint

JSON ENTRYPOINT финального образа:

entrypoint:
  - /usr/bin/python3

cmd

JSON CMD финального образа:

cmd:
  - /bin/bash

Для distroless-образов shell-form использовать нельзя, если shell не добавлен в rootfs явно.

Примеры из org/base

Этот раздел показывает, какие паттерны уже используются в org/base.

distroless-static

distroless-static строится от scratch и задаёт самый маленький базовый rootfs:

binary_packages:
source_images:
  - base/distroless-builder
distroless:
  from: scratch
  rootfs:
    files:
      - /bin
      - /etc
      - /usr
      - /var
      # в оригинальном файле список файлов намного длиннее
  user: nonroot
  workdir: /home/nonroot

Главная идея: rootfs задаётся минимальным набором каталогов и системных файлов. Этот образ подходит для статически связанных программ или как основа для образов, которым не нужен glibc runtime.

distroless-base

distroless-base добавляет динамический runtime:

binary_packages:
  - glibc-core
  - glibc-pthread
  - glibc-timezones
  - libselinux
  - libssl3
  - tzdata
  - zlib
source_images:
  - base/distroless-builder
  - base/distroless-static
distroless:
  from: "{{ registry }}{{ branch }}/distroless-static:latest"
  rootfs:
    packages:
      - glibc-core
      - glibc-pthread
      - glibc-timezones
      - tzdata
      - zlib
    timezone: Europe/Moscow

Здесь появляется важный паттерн: binary_packages может быть шире, чем rootfs.packages. Часть пакетов нужна для отслеживания обновлений и builder-стадии, но в rootfs может попадать только выбранный набор файлов.

distroless-cc

distroless-cc добавляет C/C++ runtime-пакеты целиком:

В этом и следующем примере используется сокращение @binary_packages; его смысл описан в формате спецификаций.

binary_packages:
  - libgcc1
  - libgomp1
  - libstdc++6
source_images:
  - base/distroless-builder
  - base/distroless-base
distroless:
  from: "{{ registry }}{{ branch }}/distroless-base:latest"
  builder:
    reinstall_packages: "@binary_packages"
  rootfs:
    packages: "@binary_packages"

Это хороший шаблон для runtime-слоя, где нужно добавить несколько rpm-пакетов целиком.

distroless-python3

Python runtime строится поверх distroless-cc:

binary_packages:
  - ca-certificates
  - ca-trust
  - python3
  - python3-base
  - python3-modules-curses
  - python3-modules-sqlite3
  - libpython3
source_images:
  - base/distroless-builder
  - base/distroless-cc
distroless:
  from: "{{ registry }}{{ branch }}/distroless-cc:latest"
  builder:
    reinstall_packages: "@binary_packages"
  rootfs:
    full_files:
      - /usr/bin/python3
    library_files:
      - /usr/lib64/python3*/lib-dynload/*.so
    packages: "@binary_packages"
  env:
    LANG: C.UTF-8
    SSL_CERT_FILE: /etc/pki/tls/certs/ca-bundle.crt
  user: nonroot
  workdir: /home/nonroot
  entrypoint:
    - /usr/bin/python3

Здесь используются сразу три механизма rootfs:

  • full_files для /usr/bin/python3;
  • library_files для Python extension modules;
  • packages для файлов из runtime-пакетов, перечисленных в binary_packages.

Dockerfile дополнительно создаёт symlink /usr/bin/python -> python3, если его нет.

distroless-gotop

Простой пример динамического бинарника:

binary_packages:
  - gotop
source_images:
  - base/distroless-builder
  - base/distroless-base
distroless:
  from: "{{ registry }}{{ branch }}/distroless-base:latest"
  builder:
    reinstall_packages:
      - gotop
  rootfs:
    full_files:
      - /usr/bin/gotop
  entrypoint:
    - /usr/bin/gotop

Для похожих утилит это один из лучших стартовых шаблонов.

distroless-devel

distroless-devel не является минимальным runtime. Это отладочный образ:

distroless:
  from: "{{ registry }}{{ branch }}/distroless-base:latest"
  rootfs:
    full_files:
      - /bin/bash
      - /usr/bin/curl
      - /usr/bin/gdb
    files:
      - /etc/alternatives/links/*
      - /usr/bin/vim
    library_files:
      - /usr/bin/vim
      - /usr/bin/ss
    packages:
      - coreutils
      - findutils
      - glibc-utils
      - iproute2
  cmd:
    - /bin/bash

Используйте этот образ как пример, когда нужен shell, диагностика и инструменты. Не копируйте его как базу для production runtime без необходимости.

distroless-busybox

distroless-busybox отличается от стандартных distroless-образов. В нём есть секция distroless, но Dockerfile написан вручную:

FROM {{ registry }}{{ branch }}/{{ alt_image }}:latest AS prepare

{{ install_packages("busybox") }}

RUN mkdir -p /rootfs/usr/bin && \
    cp -aL /usr/bin/busybox /rootfs/usr/bin/busybox && \
    /usr/bin/busybox --list | while read -r applet; do \
        [ "$applet" != busybox ] || continue; \
        ln -sf busybox "/rootfs/usr/bin/$applet"; \
    done

FROM {{ registry }}{{ branch }}/distroless-base:latest

COPY --from=prepare /rootfs /
CMD ["/usr/bin/sh"]

Такой подход допустим, если helper'ов DistrolessImage недостаточно. Но для новых образов сначала попробуйте стандартный шаблон с distroless_add().

Образы вне org/base на базе distroless

Не все образы, которые используют distroless runtime, обязаны создаваться через DistrolessImage. Например org/k8s-extra/kube-rbac-proxy может оставаться обычным AltImage, если в info.yaml нет секции distroless, но финальная стадия его Dockerfile наследуется от distroless-base:

FROM {{ registry }}{{ branch }}/{{ alt_image }}:latest AS prepare

{{ install_packages("kube-rbac-proxy") }}

RUN mkdir -p /rootfs/usr/bin && cp -aL /usr/bin/kube-rbac-proxy /rootfs/usr/bin/kube-rbac-proxy

FROM {{ registry }}{{ branch }}/distroless-base:latest

COPY --from=prepare /rootfs /
ENTRYPOINT ["/usr/bin/kube-rbac-proxy"]

В info.yaml это фиксируется через source_images:

target_package: kube-rbac-proxy
binary_packages:
  - kube-rbac-proxy
source_images:
  - alt
  - base/distroless-base

Такой способ подходит, если образу достаточно вручную скопировать несколько файлов из prepare-стадии. Если нужен автоматический сбор rootfs по rpm-пакетам, ldd, ENV, USER, WORKDIR, ENTRYPOINT и CMD из info.yaml, добавьте секцию distroless и используйте renderer DistrolessImage.

Проверки и типичные ошибки

altflow-test

Общий формат altflow-test описан в спецификации образов. Для distroless-образов важно учитывать состав rootfs:

  • если shell не добавлен в rootfs, нельзя использовать /bin/sh;
  • если бинарник не поддерживает --help или --version, не используйте эти флаги;
  • если entrypoint запускает daemon или controller, тест не должен зависать;
  • для образов на scratch проверяйте только реально существующие бинарники.

Команды без shell

Distroless-образ не обязан содержать /bin/sh. Поэтому такие тесты часто неверны:

[
  [
    "/bin/sh",
    "-ec",
    "test -x /usr/bin/app"
  ]
]

Они подходят только для образов, где shell явно добавлен, например для distroless-devel, distroless-toybox или distroless-busybox.

Динамический бинарник не запускается

Если бинарник есть, но запуск падает из-за отсутствующей .so, проверьте:

  • добавлен ли бинарник через full_files или library_files;
  • поддерживает ли ldd этот файл;
  • есть ли нужные библиотеки в пакетах из binary_packages;
  • не нужен ли runtime-слой distroless-base или distroless-cc.

Для динамического исполняемого файла обычно нужен один из вариантов:

rootfs:
  full_files:
    - /usr/bin/app

или:

rootfs:
  files:
    - /usr/bin/app
  library_files:
    - /usr/bin/app

Когда нужен custom Dockerfile

Стандартный distroless-шаблон подходит для большинства runtime-образов. Custom Dockerfile нужен, если требуется нестандартная подготовка rootfs:

  • создать symlink'и вручную;
  • скопировать applet'ы busybox;
  • выполнить дополнительные команды после distroless_add();
  • проверить или изменить файлы в /rootfs.

Даже в custom Dockerfile секция distroless может быть полезна: она включает DistrolessImage и даёт доступ к helper'ам renderer'а.