Тема
Перенос 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-кластере.
- Регулярное обновление ПО в контуре, изолированном на физическом или сетевом уровне.
Краткий путь
- Подготовьте внешнюю машину с доступом в Интернет (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.
Проверьте установку:
bash
helm version
docker version
skopeo --version
jq --version
yq --version
git --version1.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.23.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.com3.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 digest3.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
hooks4.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:xxxxxxxxTag для человека, 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.json4.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.txtbash
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.0text
# images.txt
docker.io/library/nginx:1.29.1
docker.io/library/redis:7.4.1
quay.io/example/exporter:2.1.0text
# 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: OK7.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.sha2567.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.localbash
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.local9.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 CAKubernetes 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.tgz10.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.yaml10.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 imageOperator без 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-chartbash
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-namespacebash
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/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
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.shB. 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 PackageD. Рекомендуемая политика 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
└── dependenciesE. Политики для 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 среду чаще всего забывают:
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
provenancetext
image
├── manifest
├── layers
├── signature
└── SBOMЕсли используется Cosign/Notation или другая система подписи, подписи должны быть перенесены и проверены уже внутри закрытого контура. При использовании supply-chain security необходимо определить полный перечень OCI artifacts.