Нередко на технических собеседованиях на позицию DevOps-инженера задают каверзный вопрос: «В чем отличие контейнера от виртуальной машины?». Большинство кандидатов теряются при ответе на этот вопрос, а некоторые интервьюеры и сами до конца не понимают, какой ответ они хотят услышать от собеседуемого. Чтобы наглядно понять различия и никогда не возвращаться к этому вопросу, я покажу, как превратить контейнер в виртуальную машину и запустить ее в облаке Timeweb Cloud.
Процесс, описанный в статье, поможет лучше понять ключевые различия между контейнерами и виртуальными машинами, а также продемонстрирует практическое применение каждого из этих подходов. Статья будет особенно полезна для тех, кто работает с системами, требующими специфического окружения.
Все дальнейшие действия я буду выполнять в окружении ОС Linux. Для подготовки необходимого образа я буду использовать виртуальную машину на базе гипервизора KVM, созданную с помощью VirtualBox. Вы также можете использовать любые другие провайдеры, например VMware, QEMU или virt-manager.
Предлагаю начать этот увлекательный путь с создания контейнера. Для этого мы воспользуемся Docker. Если он у вас всё ещё не установлен, установите его с помощью команды, приведенной ниже (перед этим может потребоваться обновить список доступных пакетов — sudo apt update
).
sudo apt install docker.io -y
Создайте контейнер на основе минимального образа alpine и прикрепитесь к его командной оболочке
sudo docker run --name test -it alpine sh
Установите с помощью пакетного менеджера apk
необходимые программы, которые вы планируете использовать в будущей виртуальной машине. Вам не обязательно ограничиваться пакетами из стандартного репозитория Alpine — вы также можете добавить другие репозитории, а при необходимости скачать или собрать пакеты прямо в контейнере.
apk add tmux busybox-extras openssh-client openssh-server iptables dhclient ppp socat tcpdump vim openrc mkinitfs grub grub-bios
Вот список минимально необходимых пакетов:
tmux
— консольный мультиплексор. Он пригодится для сохранения пользовательской сессии и контекста выполняемых процессов в случае разрыва сетевого соединения.
busybox-extras
— расширенная версия BusyBox, которая включает в себя дополнительные утилиты, но по-прежнему остается компактным дистрибутивом стандартных инструментов.
openssh-client
и openssh-server
— клиент и сервер OpenSSH, необходимые для организации удаленных подключений.
iptables
— утилита для настройки правил фильтрации IP-пакетов.
dhclient
— DHCP-клиент, автоматизирующий конфигурацию сетевых настроек.
ppp
— пакет для реализации канального протокола Point-to-Point.
socat
— программа для создания туннелей, аналогичная netcat
, с поддержкой шифрования и функционалом интерактивной командной оболочки.
tcpdump
— утилита для захвата трафика, может пригодиться для отладки сетевых неисправностей.
vim
— консольный текстовый редактор с богатыми возможностями кастомизации. Популярен среди опытных пользователей ОС Linux.
openrc
— система инициализации, основанная на управлении зависимостями, работающая в связке с SysVinit. Один из ключевых компонентов, необходимых для превращения контейнера в виртуальную машину, поскольку контейнеры не имеют ее по умолчанию.
mkinitfs
— пакет для генерации initramfs, позволяющий собрать необходимые драйверы и модули, загружаемые на начальном этапе инициализации системы.
grub
и grub-bios
— загрузчик ОС. В данном случае нас интересует именно создание загрузчика для систем с BIOS, использующих таблицу разделов MBR.
Задайте пароль суперпользователю:
export PASSWORD=<ваш секретный пароль>
echo "root:$PASSWORD" | chpasswd
Создайте пользователя. В дальнейшем он понадобится для удаленного подключения по SSH:
export USERNAME=<имя пользователя>
adduser -s /bin/sh $USERNAME
Установите SUID-бит на исполняемый файл busybox
. Это нужно для того, чтобы пользователь мог запускать команды с правами суперпользователя:
chmod u+s /bin/busybox
Создайте скрипт, который будет выполняться при инициализации системы
cat <<EOF > /etc/local.d/init.start
#!/bin/sh
dmesg -n 1
mount -o remount,rw /
ifconfig lo 127.0.0.1 netmask 255.0.0.0
dhclient eth0
# ifconfig eth0 172.16.0.200 netmask 255.255.255.0
# route add -net default gw 172.16.0.1
busybox-extras telnetd
EOF
Рассмотрим скрипт построчно:
dmesg -n 1
— выводит критические сообщения из буфера ядра Linux, чтобы при запуске можно было обнаружить возможные неполадки.
mount -o remount,rw /
— перемонтирует корневую ФС (/
) с параметром rw
(чтение и запись). Эта манипуляция позволит изменять ФС после загрузки.
ifconfig lo 127.0.0.1 netmask 255.0.0.0
— настраивает интерфейс loopback
(lo
) с IP-адресом 127.0.0.1
и маской подсети 255.0.0.0
. Это необходимо для обеспечения внутренней сетевой связи на машине.
dhclient eth0
— запускает DHCP-клиент для интерфейса eth0
, чтобы автоматически получить настройки IP-адреса и другие параметры сети от DHCP-сервера.
# ifconfig eth0 172.16.0.200 netmask 255.255.255.0
— эта строка закомментирована, но при раскомментировании задаст статический IP-адрес 172.16.0.200
и маску подсети 255.255.255.0
для интерфейса eth0
. Я добавил эту строку в скрипт на случай, если потребуется статическая конфигурация сети.
# route add -net default gw 172.16.0.1
— эта строка тоже закомментирована, но при раскомментировании добавит маршрут по умолчанию с шлюзом 172.16.0.1
. Это определяет, как пакеты будут маршрутизироваться в сеть за пределами локальной сети.
busybox-extras telnetd
— запускает Telnet-сервер. Обратите внимание, что использование протокола Telnet в продуктивных средах не рекомендуется из-за отсутствия шифрования при передаче данных.
Сделайте скрипт исполняемым:
chmod +x /etc/local.d/init.start
Добавьте скрипт автоматической настройки в автозапуск:
rc-update add local
Добавьте демон OpenSSH-сервера в автозапуск. Это понадобится для того, чтобы в дальнейшем подключаться к облачному серверу по SSH:
rc-update add sshd default
Установите DNS-сервер по умолчанию:
echo nameserver 8.8.8.8 > /etc/resolv.conf
Открепитесь от терминала с помощью команды exit
или комбинации клавиш CRTL+D. На следующем этапе нам нужно сохранить файловую систему контейнера на хосте в виде архива, что также можно сделать с помощью Docker. В моем случае конечный артефакт занимает всего 75 мегабайт.
sudo docker export test > test.tar
cloud
Контейнеры представляют собой Linux-специфичную технологию, поскольку они не имеют своего ядра и используют абстракции хостового ядра Linux для обеспечения изоляции и управления ресурсами. К основным абстракциям относятся:
namespaces — изоляция относительно пространств имен: USER, TIME, PID, NET, MOUNT, UTS, IPC, CGROUP.
cgroups — ограничение относительно ресурсов: CPU, RAM, I/O.
capabilities — набор возможностей для выполнения определенных, в том числе привилегированных, операций без прав суперпользователя.
Эти компоненты ядра Linux делают Docker и другие контейнерные технологии тесно связанными с Linux, что делает невозможной их нативную работу на других операционных системах, таких как Windows, macOS или BSD*.
Для работы на Windows, macOS и BSD* существует Docker Desktop, который представляет собой виртуальную машину с минимальной операционной системой на основе ядра Linux. Внутри этой виртуальной машины установлен и запущен Docker Engine, который позволяет пользователям управлять контейнерами и образами Docker в привычной среде.
Поскольку нам нужна полноценная операционная система, а не просто контейнер, то потребуется собственное ядро.
Создайте файл-образ, с которым мы будем работать дальше:
truncate -s 200M test.img
С помощью fdisk
создайте раздел на образе test.img
.
echo -e "n\np\n1\n\n\nw" | fdisk test.img
n
— создание нового разделаp
— указание того, что это будет основной (primary) раздел1
— номер нового раздела\n\n
— использование значений по умолчанию для начального и конечного секторовw
— запись измененийТеперь свяжите файл-образ test.img
с устройством /dev/loop3
, начиная со смещения 2048 блоков по 512 байт каждый (1 МБ):
sudo losetup -o $[2048*512] /dev/loop3 test.img
Обратите внимание, что устройство /dev/loop3
может быть уже задействовано — часто первые несколько loop
-интерфейсов уже используются. Вы можете проверить занятые устройства с помощью следующей команды:
losetup -l
Отформатируйте раздел, связанный с /dev/loop3
, в файловую систему EXT4:
sudo mkfs.ext4 /dev/loop3
Смонтируйте /dev/loop3
в директорию /mnt
:
sudo mount /dev/loop3 /mnt
Разархивируйте предварительно созданный Docker-образ test.tar
в директорию /mnt
:
sudo tar xvf test.tar -C /mnt
Создайте директорию /mnt/boot
для размещения файлов загрузчика и ядра:
sudo mkdir -pv /mnt/boot
Скачайте исходный код ядра Linux:
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.8.9.tar.xz
Разархивируйте исходный код ядра Linux в текущую директорию:
tar xf linux-6.8.9.tar.xz
Установите необходимые пакеты для сборки ядра Linux:
sudo apt install git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc flex libelf-dev bison -y
Перейдите в директорию c исходным кодом и создайте конфигурационный файл для сборки по умолчанию:
cd linux-6.8.9
make defconfig
Добавьте необходимые параметры в файл конфигурации (вы также можете воспользоваться menuconfig
— sudo make menuconfig
):
echo -e "CONFIG_BRIDGE=y\nCONFIG_TUN=y\nCONFIG_PPP=y\nCONFIG_PPP_ASYNC=y\nCONFIG_PPP_DEFLATE=y" >> .config
CONFIG_BRIDGE=y
— поддержка сетевых мостов для объединения нескольких сетевых интерфейсов в один.
CONFIG_TUN=y
— поддержка виртуальных сетевых интерфейсов TUN/TAP. Может пригодиться, если понадобится использовать VPN.
CONFIG_PPP=y
— поддержка протокола Point-to-Point Protocol (PPP).
CONFIG_PPP_ASYNC=y
— обеспечение работы асинхронного режима PPP для последовательных портов.
CONFIG_PPP_DEFLATE=y
— сжатие данных PPP с помощью алгоритма DEFLATE.
Подготовьте исходный код, создайте вспомогательные скрипты, соберите сжатый образ ядра (bzImage) и модули ядра. На этапе выбора параметров оставьте все значения по умолчанию
make prepare -j4
make scripts -j4
make bzImage -j4
make modules -j4
Установите собранное ядро и модули в директорию, где находится ФС нашего образа — /mnt/boot
.
sudo make INSTALL_PATH=/mnt/boot install
sudo make INSTALL_MOD_PATH=/mnt modules_install
Установите загрузчик GRUB для образа test.img
в директорию /mnt/boot
(убедитесь, что вы находитесь в директории с образом test.img
):
sudo grub-install --target=i386-pc --boot-directory=/mnt/boot/test.img --modules='part_msdos'
Смонтируйте директории хостовой системы /proc
, /sys
и /dev
в /mnt
. Это понадобится для создания initramfs
:
sudo mount --bind /proc /mnt/proc/
sudo mount --bind /sys /mnt/sys/
sudo mount --bind /dev /mnt/dev/
Произведите chroot
— прикрепитесь к ФС, находящейся в /mnt
, с помощью оболочки командной строки sh
:
sudo chroot /mnt /bin/sh
Создайте initramfs:
mkinitfs -k -o /boot/initrd.img-6.8.9 6.8.9
Сгенерируйте конфигурацию загрузчика GRUB:
grub-mkconfig -o /boot/grub/grub.cfg
Создание образа завершено. В результате мы получили артефакт в виде небольшого образа для виртуальной машины
Для локальной проверки работоспособности наиболее удобно использовать QEMU. Этот пакет доступен для Windows, macOS и Linux. Установите его, следуя инструкции для вашей ОС на официальном сайте, если он еще не установлен.
Конвертируйте test.img
в формат qcow2. Это позволит уменьшить размер итогового образа с 200 МБ до 134 МБ.
qemu-img convert test.img -O qcow2 test.qcow2
Запустите образ с помощью QEMU.
qemu-system-x86_64 -hda test.qcow2
Если все шаги были выполнены корректно, процесс инициализации пройдет успешно, и появится интерактивное меню для ввода логина и пароля.
Чтобы проверить версию установленного ядра, используйте команду uname -a
, которая выведет необходимую информацию.
Перейдите в раздел «Облачные серверы» и начните процесс создания нового сервера. В качестве образа для сервера выберите только что подготовленный и проверенный образ. Для этого сначала добавьте его в список доступных образов. В качестве поддерживаемых форматов доступны: iso, qcow2, vmdk, vhd, vhdx, vdi, raw, img.
Загрузите образ одним из доступных вариантов: с компьютера или по ссылке.
Обратите внимание, что после загрузки образ также становится доступен по URL.
Продолжите создание облачного сервера и укажите остальные параметры его конфигурации. Так как образ минимальный, то и запустить его можно будет на самой маленькой конфигурации.
Когда облачный сервер будет создан, перейдите во вкладку «Консоль» и проверьте, успешно ли завершилось создание виртуальной машины из образа.
Виртуальная машина создана и работает корректно.
Поскольку мы добавили в автозагрузку демон OpenSSH заранее, теперь возможно установить полноценное удаленное подключение к серверу с помощью имени пользователя, IP-адреса и пароля.
Разворачивайте свои проекты в облаке Timeweb Cloud
Для того чтобы превратить контейнер в полноценную легковесную виртуальную машину, мы последовательно добавили ключевые компоненты: систему инициализации OpenRC, загрузчик GRUB, ядро Linux и initramfs. Этот процесс подчеркнул важность каждого компонента в общей архитектуре виртуальной машины и продемонстрировал на практике отличия от контейнерных сред.
В результате проделанного эксперимента мы выяснили, насколько важно понимать архитектуру и функции каждого компонента, чтобы успешно создавать образы для своих нужд и более эффективно управлять виртуальными машинами с точки зрения ресурсов. Собранный в рамках статьи образ является довольно минимальным, так как это Proof-of-Concept, но можно пойти еще дальше, например, использовать специальную инструкцию по минимизации ядра, а также обратить внимание на минимальные дистрибутивы Linux, такие как Tiny Core Linux или SliTaz. В то же время, если ваш выбор склоняется к добавлению функционала путем увеличения размера образа, настоятельно рекомендую ознакомиться с Gentoo Wiki. Этот ресурс предлагает обширную информацию относительно точечной настройки системы.