Контейнер как процесс
Контейнер как обычный процесс: namespaces + cgroups + capabilities.
Контейнер — это процесс
Слово «контейнер» звучит как отдельная сущность, коробка со своим устройством внутри. На деле это обычный процесс Linux, которому при запуске выдали четыре вещи: ограниченную видимость, лимиты на ресурсы, урезанный набор прав и свой корень файловой системы.
Контейнер — это процесс, вокруг которого ядро выставило изоляцию через namespaces, ограничило ресурсы через cgroups и обрезало права через capabilities. Никакого отдельного «движка виртуализации» под ним нет.
Проверить это можно прямо на хосте. Запусти контейнер и найди его в общем списке процессов:
docker run -d --name web nginx
ps aux | grep nginx
$ ps aux | grep nginx
root 20431 0.0 0.1 10620 6200 Ss 12:03 0:00 nginx: master process
101 20489 0.0 0.0 11072 2600 S 12:03 0:00 nginx: worker
PID 20431 — это номер в общей таблице процессов хоста, не какой-то
внутренний. С точки зрения ядра хоста контейнер ничем не отличается от
любого другого запущенного бинарника.
Четыре слоя изоляции
namespaces (namespace — пространство имён) изолируют то, что процесс видит: свои PID, свою сеть, свои точки монтирования. Внутри контейнера процесс думает, что он PID 1, а снаружи у него номер вроде 20431.
# заглянуть, в каких namespaces сидит процесс
ls -l /proc/20431/ns
lrwxrwxrwx net -> 'net:[4026532211]'
lrwxrwxrwx pid -> 'pid:[4026532213]'
lrwxrwxrwx mnt -> 'mnt:[4026532209]'
cgroups (control group — контрольная группа) вешают лимиты: сколько CPU и памяти процессу можно съесть. Если задать лимит памяти, ядро убьёт процесс при его превышении.
docker run -m 256m nginx
cat /sys/fs/cgroup/memory.max # внутри контейнера покажет 268435456
capabilities (capability — отдельное право суперпользователя) разбивают всевластие root на мелкие разрешения. Контейнер получает лишь часть из них, а не полный набор.
docker run --rm alpine grep CapEff /proc/1/status
CapEff: 00000000a80425fb
CapEff — битовая маска действующих capabilities процесса: каждый бит
отвечает за одно право. У root на хосте маска забита единицами
(0000003fffffffff), у процесса в контейнере она заметно короче.
Расшифровать маску в имена помогает capsh из пакета libcap2-bin. В alpine
его нет, поэтому запускать удобнее с хоста:
$ capsh --decode=00000000a80425fb
0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,...
В списке нет, например, cap_sys_admin, поэтому смонтировать
произвольную файловую систему изнутри такой контейнер не сможет.
Корень из образа подставляется сменой корня файловой системы:
chroot или pivot_root переносит / процесса на распакованный образ.
Сам образ лежит слоями поверх overlay-файловой системы (overlayfs
складывает несколько каталогов в одно дерево, где верхний слой
доступен на запись).
Чем это отличается от виртуальной машины
Виртуальная машина несёт своё ядро и работает поверх гипервизора (hypervisor — слой, эмулирующий железо). Контейнер своего ядра не имеет:
uname -r # даст одинаковую версию и на хосте, и в контейнере
Один и тот же результат внутри и снаружи означает, что системный вызов из контейнера обрабатывает то же самое ядро хоста. Отсюда практический вывод: граница изоляции у контейнера тоньше, чем у виртуальной машины. Дыра в ядре бьёт по всем контейнерам сразу, тогда как виртуалки разделены собственными ядрами.
Две частые ловушки
Первая: «изоляция контейнера равна изоляции виртуальной машины». Это не так. Общее ядро означает меньшую границу. Уязвимость ядра или ошибка в настройке namespaces способна вывести процесс за пределы контейнера.
Вторая: «root в контейнере — это root на хосте». Тоже мимо. root внутри
ограничен выданными capabilities, а при включённом user namespace его
UID 0 отображается на непривилегированный UID хоста.
# внутри контейнера
id # uid=0(root)
# на хосте тот же процесс
cat /proc/20489/status | grep Uid # Uid: 100000 100000 ...
Внутри процесс видит себя root, а хост видит обычного пользователя
100000. Захват такого root не даёт власти над машиной.
Что важно запомнить
Контейнер — это процесс с изоляцией, а не отдельная машина. На хосте он
виден в ps как любой другой.
Четыре слоя: namespaces (видимость), cgroups (ресурсы), capabilities (права), корень из образа поверх overlay.
Ядро общее с хостом. Поэтому граница изоляции тоньше, чем у виртуальной машины с её собственным ядром.
root в контейнере ограничен capabilities и user namespace. Это не root на хосте.
Дальше эти механизмы собирает вместе Docker, а потом оркестрирует Kubernetes.
Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс