Використовуємо Docker... без Docker
Всім привіт. Я Сергій Моренець, розробник, викладач, тренер, спікер і технічний письменник, хочу поділитися з вами своїм досвідом роботи з Docker та різними системами контейнеризації.
Останнім часом Docker та Docker контейнери стали необхідною частиною нашої роботи, але потрібно пам’ятати про стандарт OCI (Open Container Initiative) і те, що вам не обов’язково використовувати Docker для запуску та створення images. У цій статті я розповім про різні альтернативи Docker, особливості їх використання і можливості інтеграції з Docker. Сподіваюся, що ця стаття буде корисною для всіх, хто хоче більше дізнатися про останні тенденції у світі контейнерів і все, що з ним пов’язано.
Обмеження Docker
Останнім часом ми використовуємо в наших проєктах бібліотеку TestContainers для інтеграційних тестів:
@SpringJUnitConfig(MainApplication.class) @TestPropertySource("classpath:application.properties") @Testcontainers class KafkaEventConsumerTest { @Container @ServiceConnection static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.2")).withKraft();
Вона дозволяє запустити та налаштувати практично будь-який Docker контейнер на стадії запуску тестів. Крім того, вона дозволяє запускати контейнери паралельно, перевикористовувати їх у тестах та інтегруватися із Spring Boot. TestContainers спочатку написана на Java, але потім портована на .NET/Go/Node.js. Якщо ви збираєте ваш проєкт на основній (host) машині, то тут немає жодних обмежень або підводного каміння.
Зовсім інша ситуація, якщо ви запускаєте збірку вашого проєкту під час складання Docker images. Зазвичай це застосовується у multi-stage builds. Це дозволяє нам не прив’язуватися до локальних систем складання (Maven/Gradle), JDK і не вимагати їх наявності. На жаль, не можна так просто взяти та запустити Docker усередині Docker-контейнера (Docker in Docker). Є обхідні шляхи, як це можна зробити:
- використовувати опцію
---privileged
; - використовувати в контейнерах Docker демон з host-машини;
- використання такого oпенсорс-проєкту Sysbox.
Опція --privileged
запускає контейнер у привілейованому режимі, коли ваш root-користувач у контейнері має ті ж права, що і root-користувач на основній машині, та всі перевірки безпеки в cgroups
скасовуються.
Остання опція (Sysbox) дозволяє запустити не тільки Docker, але й Kubernetes усередині вашого контейнера, а разом з ним будь-яку кількість «внутрішніх» контейнерів. Але вона вимагає встановлення додаткового софту на вашій машині, причому підтримка Windows з’явилася тільки зовсім недавно.
Open Container Initiative
Тобто в цілому всі три варіанти пов’язані або з додатковою конфігурацією вашої host-машини, або з ризиками в плані безпеки. І тут може виникнути для когось парадоксальна, а для когось крамольна думка. А чи потрібний взагалі Docker для збирання та запуску контейнерів?
Ми ж не використовуємо пропрієтарну Oracle JDK для складання проєктів, а натомість давно вже застосовуємо дистрибутиви OpenJDK. Виявляється, що ще з 2015 року існує такий проєкт як Open Container Initiative (OCI), серед засновників якого до речі входить і Docker (разом з Google, Red Hat, Microsoft, Intel та багатьма іншими). OCI містить три специфікації:
- runtime-spec (як запускати контейнери);
- image-spec (структура image, де є вся інформація для запуску контейнера);
- distribution-spec (API для розповсюдження images).
Таким чином, структура images і API для запуску є стандартом, і є досить багато сторонніх інструментів, які реалізують наведені вище специфікації, а Docker є лише одним з них. Так, наприклад, у Kubernetes створили свій container runtime CRI-O для запуску контейнерів у кластері.
Альтернативи Docker
Але повернемось до нашої проблеми. Чи існують інструменти, досить легковагі, і можливо більш ефективні ніж Docker, які дозволили б нам запускати контейнери в контейнери на локальній host-машині? Нам навіть не потрібно, щоб вони вміли збирати зображення, тому що ми використовуємо готові образи з Docker Hub. На цю мить серед найбільш популярних технологій можна відзначити три:
- Podman.
- Buildah.
- Kaniko.
Podman (Pod manager) — найвідоміша і найпопулярніша версія була створена компанією Red Hat у 2017 році для складання та запуску контейнерів, переважно для Kubernetes-кластерів. Тому не викликає подив, що це container runtime за замовчуванням в RedHat 8 і CentOS 8. Тут немає демона (сервісу), як в Docker, і нема потреби запускати щось від root-користувача (як спочатку було в Docker).
Але для складання images потрібна інша технологія — Buildah. Цікаво, що Podman підтримує скрипти Dockerfile, але оскільки Podman сам не має відношення до Docker, тут пропонується інша назва для цих скриптів — Containerfile. Якщо ви давно користуєтесь Docker, то напевно знаєте про його UI — Docker Dashboard, у Podman — теж є свій графічний інтерфейс — Podman Desktop.
При цьому слід зазначити, що використовувати контейнери звичайним способом можна лише на Linux-системах. Якщо у вас MacOS або Windows, то доводиться використовувати окремий компонент — Podman Machine, яка містить:
- QEMU (Quick Emulator) — система емуляції, яка дозволяє запускати віртуальні машини на тих системах, які не підтримують віртуалізацію або, наприклад, x86-сумісний код на Pow-erPC;
- Fedora Core OS — це та віртуальна машина, де запускатимуться ваші контейнери. Скорочено вона позначається як FCOS або Fedora container optimized OS;
- Ignition — утиліта для доступу та роботи з файловою системою віртуальної машини;
- gVisor — мережевий стек для комунікації контейнерів та host-системи.
І в такому випадку необхідно спочатку створити таку машину:
podman machine init
А потім її запустити перед початком роботи
podman machine start
При цьому Podman-клієнт утворює SSH-з’єднання з віртуальною машиною для надсилання команд.
Buildah був написаний у 2018 році на Go розробниками з Red Hat для того, щоб збирати OCI-сумісні images без наявності будь-якого container runtime. Так що одна зі сфер його застосування — CI/CD pipeline. Він підтримує інструкції з Dockerfile, а головний мінус — працює тільки на Linux-системах. Ще один порівняльний недолік порівняно з конкурентами — великий розмір офіційного зображення (720 Мб).
Kaniko з’явилася у 2018 році завдяки компанії Google. Її головна фіча — можливість збирати зображення всередині Kubernetes, gVisor, Google Cloud Build або unprivileged-контейнерів (Docker). На жаль, вона також підтримує лише Linux. З іншого боку, вона вміє працювати не тільки з локальними Dockerfile, але і з тими, що знаходяться в S3, GCS, Azure Blob Storage або Git репозиторії.
Нам підійде Podman, тому що ми хочемо запускати контейнери без необхідності їх складання. Але чи підтримує його TestContainers? Адже офіційно цей проєкт було створено для роботи з Docker та його Docker-клієнтом (у нашому випадку — через Java SDK). Як стверджують самі автори цієї бібліотеки, жодних проблем з інтеграцією з Podman зараз немає, потрібно лише деяке налаштування, а перевірити справедливість цього твердження ми зможемо у наступному розділі.
Використовуємо Podman і TestContainers
Використовувати Podman для вашого проєкту можна трьома способами. Перший, найпростіший, полягає у його локальній інсталяції на ваш комп’ютер. Займає він, до речі, всього 40 Мб. Але чи буде він автоматично сумісний із Docker та Docker SDK? Якщо просто запустити складання проєкту, то ви гарантовано отримаєте виключення:
Caused by: java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration at org.testcontainers.dockerclient.DockerClientProviderStrategy.lambda$getFirstValidStrategy$7(DockerClientProviderStrategy.java:277) at java.base/java.util.Optional.orElseThrow(Optional.java:403) at org.testcontainers.dockerclient.DockerClientProviderStrategy.getFirstValidStrategy(DockerClientProviderStrategy.java:268)
Та помилку в логах:
ERROR 627 --- [ Test worker] o.t.d.DockerClientProviderStrategy : Could not find a valid Docker environment. Please check configuration. Attempted configurations were: UnixSocketClientProviderStrategy: failed with exception InvalidConfigurationException (Could not find unix domain socket). Root cause NoSuchFileException (/var/run/docker.sock)
Тобто як би нам не хотілося, але проста установка Podman не ввімкне режим «емуляції Docker». Однак у лозі промайнув цікавий файл /var/run/docker.sock. Навіщо він потрібен, і чому TestContainers намагається його знайти? Це дефолтний сокет Unix (або Unix Domain), який Docker-демон «слухає» для отримання команд через так званий Inter-Process Communication. Оскільки у нас в Podman цього сокету не існує, то TestContainers (а скоріше Docker-клієнт) робить висновок, що і Docker у нас немає.
Але Podman підтримує використання такого типу комунікації (починаючи з версії 3.0), правда, його потрібно додатково дозволити:
systemctl --user enable podman.socket –now
Як тепер TestContainers дізнається адресу сокета? Для цього потрібно отримати ідентифікатор поточного користувача ${UID} і результівну адресу присвоїти змінній оточенню DOCK-ER_HOST, яку використовує Docker-клієнт:
DOCKER_HOST=unix:///run/user/${UID}/podman/podman.sock
Але що, якщо у вас не Linux/Unix і немає ніяких сокетів Unix (а, наприклад, у вас Windows і named pipes)? Тоді ви повинні використовувати Podman machine та команду podman machine inspect
:
podman machine inspect –format '{{.ConnectionInfo.PodmanPipe.Path}}'
Вона поверне адресу named pipe
, частину конфігурації, яку потрібно записати в DOCKER_HOST:
DOCKER_HOST=npipe:////./pipe/podman-machine-default
TestContainers також використовує спеціальний контейнер Ryuk, який видаляє всі створені в тестах контейнери, мережі та volumes. Це легко зробити, тому що кожному створеному контейнеру надається спеціальна мітка (label), за якою його легко знайти. Podman не сумісний з Ryuk, тому його використання потрібно глобально заборонити за допомогою спеціальної змінної оточення:
TESTCONTAINERS_RYUK_DISABLED=true
Тепер TestContainers працює без будь-яких проблем.
Контейнер у контейнері
Перейдемо до другого способу, складнішого. Ми хочемо запустити контейнер за допомогою Podman у складанні нашого проєкту. Наразі Dockerfile починаються з наступних інструкцій:
FROM gradle:8-jdk21-alpine as gradle USER root
Далі вже йде складання проєкту (включно з запуском тестів). Тепер потрібно перед складанням встановити Podman. Для цього достатньо команди apk add podman
, тому що ми використовуємо Alpine як базову ОС:
RUN apk add podman
Після встановлення Podman спробуємо вивести інформацію про нього: podman info
і отримаємо перший сюрприз. Версія встановленого Podman — 4.5.1, хоч поточна — вже 5.0.0. Справа в тому, що ми використовуємо з Gradle версію 3.18, а не останню — 3.19.1
Для тестування пробуємо запустити найпростіший контейнер, використовуючи Podman:
podman run hello-world
І відразу отримуємо помилку:
Error: 'overlay' is not supported over overlayfs, a mount_program is required: backing file system is unsupported for this graph driver
Ця помилка вже відома, і вона має кілька шляхів вирішення, найпростіший з яких — монтувати папку /var/lib/containers, щоб вона використовувала файлову систему host system:
VOLUME /var/lib/containers
Знову намагаємося запустити контейнер через Podman, але отримуємо помилку при завантаженні image:
Error: /usr/bin/slirp4netns failed: "WARNING: Support for seccomp is experimental\nopen(\"/dev/net/tun\"): No such file or directory\nWARNING: Support for IPv6 is experimental\nchild failed(1)\nWARNING: Support for IPv6 is experimental\n"
Ця помилка зустрічається рідко і говорить про те, що у вашої Linux-системи немає tun-модуля. Таке може бути, наприклад, для WSL-систем, де ця помилка не має очевидного рішення.
Спробуємо оновити версію Alpine і, відповідно, Podman. Додамо команду, яка замінює версію Alpine з 3.18 на 3.19 у конфігураційному файлі /etc/apk/repositories:
RUN sed -i -e 's/v3\.18/v3\.19/g' /etc/apk/repositories
І потім оновимо ОС та Podman:
RUN apk update && apk upgrade && apk add podman
На жаль, хоча версія Podman тепер 4.8.3, то отримуємо вже нову помилку під час запуску Podman-контейнера:
panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x38 pc=0x55bbb1e67354]
Таким чином, запустити Podman в контейнері Docker на WSL 2 і Windows не виходить. Найімовірніше, нам потрібна Linux-система як основна. Втім, і тут будуть потрібні певні налаштування. Річ у тім, що при запуску контейнера Docker через docker run
є спеціальна опція --privileged
, про яку я вже говорив. Але в нашому випадку потрібно буде дати розширені повноваження при виконанні команди docker build
, де немає цієї опції. Але є інший, складніший підхід, пов’язаний з новим компонентом збирання images Buildkit.
Buildkit дозволяє створювати так звані builders, тобто конфігурації для складання і вказувати під час самої складання. Крім того, він додає нові опції для інструкції RUN, але потрібно попередньо дозволити їх на початку Dockerfile:
# syntax=docker/dockerfile:1.3-labs
Потім вказати в самій інструкції опцію --security
:
RUN --security=insecure podman run hello-world
І нарешті створити новий builder з ослабленими політиками безпеки та вказати опцію --allow security.insecure
у самій збірці:
docker buildx create --use --name insecure-builder --buildkitd-flags "--allow-insecure-entitlement security.insecure" docker buildx build --allow security.insecure .
Якщо у вас Docker Compose, новий builder можна вказати за допомогою опції --builder
:
docker compose build --builder insecure-builder
Лише тепер Podman-контейнер запуститься під час збирання. Але якщо ми спробуємо зібрати наш проєкт із запуском тестів через TestContainers, то знову отримаємо помилку про те, що TestContainers не може знайти Docker environment. За замовчуванням Podman не запускається як сервіс (на відміну від Docker), і Docker-клієнт не може з ним з’єднатися. Як зробити так, щоб Podman активував свій Unix-сокет (/run/podman/podman.sock)? Офіційна документація радить таку команду:
systemctl --user start podman.socket
Але у нас не Ubuntu, команди systemctl
немає, а в Alpine є аналог OpenRC, але у нього немає можливості для такої активації. Однак у Podman є й інший спосіб активації сокета — команда podman system service
:
podman system service --time=0
Тут аргумент time передає час, який Podman чекатиме спроб звернутися до сокета (0, якщо нескінченно). Однак просто так цю команду не можна викликати, оскільки вона буде блокувати поточний процес в очікуванні підключення, тому щоб перевести її в background-виконання додамо символ &:
podman system service --time=0 &
І оскільки ця активація займає деякий час, то якщо ми відразу запустимо складання, то сокет може бути недоступний. Тому додамо паузу в одну секунду і остаточна команда виглядає так:
podman system service --time=0 & sleep 1 && gradle --no-daemon build
Все це запускається однією командою, тому що у новому терміналі сокет буде недоступним. Весь Dockerfile повністю:
# syntax=docker/dockerfile:1-labs FROM gradle:jdk21-alpine VOLUME /var/lib/containers ENV DOCKER_HOST=unix:///run/podman/podman.sock \ TESTCONTAINERS_RYUK_DISABLED=true RUN apk update && apk add podman fuse-overlayfs COPY . /home/gradle/ RUN --security=insecure --mount=type=cache,target=/home/gradle/.gradle podman system service --time=0 & sleep 1 && gradle --no-daemon build
Висновки
Якщо вам не підходить Docker для складання та запуску контейнерів, як альтернативу можна використовувати Podman, Buildah або Kaniko. Вони не вимагають наявності сервісу/демона, підтримують наявні Dockerfile і генерують OCI-сумісні зображення.
Якщо вам потрібно запустити контейнер у контейнері, то це можливо зробити, але тільки в режимі зниженої безпеки. Під час запуску контейнера необхідна опція --privileged
, а при складанні необхідно використовувати можливості Buildkit (builders) і налаштовувати їх на роботу в небезпечному режимі. Також хочу зазначити, що якщо ви в проєкті використовуєте TestContainers, то вони підтримують і Podman для запуску контейнерів.
15 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів