Как создать исполняемый файл из docker образа?

Сегодня я прочитал на Hacker News о новом проекте dockerc, который позволяет сконвертировать docker image в исполняемый файл, который не требует наличия докера на компьютере. Мне стало интересно, как оно работает внутри, и я попытался разобраться с этим.

На самом деле, это далеко не первая попытка упаковать приложение (или докер образ) в исполняемый файл. Наверное, самый громкий похожий проект — это AppImage. Там ребята решают похожую проблему, но не для докер образов, а в целом для приложений. Они говорят — у нас есть много Linux дистрибутивов, и разработчикам софта сложно выпускать приожение под каждый пакетный менеджер (snap, deb, etc.). Поэтому, давайте позволим упоковывать приложение в универсальный «контейнер» (не путать с докер контейнером), который сможет запускаться на почти любом линуксе без каких либо дополнительных настроек. Все это реализуется путем упаковки приложения в ISO образ, и добавлением дополнительного запускатора (AppRun) в бинарный файл. Когда пользователь запускает бинарный файл, запускатор монтирует ISO образ с приложением, и запускает его.

Dockerc работает похожим образом. Он построен на основе нескольких опенсорс продуктов, которые помогают решить задачу:

  • crun — runtime, который позволяет запускать OCI-совместимые контейнеры. Аналог runc.
  • fuse-overlayfs; докер образ — это, по сути, набор неизменяемых слоев. Обычно эти слои представлены с помощью OverlayFS файловой системы. fuse-overlayfs — это FUSE имплементация для OverlayFS, которая позволяет монтировать файловую систему из User Space.
  • mksquashfs — инструмент для добавления файлов к SquashFS — неизменяемой, сжатой файловой системе.
  • squashfuse — реализация SquashFS в FUSE.
  • umoci — инструмент для извлечения config.json и rootfs для OCI bundle из существующего docker image.

Принципиальная схема работы dockerc — Часть 1 — Создание исполняемого файла

Я могу упустить какие-то детали, потому что dockerc написан на Zig языке программирования, который я не знаю хорошо, но я думаю, что примерно понимаю общую картину.

Я вижу следующие шаги, как мы конвертируем докер образ в исполняемый файл на Linux:

  • skopeo copy … — мы конвертируем докер образ в OCI Bundle.
  • umoci unpack —image — мы берем OCI Bundle, и извлекаем из него rootfs и config.json (OCI bundle конфиг)
  • mksquashfs … — мы упоковываем rootfs и config.json в SquashFS. По сути, мы взяли докер образ, и сделали из него OCI-совместимый образ, упакованный в SquashFS.
  • Создание бинарника. Мы соединяем Райнтайм (наш код, аналог AppRun из AppImage) и SquashFS, который содержит в себе OCI bundle нашего образа.
  • Устанавливаем 755 права на бинарный файл. То есть, теперь разрешено просто выполнять данный файл. Когда пользователь запустит данный файл, начнет выполняться наш рантайм, но в этот же бинарный файл встроен наш докер образ, и мы можем прочитать его по нужному оффсету.

Часть 2 — Запуск бинарного файла

Когда пользователь запускает созданный ранее бинарный файл, нужно выполнить несколько шагов:

  • Мы знаем оффсет, начиная с которого лежит наш OCI bundle в SquashFS формате. Поэтому мы можем прочитать бандл из нашего бинарника. Далее, мы можем примонтировать его, используя squashfuse.
  • Мы создаем OverlayFS файловую систему, используя fuse-overlayfs. Она состоит из двух частей — lowerdir (нижняя часть), и upperdir (верхняя часть). Нижняя часть — это неизменяемые данные. В нашем случае мы используем SquashFS из первого шага, как lowerdir в OverlayFS.
  • crun run —bundle … — мы запускаем наш докер образ (преобразованный в OCI bundle), используя в качестве бандла OverlayFS.
  • Когда программа закончила работать, мы отмантируемые бандл, чтобы подчистить за собой.

Не сказать, чтобы тут бы какой-то Rocket science, но я без анализа исходного кода с ходу не придумал, как я бы реализовал подобную программу. Вроде бы я слышал про каждый из инструментов, но про использование вместе — не догадался.

Категории: Программирование