
Привет! Меня зовут Георг Гаал. Я CTO в AEnix, и мы разработали платформу cozystack на базе технологий Talos Linux и Kubernetes. Она позволяет легко и просто запустить своё частное или даже публичное облако. У нас уже есть множество клиентов, в том числе и среди хостинговых компаний, и у них регулярно возникает вопрос: «можно ли запустить систему в air-gapped режиме?» Ответ будет универсальным для любого дистрибутива kubernetes. Частности будут в названии образов. Давайте разберёмся как же можно этого добиться, но начнём с определений.
Air gap — это способ размещения информационных систем, когда они отрезаны от интернета. В английском «air gap» означает воздушную прослойку, что отражает суть этого способа изоляции. То есть в режиме air gap режиме наши сервера полностью автономны от внешней сети Интернет и должны быть способны работать в случае отключения или отказа Интернета. Это приводит к дополнительным накладным расходам при настройке серверов, так как уже нельзя напрямую зайти на них по ssh и что-то настроить. А ещё необходимо специальным образом настраивать внутренние репозитории, чтобы была возможность устанавливать пакеты на сервера, обновлять их и производить прочие поддерживающие мероприятия.
Можно сказать, что бывает частичный air gap. Когда сервера имеют доступ в Интернет, но очень ограниченный — через какие-либо прокси, шлюзы или попросту по разрешённому списку доменов или IP-адресов. В этом случае всё равно возникает необходимость дополнительных настроек и организации внутренних репозиториев.
Первоочерёдным элементом для построения своей air gap установки является запуск своего собственного контейнерного репозитория. Наверное, наиболее известным репозиторием является docker distribution. К сожалению, он очень упрощённый, поэтому вряд ли будет хорошим выбором. Хорошим выбором могло быть использование комбайнов вроде Nexus или JFrog Artifactory. Одно или другое решение уже присутствуют во многих технологических компаниях, поэтому есть шанс, что не придётся притаскивать какое-то новое решение в ИТ-ландшафт.
Из доклада «Постигая реестры Docker-контейнеров: от архитектуры до безопасности» с буквально недавно прошедшей в Сколково DevOpsConf 2025 я с интересом узнал, что GitLab внесли очень много изменений в ванильный docker distribution и, по сути, сделали свой форк. Возможно, кому-то он понравится. Лично я же предпочитаю полностью open source решения, и при выборе технологии опираюсь на карту технологического ландшафта от фонда CNCF.
В нём можно найти решение Harbor. Изначально разработанный vmware, сейчас этот репозиторий образов разрабатывается сообществом. Более того, он принят в фонд CNCF и уже достиг уровня зрелости Graduated. Это означает, что это достаточно стабильное и популярное решение, которое будет и в дальнейшем развиваться и на него можно положиться. Именно с ним мы и будем дальше работать.
Развертывание harbor очень простое. Так как мы хотим использовать его для хранения базовых образов, из которых будем запускать kubernetes кластер, то очевидно, что нам нужен отдельный сервер. Хоть Harbor может быть запущен и в kubernetes кластере внутри. Существует превосходный helm чарт, который поможет это сделать. Но есть нюанс. Он заключается в том, что этот кластер нужно каким-то образом поднять в том же окружении. То есть мы упираемся в классическую проблему курицы и яйца. А даже если бы нам удалось каким-то образом поднять kubernetes кластер без harbor в закрытом окружении, потом установить в него harbor, переложить туда образы, то потом потенциально в случае отказа мы могли бы попасть в ситуацию, когда восстановление осложнено. Все-таки kubernetes с хранением данных — это совершенно другой уровень ответственности и другой уровень необходимых знаний для поддержки. Поэтому идем самым простым и логичным путем: берем отдельный сервер.
Далее на этот сервер необходимо установить docker демон и утилиты docker compose. Это не должно быть проблемой. Как я уже сказал, обычно в закрытом окружении уже есть репозитории пакетов операционной системы (иначе как вы настраиваете сервера?), поэтому должно быть легко воспользоваться штатным пакетным менеджером и мы этот вопрос разбирать не будем в настоящей статье.
К нашему счастью, у Harbor уже имеется подробная инструкция по так называемой offline установке, которую можно найти тут.
Ничего сложного тут нет, я проходил этот путь неоднократно. Скачиваем архив (по состоянию на 22 апреля 2025 года актуальный файл harbor-offline-installer-v2.13.0.tgz), переносим любым удобным способом на сервер, распаковываем. При этом получается следующая структура файлов:
root@ubuntu-16gb-hel1-1:~/harbor# pwd /root/harbor root@ubuntu-16gb-hel1-1:~/harbor# tree . ├── LICENSE ├── common.sh ├── harbor.v2.13.0.tar.gz ├── harbor.yml.tmpl ├── install.sh └── prepare 1 directory, 6 files
Следующий этап — подготовка конфигурационного файла для запуска. Все настройки описаны тут. По сути, нам нужно только указать доменное имя, которое вы заранее завели в DNS сервере внутри сети. А ещё хорошая идея — сразу заказать сертификат для этого доменного имени у любого известного удостоверяющего центра (например, Let’s Encrypt или thawte). Если же сгенерировать самоподписанный сертификат, потом будут большие сложности в работе с контейнерным реестром, так как во все потребители образов надо будет этот сертификат добавлять как доверенный. Процедура не очень приятная, проще с этим не связываться.
Измените необходимые конфигурационные значения в файле
harbor.yml.tmpl
и сохраните его как
harbor.yml
Следующий этап запуск скрипта установки install.sh. Скрипт генерирует необходимые конфигурационные файлы и запускает все компоненты хранилища образов Harbor.
root@ubuntu-16gb-hel1-1:~/harbor# ./install.sh [Step 0]: checking if docker is installed ... Note: docker version: 28.1.1 [Step 1]: checking docker-compose is installed ... Note: Docker Compose version v2.35.1 [Step 2]: loading Harbor images ... 874b37071853: Loading layer [==================================================>] 40.92MB/40.92MB 7824f74aeb34: Loading layer [==================================================>] 16.73MB/16.73MB 6409b9df8a24: Loading layer [==================================================>] 175.4MB/175.4MB be0b74c61638: Loading layer [==================================================>] 26.55MB/26.55MB fa43dc6ea88a: Loading layer [==================================================>] 18.8MB/18.8MB 129629e46413: Loading layer [==================================================>] 5.12kB/5.12kB 0e73caa4009a: Loading layer [==================================================>] 6.144kB/6.144kB 489c5f744457: Loading layer [==================================================>] 3.072kB/3.072kB 651edf4d986c: Loading layer [==================================================>] 2.048kB/2.048kB 7188338bd824: Loading layer [==================================================>] 2.56kB/2.56kB 43fa9ff6ed1b: Loading layer [==================================================>] 14.85kB/14.85kB Loaded image: goharbor/harbor-db:v2.13.0 47e634084453: Loading layer [==================================================>] 11.62MB/11.62MB 3a3567e1c917: Loading layer [==================================================>] 3.584kB/3.584kB c6d913de9882: Loading layer [==================================================>] 2.56kB/2.56kB 9bd0e4a44d4e: Loading layer [==================================================>] 61.26MB/61.26MB 25999a94a919: Loading layer [==================================================>] 62.05MB/62.05MB Loaded image: goharbor/harbor-jobservice:v2.13.0 26c6fb84ca6c: Loading layer [==================================================>] 8.665MB/8.665MB 4a3bd93bafd4: Loading layer [==================================================>] 4.096kB/4.096kB 410c3a1ef2e9: Loading layer [==================================================>] 18.22MB/18.22MB 53b60af6945a: Loading layer [==================================================>] 3.072kB/3.072kB 4818b20f0cba: Loading layer [==================================================>] 37.94MB/37.94MB e4df98227fc6: Loading layer [==================================================>] 56.95MB/56.95MB Loaded image: goharbor/harbor-registryctl:v2.13.0 9b35d04891ad: Loading layer [==================================================>] 16.73MB/16.73MB 1c869aa72f2e: Loading layer [==================================================>] 110.6MB/110.6MB 3d97927b9035: Loading layer [==================================================>] 3.072kB/3.072kB fdb6e6094f6f: Loading layer [==================================================>] 59.9kB/59.9kB 2eab092d6e79: Loading layer [==================================================>] 61.95kB/61.95kB Loaded image: goharbor/redis-photon:v2.13.0 a2ce174bdb10: Loading layer [==================================================>] 9.158MB/9.158MB 4adc2eaaef28: Loading layer [==================================================>] 4.096kB/4.096kB 5cf35bad98bc: Loading layer [==================================================>] 3.072kB/3.072kB 6aa111c0e634: Loading layer [==================================================>] 150.2MB/150.2MB 9b35ed6f2a08: Loading layer [==================================================>] 15.56MB/15.56MB 32034b7661fe: Loading layer [==================================================>] 166.6MB/166.6MB Loaded image: goharbor/trivy-adapter-photon:v2.13.0 db88ef857466: Loading layer [==================================================>] 112.5MB/112.5MB Loaded image: goharbor/nginx-photon:v2.13.0 6e268cafc739: Loading layer [==================================================>] 8.665MB/8.665MB a2e97a249fd0: Loading layer [==================================================>] 4.096kB/4.096kB 0901ab91bc55: Loading layer [==================================================>] 3.072kB/3.072kB 50f38dcb8483: Loading layer [==================================================>] 18.22MB/18.22MB ee1edf422bf9: Loading layer [==================================================>] 19.01MB/19.01MB Loaded image: goharbor/registry-photon:v2.13.0 72fbfac481fb: Loading layer [==================================================>] 103.4MB/103.4MB 3fe27a92af0d: Loading layer [==================================================>] 48.34MB/48.34MB 64517d360781: Loading layer [==================================================>] 14.22MB/14.22MB f3f3183409fd: Loading layer [==================================================>] 66.05kB/66.05kB c08b9ad8fb5f: Loading layer [==================================================>] 2.56kB/2.56kB eb80e088a551: Loading layer [==================================================>] 1.536kB/1.536kB 7baa5e1526a1: Loading layer [==================================================>] 12.29kB/12.29kB 8ee5ec881429: Loading layer [==================================================>] 3.412MB/3.412MB 1a7a322af2a1: Loading layer [==================================================>] 561.7kB/561.7kB Loaded image: goharbor/prepare:v2.13.0 f07eaa0d2fe4: Loading layer [==================================================>] 112.5MB/112.5MB d6cc50e9d17d: Loading layer [==================================================>] 6.835MB/6.835MB 56cce4852462: Loading layer [==================================================>] 252.9kB/252.9kB 9a0681ff3216: Loading layer [==================================================>] 1.539MB/1.539MB Loaded image: goharbor/harbor-portal:v2.13.0 73a766bfd622: Loading layer [==================================================>] 11.62MB/11.62MB 60054c673e23: Loading layer [==================================================>] 3.584kB/3.584kB 50a9f5d3c46a: Loading layer [==================================================>] 2.56kB/2.56kB 3be02743af9a: Loading layer [==================================================>] 72.79MB/72.79MB 3d523d6680c1: Loading layer [==================================================>] 5.632kB/5.632kB d0a9846c2224: Loading layer [==================================================>]128kB/128kB a1a1bb15583a: Loading layer [==================================================>] 209.9kB/209.9kB 705224c9382c: Loading layer [==================================================>] 73.92MB/73.92MB a9ae194dbd47: Loading layer [==================================================>] 2.56kB/2.56kB Loaded image: goharbor/harbor-core:v2.13.0 2f66f2193f14: Loading layer [==================================================>] 125.3MB/125.3MB 8f72c6b5c033: Loading layer [==================================================>] 3.584kB/3.584kB 075f0064255b: Loading layer [==================================================>] 3.072kB/3.072kB 69682d7bfa3e: Loading layer [==================================================>] 2.56kB/2.56kB a3f75b6ab7bb: Loading layer [==================================================>] 3.072kB/3.072kB ace7e62314e6: Loading layer [==================================================>] 3.584kB/3.584kB c0ffef127b98: Loading layer [==================================================>] 20.48kB/20.48kB Loaded image: goharbor/harbor-log:v2.13.0 0f653918fb04: Loading layer [==================================================>] 11.62MB/11.62MB 448770b89f7c: Loading layer [==================================================>] 38.16MB/38.16MB 149d5517f77b: Loading layer [==================================================>] 4.608kB/4.608kB 832349ff3d50: Loading layer [==================================================>] 38.95MB/38.95MB Loaded image: goharbor/harbor-exporter:v2.13.0 [Step 3]: preparing environment ... [Step 4]: preparing harbor configs ... prepare base dir is set to /root/harbor Generated configuration file: /config/portal/nginx.conf Generated configuration file: /config/log/logrotate.conf Generated configuration file: /config/log/rsyslog_docker.conf Generated configuration file: /config/nginx/nginx.conf Generated configuration file: /config/core/env Generated configuration file: /config/core/app.conf Generated configuration file: /config/registry/config.yml Generated configuration file: /config/registryctl/env Generated configuration file: /config/registryctl/config.yml Generated configuration file: /config/db/env Generated configuration file: /config/jobservice/env Generated configuration file: /config/jobservice/config.yml copy /data/secret/tls/harbor_internal_ca.crt to shared trust ca dir as name harbor_internal_ca.crt ... ca file /hostfs/data/secret/tls/harbor_internal_ca.crt is not exist copy to shared trust ca dir as name storage_ca_bundle.crt ... copy None to shared trust ca dir as name redis_tls_ca.crt ... Generated and saved secret to file: /data/secret/keys/secretkey Successfully called func: create_root_cert Generated configuration file: /compose_location/docker-compose.yml Clean up the input dir Note: stopping existing Harbor instance ... [Step 5]: starting Harbor ... [+] Running 10/10 ✔ Network harbor_harbor Created 0.1s ✔ Container harbor-log Started 0.4s ✔ Container harbor-db Started 0.5s ✔ Container harbor-portal Started 0.6s ✔ Container redis Started 0.6s ✔ Container registryctl Started 0.6s ✔ Container registry Started 0.6s ✔ Container harbor-core Started 0.7s ✔ Container harbor-jobservice Started 0.8s ✔ Container nginx Started 0.8s ✔ ----Harbor has been installed and started successfully.----
После установки можно будет пройти по доменному имени, которое вы привязали к серверу, и залогиниться в Harbor.
Поздравляем! На этом моменте у вас есть полностью рабочее хранилище образов.
Хорошей идеей будет изменить пароль по умолчанию для административного пользователя и завести отдельных пользователей для каждой из задач.
Harbor внутри имеет множество репозиториев, каждый из которых называется «проектом». Создадим новый проект и назовем его air-gap.
Теперь нужно его наполнить образами.
Первое действие для этого — получить список необходимых образов. Для дистрибутивов на базе kubeadm это может быть сделано следующей командой:
$ kubeadm config images list registry.k8s.io/kube-apiserver:v1.31.7 registry.k8s.io/kube-controller-manager:v1.31.7 registry.k8s.io/kube-scheduler:v1.31.7 registry.k8s.io/kube-proxy:v1.31.7 registry.k8s.io/coredns/coredns:v1.11.3 registry.k8s.io/pause:3.10 registry.k8s.io/etcd:3.5.15-0
Локально скачиваем все эти образа по одному.
docker pull registry.k8s.io/kube-apiserver:v1.31.7 docker pull registry.k8s.io/kube-controller-manager:v1.31.7 docker pull registry.k8s.io/kube-scheduler:v1.31.7 docker pull registry.k8s.io/kube-proxy:v1.31.7 docker pull registry.k8s.io/coredns/coredns:v1.11.3 docker pull registry.k8s.io/pause:3.10 docker pull registry.k8s.io/etcd:3.5.15-0
Перетегировать
docker tag registry.k8s.io/kube-apiserver:v1.31.7 registry.gecube.eu/air-gap/kube-apiserver:v1.31.7 docker tag registry.k8s.io/kube-controller-manager:v1.31.7 registry.gecube.eu/air-gap/kube-controller-manager:v1.31.7 docker tag registry.k8s.io/kube-scheduler:v1.31.7 registry.gecube.eu/air-gap/kube-scheduler:v1.31.7 docker tag registry.k8s.io/kube-proxy:v1.31.7 registry.gecube.eu/air-gap/kube-proxy:v1.31.7 docker tag registry.k8s.io/coredns/coredns:v1.11.3 registry.gecube.eu/air-gap/coredns:v1.11.3 docker tag registry.k8s.io/pause:3.10 registry.gecube.eu/air-gap/pause:3.10 docker tag registry.k8s.io/etcd:3.5.15-0 registry.gecube.eu/air-gap/etcd:3.5.15-0
Не забудьте перед тем, как загрузить образа в хранилище аутентифицироваться в нем при помощи команды
docker login registry.gecube.eu
Запушить в хранилище
docker push registry.gecube.eu/air-gap/kube-apiserver:v1.31.7 docker push registry.gecube.eu/air-gap/kube-controller-manager:v1.31.7 docker push registry.gecube.eu/air-gap/kube-scheduler:v1.31.7 docker push registry.gecube.eu/air-gap/kube-proxy:v1.31.7 docker push registry.gecube.eu/air-gap/coredns:v1.11.3 docker push registry.gecube.eu/air-gap/pause:3.10 docker push registry.gecube.eu/air-gap/etcd:3.5.15-0
Так как скачивание происходит из интернета, то удобно это делать на ноутбуке администратора. Который или подключён к двум сетям одновременно (публичной и внутренней). Или произвести переключение — переключиться на публичную сеть, скачать образа, потом переключиться во внутреннюю и уже загрузить образа в хранилище.
Дальше при инициализации кластера необходимо передать правильные имена образов. Для kubeadm установки это делается в двух местах:
-
в настройках containerd демона необходимо указать наш pause образ:
root@ubuntu-16gb-hel1-1:~# cat /etc/containerd/config.toml ... [plugins] [plugins."io.containerd.gc.v1.scheduler"] deletion_threshold = 0 mutation_threshold = 100 pause_threshold = 0.02 schedule_delay = "0s" startup_delay = "100ms" [plugins."io.containerd.grpc.v1.cri"] device_ownership_from_security_context = false disable_apparmor = false disable_cgroup = false disable_hugetlb_controller = true disable_proc_mount = false disable_tcp_service = true enable_selinux = false enable_tls_streaming = false enable_unprivileged_icmp = false enable_unprivileged_ports = false endpoint = "unix:///var/run/containerd/containerd.sock" ignore_image_defined_volumes = false max_concurrent_downloads = 3 max_container_log_line_size = 16384 netns_mounts_under_state_dir = false restrict_oom_score_adj = false sandbox_image = "registry.gecube.eu/air-gap/pause:3.10" selinux_category_range = 1024 stats_collect_period = 10 stream_idle_timeout = "4h0m0s" stream_server_address = "127.0.0.1" stream_server_port = "0" systemd_cgroup = false tolerate_missing_hugetlb_controller = true unset_seccomp_profile = "" ...
Нас интересует ключ sandbox_image в конфигурационном файле /etc/containerd/config.toml. После изменения файла необходимо не забыть перезапустить демон containerd командой systemctl restart containerd (или аналогичной)
-
в файле конфигурации kubeadm в секции ClusterConfiguration необходимо указать параметр ImageRepository
root@ubuntu-16gb-hel1-1:~# cat kubeadm-config.yaml kind: ClusterConfiguration apiVersion: kubeadm.k8s.io/v1beta3 kubernetesVersion: v1.28.2 networking: serviceSubnet: 100.65.0.0/16 podSubnet: 100.64.0.0/16 apiServer: certSANs: - "127.0.0.1" - "localhost" - "10.0.0.17" - "212.233.76.0" imageRepository: "registry.gecube.eu" --- kind: KubeletConfiguration apiVersion: kubelet.config.k8s.io/v1beta1 cgroupDriver: systemd
Далее при разворачивании кластера можно применить этот конфигурационный файл через ключ —config. В остальном установка точно такая же, как и не в air-gap среде.
kubeadm init --config kubeadm-config.yaml --upload-certs -v=5 --skip-phases=addon
Поздравляем! Теперь вы видите насколько просто можно произвести air gapped установку. Дальнейшие действия могут быть установкой CNI (так же с зеркалированием образов в наш репозиторий), установка дополнительных компонентов кластера таких как мониторинг, логирование, ingress контроллеры и прочее прочее.
Дополнительно обращаю ваше внимание, что своё хранилище образов это всегда хорошо, так как:
-
это позволяет не зависеть от внешних зависимостей вроде докерхаба. Уже были истории с блокировками Докерхаба в РФ.
-
докерхаб ввел лимиты на скачивание образов — 100 образов за 6 часов. Это ещё раз доказывает, что ставить работоспособность своей системы (в частности, такой динамичной системы как любой кластер k8s) в зависимость от внешних ресурсов — очень плохая идея.
-
можно сэкономить много трафика, так как каждый перезапуск пода фактически может приводить к перекачиванию образа, а образы иногда бывают достаточно большими (например, если речь про ML — скажем, 5ГБ)
-
это повышает безопасность, так как есть возможность запуска только известных образов
-
нет риска того, что образ был подменён (умышленно или случайно) в исходном хранилище (например, на докерхабе — такие прецеденты уже бывали)
Kubernetes Мега — продвинутый курс по k8s от учебного центра Слёрм. За 7 недель обучим глубокими практическим навыкам управления и масштабирования контейнерных приложений с использованием Kubernetes в производственной среде. Для тех, кто уже работал с k8s или прошел Kubernetes База
ссылка на оригинал статьи https://habr.com/ru/articles/903186/
Добавить комментарий