|
|
|
Professor Seleznov
|
В прошлой статье я рассказывал, как безопасники отключили интернет на учебном стенде, а мне нужно было провести курс для ста преподавателей. Тогда я выкрутился с помощью офлайн-образа виртуальной машины, где все необходимое уже внутри. Рассказываю, как сделать такой образ. Оговорка: в боевом образе для студентов были еще Kubernetes и Helm — полный набор для прохождения курса. Здесь я покажу фундамент: как собрать автономную DevOps-среду с Docker на борту. Масштаб немного другой, но принцип одинаковый. Если вы поймете, как запечатать Docker, то K8s и Helm — вопрос количества артефактов, а не подхода.
 Идея: образ-консерва Задача звучит просто: взять чистый образ Linux, засунуть туда Docker, контейнерные образы, скрипты настройки — и сделать так, чтобы при первом запуске все заработало без единого обращения в сеть. Как консервная банка: открыл — и готово. На практике есть три проблемы. Первая — Docker при установке из репозитория хочет в интернет. Вторая — при запуске контейнеров Docker тянет образы из Docker Hub. Третья, неочевидная — даже в офлайне Docker создает сетевые мосты, и если не настроить маршрутизацию правильно, контейнеры просто не запустятся. Часть 1. Собираем рабочее место Нам нужна виртуальная машина — билдер — место, где мы соберем образ. Я использовал Proxmox, но подойдет любой гипервизор. Главное — билдер должен иметь доступ в интернет (на этапе сборки он нам нужен), а вот финальный образ будет работать без него. Создаем ВМ
$ qm create 100 --name image-builder \ --net0 virtio,bridge=vmbr0 \ --memory 4096 \ --cores 4 \ --ostype l26 \ --cpu host \ --onboot no
Скачиваем основу Берем облачный образ Debian 12 — легкий, чистый, без лишнего: $ wget https://cloud.debian.org/images/cloud/bookworm/late...eric-amd64.qcow2
 Облачный образ весит около 400 МБ, а диск в нем — пару гигабайт. Для нашей сборки этого мало, расширяем до 20 ГБ: $ qemu-img resize debian-12-generic-amd64.qcow2 20G Подключаем диск к ВМ Импортируем скачанный образ как диск виртуальной машины. Название хранилища у вас может отличаться — у меня local-zfs, у вас может быть local-lvm или что-то еще: $ qm importdisk 100 debian-12-generic-amd64.qcow2 local-zfs Диск импортирован, но еще не подключен к ВМ. Привязываем его к контроллеру и делаем загрузочным: $ qm set 100 --scsihw virtio-scsi-pci --scsi0 local-zfs:vm-100-disk-0 $ qm set 100 --boot c --bootdisk scsi0 Добавляем виртуальный CD-привод для cloud-init — через него зададим пароль и сеть: $ qm set 100 --ide2 local-zfs:cloudinit
 Настраиваем доступ Облачные образы по умолчанию не имеют пароля — подразумевается вход только по ключам. Для работы в консоли зададим пароль и включим DHCP: $ qm set 100 --cipassword superpass --ciuser root $ qm set 100 --ipconfig0 ip=dhcp Запускаем и подключаемся $ qm start 100 $ qm status 100
 Подождем, пока ВМ загрузится и сконфигурирует заданный нами пароль. Спустя минуту переходим в веб-интерфейс Proxmox, выбираем нашу ВМ и вкладку «Console» (логин root, пароль — тот, что задали выше):
 Если в веб-интерфейсе Proxmox работать неудобно, настроим SSH. Редактируем /etc/ssh/sshd_config: PermitRootLogin yes PasswordAuthentication yes
 Перезапускаем SSH и узнаем IP-адрес, который получила наша ВМ: $ systemctl restart ssh $ ip addr show eth0
 С рабочей машины копируем публичный ключ: $ ssh-copy-id root@192.168.30.12
 Теперь в конфиге виртуальной машины можно отключить парольную аутентификацию:
 Проверяем, что подключение по ключу работает: $ ssh root@192.168.30.12
 Удобство обеспечено, можно приступать к сборке. Ставим инструменты Делаем все одной командой: $ apt update && apt install -y qemu-utils libguestfs-tools docker.io zstd curl tar tcpdump iptables nfs-common wget
 Здесь два ключевых пакета: libguestfs‑tools (позволит модифицировать образ без запуска ВМ) и docker.io (понадобится, чтобы скачать и сохранить контейнерные образы). (Опционально) NFS для тяжелых файлов Если не хотите забивать локальный диск — подключите сетевую шару (в нашем примере это nas). Работать с тяжелыми образами по внутренней сети быстрее. Создадим папĸу монтирования: $ mkdir -p /mnt/nas Подĸлючим сетевую папĸу (здесь я использую ip моего nas из другой сети): $ mount -t nfs 192.168.40.10:/volume1/rapax /mnt/nas Перейдем в рабочую диреĸторию проеĸта: $ mkdir -p /mnt/nas/offline-image && cd /mnt/nas/offline-image
 Скачиваем артефакты Теперь самое важное — собираем все, что понадобится в офлайне. Это три вещи: чистый образ ОС (болванка для кастомизации), deb-пакеты Docker (чтобы установить его без интернета) и контейнерный образ для проверки. Болванка: $ wget https://cloud.debian.org/images/cloud/bookworm/late...eric-amd64.qcow2 -O offline_base.qcow2
 Пакеты Docker: $ mkdir -p ./docker_packages && cd ./docker_packages $ apt-get download docker.io containerd runc
 Команда apt-get download скачивает deb-файлы, не устанавливая их. Именно эти файлы мы потом инжектируем в образ. Подготовим тестовый образ nginx в виде архива: $ docker pull nginx:latest $ docker save nginx:latest -o nginx_offline.tar
 docker save — ключевая команда. Она сохраняет образ со всеми слоями в tar-архив. Потом, в офлайне, docker load прочитает этот архив и восстановит образ без обращения к Docker Hub. К этому моменту у нас есть три артефакта:
- offline_base.qcow2 (основа для кастомизации),
- docker_packages/ (deb-пакеты),
- nginx_offline.tar (контейнерный образ для финального теста).
 Часть 2. Кастомизация образа Здесь начинается самое интересное. Нужно взять чистую болванку Debian и, не запуская ее, засунуть внутрь Docker, контейнерные образы и скрипт, который все настроит при первом запуске. Расширяем диск Базовый образ слишком мал для Docker-контейнеров. Добавляем 15 ГБ: $ qemu-img resize offline_base.qcow2 +15G
 Скрипт автоматизации Дальше нам нужен скрипт, который virt-customize — утилиту из пакета libguestfs-tools — выполнит внутри образа. Скрипт установит Docker из локальных пакетов и создаст systemd-сервис для первого запуска:
$ nano offline_setup.sh #!/bin/bash # Установка Docker из локальных пакетов dpkg -i /opt/offline/docker_packages/*.deb # Включаем Docker-демон systemctl enable docker # Создаем скрипт, который отработает один раз при первом запуске cat <<'BOOT_EOF' > /usr/local/bin/first_boot_logic.sh #!/bin/bash # Включаем пересылку трафика в ядре. Без этого Docker не сможет маршрутизировать пакеты между контейнерами, даже в офлайне sysctl -w net.ipv4.ip_forward=1 # Отключаем Spanning Tree Protocol на мосте docker0. В изолированной среде STP не нужен, а его включение может задерживать поднятие сетевого моста на 30 секунд ip link set docker0 type bridge stp_state 0 2>/dev/null || true # Настройка файрвола: iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT ACCEPT iptables -A INPUT -i lo -j ACCEPT iptables -A INPUT -i docker0 -j ACCEPT iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A FORWARD -i docker0 -o docker0 -j ACCEPT iptables -A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # Запечатываем контур. Блокируем любые новые исходящие соединения во внешний мир iptables -A OUTPUT -o ens18 -m conntrack --ctstate NEW -j DROP # Загружаем офлайн-образ nginx sleep 5 docker load -i /opt/offline/nginx_offline.tar # Сервис отработал — убираем systemctl disable first-boot.service BOOT_EOF chmod +x /usr/local/bin/first_boot_logic.sh # Создание Unit-файла для Systemd cat <<'UNIT_EOF' > /etc/systemd/system/first-boot.service [Unit] Description=Offline Environment Initialization After=docker.service [Service] Type=oneshot ExecStart=/usr/local/bin/first_boot_logic.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target UNIT_EOF systemctl enable first-boot.service
Разберу логику файрвола отдельно: мы разрешаем Docker-контейнерам работать внутри виртуальной машины (мост docker0), но при этом запрещаем любые исходящие соединения через физический интерфейс ens18. Получается запечатанный контур: контейнеры живут и общаются друг с другом, но во внешний мир ни один пакет не уйдет. Инжектируем все в образ Тут начинает работать virt-customize: утилита монтирует образ диска, закидывает внутрь файлы, выполняет скрипты и отмонтирует обратно. Все это без запуска ВМ — чистая работа с файловой системой:
$ virt-customize -a offline_base.qcow2 \ --mkdir /opt/offline \ --copy-in ./docker_packages:/opt/offline/ \ --upload ./nginx_offline.tar:/opt/offline/ \ --upload ./offline_setup.sh:/usr/local/bin/offline_setup.sh \ --run-command "bash /usr/local/bin/offline_setup.sh" \ --root-password password:student \ --hostname offline-devops-box
Внутри команды шесть действий: создать директорию, скопировать пакеты Docker, закинуть tar-архив с образом nginx, загрузить скрипт настройки, выполнить его и задать пароль. На выходе — образ, готовый к офлайн-работе.
 Оптимизируем размер Образ после кастомизации содержит пустые блоки от удаленных временных файлов. virt-sparsify убирает «воздух» и сжимает результат: $ virt-sparsify --compress offline_base.qcow2 offline_base_optimized.qcow2
 Проверяем содержимое Самое время проверить, что образ внутри не содержит непредвиденных ошибок — особенно если вы будете передавать его другим людям (студентам, администраторам, техподдержке). Убедимся, что скрипт для службы первого запуска добавлен без ошибок: $ virt-cat -a offline_base_optimized.qcow2 /usr/local/bin/first_boot_logic.sh
 Ну, и напоследок — проверим, на месте ли подготовленные Docker-образы (директорию мы указывали в скрипте для службы первого запуска): $ virt-ls -a offline_base_optimized.qcow2 /opt/offline/
 Архивируем для передачи Сжимаем образ для отправки. Алгоритм Zstandard на максимальных настройках выжимает из qcow2 все возможное: $ zstd -19 --long -T0 offline_base_optimized.qcow2 -o offline_base_optimized.qcow2.zst Флаг -T0 задействует все ядра процессора, --long включает расширенное окно поиска повторов, а -19 — почти максимальный уровень сжатия. Получается файл, который удобно передать техподдержке или залить на стенд.
 Часть 3. Тестируем в офлайне Образ собран, но работает ли он? Создаем проверочную виртуальную машину, которая имитирует среду, например студента. Создаем изолированную ВМ
$ qm create 101 --name offline-student-pc \ --memory 2048 \ --cores 2 \ --cpu host \ --net0 virtio,bridge=vmbr0 \ --bios ovmf
Импортируем собранный образ: $ qm importdisk 101 /mnt/pve/rapax/offline-image/offline_base_optimized.qcow2 local-zfs
 Привязываем диск и настраиваем загрузку: $ qm set 101 --scsihw virtio-scsi-pci --scsi0 local-zfs:vm-101-disk-0 $ qm set 101 --boot c --bootdisk scsi0
 Отключаем сеть на уровне гипервизора В скрипте первого запуска мы уже запретили исходящие соединения через iptables. Но для чистоты эксперимента отключим сетевой интерфейс еще и на уровне Proxmox — так, будто сетевой кабель вынут: Datacenter → VM 101 → Hardware → Network Device → Edit → Disconnected ✓
 Двойная изоляция: программная (iptables) и аппаратная (отключенный интерфейс). Даже если в скрипте что-то пойдет не так, ни один пакет не уйдет наружу. Запускаем и проверяем Запускаем виртуальную машину, ждем минуту-две (скрипт первого запуска должен отработать) и логинимся: root / student.
 Проверяем интерфейсы: $ ip address show
 Должны увидеть loopback, физический интерфейс (без IP, потому что отключен) и мост docker0. Проверяем, что сеть действительно молчит: $ tcpdump -i any
 Запускаем nginx из предзагруженного образа: $ docker run -d -p 80:80 nginx
 Обратите внимание на сетевые политики: трафик ограничен только внутренней работой Docker-сети. Проверяем: $ curl localhost
 Если в ответ пришла стандартная страница nginx — образ работает. Docker поднял контейнер из локального tar-архива, создал сеть, прокинул порт — и все это без выхода в интернет. Для полноты картины можно попробовать достучаться наружу — убедиться, что не получится:
 При запущенных контейнерах поднимаются только сетевые интерфейсы, обслуживающие Docker-сеть:
 При остановке контейнеров контейнерные сети переходят в состояние DOWN:
 Образ ведет себя ровно так, как задумано. Что дальше Этот гайд показывает базовый сценарий — один контейнерный образ, один инструмент. Боевой образ для нашего курса устроен сложнее: там Kubernetes (K3s), Helm-чарты, предзагруженные системные образы для кластера, дополнительные утилиты. Но фундамент — тот же самый:
- Скачали все что нужно;
- Инжектировали в образ через virt-customize;
- Настроили скрипт первого запуска, который соберет все воедино;
- Запечатали сетевой контур;
- Проверили, что ничего не просится наружу.
Если вы готовите учебные стенды, лабораторные среды или демо-образы для закрытых контуров — не надейтесь на интернет, запечатайте зависимости заранее. Потратите пару часов на подготовку, но избежите ситуации, когда сто человек сидят перед черным экраном, потому что docker pull уходит в таймаут.-Источник
|
|
|
|