Використовуємо Docker... без Docker

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

Всім привіт. Я Сергій Моренець, розробник, викладач, тренер, спікер і технічний письменник, хочу поділитися з вами своїм досвідом роботи з 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. На цю мить серед найбільш популярних технологій можна відзначити три:

  1. Podman.
  2. Buildah.
  3. 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 для запуску контейнерів.

👍ПодобаєтьсяСподобалось11
До обраногоВ обраному7
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Не согласен, WebAssembly может быть более эффективным выбором по сравнению с Docker в сценариях, требующих высокой производительности вычислений, компактности приложений и строгой безопасности. А об этом ни слова...

Вроде слово образ в украинском вполне себе есть. При чем тут изображение?

Там написание с кацапским совпадает, а по слову «зображення» всем сразу ясно, что не один народ. Самоидентификация иначе сразу исчезает.

OCI-сумісні зображення

— Все таки не зображення, а образи ))

Згоден, слово «зображення» не відповідає фактичному значенню терміну image.
На мою думку, краще «образ» або «зліпок».
P.S. добре, що не переклали слово «сокет» буквально )

Kaniko непогано себе показав коли потрібно збирати зображення «на сервері», наприклад, пайплайном gitlab. Спроби робити це використовуючи підхід docker-in-docker працювали набагато повільніше не кажучи про необхідність контейнерів з підвищеними правами.

Якщо вам потрібно запустити контейнер у контейнері, то це можливо зробити, але тільки в режимі зниженої безпеки.

Не простіше тоді просто в chroot той вторинний докер поставити, скажемо, на розвернутий через debootstrap мінімальний debian/ubuntu?

як альтернативу можна використовувати Podman, Buildah або Kaniko

Як альтернативу Podman, Buildah або Kaniko використовувати можна.
Але.
Розробникам бажано використовувати той самий інструмент для створення контейнерів, який використовується і для реальних білдів в вашій інфраструктурі.
Інакше може бути втрачена перевага гарантування консистентності середовища та/або поведінки коду в інфраструктурі.

Яким боком тест контейнери до поведінки коду в інфраструктурі?

Наприклад, щоб ганяти тести під час ci/cd

Якась дивна логіка. У тестконтейнері назовні видно тільки апішку якогось сервісу. Яким чином це впливає на код додатку який клристується цим апі?

А нахіба взагалі локальні тести з testcontainers? It works on my machine?

Тобто на локалі і на ci/cd ти хочеш тримати всі потрібні сервіси? Типу баз даних і всяких кафок? Тоді там буде засрано всякою фігньою.

Тестконейнери однаково працюють і на локалі і на ci/cd

Підписатись на коментарі