Перейти к содержимому

Перенос Helm-чартов и контейнерных образов в закрытый контур ​

В данной инструкции описан порядок переноса программных артефактов Kubernetes из публичных репозиториев, доступных через сеть Интернет, в изолированный частный контур, не имеющий доступа к Интернету.

Рассматриваются следующие типы артефактов:

  • Helm Charts и их зависимости;
  • Kubernetes container images, в том числе multi-architecture;
  • OCI Helm Charts;
  • CRD и дополнительные Kubernetes-манифесты;
  • файлы конфигурации и metadata, необходимые для воспроизводимой установки;
  • контрольные суммы и списки артефактов;
  • перенос через промежуточный носитель, например USB Flash Drive.

Единица поставки — Application Release, а не Chart

Перенос одного Helm Chart без образов, зависимостей, CRD и манифестов практически бесполезен. Единица поставки в закрытый контур — весь набор артефактов приложения:

text
Application Release
├── Helm Chart
├── Helm dependencies
├── CRDs
├── Kubernetes manifests
├── Container images (amd64, arm64, ...)
├── Image digests
├── Configuration
└── Integrity metadata

Общая схема процесса:

text
                 INTERNET
                     │
             ┌───────▼────────┐
             │  Внешняя машина │
             │  с доступом     │
             │  к Интернету    │
             └───────┬────────┘
                     │  скачать / собрать все артефакты
             ┌───────▼────────┐
             │ Transfer Package│
             │ Charts / Images │
             │ Dependencies /  │
             │ Checksums /     │
             │ Manifest        │
             └───────┬────────┘
                     │  USB / иной контролируемый носитель
             ┌───────▼────────┐
             │  Закрытый контур│
             │ Private Registry│
             │ Helm Repository │
             └───────┬────────┘
                     ▼
                Kubernetes

Когда это пригодится ​

  • Организация поставки сторонних Helm Charts и образов в закрытый контур без доступа к Интернету.
  • Подготовка воспроизводимого набора артефактов для установки приложений в изолированном Kubernetes-кластере.
  • Регулярное обновление ПО в контуре, изолированном на физическом или сетевом уровне.

Краткий путь ​

  1. Подготовьте внешнюю машину с доступом в Интернет (Helm, Skopeo, Docker/Podman, jq, yq).
  2. Скачайте Chart нужной версии и все его зависимости (с Chart.lock).
  3. Отрендерьте Chart, соберите полный список образов и зафиксируйте их digest.
  4. Перенесите Chart и образы в закрытый контур: напрямую (Skopeo) или через USB-носитель.
  5. Импортируйте образы и Charts в private OCI registry (skopeo copy, helm push).
  6. Подготовьте values-airgap.yaml и проверьте отсутствие внешних references в отрендеренных манифестах.
  7. Установите Chart и проверьте события кластера (kubectl get events).

Предварительные требования ​

  • Внешняя машина (VM или сервер) с доступом к сети Интернет. Желательно не собирать артефакты с рабочего ноутбука администратора.
  • Закрытый контур с OCI-реестром. В закрытом контуре рекомендуется иметь собственный registry, поддерживающий OCI — в нём могут находиться как container images, так и Helm Charts.
  • Доступность private registry для всех будущих узлов кластера и АРМ администратора (DNS, TCP, TLS).
  • Достаточный объём дискового пространства на внешней машине и на машине закрытого контура.
  • Промежуточный носитель (USB Flash Drive) — если между контурами нет сетевой связи.

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

ПлейсхолдерЗначение
registry.example.localадрес приватного OCI-реестра в закрытом контуре
charts.example.comадрес внешнего Helm-репозитория
SOURCE_REPOимя внешнего Helm-репозитория в Helm
my-chart / my-applicationпримеры имени чарта и приложения
my-releaseимя Helm-релиза
my-namespacenamespace установки
SOURCE/image:tagпроизвольный образ-источник
USER / PASSWORDучётные данные реестра

Шаг 1. Подготовка внешней машины ​

Для скачивания артефактов используется отдельная машина, имеющая доступ к Интернету. Рекомендуется использовать специальную VM или сервер (например, airgap-staging).

1.1. Установка инструментов ​

На внешней машине устанавливаются:

  • Helm;
  • Docker или Podman;
  • Skopeo;
  • необходимые registry clients;
  • Git;
  • jq;
  • yq;
  • curl;
  • sha256sum.

Проверьте установку:

bash
helm version
docker version
skopeo --version
jq --version
yq --version
git --version

1.2. Фиксация версий инструментов ​

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

bash
helm version > TOOL-VERSIONS.txt
docker version >> TOOL-VERSIONS.txt
skopeo --version >> TOOL-VERSIONS.txt
jq --version >> TOOL-VERSIONS.txt
yq --version >> TOOL-VERSIONS.txt

Шаг 2. Создание рабочего каталога ​

Создайте рабочий каталог:

bash
export WORKDIR=/opt/airgap-transfer
mkdir -p "$WORKDIR"/{charts,images,manifests,checksums,logs,scripts}

Структура каталога:

text
/opt/airgap-transfer/
├── charts/
├── images/
├── manifests/
├── checksums/
├── logs/
└── scripts/

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

Шаг 3. Получение Helm Chart ​

3.1. Добавление внешнего Helm Repository ​

Классический Helm Repository имеет URL вида https://charts.example.com и содержит index.yaml.

Добавьте repository:

bash
helm repo add SOURCE_REPO https://charts.example.com

Обновите индекс:

bash
helm repo update

Проверьте:

bash
helm search repo SOURCE_REPO

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

bash
helm search repo SOURCE_REPO/my-chart --versions

Пример вывода:

console
NAME                  CHART VERSION
SOURCE_REPO/my-chart  1.5.0
SOURCE_REPO/my-chart  1.4.3
SOURCE_REPO/my-chart  1.4.2

3.2. Скачивание конкретной версии ​

Не используйте helm pull без указания версии

Для воспроизводимого набора всегда явно указывайте версию. Команда helm pull SOURCE_REPO/my-chart без --version скачает последнюю версию, что делает поставку непредсказуемой.

Скачайте нужную версию:

bash
helm pull SOURCE_REPO/my-chart \
    --version 1.5.0 \
    --destination "$WORKDIR/charts"

Результат:

text
charts/
└── my-chart-1.5.0.tgz

Проверьте содержимое:

bash
helm show chart "$WORKDIR/charts/my-chart-1.5.0.tgz"
helm show all "$WORKDIR/charts/my-chart-1.5.0.tgz"

3.3. Анализ Chart и его зависимостей ​

Для анализа Chart его удобно распаковать:

bash
mkdir -p "$WORKDIR/chart-source"
tar -xzf \
    "$WORKDIR/charts/my-chart-1.5.0.tgz" \
    -C "$WORKDIR/chart-source"

Результат:

text
chart-source/
└── my-chart/
    ├── Chart.yaml
    ├── values.yaml
    ├── templates/
    ├── charts/
    └── ...

Особое внимание уделите Chart.yaml. Пример:

yaml
apiVersion: v2
name: my-chart
version: 1.5.0
dependencies:
  - name: redis
    version: 20.5.0
    repository: https://charts.example.com
  - name: postgresql
    version: 16.7.0
    repository: https://another.example.com

Перенос только основного Chart недостаточен

Самая распространённая ошибка — перенести только основной Chart. Если у Chart есть зависимости, простого переноса my-chart-1.5.0.tgz недостаточно — необходимо перенести и их:

text
my-application
├── redis
├── postgresql
├── nginx
└── prometheus

Если исходный Chart является локальным, проверьте дерево зависимостей:

bash
cd "$WORKDIR/chart-source/my-chart"
helm dependency list .

Пример вывода:

console
NAME        VERSION   REPOSITORY
redis       20.5.0    https://charts.example.com
postgresql  16.7.0    https://another.example.com

3.4. Загрузка зависимостей и фиксация Chart.lock ​

Во внешнем контуре загрузите зависимости:

bash
helm dependency update .

После этого в каталоге Chart появятся:

text
charts/
├── redis-20.5.0.tgz
└── postgresql-16.7.0.tgz

Проверьте:

bash
ls -la

Должны присутствовать:

text
Chart.yaml
Chart.lock
charts/

Зачем переносить Chart.lock

Chart.yaml описывает зависимости, а lock-файл фиксирует конкретный набор версий зависимостей и их digest.

Без lock-файла при формально неизменном диапазоне версии может «уехать» версия зависимости:

text
Сегодня:      postgresql 16.7.0
Через полгода: postgresql 16.7.1

Для air-gapped поставки желательно фиксировать:

text
Chart version
+ Dependency version
+ Dependency digest
+ Container image digest

3.5. Упаковка Chart вместе с зависимостями ​

Если Chart поставляется с зависимостями внутри каталога charts/, их также необходимо перенести. В большинстве случаев достаточно упаковать итоговый Chart:

bash
helm package .

Получится my-chart-1.5.0.tgz. Убедитесь, что зависимости действительно попали внутрь:

bash
tar -tzf my-chart-1.5.0.tgz

Пример вывода:

text
my-chart/Chart.yaml
my-chart/Chart.lock
my-chart/values.yaml
my-chart/templates/...
my-chart/charts/redis-20.5.0.tgz
my-chart/charts/postgresql-16.7.0.tgz

Шаг 4. Поиск container images ​

Chart без образов бесполезен

Перенос Chart без образов практически бесполезен. Необходимо получить полный список image references.

4.1. Рендеринг Chart и сбор списка образов ​

Лучший способ найти образы — отрендерить Chart:

bash
helm template my-release \
    "$WORKDIR/charts/my-chart-1.5.0.tgz" \
> "$WORKDIR/manifests/rendered.yaml"

Найдите образы в отрендеренном манифесте:

bash
grep -E '^[[:space:]]*image:' "$WORKDIR/manifests/rendered.yaml"

Пример вывода:

text
image: nginx:1.29.1
image: redis:7.4.1
image: quay.io/example/exporter:2.1.0

Получите уникальный список:

bash
grep -E '^[[:space:]]*image:' \
    "$WORKDIR/manifests/rendered.yaml" \
    | sed 's/.*image:[[:space:]]*//' \
    | sort -u \
> "$WORKDIR/manifests/images.txt"

Проверьте:

bash
cat "$WORKDIR/manifests/images.txt"

Пример содержимого:

text
docker.io/library/nginx:1.29.1
docker.io/library/redis:7.4.1
quay.io/example/exporter:2.1.0
registry.example.com/myteam/application:3.7.2

Риск grep-зависимостей

Простой поиск image: через grep не понимает структуру Kubernetes-манифеста и шаблонов Helm. Образ может формироваться шаблоном:

yaml
image:
  registry: docker.io
  repository: nginx
  tag: "1.29"
```yaml

image: repository: "{{ .Values.global.registry }}/my-app"


```yaml
image: "{{ include "myapp.image" . }}"

Для production рекомендуется использовать полноценный YAML-парсер вместо обработки grep.

4.2. Почему одного helm template недостаточно ​

Не все образы обязательно присутствуют в результате стандартного рендеринга. Образ может использоваться в:

text
initContainer
Job
hook
test
optional component

Поэтому дополнительно проверьте:

text
values.yaml
Chart.yaml
templates/
README.md

и документацию самого продукта. Особое внимание:

text
initContainers
sidecars
webhooks
controllers
exporters
test images
migration jobs
hooks

4.3. Поиск образов в values.yaml и по всему Chart ​

Поиск в values.yaml:

bash
grep -RniE \
    'image|repository|tag' \
    "$WORKDIR/chart-source/my-chart/values.yaml"

Можно использовать yq:

bash
yq '.. | select(has("image"))' \
    "$WORKDIR/chart-source/my-chart/values.yaml"

Если структура известна, лучше использовать точечные запросы:

bash
yq '.image' values.yaml

Грубая проверка по всему Chart (хорошо обнаруживает забытые references, но не является полноценным парсером):

bash
grep -RniE \
    'docker\.io/|quay\.io/|ghcr\.io/|registry\.|image:' \
    "$WORKDIR/chart-source/my-chart"

4.4. Список образов как отдельный артефакт ​

Рекомендуемый формат manifests/images.txt:

text
docker.io/library/nginx:1.29.1
docker.io/library/redis:7.4.1
quay.io/example/exporter:2.1.0

Ещё лучше зафиксировать digest:

text
docker.io/library/nginx@sha256:xxxxxxxx
docker.io/library/redis@sha256:xxxxxxxx
quay.io/example/exporter@sha256:xxxxxxxx

Tag для человека, digest для целостности

Tag удобен для человека, но может измениться. Digest неизменяем и важен для воспроизводимости и контроля целостности.

4.5. Проверка digest ​

Для registry можно использовать Skopeo:

bash
skopeo inspect \
    docker://docker.io/library/nginx:1.29.1

Пример вывода:

console
{
    "Name": "docker.io/library/nginx",
    "Digest": "sha256:..."
}

Для automation:

bash
skopeo inspect \
    --format '{{.Digest}}' \
    docker://docker.io/library/nginx:1.29.1

Полная информация об образе (manifest, manifest list, platform, digest, media types):

bash
skopeo inspect \
    --raw \
    docker://docker.io/library/nginx:1.29.1 \
> nginx-manifest.json

4.6. Multi-architecture images ​

Multi-arch: обычный pull может сохранить только одну платформу

Образ nginx:1.29.1 может содержать несколько вариантов:

text
linux/amd64
linux/arm64
linux/arm/v7
linux/ppc64le
linux/s390x

Если выполнить обычный pull на amd64-машине, можно получить только amd64-вариант. Для закрытого Kubernetes-кластера это проблема, если в нём присутствуют разные архитектуры.

Для переноса полного образа используйте Skopeo с опцией --all (копирует image list вместе с вариантами для разных платформ):

bash
skopeo copy \
    --all \
    docker://docker.io/library/nginx:1.29.1 \
    docker://registry.example.local/third-party/nginx:1.29.1

Для registry-to-registry переноса Skopeo предпочтительнее, чем пара docker pull + docker push.

Шаг 5. Перенос Helm Chart в закрытый контур ​

Шаг 6. Перенос container images ​

6.1. Аутентификация Skopeo ​

Выполните login в исходные registry:

bash
skopeo login docker.io

или:

bash
skopeo login quay.io

Для private destination:

bash
skopeo login registry.example.local

После этого:

bash
skopeo copy \
    --all \
    docker://SOURCE/image:tag \
    docker://registry.example.local/namespace/image:tag

В automation можно указать credentials явно:

bash
skopeo copy \
    --src-creds "${SRC_USER}:${SRC_PASSWORD}" \
    --dest-creds "${DST_USER}:${DST_PASSWORD}" \
    --all \
    docker://SOURCE/image:tag \
    docker://registry.example.local/image:tag

Не передавайте пароль в командной строке

Передача password непосредственно в командной строке нежелательна (попадает в историю и логи процессов). Лучше использовать skopeo login или отдельный authfile.

Шаг 7. Формирование Transfer Package ​

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

text
airgap-package/
├── charts/
├── images/
├── manifests/
├── checksums/
├── tools/
└── README.txt
bash
mkdir -p "$WORKDIR/package"

cp -r "$WORKDIR/charts" "$WORKDIR/package/"
cp -r "$WORKDIR/images" "$WORKDIR/package/"
cp -r "$WORKDIR/manifests" "$WORKDIR/package/"
cp -r "$WORKDIR/checksums" "$WORKDIR/package/"

7.1. Inventory ​

Очень желательно иметь файл manifest.txt. Например:

text
PACKAGE_NAME=my-application
PACKAGE_VERSION=1.5.0

HELM_CHART=my-chart
HELM_CHART_VERSION=1.5.0

IMAGES:
- docker.io/library/nginx:1.29.1
- docker.io/library/redis:7.4.1
- quay.io/example/exporter:2.1.0

Более формальный inventory:

text
artifact-type|name|version|source|destination
chart|my-chart|1.5.0|SOURCE_REPO/my-chart|registry.example.local/helm/my-chart:1.5.0
image|nginx|1.29.1|docker.io/library/nginx:1.29.1|registry.example.local/third-party/nginx:1.29.1
image|redis|7.4.1|docker.io/library/redis:7.4.1|registry.example.local/third-party/redis:7.4.1

Рекомендуемая структура каталога manifests/:

text
manifests/
├── charts.txt
├── images.txt
├── images-digests.txt
├── crds.txt
├── artifacts.txt
└── rendered.yaml

Примеры содержимого:

text
# charts.txt
my-chart|1.5.0
redis|20.5.0
postgresql|16.7.0
text
# images.txt
docker.io/library/nginx:1.29.1
docker.io/library/redis:7.4.1
quay.io/example/exporter:2.1.0
text
# images-digests.txt
docker.io/library/nginx@sha256:...
docker.io/library/redis@sha256:...
quay.io/example/exporter@sha256:...

7.2. Контрольные суммы ​

После завершения скачивания посчитайте SHA-256:

bash
find "$WORKDIR/charts" "$WORKDIR/images" \
    -type f \
    -exec sha256sum {} \; \
> "$WORKDIR/checksums/SHA256SUMS"

Проверка:

bash
sha256sum -c \
    "$WORKDIR/checksums/SHA256SUMS"

Ожидаемый результат:

text
... nginx-1.29.1.tar: OK
... redis-7.4.1.tar: OK
... my-chart-1.5.0.tgz: OK

7.3. Общий архив и контрольная сумма пакета ​

Создайте общий архив:

bash
tar \
    -czf \
    my-application-1.5.0-airgap.tar.gz \
    -C "$WORKDIR" \
    charts \
    images \
    manifests \
    checksums

Проверьте содержимое:

bash
tar -tzf my-application-1.5.0-airgap.tar.gz

Посчитайте контрольную сумму самого пакета:

bash
sha256sum \
    my-application-1.5.0-airgap.tar.gz \
> my-application-1.5.0-airgap.tar.gz.sha256

Теперь на USB переносится:

text
my-application-1.5.0-airgap.tar.gz
my-application-1.5.0-airgap.tar.gz.sha256

7.4. Запись на USB ​

Допустим, USB смонтирован в /media/usb. Копируем:

bash
cp \
    my-application-1.5.0-airgap.tar.gz \
    /media/usb/

cp \
    my-application-1.5.0-airgap.tar.gz.sha256 \
    /media/usb/

Затем обязательно:

bash
sync

После чего безопасно размонтируйте носитель:

bash
umount /media/usb

Шаг 8. Импорт в закрытом контуре ​

8.1. Проверка носителя и распаковка ​

Обязательная проверка checksum перед импортом

На машине закрытого контура сначала проверьте контрольную сумму пакета и только после успешной проверки распаковывайте архив:

bash
sha256sum \
    -c \
    my-application-1.5.0-airgap.tar.gz.sha256

Ожидаемый результат:

text
my-application-1.5.0-airgap.tar.gz: OK

Распаковка:

bash
mkdir -p /opt/airgap-import

tar \
    -xzf \
    my-application-1.5.0-airgap.tar.gz \
    -C /opt/airgap-import

Результат:

text
/opt/airgap-import/
├── charts/
├── images/
├── manifests/
└── checksums/

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

bash
cd /opt/airgap-import
sha256sum -c checksums/SHA256SUMS

Все файлы должны иметь OK.

При любой ошибке импорт необходимо остановить

Если любой файл получил FAILED — импорт необходимо остановить и выяснить причину повреждения.

8.2. Импорт container images ​

Для OCI archive:

bash
skopeo copy \
    oci-archive:images/nginx-1.29.1.tar \
    docker://registry.example.local/third-party/nginx:1.29.1

Для multi-arch archive:

bash
skopeo copy \
    --all \
    oci-archive:images/nginx-1.29.1.tar \
    docker://registry.example.local/third-party/nginx:1.29.1

Если используется docker archive:

bash
docker load \
    -i images/nginx-1.29.1.tar

Затем:

bash
docker tag \
    nginx:1.29.1 \
    registry.example.local/third-party/nginx:1.29.1

И:

bash
docker push \
    registry.example.local/third-party/nginx:1.29.1

Для массовой миграции Skopeo обычно удобнее, поскольку позволяет работать непосредственно с registry и различными локальными transport.

8.3. Массовый импорт images ​

Если имеется:

text
images/
├── nginx-1.29.1.tar
├── redis-7.4.1.tar
├── exporter-2.1.0.tar
└── app-3.7.2.tar

можно сделать скрипт:

bash
#!/usr/bin/env bash
set -euo pipefail

REGISTRY="registry.example.local"

skopeo copy \
    oci-archive:images/nginx-1.29.1.tar \
    docker://${REGISTRY}/third-party/nginx:1.29.1

skopeo copy \
    oci-archive:images/redis-7.4.1.tar \
    docker://${REGISTRY}/third-party/redis:7.4.1

skopeo copy \
    oci-archive:images/exporter-2.1.0.tar \
    docker://${REGISTRY}/third-party/exporter:2.1.0

Для большого количества images лучше не писать такие команды вручную, а хранить mapping:

text
source|archive|destination
docker.io/library/nginx:1.29.1|nginx-1.29.1.tar|registry.example.local/third-party/nginx:1.29.1
docker.io/library/redis:7.4.1|redis-7.4.1.tar|registry.example.local/third-party/redis:7.4.1

и обрабатывать его скриптом.

8.4. Импорт Helm Chart ​

Выполните login и перенесите Charts:

bash
skopeo login registry.example.local
helm registry login registry.example.local
bash
helm push \
    charts/my-chart-1.5.0.tgz \
    oci://registry.example.local/helm

Проверьте импорт:

bash
helm show chart \
    oci://registry.example.local/helm/my-chart \
    --version 1.5.0

Шаг 9. Настройка установки в закрытом контуре ​

9.1. Переписывание image references ​

После переноса в private registry Kubernetes больше не должен пытаться обращаться к внешним registry:

text
docker.io
quay.io
ghcr.io

Например, было:

yaml
image:
  repository: docker.io/library/nginx
  tag: "1.29.1"

должно стать:

yaml
image:
  repository: registry.example.local/third-party/nginx
  tag: "1.29.1"

Лучше не изменять сам Chart

Если Chart предоставляет параметр global.imageRegistry, используйте его вместо ручного изменения Chart:

bash
helm install my-release \
    ./my-chart-1.5.0.tgz \
    --set global.imageRegistry=registry.example.local

Если Chart поддерживает:

yaml
image:
  registry:
  repository:
  tag:

можно передать:

bash
--set image.registry=registry.example.local

9.2. values-airgap.yaml ​

Для production-системы рекомендуется создать отдельный файл values-airgap.yaml. Например:

yaml
global:
  imageRegistry: registry.example.local

image:
  registry: registry.example.local
  repository: third-party/my-application
  tag: "1.5.0"

После этого:

bash
helm upgrade --install my-release \
    oci://registry.example.local/helm/my-chart \
    --version 1.5.0 \
    -f values-airgap.yaml

Это намного лучше, чем вручную менять Chart.

9.3. Проверка отрендеренных манифестов ​

До реального deployment выполните:

bash
helm template my-release \
    ./my-chart-1.5.0.tgz \
    -f values-airgap.yaml \
> rendered-airgap.yaml

Затем:

bash
grep -E '^[[:space:]]*image:' \
    rendered-airgap.yaml

В выводе не должно быть:

text
docker.io/
quay.io/
ghcr.io/
registry-1.docker.io/

если эти registry недоступны в закрытом контуре.

Проверка:

bash
grep -E \
    'docker\.io|quay\.io|ghcr\.io|registry-1\.docker\.io' \
    rendered-airgap.yaml

Команда должна вернуть пустой результат.

Получите полный список references:

bash
grep -E '^[[:space:]]*image:' rendered-airgap.yaml \
    | sed 's/.*image:[[:space:]]*//' \
    | sort -u

Пример вывода:

text
registry.example.local/third-party/nginx:1.29.1
registry.example.local/third-party/redis:7.4.1
registry.example.local/third-party/exporter:2.1.0
registry.example.local/application/my-app:3.7.2

Этот список должен совпадать с inventory.

9.4. Доступ Kubernetes к private registry (imagePullSecrets) ​

Если private registry требует authentication, Kubernetes должен иметь credentials:

bash
kubectl create secret docker-registry registry-credentials \
    --docker-server=registry.example.local \
    --docker-username=USER \
    --docker-password='PASSWORD' \
    --namespace=my-namespace

После этого укажите secret в манифестах:

yaml
imagePullSecrets:
  - name: registry-credentials

Или настройте secret на ServiceAccount.

9.5. Проверка доступности registry (DNS, TLS, crictl) ​

Из Kubernetes node проверьте доступность registry:

bash
curl -k https://registry.example.local/v2/

В зависимости от конфигурации registry можно получить 401 Unauthorized — это нормально, если registry требует authentication. Важно, чтобы сам registry был доступен.

Проверьте DNS:

bash
getent hosts registry.example.local

Проверьте TCP:

bash
nc -vz registry.example.local 443

Проверка TLS. Если registry использует собственный CA:

text
registry.example.local
        │
        └── internal CA

Kubernetes nodes/container runtime должны доверять этому CA. Проверка:

bash
openssl s_client \
    -connect registry.example.local:443 \
    -servername registry.example.local

Установка CA

Если используется self-signed/internal certificate, необходимо корректно установить CA на:

  • Kubernetes nodes;
  • containerd/CRI-O;
  • registry clients;
  • при необходимости Helm/Skopeo host.

Если установлен crictl, более полезная проверка, чем docker pull:

bash
crictl pull \
    registry.example.local/third-party/nginx:1.29.1

Если pull проходит (Image is up to date ...), можно переходить к Helm deployment.

Шаг 10. CRD, webhooks и дополнительные артефакты ​

10.1. CRD внутри Chart ​

Некоторые Helm Charts устанавливают CRD:

text
crds/
├── example.io_widgets.yaml
└── example.io_clusters.yaml

Их необходимо учитывать отдельно. Проверьте:

bash
tar -tzf my-chart-1.5.0.tgz | grep '^my-chart/crds/'

Также полезно:

bash
helm show all my-chart-1.5.0.tgz

10.2. CRD и манифесты вне Chart ​

Если документация продукта требует, например:

bash
kubectl apply -f https://example.com/crd.yaml

то в air-gapped среде такая команда работать не будет. Нужно заранее скачать:

bash
curl -LO https://example.com/crd.yaml

и включить файл в transfer package:

text
manifests/
└── crd.yaml

То же самое относится к манифестам, поставляемым отдельно:

text
MutatingWebhookConfiguration
ValidatingWebhookConfiguration
ServiceAccount
ClusterRole
ClusterRoleBinding
ConfigMap
Secret
Namespace

Особенно внимательно проверять файлы:

text
install.yaml
operator.yaml
bundle.yaml
deploy.yaml

10.3. Operators, hooks, tests ​

Для Operator-поставок схема становится сложнее:

text
Operator
├── Controller image
├── Webhook image
├── Init image
├── Sidecar image
└── Operand images

Кроме самого Operator image необходимо переносить images управляемого приложения. Например:

text
postgres-operator
       │
       ├── operator image
       │
       └── PostgreSQL image

Operator без operand-образа не заработает

Если перенести только operator image, а PostgreSQL image оставить на docker.io/postgres:... — air-gapped установка не заработает.

Helm hooks. Обязательно учитывайте:

yaml
annotations:
  "helm.sh/hook": pre-install

Такие hooks могут запускать Job, Pod, migration, init process со своими images. Поэтому поиск images необходимо выполнять по полностью отрендеренным manifests, включая hooks.

Helm tests. Chart может иметь templates/tests/:

yaml
helm.sh/hook: test

Если тесты запускаются в закрытом контуре, их images также должны быть доступны.

Проверка результата ​

Проверка целостности и digests ​

  • Контрольная сумма пакета: sha256sum -c my-application-1.5.0-airgap.tar.gz.sha256 — должен вернуть OK.
  • Контрольная сумма файлов после распаковки: sha256sum -c checksums/SHA256SUMS — все файлы OK.

Сравните digest после импорта:

bash
skopeo inspect \
    docker://registry.example.local/third-party/nginx:1.29.1

Пример вывода:

console
Digest: sha256:...

Сравните с digest, зафиксированным при загрузке:

text
SOURCE:  sha256:AAA...
PRIVATE: sha256:AAA...

Digest отличается — выясните причину

Если digest после импорта отличается от зафиксированного при загрузке — необходимо выяснить причину и не использовать такой образ.

Не полагайтесь только на tag:

text
nginx:latest      # плохо
nginx:1.29.1      # лучше
nginx@sha256:...  # ещё лучше для production

Причина — tag может измениться, digest — нет.

Проверка наличия каждого image в private registry ​

bash
skopeo inspect \
    docker://registry.example.local/third-party/nginx:1.29.1

Для каждого image должна возвращаться metadata. Можно автоматизировать:

bash
while read -r image; do
    echo "Checking $image"
    skopeo inspect "docker://$image" >/dev/null
done < manifests/private-images.txt

При ошибке (set -e) скрипт остановится.

Проверка манифестов ​

  • helm template с -f values-airgap.yaml не должен содержать docker.io/, quay.io/, ghcr.io/, registry-1.docker.io/.
  • Список references из отрендеренного манифеста должен совпадать с inventory.

Проверка deployment ​

bash
helm lint ./my-chart
bash
helm template my-release \
    ./my-chart-1.5.0.tgz \
    -f values-airgap.yaml

Установка:

bash
helm upgrade --install \
    my-release \
    ./my-chart-1.5.0.tgz \
    --namespace my-namespace \
    --create-namespace \
    -f values-airgap.yaml

Если Chart находится в OCI registry:

bash
helm upgrade --install \
    my-release \
    oci://registry.example.local/helm/my-chart \
    --version 1.5.0 \
    --namespace my-namespace \
    --create-namespace \
    -f values-airgap.yaml

Проверьте Pod'ы и события:

bash
kubectl get pods -n my-namespace
bash
kubectl get events \
    -n my-namespace \
    --sort-by=.lastTimestamp

Особенно обращать внимание на:

text
ImagePullBackOff
ErrImagePull
Failed
Back-off pulling image

Если Pod пытается выйти в Интернет — проверьте, на какой image ссылается Pod:

bash
kubectl get pods \
    -n my-namespace \
    -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}'
bash
kubectl describe pod POD_NAME \
    -n my-namespace

Смотреть поле Image: — все images должны ссылаться на private registry.

Частые ошибки ​

СимптомВероятная причинаРешение
ImagePullBackOff / ErrImagePullОбраз не перенесён или указан неверный referenceПроверьте images.txt, наличие образа в registry (skopeo inspect), сверьте с inventory
Chart установился, но зависимость не развернуласьПеренесён только основной Chart без dependenciesВыполните helm dependency update во внешнем контуре, перенесите charts/ и Chart.lock, упакуйте helm package
Pod тянет образ из docker.io / quay.ioНе переписаны references / не применён values-airgap.yamlОтрендерьте helm template -f values-airgap.yaml, проверьте `grep -E 'docker.io
sha256sum -c вернул FAILEDПовреждение носителя или архива при переносеПересоздайте пакет, повторите перенос; не распаковывайте повреждённый архив
exec format error при запуске PodПеренесён только amd64-вариант multi-arch образаКопируйте с skopeo copy --all, проверьте сохранение manifest list
Registry недоступен с узлов кластераОшибка DNS, TCP или TLS/CAgetent hosts, nc -vz, openssl s_client, установите внутренний CA на nodes/containerd/CRI-O
Версия артефакта «уехала» при повторной поставкеИспользование latest или отсутствие Chart.lockФиксируйте версии и digest, переносите Chart.lock
Часть образов не обнаружена (initContainer, hook, test)grep не понимает структуру манифестов и шаблоновРендерьте Chart, анализируйте values.yaml, Chart.yaml, templates/, README, документацию продукта
Установка Operator не работаетПеренесён только operator image, operand-образ остался во внешнем registryПеренесите все operand images (PostgreSQL и т.п.)
Chart установлен, но CRD отсутствуютCRD устанавливаются отдельным манифестомСкачайте crd.yaml заранее, включите в transfer package, примените вручную

Приложения ​

A. Рекомендуемая структура Transfer Package ​

text
application-1.5.0/
│
├── README.md
│
├── manifest.yaml
│
├── charts/
│   ├── my-chart-1.5.0.tgz
│   ├── redis-20.5.0.tgz
│   └── postgresql-16.7.0.tgz
│
├── images/
│   ├── nginx-1.29.1.oci.tar
│   ├── redis-7.4.1.oci.tar
│   └── exporter-2.1.0.oci.tar
│
├── manifests/
│   ├── rendered.yaml
│   ├── images.txt
│   ├── images-digests.txt
│   ├── charts.txt
│   └── crds/
│
├── values/
│   └── values-airgap.yaml
│
├── checksums/
│   └── SHA256SUMS
│
└── scripts/
    ├── import-images.sh
    ├── import-charts.sh
    └── verify.sh

B. manifest.yaml ​

Полезно иметь машиночитаемый manifest. Например:

yaml
package:
  name: my-application
  version: 1.5.0
  created: 2026-08-31

helm:
  charts:
    - name: my-chart
      version: 1.5.0
      source: https://SOURCE-URL
      destination: registry.example.local/helm/my-chart

images:
  - source: docker.io/library/nginx:1.29.1
    destination: registry.example.local/third-party/nginx:1.29.1
    digest: sha256:...

  - source: docker.io/library/redis:7.4.1
    destination: registry.example.local/third-party/redis:7.4.1
    digest: sha256:...

  - source: quay.io/example/exporter:2.1.0
    destination: registry.example.local/third-party/exporter:2.1.0
    digest: sha256:...

Это уже практически полноценная SBOM-like inventory для поставки.

C. Скрипты автоматизации ​

Сбор списка образов из отрендеренного Chart:

bash
helm template my-release ./my-chart \
    | grep -E '^[[:space:]]*image:' \
    | sed 's/.*image:[[:space:]]*//' \
    | sort -u \
> images.txt

Риск grep-зависимостей

Для production рекомендуется использовать более полноценный parser YAML, потому что простая обработка grep не понимает структуру Kubernetes manifest.

Автоматизация переноса images по списку:

bash
#!/usr/bin/env bash
set -euo pipefail

DEST_REGISTRY="registry.example.local"

while read -r IMAGE; do
    [ -z "$IMAGE" ] && continue

    echo "Processing: $IMAGE"

    # Здесь необходимо определить destination path
    # в соответствии с принятой политикой naming.
done < images.txt

На практике лучше заранее определить правила mapping. Например:

text
docker.io/library/nginx:1.29.1
        ↓
registry.example.local/docker.io/library/nginx:1.29.1

quay.io/example/exporter:2.1.0
        ↓
registry.example.local/quay.io/example/exporter:2.1.0

Это позволяет сохранить исходную структуру registry.

Проверка transfer package:

bash
#!/usr/bin/env bash
set -euo pipefail

echo "Checking checksums..."
sha256sum -c checksums/SHA256SUMS

echo
echo "Checking required files..."
test -f manifest.yaml
test -f manifests/images.txt
test -f manifests/images-digests.txt

echo
echo "Package verification successful."

Проверка private registry:

bash
#!/usr/bin/env bash
set -euo pipefail

REGISTRY="registry.example.local"

while read -r IMAGE; do
    [ -z "$IMAGE" ] && continue

    echo "Checking: $IMAGE"
    skopeo inspect \
        "docker://${REGISTRY}/${IMAGE}"
done < manifests/private-images.txt

Рекомендуемая автоматизация для регулярных поставок:

text
Application release
       │
       ▼
Helm Chart
       │
       ├── resolve dependencies
       ├── render
       ├── discover images
       ├── resolve digests
       ├── download images
       ├── download charts
       ├── generate SBOM
       ├── generate manifest
       ├── calculate SHA256
       ▼
Transfer Package

D. Рекомендуемая политика naming ​

Лучше не делать:

text
registry.example.local/nginx
registry.example.local/redis
registry.example.local/exporter

потому что через год станет непонятно, откуда это приехало. Лучше:

text
registry.example.local/docker.io/library/nginx
registry.example.local/docker.io/library/redis
registry.example.local/quay.io/example/exporter

или:

text
registry.example.local/third-party/docker.io/library/nginx
registry.example.local/third-party/quay.io/example/exporter

Это значительно упрощает аудит.

Пример конечной структуры private registry:

text
registry.example.local/
│
├── third-party/
│   ├── docker.io/
│   │   └── library/
│   │       ├── nginx
│   │       ├── redis
│   │       └── busybox
│   │
│   └── quay.io/
│       └── example/
│           └── exporter
│
├── internal/
│   ├── application-a
│   └── application-b
│
└── helm/
    ├── application-a
    ├── application-b
    └── dependencies

E. Политики для air-gapped поставок ​

Для промышленной эксплуатации рекомендуется установить следующие правила:

  1. Правило 1. Не использовать latest.
  2. Правило 2. Все версии фиксируются: Chart version, Image tag, Image digest.
  3. Правило 3. Каждая поставка получает уникальный ID (например, application-1.5.0).
  4. Правило 4. Для каждого transfer package существует: SHA256, manifest, inventory.
  5. Правило 5. Нельзя устанавливать Chart, если он содержит неизвестный внешний image.
  6. Правило 6. Нельзя разрешать Kubernetes nodes выходить в Интернет только для скачивания image.
  7. Правило 7. Весь набор dependencies должен быть известен до начала импорта.

F. Что часто забывают ​

При переносе Kubernetes-приложений в air-gapped среду чаще всего забывают:

text
1.  Helm dependencies
2.  initContainer images
3.  sidecar images
4.  webhook images
5.  operator images
6.  CRD
7.  Helm hooks
8.  Helm test images
9.  migration Job images
10. multi-arch variants
11. imagePullSecrets
12. internal CA
13. дополнительные manifests
14. images, задаваемые через values
15. images, которые формируются шаблонами
16. отдельные registry credentials
17. admission webhooks
18. дополнительные charts операторов

G. Чек-лист перед передачей USB ​

H. Чек-лист после переноса ​

В закрытом контуре:

I. Подписи и SBOM (рекомендации на будущее) ​

Рекомендация, а не штатная функция

Подписи и SBOM — это возможности для усиления защищённых контуров «на будущее». На текущий момент это рекомендации по процессу, а не штатная функция платформы «Боцман» или описанного здесь инструментария.

Современная инфраструктура может использовать registry не только для container images и Helm Charts. В registry могут находиться:

text
Helm Charts
Container Images
SBOM
Signatures
Cosign artifacts
OCI artifacts

В защищённых контурах желательно переносить не только image, но и:

text
signature
SBOM
provenance
text
image
 ├── manifest
 ├── layers
 ├── signature
 └── SBOM

Если используется Cosign/Notation или другая система подписи, подписи должны быть перенесены и проверены уже внутри закрытого контура. При использовании supply-chain security необходимо определить полный перечень OCI artifacts.

Команда Боцман