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; если файла нет или он отличается, файл остаётся и попадёт в новый слой.