В этой статье я хотел бы рассказать как можно создавать сценарии сборки имиджей для Docker контейнеров с помощью системы многоцелевых сценариев Sparrow*.
(*) Примечание — для понимания некоторых технических нюансов данной статьи желательно иметь хотя бы поверхностное знакомство с системой многоцелевых сценариев Sparrow, краткое введение в которую ( помимо страниц документации ) можно найти в моей предыдущей статье на habrahabr.ru.
Разработка Docker контейнеров
Вначале немного проблематики. Имеем задачу описать сборку Docker имиджа с помощью Dockerfile. Если сценарий сборки нетривиален и содержит множество инструкций, нужно как-то выкручиваться. Помимо того, что Dockerfile не может содержать более 120 слоев ( насколько я правильно понял из документации по Docker ), иметь дело с развесистым Dockerfile не очень приятно. Что можно с этим поделать? Очевидные варианты — вынести код сборки в отдельные Bash скрипты в рабочую директорию и делать установку и настройку системы прямо из них. Другой способ — "прикручивать" сбоку какой-нибудь configuration management tool типа chef или ansible. Оставляю на откуп читателю оценку данных альтернатив ( IMHO у них есть свои плюсы и минусы ) и предлагаю третий способ — использование Sparrow.
Прежде чем приводить детали реализации хочется сказать:
-
Вариант со Sparrow чем-то то очень похож на использование Bash скриптов, с той лишь разницей, что вся логика установки выносится в Sparrow плагин, со своим исходным кодом, хранящимся в отдельном месте ( git репозитарий или центральное хранилище ).
-
Таким образом, базовая настройка системы в контексте Docker описана в Dockerfile, а более тонкая и сложная внутри Sparrow плагина.
- Разработка плагина может быть отделена от контекста Dockerfile — что удобно, например вы можете на существующем Docker контейнере отлаживать процесс установки, на прибегая каждый раз к постройке имиджа командой
docker build
, и, как только плагин будет отлажен и готов к работе, можно еще раз прогнать полный цикл сборки системы посредством все той же командойdocker build
( сбросив при этом docker кэш разумеется ).
Пример реализации
Итак, покажем все на конкретной системе. Требуется собрать имидж с дистрибутивом CentOS и установить приложение, написанное на Ruby версии равной 2.3. После этого запустить основной скрипт приложения из под выделенного пользователя. Исходный код приложения скачивается с некого архивного сервера. Пример взят из реальной жизни, хотя некоторые детали намеренно опущены, дабы не перегружать статью материалом.
Базовая конфигурация системы
Прежде чем писать код плагина, создадим Dockerfile. За базовый имидж я взял tutum/centos по причине его легковесности. По этой же причине приходится доставлять часть пакетов, но в целом это не является какой-то проблемой.
$ cat Dockerfile
FROM tutum/centos
MAINTAINER "melezhik" <melezhik@gmail.com>
RUN yum clean all
RUN yum -y install nano git-core
RUN yum -y install make
RUN yum -y install gcc
RUN yum -y install perl perl-devel
perl-Test-Simple perl-Digest-SHA perl-Digest-MD5 perl-CPAN-Meta
perl-CPAN-Meta-Requirements perl-Getopt-Long
perl-JSON perl-Module-CoreList perl-Module-Metadata perl-parent perl-Path-Tiny perl-Try-Tiny
perl-App-cpanminus perl-JSON-PP perl-Algorithm-Diff perl-Text-Diff
perl-Spiffy perl-Test-Base perl-YAML perl-File-ShareDir-Install perl-Class-Inspector
perl-File-ShareDir perl-File-ShareDir-Install perl-Config-General
RUN cd /bin/ && curl -L https://cpanmin.us/ -o cpanm && chmod +x cpanm
RUN cpanm Sparrow -q
Немного комментариев по Dockerfile .
-
nano
иgit-core
необходимы для разработки Sparrow плагина ( смотрите далее ) — мы будем редактировать код сценариев и коммитить изменения в удаленный git репозитарий. -
gcc
,make
потребуются для сборки RubyGems и CPAN пакетов. Первые потребуются при установки Ruby через rvm, последние для установки Sparrow. -
Установка многочисленных
perl-*
пакетов черезyum
необходима для оптимизации процесса сборки по скорости, можно было бы не делать этого, т.к. следующая инструкцияcpanm -q Sparrow
установила бы требуемые зависимости сама, но установка зависимостей через cpanm в общем случае требует гораздо больше времени, чем установка "нативных" для CentOS rpm-ок. - Инструкция
cpanm Sparrow -q
ставит среду разработки многоцелевых сценариев, не забываем, что мы собираемся разрабатывать Sparrow прямо на запущенном Docker контейнере.
Итак, попробуем создать имидж:
$ docker build -t ruby_app .
...
...
Successfully built 25e7cd784c99
Начинаем разрабатывать плагин
Отлично, имидж с базовой инфраструктурой у нас есть, можно запустить Docker контейнер и начать разработку плагина прямо на нем.
$ docker run -t -i ruby_app /bin/bash
$ mkdir ruby-app
$ cd ruby-app
$ git init .
$ git remote add origin https://github.com/melezhik/ruby-app.git
$ touch README.md
$ git add README.md
$ git config --global user.email "melezhik@gmail.com"
$ git config --global user.name "Alexey Melezhik"
$ git commit -a -m 'first commit'
$ git push -u origin master
Вышеуказанными командами мы создали шаблон проекта для нашего плагина и закоммитили все в удаленный git репозитарий. URL репозитария мы запомним, он понадобится нам далее, когда мы будем проводить полноценную сборку имиджа командой docker build
Теперь сделаем небольшой отступление. Вспомним нашу задачу. Попробуем для удобства разбить ее на независимые части:
- Создание аккаунта пользователя приложения
- Установка Ruby посредством rvm
- Скачивание архива приложения, распаковка и установка зависимостей
Для логически отдельных задач в Sparrow предусмотрен механизм модулей, воспользуемся им. Но прежде всего создадим основную историю, в которой будем делегировать выполнение задач разным модулям. Итак, все на том же запущенном Docker контейнере:
$ nano hook.bash
action=$(config action)
for s in $action
do
run_story $s
done
set_stdout install-ok
$ nano story.check
install-ok
Немного комментариев по коду. Мы имеем три второстепенных истории ( модули ) и одну основную, заданную хук файлом (hook.bash), для того, что показать как все это работает создадим заглушки для сценариев в модулях. Да, и дефолтное значение для входного параметра action
должно быть задано в suite.ini
файле.
$ nano suite.ini
action create-user install-ruby install-app
Создаем заглушки сценариев:
$ mkdir -p modules/create-user
$ mkdir -p modules/install-ruby
$ mkdir -p modules/install-app
$ nano modules/create-user/story.bash
echo create-user-ok
$ nano modules/install-ruby/story.bash
echo install-ruby-ok
$ nano modules/install-app/story.bash
echo install-app-ok
А также проверочные файлы:
$ nano modules/create-user/story.check
create-user-ok
$ nano modules/install-ruby/story.check
install-ruby-ok
$ nano modules/install-app/story.check
install-app-ok
Теперь запустим все через strun — консольный скрипт для выполнения Sparrow сценариев:
$ strun
/tmp/.outthentic/93/ruby-app/story.t ..
# [/ruby-app/modules/create-user]
# create-user-ok
ok 1 - output match 'create-user-ok'
# [/ruby-app/modules/install-ruby]
# install-ruby-ok
ok 2 - output match 'install-ruby-ok'
# [/ruby-app/modules/install-app]
# install-app-ok
ok 3 - output match 'install-app-ok'
# [/ruby-app]
# install-ok
ok 4 - output match 'install-ok'
1..4
ok
All tests successful.
Files=1, Tests=4, 0 wallclock secs ( 0.00 usr 0.02 sys + 0.09 cusr 0.01 csys = 0.12 CPU)
Result: PASS
Отлично. Мы видим, что все сценарии отработали успешно, это и будет скелетом нашего будущего плагина. Осталось только заимплиментить заглушки наших модулей.
Сценарий создания пользователя
Будем исходить из того что имя пользователя является настраиваемым, дефолтное значение определяем в файле suite.ini
:
$ cat suite.ini
action create-user install-ruby install-app
user_name app-user
Теперь реализация сценария:
$ nano modules/create-user/story.bash
user_id=$(config user_name)
echo create user id: $user_id
useradd -r -m -d /home/$user_id $user_id || exit 1
ls -d /home/$user_id || exit 1
id $user_id || exit 1
echo create-user-ok
И запуск ( обратите внимание, что здесь мы воспользовались возможностью запуска отдельного сценария с помощью параметра action
):
$ strun --param action=create-user
/tmp/.outthentic/135/ruby-app/story.t ..
# [/ruby-app/modules/create-user]
# create user id: app-user
# /home/app-user
# uid=997(app-user) gid=995(app-user) groups=995(app-user)
# create-user-ok
ok 1 - output match 'create-user-ok'
# [/ruby-app]
# install-ok
ok 2 - output match 'install-ok'
1..2
ok
All tests successful.
Files=1, Tests=2, 0 wallclock secs ( 0.03 usr 0.00 sys + 0.11 cusr 0.04 csys = 0.18 CPU)
Result: PASS
Мы видим, что сценарий отработал и пользователь создался. Обратите внимание, что большинство Bash команд внутри сценария завершаются идиоматической конструкцией cmd || exit 1
, strun
проверяет код выполнения сценария и если он неуспешен, то соответствующий тест проваливается, например так — попробуем создать пользователя с невалидным для системы именем:
$ strun --param action=create-user --param user_name='/'
/tmp/.outthentic/160/ruby-app/story.t ..
# [/ruby-app/modules/create-user]
# create user id: /
# useradd: invalid user name '/'
not ok 1 - scenario succeeded
not ok 2 - output match 'create-user-ok'
# [/ruby-app]
# install-ok
ok 3 - output match 'install-ok'
1..3
# Failed test 'scenario succeeded'
# at /usr/local/share/perl5/Outthentic.pm line 167.
# Failed test 'output match 'create-user-ok''
# at /usr/local/share/perl5/Outthentic.pm line 213.
# Looks like you failed 2 tests of 3.
Dubious, test returned 2 (wstat 512, 0x200)
Failed 2/3 subtests
Test Summary Report
-------------------
/tmp/.outthentic/160/ruby-app/story.t (Wstat: 512 Tests: 3 Failed: 2)
Failed tests: 1-2
Non-zero exit status: 2
Files=1, Tests=3, 0 wallclock secs ( 0.02 usr 0.00 sys + 0.10 cusr 0.00 csys = 0.12 CPU)
Result: FAIL
Сделаю здесь еще небольшое отступление. Зададимся вопросом зачем нам нужно проверочные файлы, если по-сути проверки кода завершения сценария должно быть достаточно. Резонный вопрос. Мы можем думать о проверочных правилах фреймворка Sparrow как о некой альтернативном способе контроля или верификации выполнения наших скриптов. В идеологии Sparrow любой выполняемый сценарий является историей в том смысле, что это некий скрипт, который запускается и чаще всего "сообщает" что-то о своей работе — образно говоря "оставляя след в истории". Это след — стандартный выходной поток stdout, содержимое которого можно провалидировать. Почему это может быть полезно:
-
Не всегда успешный код завершения означает, что все идет хорошо
- Иногда хочется не выходить из скрипта аварийно ( посредством
cmd || exit 1
), позволив скрипту сделать свою работу до конца и отложить верификацию посредством проверки через проверочный файл.
В качестве конкретного примера можно привести сценарий установки Ruby через rvm, который идет следующим по списку в нашем плане.
Сценарий установки Ruby из rvm
Вот как будет выглядеть сценарий установки:
$ nano modules/install-ruby/story.bash
yum -y install which
curl -sSL https://rvm.io/mpapis.asc | gpg2 --import - || exit 1
curl -sSL https://get.rvm.io | bash -s stable --ruby || exit 1
source /usr/local/rvm/scripts/rvm
gem install bundler --no-ri --no-rdoc
echo ruby version: $(ruby --version)
bundler --version
echo install-ruby-ok
А это — проверочный файл:
$ nano modules/install-ruby/story.check
regexp: ruby version: ruby 2.3
install-ruby-ok
Теперь запустим данный сценарий:
$ strun --param action=install-ruby
# большая часть вывода
# здесь опущена
# ...
# ...
# ...
# ruby version: ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-linux]
# Bundler version 1.12.5
# install-ruby-ok
ok 1 - output match /ruby version: ruby 2.3/
ok 2 - output match 'install-ruby-ok'
# [/ruby-app]
# install-ok
ok 3 - output match 'install-ok'
1..3
ok
All tests successful.
Files=1, Tests=3, 91 wallclock secs ( 0.03 usr 0.00 sys + 3.24 cusr 1.03 csys = 4.30 CPU)
Result: PASS
Хочется обратить внимание, что для верификации версии установленного Ruby мы воспользовались проверочным правилом ввиде регулярного выражения:
regexp: ruby version: ruby 2.3
Конечно rvm позволяет устанавливать требуемую версию явно, просто хотелось здесь привести пример когда проверки, определенные в проверочных файлах позволяют добавить дополнительную верификацию работы сценария с минимальными усилиями.
Теперь можно перейти к сценарию установки приложения.
Сценарий установки приложения
Напомню. Нам будет необходимо:
- скачать тарбол по заданному урлу
- распаковать архив
- перейти в распакованную папку и выполнить команду
bundle install --target ./local
для установки зависимостей
На этом все. Конечно в реальном приложении, нам бы пришлось бы еще запустить какой-нибудь сервис или совершить еще какие-нибудь дополнительные операции, но для демонстрации работы плагина этого должно быть достаточно.
Опять таки же для простоты примера пусть у нас есть Ruby приложение состоящее из:
Gemfile
— в котором будут прописаны зависимостиhello.rb
— запускаемого скрипа, который просто выводит в консоль строчкуHello World
Пакуем все архив и выкладываем все на архивный сервер га локальный nginx, теперь дистрибутив будет доступен по URL:
127.0.0.1/app.tar.gz
Обновим код сценария.
$ cat suite.ini
action create-user install-ruby install-app
user_name app-user
source_url 127.0.0.1/app.tar.gz
$ cat modules/install-app/story.bash
user_id=$(config user_name)
source_url=$(config source_url)
yum -y -q install sudo
echo downloading $source_url ...
sudo -u $user_id -E bash --login -c "curl -f -o ~/app.tar.gz $source_url -s" || exit 1
echo unpacking tarball ...
sudo -u $user_id -E bash --login -c "cd ~/ && tar -xzf app.tar.gz" || exit 1
echo installing dependencies via bundler
sudo -u $user_id -E bash --login -c "cd ~/app && bundle install --quiet --path vendor/bundle " || exit 1
sudo -u $user_id -E bash --login -c "cd ~/app && bundle exec ruby hello.rb " || exit 1
echo install-app-ok
$ nano modules/install-app/story.check
install-app-ok
Hello World
Небольшие комментарии по сценарию:
-
Установку делаем из-под пользователя заданного в конфигурации плагина suite.ini. Для этого нам нужен пакет
sudo
-
Последняя команда запускает скрипт приложения
hello.rb
- В проверочном файле требуем что бы в
stdout
был виден "след" от сценария — строчка 'Hello World'
Итак, запустим сценарий:
$ strun --param action=install-app
/tmp/.outthentic/16462/ruby-app/story.t ..
# [/ruby-app/modules/install-app]
# Package sudo-1.8.6p7-17.el7_2.x86_64 already installed and latest version
# downloading 127.0.0.1/app.tar.gz ...
# unpacking app ...
# installing dependencies via bundler
# Hello World
# install-app-ok
ok 1 - output match 'install-app-ok'
ok 2 - output match 'Hello World'
# [/ruby-app]
# install-ok
ok 3 - output match 'install-ok'
1..3
ok
All tests successful.
Files=1, Tests=3, 2 wallclock secs ( 0.01 usr 0.00 sys + 1.61 cusr 0.50 csys = 2.12 CPU)
Result: PASS
Как мы видим приложение действительно установилось и скрипт hello.rb
запускается. Добавим еще один "параноидальный" ассерт в проверочный файл для демонстрации возможностей системы проверок Sparrow:
$ nano modules/install-app/story.check
install-app-ok
Hello World
generator: <<CODE
!bash
if test -d /home/$(config user_name)/app; then
echo assert: 1 directory /home/$(config user_name)/app exists
else
echo assert: 0 directory /home/$(config user_name)/app exists
fi
CODE
И запустим сценарий заново.
$ strun --param action=install-app
В выводе получим:
$ ok 3 - directory /home/app-user/app exists
Публикация Sparrow плагина
На этом создание плагина завершено. Закоммитим изменения и сделаем "push" в git репозитарий:
$ git add .
$ git commit -a -m 'all done'
$ git push
$ exit
Мы вышли из докер контейнера он нам больше не нужен, можно его удалить:
$ docker rm 5e1037fa4aef
Полный цикл сборки имиджа для Docker контейнера
Осталось чуть-чуть изменить Dockerfile, вспоминаем о том, что нам понадобится ссылка на удаленный git репозитарий, где мы разместили код нашего Sparrow плагина, окончательный вариант будет таким:
FROM tutum/centos
MAINTAINER "melezhik" <melezhik@gmail.com>
RUN yum clean all
RUN yum -y install nano git-core
RUN yum -y install make
RUN yum -y install gcc
RUN yum -y install perl perl-devel
perl-Test-Simple perl-Digest-SHA perl-Digest-MD5 perl-CPAN-Meta
perl-CPAN-Meta-Requirements perl-Getopt-Long
perl-JSON perl-Module-CoreList perl-Module-Metadata perl-parent perl-Path-Tiny perl-Try-Tiny
perl-App-cpanminus perl-JSON-PP perl-Algorithm-Diff perl-Text-Diff
perl-Spiffy perl-Test-Base perl-YAML perl-File-ShareDir-Install perl-Class-Inspector
perl-File-ShareDir perl-File-ShareDir-Install perl-Config-General
RUN cd /bin/ && curl -L https://cpanmin.us/ -o cpanm && chmod +x cpanm
RUN cpanm Sparrow -q
RUN echo ruby-app https://github.com/melezhik/ruby-app.git > /root/sparrow.list
RUN sparrow plg install ruby-app
RUN sparrow plg run ruby-app
Теперь мы можем осуществить полный цикл сборки имиджа, "проиграв" все заново:
$ docker build -t ruby_app --no-cache=true .
В итоге мы получим Docker имидж с требуемой системой.
Заключение
Применение системы многоцелевых сценариев Sparrow может быть эффективным средством построения Docker имиджей, т.к. позволяет строить сложные конфигурации, оставляя основной Dockerfile простым и лаконичным, а так же упрощая процесс разработки самих сценариев конфигурирования требуемой системы.
Спасибо за внимание.
Как обычно жду вопросов и конструктивной критики! :)
Алексей
Автор: alexey_melezhik