Docker
Docker пакує застосунок з усім, що потрібно для його роботи, в один образ і запускає цей образ як контейнер. Контейнер — це не маленька віртуальна машина. Це звичайний процес на ядрі хоста, якому дали обмежений огляд файлової системи, мережі й таблиці процесів і ліміт на те, скільки він може спожити. Образ, з якого він стартує, — це стос незмінних шарів. Ці два факти пояснюють майже все: чому контейнери стартують за мілісекунди, чому порядок у вашому Dockerfile визначає час збірки, чому стан усередині зникає і від чого насправді захищає ізоляція.
- Контейнер — це
- Процес, а не машина
- Образ — це
- Стос незмінних шарів
- Стан усередині
- Зникає разом із ним
Процес з обмеженим оглядом
Коли ви стартуєте контейнер, ядро запускає процес — такий самий, як будь-який інший на машині. Контейнером його робить те, що ядро дає йому власні простори імен: власний огляд файлової системи, власний мережевий стек, власну нумерацію процесів, — тож програма всередині бачить себе процесом номер 1 на машині, що містить лише те, що дав образ. Поруч із цим контрольні групи обмежують, скільки CPU, памʼяті та вводу-виводу він може взяти. Другої операційної системи в цій картині немає ніде — саме тому контейнер стартує за мілісекунди, а віртуальна машина за пів хвилини.
Віртуальна машина
- Має власне ядро, тож може виконувати геть іншу операційну систему.
- Завантажується десятки секунд і коштує гігабайтів ще до того, як там зʼявиться ваш код.
- Вада ядра всередині лишається всередині — це справжня межа безпеки.
- Правильний вибір, коли потрібна сильна ізоляція між орендарями, яких ви не контролюєте.
Контейнер
- Ділить ядро хоста, тож Linux-контейнеру потрібне Linux-ядро під ним.
- Стартує за мілісекунди й додає лише те, що справді потрібне вашому застосунку.
- Ізоляція — це можливість ядра, а не стіна: вихід через ядро дістає до хоста.
- Правильний вибір, щоб пакувати й запускати власні сервіси багато разів.
Три наслідки випливають одразу, і дивують вони людей у проді, а не в розробці. Контейнер відповідає архітектурі хоста, якщо ви не збирали під іншу, тож образ, зібраний на ноутбуці з ARM, може не стартувати на сервері з x86. Ядро спільне, тож контейнер не може використати модуль ядра, якого немає в хоста, а вразливість ядра є проблемою рівня хоста незалежно від того, скільки контейнерів працює. І ізоляції достатньо, щоб відділити ваші власні сервіси один від одного, але виконувати недовірений код від сторонніх на спільному ядрі — це інший клас ризику: для цього існує віртуальна машина чи середовище в пісочниці.
Шари й чому важить порядок
Образ будується по одній інструкції за раз, і кожна інструкція, що змінює файлову систему, породжує шар — незмінний діф поверх усього попереднього. Під час перезбирання демон повторно використає кешований шар лише тоді, коли ця інструкція й кожна інструкція над нею незмінні. Усе нижче за змінений шар викидається й перезбирається — і це перетворює порядок у вашому Dockerfile на рішення про продуктивність, а не про стиль.
Звідси одне правило з непропорційною віддачею: копіюйте маніфест залежностей і встановлюйте залежності до копіювання вихідного коду. Ваш код змінюється на кожен коміт, а перелік залежностей — раз на місяць, тож встановлення над копіюванням коду означає, що звичайна збірка перевикористає дорогий шар замість перевстановлювати все. Копіювання всієї теки першим — один COPY . . близько до початку — найчастіша причина, чому збірка, яка мала б тривати двадцять секунд, триває чотири хвилини, і виправляється вона одним рядком.
Багатоетапні збірки розвʼязують другу половину проблеми розміру. Компілятори, інструменти збірки й залежності розробки потрібні, щоб виготовити артефакт, і є чистим тягарем у тому, що ви постачаєте: кожен із них — це код, яким може скористатися нападник і про який доповість сканер вразливостей. Збирайте на одному етапі, копіюйте лише отриманий бінарник чи бандл у малий фінальний етап — і образ падає із сотень мегабайтів до десятків. Базовий образ обирайте так само: slim чи distroless менший і має менше що латати, ціною відсутності оболонки, коли захочеться налагоджувати всередині.
Як добре його запускати
Контейнер виконує один процес і живе рівно стільки, скільки той. Це і є контракт, і більшість експлуатаційних порад випливає з нього прямо. Запускайте по одній речі на контейнер, бо коли контейнер є одиницею перезапуску, масштабування й логування, дві програми всередині роблять усі три неоднозначними. Пишіть логи у стандартний вивід, а не у файл, бо платформа збирає потік, а файл із контейнера, якого вже немає, ніхто не дістане. Беріть конфігурацію зі змінних середовища, щоб той самий образ працював у кожному середовищі. А все, що має вижити, кладіть на змонтований том або в базу, бо шар для запису видаляється разом із контейнером.
Дві деталі про цей єдиний процес спричиняють справжні інциденти. Він виконується як PID 1, а PID 1 не має типових обробників сигналів — тож програма, яка не обробляє SIGTERM явно, не завершиться коректно: її вбʼють після пільгового часу разом із запитами в польоті. Обробляйте сигнал або використовуйте init-процес, що його передає. І типово процес виконується як root усередині контейнера, що зайве майже для кожного застосунку й перебуває за одну ваду ядра від того, щоб бути root іззовні. Створіть у образі користувача й працюйте від нього.
Виставляйте ліміти памʼяті й CPU явно і знайте, що робить кожен, коли в нього впираються, бо поводяться вони геть по-різному. Перевищення ліміту памʼяті призводить до того, що ядро вбиває процес — різко, без винятку, який ваш код міг би спіймати, і в логах це виглядає як контейнер, що просто зник. Перевищення ліміту CPU не вбиває нічого: процес пригальмовують, і це виглядає як непояснена затримка, а не як падіння. Обом потрібне середовище виконання, що читає ліміт контейнера, а не загальний обсяг хоста, — кілька мовних середовищ навчилися цього лише у відносно свіжих версіях.
Як це виявляється в реальній поставці
Тег — це змінюваний вказівник, а не ідентичність. Дві машини, які завантажили app:latest з тижневою різницею, можуть виконувати різні образи — і та сама машина після перезапуску теж; саме так середовище пливе без жодного розгортання. Дайджест — це хеш вмісту, і це єдине, що називає точний образ. Розгортайте за дайджестом або за незмінним тегом, який ваш конвеєр ніколи не перевикористовує, і фіксуйте базові образи так само, щоб перезбирання було відтворюваним.
Сканування належить конвеєру, а хитрість у тому, щоб результат був придатний до дії, а не був стіною знахідок. Slim-база зменшує звіт до чогось, що людина здатна прочитати; більшість знахідок живе в базі, а не у вашому коді, а перезбирання на свіжій базі лагодить їх більше, ніж будь-яка окрема латка. Валіть збірку на нових критичних знахідках, решту записуйте — і регулярно перезбирайте: образ, не перезібраний пів року, виконує пів року незалатаних бібліотек, хоч би яким чистим він був у день випуску.
Твердження, яке контейнери справді роблять, вужче, ніж підказує гасло, і знання цієї межі економить суперечку. Вони гарантують, що скрізь присутні та сама файлова система й ті самі залежності, — а це знімає цілу категорію вад середовища. Вони не гарантують того самого ядра, тієї самої архітектури, того самого годинника, того самого обсягу памʼяті чи тієї самої мережі. «Працює на моїй машині» стає «на обох машинах ті самі байти» — це справжнє покращення, і це не те саме речення.
Де це вироджується
- Копіювання всього дерева коду до встановлення залежностей: кожен коміт перевстановлює все.
- Секрет, скопійований і пізніше видалений: він лишається читабельним у шарі, що його додав.
- Постачання інструментарію збірки у фінальному образі: розмір і поверхня атаки задарма.
- Розгортання за змінюваним тегом: дві машини виконують різний код, і ніхто нічого не розгортав.
- Запуск від root, бо так було типово, в образі, якому привілеї не були потрібні ніколи.
- Ігнорування
SIGTERM: кожне розгортання вбиває запити в польоті після пільгового часу. - Стан, записаний у файлову систему контейнера: він зникає з наступним перезапуском.
- Кілька процесів в одному контейнері: перезапуск, масштабування й логування стають неоднозначними.
- Відсутність ліміту памʼяті: один сервіс із витоком забирає з собою весь хост.
Коли це застосовувати
Застосовуйте, коли
- Пакування сервісу так, щоб той самий артефакт працював на ноутбуці, у CI та у проді.
- Локальні залежності — база, брокер, мок, — які інакше ставлять руками, і вони розходяться.
- Середовища збірки в конвеєрі, де зафіксований образ є відтворюваним інструментарієм.
- Усе, що згодом піде до оркестратора: контейнер і є тією одиницею, яку він планує.
Уникайте, коли
- Недовірений сторонній код на спільному ядрі: правильна межа тут — віртуальна машина чи пісочниця.
- База зі станом на одному хості без стратегії томів: перший перезапуск і є інцидентом.
- Настільне чи графічне ПЗ, де проблема пакування, яку він розвʼязує, — не ваша проблема.
- Один малий застосунок на одному сервері, який пакет і юніт служби вже надійно запускають.
Було корисно?
Поділіться з тим, хто працює над тією ж задачею.