Разработка Docker контейнеров с помощью системы многоцелевых сценариев Sparrow

в 11:28, , рубрики: bash, linux, perl, ruby, UNIX

В этой статье я хотел бы рассказать как можно создавать сценарии сборки имиджей для 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

Источник

Поделиться новостью

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