Перенос Helm-чартов и контейнерных образов в закрытый контур
В данной инструкции описан порядок переноса программных артефактов Kubernetes из публичных репозиториев, доступных через сеть Интернет, в изолированный частный контур, не имеющий доступа к Интернету.
Рассматриваются следующие типы артефактов:
- Helm Charts и их зависимости;
- Kubernetes container images, в том числе multi-architecture;
- OCI Helm Charts;
- CRD и дополнительные Kubernetes-манифесты;
- файлы конфигурации и metadata, необходимые для воспроизводимой установки;
- контрольные суммы и списки артефактов;
- перенос через промежуточный носитель, например USB Flash Drive.
Единица поставки — Application Release, а не Chart
Перенос одного Helm Chart без образов, зависимостей, CRD и манифестов практически бесполезен. Единица поставки в закрытый контур — весь набор артефактов приложения:
Общая схема процесса:
INTERNET
│
┌───────▼────────┐
│ Внешняя машина │
│ с доступом │
│ к Интернету │
└───────┬────────┘
│ скачать / собрать все артефакты
┌───────▼────────┐
│ Transfer Package│
│ Charts / Images │
│ Dependencies / │
│ Checksums / │
│ Manifest │
└───────┬────────┘
│ USB / иной контролируемый носитель
┌───────▼────────┐
│ Закрытый контур│
│ Private Registry│
│ Helm Repository │
└───────┬────────┘
▼
Kubernetes
Когда это пригодится
- Организация поставки сторонних Helm Charts и образов в закрытый контур без доступа к Интернету.
- Подготовка воспроизводимого набора артефактов для установки приложений в изолированном Kubernetes-кластере.
- Регулярное обновление ПО в контуре, изолированном на физическом или сетевом уровне.
Краткий путь
- Подготовьте внешнюю машину с доступом в Интернет (Helm, Skopeo, Docker/Podman,
jq,yq). - Скачайте Chart нужной версии и все его зависимости (с
Chart.lock). - Отрендерьте Chart, соберите полный список образов и зафиксируйте их digest.
- Перенесите Chart и образы в закрытый контур: напрямую (Skopeo) или через USB-носитель.
- Импортируйте образы и Charts в private OCI registry (
skopeo copy,helm push). - Подготовьте
values-airgap.yamlи проверьте отсутствие внешних references в отрендеренных манифестах. - Установите 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-namespace |
namespace установки |
SOURCE/image:tag |
произвольный образ-источник |
USER / PASSWORD |
учётные данные реестра |
Шаг 1. Подготовка внешней машины
Для скачивания артефактов используется отдельная машина, имеющая доступ к Интернету. Рекомендуется использовать специальную VM или сервер (например, airgap-staging).
1.1. Установка инструментов
На внешней машине устанавливаются:
- Helm;
- Docker или Podman;
- Skopeo;
- необходимые registry clients;
- Git;
jq;yq;curl;sha256sum.
Проверьте установку:
1.2. Фиксация версий инструментов
Версии инструментов необходимо зафиксировать — это важно для последующего расследования проблем:
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. Создание рабочего каталога
Создайте рабочий каталог:
export WORKDIR=/opt/airgap-transfer
mkdir -p "$WORKDIR"/{charts,images,manifests,checksums,logs,scripts}
Структура каталога:
Все последующие операции выполняйте из этого каталога.
Шаг 3. Получение Helm Chart
3.1. Добавление внешнего Helm Repository
Классический Helm Repository имеет URL вида https://charts.example.com и содержит index.yaml.
Добавьте repository:
Обновите индекс:
Проверьте:
Получите список версий:
Пример вывода:
3.2. Скачивание конкретной версии
Не используйте helm pull без указания версии
Для воспроизводимого набора всегда явно указывайте версию. Команда helm pull SOURCE_REPO/my-chart без --version скачает последнюю версию, что делает поставку непредсказуемой.
Скачайте нужную версию:
Результат:
Проверьте содержимое:
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 его удобно распаковать:
mkdir -p "$WORKDIR/chart-source"
tar -xzf \
"$WORKDIR/charts/my-chart-1.5.0.tgz" \
-C "$WORKDIR/chart-source"
Результат:
Особое внимание уделите Chart.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 недостаточно — необходимо перенести и их:
Если исходный Chart является локальным, проверьте дерево зависимостей:
Пример вывода:
NAME VERSION REPOSITORY
redis 20.5.0 https://charts.example.com
postgresql 16.7.0 https://another.example.com
3.4. Загрузка зависимостей и фиксация Chart.lock
Во внешнем контуре загрузите зависимости:
После этого в каталоге Chart появятся:
Проверьте:
Должны присутствовать:
Зачем переносить Chart.lock
Chart.yaml описывает зависимости, а lock-файл фиксирует конкретный набор версий зависимостей и их digest.
Без lock-файла при формально неизменном диапазоне версии может «уехать» версия зависимости:
Для air-gapped поставки желательно фиксировать:
3.5. Упаковка Chart вместе с зависимостями
Если Chart поставляется с зависимостями внутри каталога charts/, их также необходимо перенести. В большинстве случаев достаточно упаковать итоговый Chart:
Получится my-chart-1.5.0.tgz. Убедитесь, что зависимости действительно попали внутрь:
Пример вывода:
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:
helm template my-release \
"$WORKDIR/charts/my-chart-1.5.0.tgz" \
> "$WORKDIR/manifests/rendered.yaml"
Найдите образы в отрендеренном манифесте:
Пример вывода:
Получите уникальный список:
grep -E '^[[:space:]]*image:' \
"$WORKDIR/manifests/rendered.yaml" \
| sed 's/.*image:[[:space:]]*//' \
| sort -u \
> "$WORKDIR/manifests/images.txt"
Проверьте:
Пример содержимого:
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. Образ может формироваться шаблоном:
Для production рекомендуется использовать полноценный YAML-парсер вместо обработки grep.
4.2. Почему одного helm template недостаточно
Не все образы обязательно присутствуют в результате стандартного рендеринга. Образ может использоваться в:
Поэтому дополнительно проверьте:
и документацию самого продукта. Особое внимание:
4.3. Поиск образов в values.yaml и по всему Chart
Поиск в values.yaml:
Можно использовать yq:
Если структура известна, лучше использовать точечные запросы:
Грубая проверка по всему Chart (хорошо обнаруживает забытые references, но не является полноценным парсером):
4.4. Список образов как отдельный артефакт
Рекомендуемый формат manifests/images.txt:
Ещё лучше зафиксировать digest:
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:
Пример вывода:
Для automation:
Полная информация об образе (manifest, manifest list, platform, digest, media types):
4.6. Multi-architecture images
Multi-arch: обычный pull может сохранить только одну платформу
Образ nginx:1.29.1 может содержать несколько вариантов:
Если выполнить обычный pull на amd64-машине, можно получить только amd64-вариант. Для закрытого Kubernetes-кластера это проблема, если в нём присутствуют разные архитектуры.
Для переноса полного образа используйте Skopeo с опцией --all (копирует image list вместе с вариантами для разных платформ):
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 в закрытый контур
Если закрытый registry поддерживает OCI, это предпочтительный вариант. Современный Helm поддерживает OCI registry как хранилище Charts (начиная с Helm 3.8 OCI-поддержка включена по умолчанию). Helm позволяет выполнять pull, push, install, upgrade и операции с зависимостями через oci://.
Сначала выполните login:
Затем перенесите Chart:
Helm сам использует имя Chart и его версию:
Reference для helm push
При helm push в OCI reference должен указывать registry/repository без имени Chart и без tag. Имя и версия берутся из Chart metadata.
Проверьте перенос:
Можно выполнить:
и проверить digest. Для особо критичных систем можно использовать установку по digest, поскольку digest является неизменяемым идентификатором содержимого Chart.
Если private repository не поддерживает OCI, можно использовать классический Helm Repository (например, https://helm.example.local):
Такой вариант до сих пор возможен, однако для нового решения предпочтительнее registry с OCI.
Шаг 6. Перенос container images
Если закрытый контур не требует промежуточного носителя, наиболее простой вариант:
skopeo copy \
--all \
docker://docker.io/library/nginx:1.29.1 \
docker://registry.example.local/third-party/nginx:1.29.1
Для другого образа:
skopeo copy \
--all \
docker://quay.io/example/exporter:2.1.0 \
docker://registry.example.local/third-party/exporter:2.1.0
Skopeo умеет копировать образы непосредственно между registry, а также работать с локальными dir, docker-archive и OCI transport.
Если между Internet-контуром и закрытым контуром физически нет сетевой связи, используется промежуточный носитель:
INTERNET
│
▼
┌──────────────┐
│ Staging Host │
└──────┬───────┘
│
▼
┌───────────────┐
│ USB / SSD │
│ Transfer Media│
└───────┬───────┘
│
▼
┌──────────────┐
│ Airgap Host │
└──────┬───────┘
│
▼
Private Registry
Способы сохранения образов на носитель:
Для большого количества образов предпочтительны OCI archive или OCI image layout — они лучше соответствуют современной registry/OCI-модели и позволяют сохранять структуру image artifacts (в отличие от docker save).
mkdir -p "$WORKDIR/images"
skopeo copy \
--all \
docker://docker.io/library/nginx:1.29.1 \
oci-archive:"$WORKDIR/images/nginx-1.29.1.tar":nginx:1.29.1
Аналогично:
Самый простой вариант для отдельных образов:
skopeo copy \
docker://docker.io/library/nginx:1.29.1 \
docker-archive:"$WORKDIR/images/nginx-1.29.1.tar":nginx:1.29.1
Получаем images/nginx-1.29.1.tar — архив в формате, совместимом с docker load.
Ограничение docker-archive
Для multi-arch image необходимо внимательно контролировать, что именно переносится. Если требуется сохранить полный manifest list, предпочтительнее использовать OCI archive:
Можно сохранить image в каталог:
mkdir -p "$WORKDIR/images/nginx-1.29.1"
skopeo copy \
--all \
docker://docker.io/library/nginx:1.29.1 \
dir:"$WORKDIR/images/nginx-1.29.1"
Получится структура с manifest и blobs. Этот вариант удобен, когда:
- нужно инспектировать содержимое;
- требуется повторное использование blobs;
- архив будет создаваться позднее;
- необходимо избежать большого количества отдельных tar-файлов.
Skopeo документирует dir: как локальное представление manifest/layers/signatures.
Skopeo поддерживает transports: oci:, oci-archive:, docker-archive:, dir:, docker: и другие.
6.1. Аутентификация Skopeo
Выполните login в исходные registry:
или:
Для private destination:
После этого:
skopeo copy \
--all \
docker://SOURCE/image:tag \
docker://registry.example.local/namespace/image:tag
В automation можно указать credentials явно:
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
Для небольшого количества образов удобно собрать единый пакет:
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. Например:
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:
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/:
manifests/
├── charts.txt
├── images.txt
├── images-digests.txt
├── crds.txt
├── artifacts.txt
└── rendered.yaml
Примеры содержимого:
# images.txt
docker.io/library/nginx:1.29.1
docker.io/library/redis:7.4.1
quay.io/example/exporter:2.1.0
# images-digests.txt
docker.io/library/nginx@sha256:...
docker.io/library/redis@sha256:...
quay.io/example/exporter@sha256:...
7.2. Контрольные суммы
После завершения скачивания посчитайте SHA-256:
find "$WORKDIR/charts" "$WORKDIR/images" \
-type f \
-exec sha256sum {} \; \
> "$WORKDIR/checksums/SHA256SUMS"
Проверка:
Ожидаемый результат:
7.3. Общий архив и контрольная сумма пакета
Создайте общий архив:
tar \
-czf \
my-application-1.5.0-airgap.tar.gz \
-C "$WORKDIR" \
charts \
images \
manifests \
checksums
Проверьте содержимое:
Посчитайте контрольную сумму самого пакета:
Теперь на USB переносится:
7.4. Запись на USB
Допустим, USB смонтирован в /media/usb. Копируем:
cp \
my-application-1.5.0-airgap.tar.gz \
/media/usb/
cp \
my-application-1.5.0-airgap.tar.gz.sha256 \
/media/usb/
Затем обязательно:
После чего безопасно размонтируйте носитель:
Шаг 8. Импорт в закрытом контуре
8.1. Проверка носителя и распаковка
Обязательная проверка checksum перед импортом
На машине закрытого контура сначала проверьте контрольную сумму пакета и только после успешной проверки распаковывайте архив:
Ожидаемый результат:
Распаковка:
Результат:
Проверьте checksum после распаковки:
Все файлы должны иметь OK.
При любой ошибке импорт необходимо остановить
Если любой файл получил FAILED — импорт необходимо остановить и выяснить причину повреждения.
8.2. Импорт container images
Для OCI archive:
skopeo copy \
oci-archive:images/nginx-1.29.1.tar \
docker://registry.example.local/third-party/nginx:1.29.1
Для multi-arch archive:
skopeo copy \
--all \
oci-archive:images/nginx-1.29.1.tar \
docker://registry.example.local/third-party/nginx:1.29.1
Если используется docker archive:
Затем:
И:
Для массовой миграции Skopeo обычно удобнее, поскольку позволяет работать непосредственно с registry и различными локальными transport.
8.3. Массовый импорт images
Если имеется:
можно сделать скрипт:
#!/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:
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:
Проверьте импорт:
Шаг 9. Настройка установки в закрытом контуре
9.1. Переписывание image references
После переноса в private registry Kubernetes больше не должен пытаться обращаться к внешним registry:
Например, было:
должно стать:
Лучше не изменять сам Chart
Если Chart предоставляет параметр global.imageRegistry, используйте его вместо ручного изменения Chart:
Если Chart поддерживает:
можно передать:
9.2. values-airgap.yaml
Для production-системы рекомендуется создать отдельный файл values-airgap.yaml. Например:
global:
imageRegistry: registry.example.local
image:
registry: registry.example.local
repository: third-party/my-application
tag: "1.5.0"
После этого:
helm upgrade --install my-release \
oci://registry.example.local/helm/my-chart \
--version 1.5.0 \
-f values-airgap.yaml
Это намного лучше, чем вручную менять Chart.
9.3. Проверка отрендеренных манифестов
До реального deployment выполните:
Затем:
В выводе не должно быть:
если эти registry недоступны в закрытом контуре.
Проверка:
Команда должна вернуть пустой результат.
Получите полный список references:
Пример вывода:
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:
kubectl create secret docker-registry registry-credentials \
--docker-server=registry.example.local \
--docker-username=USER \
--docker-password='PASSWORD' \
--namespace=my-namespace
После этого укажите secret в манифестах:
Или настройте secret на ServiceAccount.
9.5. Проверка доступности registry (DNS, TLS, crictl)
Из Kubernetes node проверьте доступность registry:
В зависимости от конфигурации registry можно получить 401 Unauthorized — это нормально, если registry требует authentication. Важно, чтобы сам registry был доступен.
Проверьте DNS:
Проверьте TCP:
Проверка TLS. Если registry использует собственный CA:
Kubernetes nodes/container runtime должны доверять этому CA. Проверка:
Установка CA
Если используется self-signed/internal certificate, необходимо корректно установить CA на:
- Kubernetes nodes;
- containerd/CRI-O;
- registry clients;
- при необходимости Helm/Skopeo host.
Если установлен crictl, более полезная проверка, чем docker pull:
Если pull проходит (Image is up to date ...), можно переходить к Helm deployment.
Шаг 10. CRD, webhooks и дополнительные артефакты
10.1. CRD внутри Chart
Некоторые Helm Charts устанавливают CRD:
Их необходимо учитывать отдельно. Проверьте:
Также полезно:
10.2. CRD и манифесты вне Chart
Если документация продукта требует, например:
то в air-gapped среде такая команда работать не будет. Нужно заранее скачать:
и включить файл в transfer package:
То же самое относится к манифестам, поставляемым отдельно:
MutatingWebhookConfiguration
ValidatingWebhookConfiguration
ServiceAccount
ClusterRole
ClusterRoleBinding
ConfigMap
Secret
Namespace
Особенно внимательно проверять файлы:
10.3. Operators, hooks, tests
Для Operator-поставок схема становится сложнее:
Кроме самого Operator image необходимо переносить images управляемого приложения. Например:
Operator без operand-образа не заработает
Если перенести только operator image, а PostgreSQL image оставить на docker.io/postgres:... — air-gapped установка не заработает.
Helm hooks. Обязательно учитывайте:
Такие hooks могут запускать Job, Pod, migration, init process со своими images. Поэтому поиск images необходимо выполнять по полностью отрендеренным manifests, включая hooks.
Helm tests. Chart может иметь templates/tests/:
Если тесты запускаются в закрытом контуре, их images также должны быть доступны.
Проверка результата
Проверка целостности и digests
- Контрольная сумма пакета:
sha256sum -c my-application-1.5.0-airgap.tar.gz.sha256— должен вернутьOK. - Контрольная сумма файлов после распаковки:
sha256sum -c checksums/SHA256SUMS— все файлыOK.
Сравните digest после импорта:
Пример вывода:
Сравните с digest, зафиксированным при загрузке:
Digest отличается — выясните причину
Если digest после импорта отличается от зафиксированного при загрузке — необходимо выяснить причину и не использовать такой образ.
Не полагайтесь только на tag:
Причина — tag может измениться, digest — нет.
Проверка наличия каждого image в private registry
Для каждого image должна возвращаться metadata. Можно автоматизировать:
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
Установка:
helm upgrade --install \
my-release \
./my-chart-1.5.0.tgz \
--namespace my-namespace \
--create-namespace \
-f values-airgap.yaml
Если Chart находится в OCI registry:
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'ы и события:
Особенно обращать внимание на:
Если Pod пытается выйти в Интернет — проверьте, на какой image ссылается Pod:
Смотреть поле 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|quay\.io|ghcr\.io', настройте global.imageRegistry |
sha256sum -c вернул FAILED |
Повреждение носителя или архива при переносе | Пересоздайте пакет, повторите перенос; не распаковывайте повреждённый архив |
exec format error при запуске Pod |
Перенесён только amd64-вариант multi-arch образа | Копируйте с skopeo copy --all, проверьте сохранение manifest list |
| Registry недоступен с узлов кластера | Ошибка DNS, TCP или TLS/CA | getent 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
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. Например:
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:
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 по списку:
#!/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. Например:
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:
#!/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:
#!/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
Рекомендуемая автоматизация для регулярных поставок:
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
Лучше не делать:
потому что через год станет непонятно, откуда это приехало. Лучше:
registry.example.local/docker.io/library/nginx
registry.example.local/docker.io/library/redis
registry.example.local/quay.io/example/exporter
или:
registry.example.local/third-party/docker.io/library/nginx
registry.example.local/third-party/quay.io/example/exporter
Это значительно упрощает аудит.
Пример конечной структуры private registry:
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. Не использовать
latest. - Правило 2. Все версии фиксируются: Chart version, Image tag, Image digest.
- Правило 3. Каждая поставка получает уникальный ID (например,
application-1.5.0). - Правило 4. Для каждого transfer package существует: SHA256, manifest, inventory.
- Правило 5. Нельзя устанавливать Chart, если он содержит неизвестный внешний image.
- Правило 6. Нельзя разрешать Kubernetes nodes выходить в Интернет только для скачивания image.
- Правило 7. Весь набор dependencies должен быть известен до начала импорта.
F. Что часто забывают
При переносе Kubernetes-приложений в air-gapped среду чаще всего забывают:
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
- Helm Chart скачан
- Версия Chart зафиксирована
- Chart.lock присутствует
- Все Helm dependencies скачаны
- Все dependencies имеют фиксированные версии
- Chart успешно упакован
- Chart успешно проходит helm lint
- Chart успешно render'ится
- Все images обнаружены
- InitContainer images обнаружены
- Sidecar images обнаружены
- Hook images обнаружены
- Test images обнаружены
- Operator images обнаружены
- CRD обнаружены
- Дополнительные manifests обнаружены
- Multi-arch images проверены
- Image digest сохранены
- Все images скачаны
- Все OCI archives проверены
- Private registry mapping сформирован
- SHA256 сформированы
- Transfer package сформирован
- SHA256 самого package сформирован
- Package успешно проверен
H. Чек-лист после переноса
В закрытом контуре:
- SHA256 package совпадает
- SHA256 всех файлов совпадает
- Все Charts импортированы
- Все dependencies импортированы
- Все images импортированы
- Image digests совпадают
- Multi-arch manifests сохранены
- CRD установлены
- Private registry доступен с Kubernetes nodes
- TLS работает
- CA доверен
- imagePullSecrets настроены
- helm template не содержит Internet registry
- crictl pull работает
- Helm deployment проходит
- Pods не имеют ImagePullBackOff
- Kubernetes не требует Internet access
I. Подписи и SBOM (рекомендации на будущее)
Рекомендация, а не штатная функция
Подписи и SBOM — это возможности для усиления защищённых контуров «на будущее». На текущий момент это рекомендации по процессу, а не штатная функция платформы «Боцман» или описанного здесь инструментария.
Современная инфраструктура может использовать registry не только для container images и Helm Charts. В registry могут находиться:
В защищённых контурах желательно переносить не только image, но и:
Если используется Cosign/Notation или другая система подписи, подписи должны быть перенесены и проверены уже внутри закрытого контура. При использовании supply-chain security необходимо определить полный перечень OCI artifacts.