- PVSM.RU - https://www.pvsm.ru -
Сборка образов для Docker на основе базового образа, как правило, предполагает вызов команд в окружении этого базового образа. Например — вызов команды apt-get, которая есть в базовом образе, для установки новых пакетов.
Часто возникает необходимость доустановить в базовую систему некоторый набор утилит, с помощью которых происходит установка или сборка некоторых файлов, которые требуются в итоговом образе. Например, чтобы собрать Go-приложение, надо установить компилятор Go, положить все исходные коды приложения в базовом образе, скомпилировать требуемую программу. Однако в итоговом образе требуется лишь скомпилированная программа без всего набора утилит, который использовался для компиляции этой программы.
Проблема известная: одним из путей её решения может быть сборка вспомогательного образа и перенос файлов из вспомогательного образа в результирующий. Для этого появились Docker multi-stage builds [1] или образы-артефакты в dapp [2]. И данный подход идеально решает проблему подобную переносу результатов компиляции исходных кодов в итоговый образ. Однако он не решает все возможные проблемы…
Вот другой пример: для сборки образа используется Chef в локальном режиме. Для этого в базовый образ ставится chefdk, монтируются или добавляются рецепты, запускаются эти рецепты, которые настраивают образ, устанавливают новые компоненты, пакеты, файлы-конфиги и прочее. Аналогично может быть использована другая система управления конфигурациями — например, Ansible. Однако установленный chefdk занимает около 500 Мб и существенно увеличивает размеры итогового образа — оставлять его там нет смысла.
Но multi-stage builds в Docker уже не решат эту проблему. Что, если пользователю не хочется знать о том, каков побочный эффект работы программы — в частности, какие файлы она создает? Например, чтобы не держать лишние явные описания всех экспортируемых путей из образа. Хочется просто запустить программу, получить какой-то результат в образе, но чтобы программа и все окружение, нужное для ее работы, осталось вне итогового образа.
В случае с chefdk можно было бы монтировать директорию с этим chefdk в сборочный образ на время сборки. Но с этим решением есть проблемы:
А что, если мы сможем подготовить некий статичный неизменный набор всех возможных полезных утилит, который будет так хитро слинкован, что будет работать в любом базовом образе, даже scratch? После подключения такого/таких образов в базовый, в итоговом образе останется лишь пустая директория mount-point, в которую были подключены эти утилиты.
Необходимо получить образ, в котором содержится набор программ в некоторой статически определенной нестандартной директории — например, /myutils
. Любая программа в /myutils
должна зависеть только от библиотек в /myutils
.
Динамически скомпилированная программа в Linux зависит от местоположения линкера ld-linux [3] в системе. Например, бинарник 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 [4].
Почему набор образов нашего «дистрибутива» называется dappdeps [5]? Потому что эти образы использует сборщик dapp [6] — они собираются под нужды этого проекта.
Итак, наша конечная цель:
Образы dappdeps могут зависеть друг от друга. Например, при сборке dappdeps/base требуется toolchain и glibc из образа dappdeps/toolchain. После компиляции всех утилит в dappdeps/base для их работы в runtime будут требоваться файлы из dappdeps/toolchain.
Главное условие заключается в том, чтобы утилиты из этих образов располагались в нестандартном месте, а именно — в /.dapp/deps/
, и не зависели ни от каких утилит или библиотек в стандартных системных путях. Также в dappdeps-образах не должно быть никаких других файлов, кроме /.dapp/deps
.
Такие образы позволят создавать на их основе контейнеры с томами, где содержатся утилиты, и монтировать их в другие контейнеры с использованием опции --volumes-from
для Docker.
Глава 5 «Constructing a Temporary System» [12] руководства 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 можно найти здесь [13].
Итоговый образ dappdeps/toolchain — это и есть «temporary system» в терминологии LFS. GCC в данной системе все еще завязан на системные пути к библиотекам, однако мы не будем добиваться того, чтобы GCC работал в любом базовом образе. Образ dappdeps/toolchain — вспомогательный, он будет использоваться далее, в т.ч. для сборки уже реально независимых от общих системных библиотек программ.
Для сборки таких проектов, как chefdk [14] или GitLab [15], используется Omnibus [16]. Он позволяет создать самодостаточные наборы (self-contained bundle) с программой и всеми зависимыми библиотеками, кроме системного линкера и libc. Все инструкции описываются читаемыми удобными Ruby-рецептами. Также у проекта Omnibus есть библиотека уже написанных рецептов omnibus-software [17].
Итак, попробуем описать сборку остальных 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.
Проект для Omnibus описывается с помощью файла проекта dapp/dappdeps/base/omnibus/config/projects/dappdeps-base.rb
[18]:
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 [17]), либо в директории 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 можно найти здесь [19].
В случае с dappdeps-gitartifact необходим лишь рецепт сборки Git, а он уже есть в omnibus-software — остается только подключить его в текущий Omnibus. В остальном все аналогично.
Проект Omnibus и Dockerfile для dappdeps/gitartifact можно найти здесь [20].
Для chefdk тоже уже есть готовый проект Omnibus [14]. Остается лишь добавить его в сборочный контейнер через Dockerfile и заменить стандартные пути установки chefdk /opt/chefdk
на /.dapp/deps/chefdk/2.3.17-2
(наш путь установки будет включать в себя версию Chef).
Dockerfile для сборки dappdeps/chefdk можно найти здесь [21].
Для сборки 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 можно найти здесь [22].
Чтобы пользоваться образами 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
Казалось бы, о чем еще можно мечтать?..
Необходимо провести работу по уменьшению размеров dappdeps/toolchain. Для этого надо разделить toolchain на 2 части: часть, необходимая для сборки новых утилит в dappdeps, и часть с базовыми библиотеками типа glibc, которые необходимо монтировать в runtime уже для запуска этих утилит.
Для работы модуля Ansible apt в dappdeps/ansible пришлось добавить содержимое пакета python-apt в Ubuntu прямо в образ без пересборки. В этом случае модуль apt работает без проблем в базовых образах на основе DEB, но требуется наличие glibc определенной версии. Поскольку сам apt — это дистрибутиво-специфичный модуль, то такое допустимо.
Для использования тома из образа 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 [23], dapp/dimg/builder/chef.rb [24], dapp/dimg/builder/ansible.rb [25]).
Целью данной статьи было донести саму идею и показать на реальном практическом примере возможность ее применения.
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
Источник [26]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/linux/276827
Ссылки в тексте:
[1] Docker multi-stage builds: https://docs.docker.com/develop/develop-images/multistage-build/
[2] образы-артефакты в dapp: https://flant.github.io/dapp/artifact_for_advanced_build.html
[3] ld-linux: https://linux.die.net/man/8/ld-linux
[4] Linux From Scratch: http://www.linuxfromscratch.org/lfs/view/stable/
[5] dappdeps: https://github.com/flant/dapp/tree/master/dappdeps
[6] dapp: https://github.com/flant/dapp
[7] dappdeps/toolchain: https://github.com/flant/dapp/blob/master/dappdeps/toolchain
[8] dappdeps/base: https://github.com/flant/dapp/blob/master/dappdeps/base
[9] dappdeps/gitartifact: https://github.com/flant/dapp/blob/master/dappdeps/gitartifact
[10] dappdeps/chefdk: https://github.com/flant/dapp/blob/master/dappdeps/chefdk
[11] dappdeps/ansible: https://github.com/flant/dapp/blob/master/dappdeps/ansible
[12] Глава 5 «Constructing a Temporary System»: http://www.linuxfromscratch.org/lfs/view/stable/chapter05/chapter05.html
[13] здесь: https://github.com/flant/dapp/blob/master/dappdeps/toolchain/Dockerfile
[14] chefdk: https://github.com/chef/chef-dk
[15] GitLab: https://gitlab.com/gitlab-org/omnibus-gitlab
[16] Omnibus: https://github.com/chef/omnibus
[17] omnibus-software: https://github.com/chef/omnibus-software/
[18] dapp/dappdeps/base/omnibus/config/projects/dappdeps-base.rb
: https://github.com/flant/dapp/blob/master/dappdeps/base/omnibus/config/projects/dappdeps-base.rb
[19] здесь: https://github.com/flant/dapp/tree/master/dappdeps/base
[20] здесь: https://github.com/flant/dapp/tree/master/dappdeps/gitartifact
[21] здесь: https://github.com/flant/dapp/blob/master/dappdeps/chefdk/Dockerfile
[22] здесь: https://github.com/flant/dapp/tree/master/dappdeps/ansible
[23] dapp/deps: https://github.com/flant/dapp/tree/master/lib/dapp/dapp/deps
[24] dapp/dimg/builder/chef.rb: https://github.com/flant/dapp/blob/master/lib/dapp/dimg/builder/chef.rb
[25] dapp/dimg/builder/ansible.rb: https://github.com/flant/dapp/blob/master/lib/dapp/dimg/builder/ansible.rb
[26] Источник: https://habrahabr.ru/post/352432/?utm_campaign=352432
Нажмите здесь для печати.