Linux-дистрибутив from scratch для сборки Docker-образов — наш опыт с dappdeps

в 6:53, , рубрики: dapp, devops, docker, linux, linux from scratch, omnibus, Блог компании Флант, Настройка Linux, системное администрирование
Linux-дистрибутив from scratch для сборки Docker-образов — наш опыт с dappdeps - 1

Сборка образов для Docker на основе базового образа, как правило, предполагает вызов команд в окружении этого базового образа. Например — вызов команды apt-get, которая есть в базовом образе, для установки новых пакетов.

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

Проблема известная: одним из путей её решения может быть сборка вспомогательного образа и перенос файлов из вспомогательного образа в результирующий. Для этого появились Docker multi-stage builds или образы-артефакты в dapp. И данный подход идеально решает проблему подобную переносу результатов компиляции исходных кодов в итоговый образ. Однако он не решает все возможные проблемы…

Вот другой пример: для сборки образа используется Chef в локальном режиме. Для этого в базовый образ ставится chefdk, монтируются или добавляются рецепты, запускаются эти рецепты, которые настраивают образ, устанавливают новые компоненты, пакеты, файлы-конфиги и прочее. Аналогично может быть использована другая система управления конфигурациями — например, Ansible. Однако установленный chefdk занимает около 500 Мб и существенно увеличивает размеры итогового образа — оставлять его там нет смысла.

Но multi-stage builds в Docker уже не решат эту проблему. Что, если пользователю не хочется знать о том, каков побочный эффект работы программы — в частности, какие файлы она создает? Например, чтобы не держать лишние явные описания всех экспортируемых путей из образа. Хочется просто запустить программу, получить какой-то результат в образе, но чтобы программа и все окружение, нужное для ее работы, осталось вне итогового образа.

В случае с chefdk можно было бы монтировать директорию с этим chefdk в сборочный образ на время сборки. Но с этим решением есть проблемы:

  1. Не любая программа, нужная для сборки, устанавливается в отдельный каталог, который легко примонтировать в сборочный образ. В случае с Ansible надо монтировать Python в нестандартное место, чтобы не конфликтовать с системным Python, что уже может вызывать проблемы.
  2. Примонтированная программа будет зависеть от используемого базового образа. Если программа собрана для Ubuntu, то она может не запуститься в не предусмотренном для нее окружении — например, в Alpine. Даже chefdk, который является omnibus-пакетом со всеми своими зависимостями, все равно зависит от системного glibc и не будет работать в Alpine, где используется musl libc.

А что, если мы сможем подготовить некий статичный неизменный набор всех возможных полезных утилит, который будет так хитро слинкован, что будет работать в любом базовом образе, даже scratch? После подключения такого/таких образов в базовый, в итоговом образе останется лишь пустая директория mount-point, в которую были подключены эти утилиты.

В поисках приключений

Теория

Необходимо получить образ, в котором содержится набор программ в некоторой статически определенной нестандартной директории — например, /myutils. Любая программа в /myutils должна зависеть только от библиотек в /myutils.

Динамически скомпилированная программа в Linux зависит от местоположения линкера ld-linux в системе. Например, бинарник bash в ubuntu:16.04 скомпилирован так, что зависит от линкера /lib64/ld-linux-x86-64.so.2:

$ ldd /bin/bash
        linux-vdso.so.1 =>  (0x00007ffca67d8000)
        libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007fd8505a6000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fd8503a2000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd84ffd8000)
        /lib64/ld-linux-x86-64.so.2 (0x00007fd8507cf000)

Причем эта зависимость является статической и вкомпилирована в сам бинарник:

$ grep "/lib64/ld-linux-x86-64.so.2" /bin/bash
Binary file /bin/bash matches

Таким образом, надо: а) скомпилировать условный /myutils/bin/bash так, чтобы он использовал линкер /myutils/lib64/ld-linux-x86-64.so.2; б) чтобы линкер /myutils/lib64/ld-linux-x86-64.so.2 был настроен на динамическую линковку библиотек из /myutils/{lib64,lib}.

Первым шагом будет сборка образа toolchain, который будет содержать всё, что необходимо для сборки и последующей работы других программ в нестандартной root-директории. Для этого нам как нельзя кстати придутся инструкции проекта Linux From Scratch.

Linux-дистрибутив from scratch для сборки Docker-образов — наш опыт с dappdeps - 2

Собираем дистрибутив dappdeps

Почему набор образов нашего «дистрибутива» называется dappdeps? Потому что эти образы использует сборщик dapp — они собираются под нужды этого проекта.

Итак, наша конечная цель:

  • Образ dappdeps/toolchain с компилятором GCC для сборки других приложений и библиотекой glibc.
  • Образ dappdeps/base с набором программ и всех зависимых библиотек: bash, gtar, sudo, coreutils, findutils, diffutils, sed, rsync, shadow, termcap.
  • Образ dappdeps/gitartifact с утилитой Git и всеми зависимостями.
  • Образ dappdeps/chefdk с omnibus-пакетом chefdk, который содержит все зависимости Chef, в т.ч. интерпретатор Ruby.
  • Образ dappdeps/ansible с утилитой Ansible, который содержит все зависимости, в т.ч. интерпретатор Python.

Образы dappdeps могут зависеть друг от друга. Например, при сборке dappdeps/base требуется toolchain и glibc из образа dappdeps/toolchain. После компиляции всех утилит в dappdeps/base для их работы в runtime будут требоваться файлы из dappdeps/toolchain.

Главное условие заключается в том, чтобы утилиты из этих образов располагались в нестандартном месте, а именно — в /.dapp/deps/, и не зависели ни от каких утилит или библиотек в стандартных системных путях. Также в dappdeps-образах не должно быть никаких других файлов, кроме /.dapp/deps.

Такие образы позволят создавать на их основе контейнеры с томами, где содержатся утилиты, и монтировать их в другие контейнеры с использованием опции --volumes-from для Docker.

Собираем dappdeps/toolchain

Глава 5 «Constructing a Temporary System» руководства Linux From Scratch как раз описывает процесс построения временного chroot-окружения в /tools с некоторым набором утилит, которым затем собирается главный целевой дистрибутив.

В нашем случае немного переиначим директорию chroot-окружения. В параметре --prefix при компиляции будем указывать /.dapp/deps/toolchain/0.1.1. Это та директория, которая будет появляться в сборочном контейнере, при монтировании в него dappdeps/toolchain — в ней содержатся все нужные утилиты и библиотеки. Нам требуются лишь GNU binutils, GCC и glibc.

Собирается образ с использованием Docker multi-stage builds. В образе на основе ubuntu:16.04 подготавливается все окружение и производится компиляция и установка программ в /.dapp/deps/toolchain/0.1.1. Затем эта директория копируется в scratch-образ dappdeps/toolchain:0.1.1. Dockerfile можно найти здесь.

Итоговый образ dappdeps/toolchain — это и есть «temporary system» в терминологии LFS. GCC в данной системе все еще завязан на системные пути к библиотекам, однако мы не будем добиваться того, чтобы GCC работал в любом базовом образе. Образ dappdeps/toolchain — вспомогательный, он будет использоваться далее, в т.ч. для сборки уже реально независимых от общих системных библиотек программ.

Используем Omnibus вместе с dappdeps/toolchain

Linux-дистрибутив from scratch для сборки Docker-образов — наш опыт с dappdeps - 3

Для сборки таких проектов, как chefdk или GitLab, используется Omnibus. Он позволяет создать самодостаточные наборы (self-contained bundle) с программой и всеми зависимыми библиотеками, кроме системного линкера и libc. Все инструкции описываются читаемыми удобными Ruby-рецептами. Также у проекта Omnibus есть библиотека уже написанных рецептов omnibus-software.

Итак, попробуем описать сборку остальных dappdeps-дистрибутивов с использованием Omnibus. Однако, чтобы избавиться от зависимости от системного линкера и libc, будем собирать все программы в Omnibus с использованием компилятора из dappdeps/toolchain. В этом случае программы окажутся завязаны на glibc, который тоже есть в dappdeps/toolchain.

Для этого сохраним содержимое dappdeps/toolchain как архив:

$ docker pull dappdeps/toolchain:0.1.1
$ docker save dappdeps/toolchain:0.1.1 -o dappdeps-toolchain.tar

Добавим этот архив через директиву Dockerfile ADD и распакуем содержимое архива в корень сборочного контейнера:

ADD ./dappdeps-toolchain.tar /dappdeps-toolchain
RUN tar xf /dappdeps-toolchain/**/layer.tar -C /

Перед запуском сборки через omnibus добавляем в переменную PATH путь /.dapp/deps/toolchain/0.1.1/bin в качестве приоритетного, чтобы использовался GCC из dappdeps/toolchain.

Результат работы Omnibus — это пакет (в нашем случае — DEB), содержимое которого распаковывается и переносится в /.dapp/deps/{base|gitartifact|...} с помощью Docker multi-stage builds аналогично dappdeps/toolchain.

Собираем dappdeps/base

Проект для Omnibus описывается с помощью файла проекта dapp/dappdeps/base/omnibus/config/projects/dappdeps-base.rb:

name 'dappdeps-base'

license 'MIT'
license_file 'LICENSE.txt'

DOCKER_IMAGE_VERSION = "0.2.3"

install_dir "/.dapp/deps/base/#{DOCKER_IMAGE_VERSION}"

build_version DOCKER_IMAGE_VERSION
build_iteration 1

dependency "dappdeps-base"

В этом файле указаны все зависимости Omnibus-пакета dappdeps-base и целевая директория для установки. Зависимости могут располагаться либо в отдельном репозитории (например, omnibus-software), либо в директории omnibus/config/software. Каждый файл в этой директории описывает инструкции по установке какого-то пакета/компонента. Для dappdeps-base в Omnibus написаны software-рецепты, отсутствующие в стандартном репозитории omnibus-software: acl, attr, coreutils, diffutils, findutils, gtar, rsync, sed, shadow, sudo, termcap.

Рассмотрим на примере rsync, как выглядит software-рецепт для Omnibus:

name 'rsync'
default_version '3.1.2'

license 'GPL-3.0'
license_file 'COPYING'

version('3.1.2') { source md5: '0f758d7e000c0f7f7d3792610fad70cb' }

source url: "https://download.samba.org/pub/rsync/src/rsync-#{version}.tar.gz"

dependency 'attr'
dependency 'acl'
dependency 'popt'

relative_path "rsync-#{version}"

build do
  env = with_standard_compiler_flags(with_embedded_path)

  command "./configure --prefix=#{install_dir}/embedded", env: env
  command "make -j #{workers}", env: env
  command 'make install', env: env
end

Директивой source указывается URL, откуда надо скачать исходные коды. Зависимости от других компонентов указаны директивой dependency по имени. Имя собираемого компонента задано директивой name. Каждый software-рецепт в свою очередь может указывать зависимости от других компонентов. Внутри блока build указаны стандартные команды сборки из исходных кодов.

Проект Omnibus и Dockerfile для dappdeps/base можно найти здесь.

Собираем dappdeps/gitartifact

В случае с dappdeps-gitartifact необходим лишь рецепт сборки Git, а он уже есть в omnibus-software — остается только подключить его в текущий Omnibus. В остальном все аналогично.

Проект Omnibus и Dockerfile для dappdeps/gitartifact можно найти здесь.

Собираем dappdeps/chefdk

Для chefdk тоже уже есть готовый проект Omnibus. Остается лишь добавить его в сборочный контейнер через Dockerfile и заменить стандартные пути установки chefdk /opt/chefdk на /.dapp/deps/chefdk/2.3.17-2 (наш путь установки будет включать в себя версию Chef).

Dockerfile для сборки dappdeps/chefdk можно найти здесь.

Собираем dappdeps/ansible

Для сборки Ansible также заводим Omnibus-проект, в котором устанавливаем интерпретатор Python, pip и описываем software-рецепт для Ansible:

name "ansible"

ANSIBLE_GIT_TAG = "v2.4.4.0+dapp-6"

dependency "python"
dependency "pip"

build do
  command "#{install_dir}/embedded/bin/pip install https://github.com/flant/ansible/archive/#{ANSIBLE_GIT_TAG}.tar.gz"
  command "#{install_dir}/embedded/bin/pip install pyopenssl"
end

Как видно, образ с Ansible представляет собой встроенный Python, pip и установленный через pip Ansible с зависимостями.

Проект Omnibus и Dockerfile для dappdeps/ansible можно найти здесь.

Как использовать дистрибутив dappdeps?

Чтобы пользоваться образами dappdeps через монтирование томов, предварительно для каждого образа необходимо создать контейнер и указать, какой том хранится в этом контейнере. Этого требует Docker на данный момент.

$ docker create --name dappdeps-toolchain --volume /.dapp/deps/toolchain/0.1.1 dappdeps/toolchain:0.1.1 no-such-cmd
13edda732176a44d7d822202d8327565b78f4a2190368bb1df46cdad1e127b6e
$ docker ps -a | grep dappdeps-toolchain
13edda732176        dappdeps/toolchain:0.1.1      "no-such-cmd"       About a minute ago   Created                                 dappdeps-toolchain

Контейнер называется dappdeps-toolchain: по этому имени все объявленные томы этого контейнера можно использовать для монтирования в другие контейнеры с помощью --volumes-from. Параметр-команду с произвольным текстом no-such-cmd требуется указать для Docker, но данный контейнер никогда не будет запущен — он так и останется в состоянии Created.

Создаем остальные контейнеры:

$ docker create --name dappdeps-base --volume /.dapp/deps/base/0.2.3 dappdeps/base:0.2.3 no-such-cmd
20f524c5b8b4a59112b4b7cb85e47eee660c7906fb72a4935a767a215c89964e
$ docker create --name dappdeps-ansible --volume /.dapp/deps/ansible/2.4.4.0-10 dappdeps/ansible:2.4.4.0-10 no-such-cmd
cd01ae8b69cd68e0611bb6c323040ce202e8e7e6456a3f03a4d0a3ffbbf2c510
$ docker create --name dappdeps-gitartifact --volume /.dapp/deps/gitartifact/0.2.1 dappdeps/gitartifact:0.2.1 no-such-cmd
2c12a8743c2b238d90debaf066e29685b41b138c10f2b893a815931df866576d
$ docker create --name dappdeps-chefdk --volume /.dapp/deps/chefdk/2.3.17-2 dappdeps/chefdk:2.3.17-2 no-such-cmd
4dffe74c49c8e4cdf9d749177ae9efec3bdae6e37c8b6df41b6eb527a5c1d891

Вот мы и дошли до кульминации, ради которой задумывался весь этот сыр-бор. Итак, в качестве демонстрации возможностей установим в образ Alpine пакеты nginx и tree, запустив Ansible из dappdeps/ansible через Bash из dappdeps/base:

$ docker run -ti --name mycontainer --volumes-from dappdeps-toolchain --volumes-from dappdeps-base --volumes-from dappdeps-gitartifact --volumes-from dappdeps-ansible --volumes-from dappdeps-chefdk alpine:latest /.dapp/deps/base/0.2.3/embedded/bin/bash -lc '/.dapp/deps/ansible/2.4.4.0-10/embedded/bin/ansible localhost -m apk -a "name=nginx,tree update_cache=yes"'
 [WARNING]: Unable to parse /etc/ansible/hosts as an inventory source

 [WARNING]: No inventory was parsed, only implicit localhost is available

 [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

localhost | SUCCESS => {
    "changed": true, 
    "failed": false, 
    "msg": "installed nginx tree package(s)", 
    "packages": [
        "pcre", 
        "nginx", 
        "tree"
    ], 
    "stderr": "", 
    "stderr_lines": [], 
    "stdout": "(1/3) Installing pcre (8.41-r1)n(2/3) Installing nginx (1.12.2-r3)nExecuting nginx-1.12.2-r3.pre-installn(3/3) Installing tree (1.7.0-r1)nExecuting busybox-1.27.2-r7.triggernOK: 6 MiB in 14 packagesn", 
    "stdout_lines": [
        "(1/3) Installing pcre (8.41-r1)", 
        "(2/3) Installing nginx (1.12.2-r3)", 
        "Executing nginx-1.12.2-r3.pre-install", 
        "(3/3) Installing tree (1.7.0-r1)", 
        "Executing busybox-1.27.2-r7.trigger", 
        "OK: 6 MiB in 14 packages"
    ]
}

Финальный аккорд — создаем образ из получившегося контейнера и… видим, что от dappdeps в нем остались лишь пустые директории mount-point'ов!

$ docker commit mycontainer myimage
sha256:9646be723b91daeaf538b7d92bb8844578abc7acd3028394f543e883eeb382bb
$ docker run -ti --rm myimage tree /.dapp
/.dapp
└── deps
    ├── ansible
    │   └── 2.4.4.0-10
    ├── base
    │   └── 0.2.3
    ├── chefdk
    │   └── 2.3.17-2
    ├── gitartifact
    │   └── 0.2.1
    └── toolchain
        └── 0.1.1

11 directories, 0 files

Linux-дистрибутив from scratch для сборки Docker-образов — наш опыт с dappdeps - 4

Казалось бы, о чем еще можно мечтать?..

Дальнейшие работы и проблемы

Какие проблемы с dappdeps?

Необходимо провести работу по уменьшению размеров dappdeps/toolchain. Для этого надо разделить toolchain на 2 части: часть, необходимая для сборки новых утилит в dappdeps, и часть с базовыми библиотеками типа glibc, которые необходимо монтировать в runtime уже для запуска этих утилит.

Для работы модуля Ansible apt в dappdeps/ansible пришлось добавить содержимое пакета python-apt в Ubuntu прямо в образ без пересборки. В этом случае модуль apt работает без проблем в базовых образах на основе DEB, но требуется наличие glibc определенной версии. Поскольку сам apt — это дистрибутиво-специфичный модуль, то такое допустимо.

Чего не хватает в Dockerfile?

Для использования тома из образа dappdeps/toolchain приходится сначала создавать архив этого образа, а затем добавлять его в другой образ через директиву Dockerfile ADD (см. раздел «Используем Omnibus вместе с dappdeps/toolchain»). Со стороны Dockerfile не хватает функционала, который бы позволял просто подключать директорию другого образа на время сборки как VOLUME, т.е. аналог опции --volumes-from для Dockerfile.

Выводы

Мы убедились, что идея работает и позволяет использовать в сборочных инструкциях GNU- и другие CLI-утилиты, запускать интерпретатор Python или Ruby, запускать даже Ansible или Chef в Alpine или scratch-образах. При этом писателю сборочных инструкций не требуется знать побочный эффект выполнения запускаемых команд и явно перечислять, какие файлы необходимо импортировать, как в случае с Docker multi-stage builds.

Результаты данной работы применяются и на практике: dapp использует dappdeps-образы в сборочных контейнерах. Например, Git из dappdeps/gitartifact используется для работы с патчами, и утилита Git с некоторой гарантией ведет себя одинаково во всех базовых образах. Однако то, как dapp использует dappdeps, выходит за рамки данной статьи (ссылки на код для самых любопытных: dapp/deps, dapp/dimg/builder/chef.rb, dapp/dimg/builder/ansible.rb).

Целью данной статьи было донести саму идею и показать на реальном практическом примере возможность ее применения.

P.S. Все описанные dappdeps-образы доступны на hub.docker.com: dappdeps/toolchain:0.1.1, dappdeps/base:0.2.3, dappdeps/gitartifact0.2.1, dappdeps/ansible:2.4.4.0-10, dappdeps/chefdk:2.3.17-2 — ими можно пользоваться.

Автор: tkir

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js