Выпуск 32. Октябрь 2015
← Как стать Perl-автором | Содержание | Обзор CPAN за сентябрь 2015 г. →Развертывание Perl приложений при помощи Docker
Плюсы и минусы такой инфраструктуры
Вы все еще боитесь деплоить по пятницам? За предложение поставить компилятор на продакшен сервер сисадмин 2 часа бегал за вами с топором? Обновление библиотек ломает все приложения которые только может сломать и вдобавок сжигает блок притания сервера? Эти и другие проблемы нам поможет решить Docker
(а так же создаст некоторое кол-во новых).
Краткий ликбез.
Q: что такое Docker
?
A: Docker это инструмент для контейнерной виртуализации. Представляет собой обертку (libcontainer
) вокруг технологий изоляции приложений предоставляемых LXC
.
Q: Какая от него польза?
A: Он инкапсулирует зависимости и настройки приложения внутри контейнера. Грубо говоря, проблема поддержания окружения нужного для работы приложения инкапсулируется внутрь контейнера и сисадмину больше не приходится придумывать как заставить работать на одном сервере 2 приложения которые требуют несовместимых версий одной библиотеки. В идеальном случае ему нужен только сервер на котором стоит демон Docker
.
Q: Чем это лучше виртуальной машины?
A: Меньше сложность, в общем случае вам не надо настраивать ничего кроме вашего приложения. Ниже оверхед производительности и использования ОЗУ и диска хоста.
Q: Это можно использовать для обеспечения безопасности хоста?
A: Краткий ответ — нет, в общем случае Docker
ухудшает параметры безопасности хост-машины. Длинный ответ.
Приступим к внедрению
В офицальном репозитории Docker
есть готовые образы perl, но там есть только последние стабильные выпуски (5.20 и 5.22). Соответственно, если нужна другая версия или специфичные флаги компиляции — придется собирать образ самому. Чем мы и займемся.
Установим Docker
Для начала надо поставить Docker
. В репозитории Ubuntu 14.04
(которым я пользуюсь дома) версия Docker
безнадежно древняя (1.0.1) и кривая. Пользователям CentOS 6/7
повезло больше, в EPEL
версия 1.7, что практически близко к текущей 1.8. Так что для установки воспользуемся скриптом с официального сайта:
wget -qO- https://get.docker.com/ | sh
Да, не делайте так ни на чем кроме тестовой виртуалки, истинные параноики и те кто уже делают бэкапы не одобряют. Хоть эта команда и дана в официальной доке, но она легко может скомпрометировать вашу машину при определенных условиях.
Но для опытов сойдет, а для продакшена есть мануал по установке куда угодно — http://docs.docker.com/installation.
Тем временем у нас уже установился Docker
, проверим:
root@docker:~# docker --version
Docker version 1.8.3, build f4bf5c7
По умолчанию Docker
работает от root
, для использования в продакшене вариант спорный, но пока мы на этом заострять внимание не будем (у нас же пока тестовая виртуалка).
Проверим что все установилось корректно и даже делает вид что работает:
root@docker:~# docker run hello-world
Если все в порядке, то мы увидим много букв вида:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b901d36b6f2f: Pull complete
0a6ba66e537a: Pull complete
Digest: sha256:517f03be3f8169d84711c9ffb2b3235a4d27c1eb4ad147f6248c8040adb93113
Status: Downloaded newer image for hello-world:latest
WARNING: Your kernel does not support memory swappiness capabilities, memory swappiness discarded.
Docker
попытался запустить контейнер hello-world
, не нашел его локально, скачал из registry
и запустил. Warning вылез т.к. у меня на машине используется Debian 8, а ядром 3.16 поддерживаются не все функциональные возможности. Для совсем полной поддержки требуется ядро из 4-й ветки.
Далее мы увидим текст вида:
Hello from Docker.
This message shows that your installation appears to be working correctly.
....
Поздравляю, Docker
установился и работает.
Теперь пришло время создать наш собственный образ.
Создадим директорию для этих целей:
mkdir docker-perl
cd docker-perl
Создадим Dockerfile
:
FROM ubuntu:14.04
Можно собрать образ и посмотреть как это бывает:
root@docker:~/docker-perl# docker build -t docker-perl .
Sending build context to Docker daemon 2.048 kB
Step 0 : FROM debian:jessie
jessie: Pulling from library/debian
d0ca40da9e35: Pull complete
d1f66aef36c9: Pull complete
Digest: sha256:1179b696ceb85111a6928ba699d38a56a1f5e89fe0eef3277a3a2f51572da37a
Status: Downloaded newer image for debian:jessie
---> d1f66aef36c9
Removing intermediate container d0ca40da9e35
Successfully built d7b316059744
Ура, мы собрали первую часть базового образа Perl
, время создать образ с нужной нам версией.
Для установки Perl
в контейнер есть 2 варианта:
Первый — собрать его локально и сунуть в контейнер. Плюсы этого варианта — нам не требуется в контейнере ничего лишнего, что достаточно актуальная проблема. Т.к. после установки пакетов для сборки наш контейнер потолстел со 125мб, до 313мб:
root@docker:~/docker-perl# docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
docker-perl latest d7b316059744 8 minutes ago 313 MB
debian jessie d1f66aef36c9 5 days ago 125.1 MB
Минус — все пакеты придется собирать на хосте, что не имеет особого смысла если вы не собираетесь все это применять в промышленных масштабах.
Второй — собрать руками в контейнере нужную версию. Вариант для простых людей которым нужно чтобы просто работало.
Есть еще третий вариант — установить через perlbrew
в контейнере. Но для контейнера это самый сложный вариант с наибольшим кол-вом граблей по пути и неясным профитом. Я пробовал — если расписать весь объем проблем, то статья выйдет побольше этой.
Установка Perl в базовую систему и перенос в контейнер
Итак, рассмотрим первый вариант. Я буду делать не очень канонично — просто установлю Perl
с помощью perlbrew
в то же место на ФС где он и будет лежать в образе. Правильнее будет настроить chroot и установить туда или установить в нужный префикс, а потом перенести в контейнер. Но глобальных отличий между методами не будет, а люди которые собирают кастомный Perl настроят chroot без проблем, ну мне так кажется.
Устанавливаем perl
в /perl5
:
root@docker:~# export PERLBREW_ROOT=/perl5
root@docker:~# perlbrew init
root@docker:~# source /perl5/etc/bashrc
root@docker:~# perlbrew install perl-5.18.4
Fetching perl 5.18.4 as /perl5/dists/perl-5.18.4.tar.bz2
Download http://www.cpan.org/src/5.0/perl-5.18.4.tar.bz2 to /perl5/dists/perl-5.18.4.tar.bz2
Installing /perl5/build/perl-5.18.4 into /perl5/perls/perl-5.18.4
...
Копируем установленный Perl
в каталог сборки:
root@docker:~/docker-perl# cp -r /perl5 .
И приведем Dockerfile
к виду:
FROM ubuntu:14.04
COPY perl5 /perl5
ENV PATH /perl5/perls/perl-5.18.4/bin:$PATH
Зачем копировать Perl
в рабочую папку? При запуске docker build
, грубо говоря, создается chroot
в текущей папке сборки. И все что находится вне ее становится недоступно. Проблема в том, что символические ссылки при этом перестают работать.
Если сделать так:
root@docker:~/docker-perl# ln -s /perl5 perl5
То при сборке образа мы получим сообщение об ошибке:
root@docker:~/docker-perl# docker build -t docker-perl .
Sending build context to Docker daemon 2.56 kB
Step 0 : FROM ubuntu:14.04
---> 1d073211c498
Step 1 : COPY perl5 /
Forbidden path outside the build context: perl5 (/perl5)
Теоретически можно использовать hard-link, но в Ubuntu
хардлинки на директории запрещены в настройках, а менять я считаю не очень целесообразным.
Итак, скопировали Perl
, запускаем сборку:
root@docker:~/docker-perl# docker build -t docker-perl .
Sending build context to Docker daemon 250.5 MB
Step 0 : FROM ubuntu:14.04
---> 1d073211c498
Step 1 : COPY perl5 /perl5
---> 6039c2920dcf
Removing intermediate container ef8bbc7532d2
Step 2 : ENV PATH /perl5/perls/perl-5.18.4/bin:$PATH
---> Running in 51a9fe8521b0
---> 77e9682a6241
Removing intermediate container 51a9fe8521b0
Проверяем:
root@docker:~/docker-perl# docker run -it docker-perl bash
root@98d5a2b98558:/#
Приглашение тонко намекает нам что мы находимся в более другом шелле чем до запуска контейнера, а именно в шелле контейнера.
Проверяем что Perl установлен корректно и нужной нам версии:
root@8ea3d44d36c0:/# perl -v
This is perl 5, version 18, subversion 4 (v5.18.4) built for x86_64-linux
...
Если вы увидели что это приглашение — то все в порядке и можно собирать контейнер дальше.
Запускать мы будем классическое Enterprise приложение “hello world”
Для этого создадим еще один контейнер дабы не пересобирать каждый раз основной, т.к. это долго и приводит к замусориванию системы контейнерами. Т.к. на каждую сборку Docker создает промежуточный контейнер и не удаляет его автоматически.
root@docker:~# mkdir docker-app
root@docker:~# cd docker-app
root@docker:~/docker-app# cp ../docker-perl/Dockerfile .
root@docker:~/docker-app# vim app.pl
Содержимое app.pl
#!/usr/bin/env perl
print "Hello world from container!\n";
Выдадим ему права на запуск, затем создадим Dockerfile
:
root@docker:~/docker-app# chmod +x app.pl
root@docker:~/docker-app# vim Dockerfile
Содержимое Dockerfile
:
FROM docker-perl
COPY app.pl /app/
WORKDIR /app
CMD /app/app.pl
И соберем контейнер:
root@docker:~/docker-app# docker build -t perl-app .
Sending build context to Docker daemon 3.072 kB
Step 0 : FROM docker-perl
---> 77e9682a6241
Step 1 : COPY app.pl /app/
---> Using cache
---> 045ca8292484
Step 2 : WORKDIR /app
---> Using cache
---> 50b52640e8d1
Step 3 : CMD /app/app.pl
---> Running in 1ef85c52970b
---> fe01b30beff4
Removing intermediate container 1ef85c52970b
Successfully built fe01b30beff4
И запускаем его:
root@docker:~/docker-app# docker run perl-app
Hello from container!
Поздравляю, вы только что собрали и запустили контейнер с perl-приложением.
Что делать если приложению нужны модули устанавливаемые в систему? Краткий ответ — страдать. Длинный ответ — модули надо установить в базовую систему и обновить образ docker-perl от которого унаследован наш образ, затем обновить образ с приложением. Т.е. каждый раз повторять все что мы проделали выше в немного сокращенном виде.
Так заморачиваться имеет смысл, если стоят повышенные требования к составу ПО в образе или из любви к прекрасному (но тогда надо собирать deb-пакеты, чтобы уж совсем прекрасно было). Если их нет — то все можно сделать проще — собирать зависимости прямо в контейнере.
Устанавливаем Perl в контейнере
Модифицируем наш первоначальный образ:
Во первых — установим мета-пакет для сборки. Во вторых — установим нужный Perl
прямо в контейнер.
Приведем Dockerfile
к виду:
FROM ubuntu:14.04
RUN apt-get -y update && apt-get install -y build-essential
ADD http://www.cpan.org/src/5.0/perl-5.18.4.tar.gz /tmp/
RUN cd /tmp && tar -zxf /tmp/perl-5.18.4.tar.gz
WORKDIR /tmp/perl-5.18.4
RUN ./Configure -des -Dprefix=/perl5 && make && make test && make install
ENV PATH /perl5/bin:$PATH
WORKDIR /
RUN rm -rf /tmp/perl-5.18.4.tar.gz /tmp/perl-5.18.4
Что здесь происходит?
RUN apt-get -y update && apt-get install -y build-essential
Эта команда запустит apt который обновит дерево пакетов и установит метапакет с компиляторами и заголовочными файлами.
ADD http://www.cpan.org/src/5.0/perl-5.18.4.tar.gz /tmp/
Скачает с указанного адреса Perl
и положит в /tmp/
. Если у вас не очень быстрый интернет — то можно скачать дистрибутив вручную и добавлять его из папки где происходит сборка. Тогда будет не нужна следующая операция распаковки архива, т.к. ADD
при работе с локальными архивами распаковывает их в целевую директорию. Но на моем интернете получается быстрее скачать, чем аплоадить увеличившийся build-context
в docker-демон.
Дальше все стандартно — устанавливаем рабочую директорию в директрорию где распаковыны сырцы и собираем Perl
в /perl5
, после чего выставляем окружение и чистим за собой.
Теперь пришло время создать контейнер для нашего enterprise-приложения. Переходим в директорию где оно собирается и модифицируем app.pl
:
#!/usr/bin/env perl
use Dancer2;
get '/' => sub {
return 'Hello World!';
};
start;
Теперь правим Dockerfile
т.к. нам надо установить зависимости приложения.
FROM docker-perl
COPY app.pl /app/
WORKDIR /app
RUN cpan -i Dancer2
ENTRYPOINT /app/app.pl
Собираем:
root@docker:~/docker-app# docker build -t perl-app .
Sending build context to Docker daemon 3.072 kB
Step 0 : FROM docker-perl
---> 987aba3529c5
Step 1 : COPY app.pl /app/
---> 134e2e82fb74
Removing intermediate container 47436b48e7d5
Step 2 : WORKDIR /app
---> Running in 998eb9641a97
---> eb3bfc1ccc34
Removing intermediate container 998eb9641a97
Step 3 : RUN cpan -i Dancer2
---> Running in 6609cd09aca6
...
Пока ставятся зависимости к Dancer2 можно сходить попить чайку.
Запускаем:
root@docker:~/docker-app# docker run -td -p 3000:3000 perl-app
4548826053f5500203f7b516040b34dfab9e246086924cf18b73a0e02da4947b
Контейнер запустился и выдал свой ID. Теперь если зайти на 3000 порт хост-машины мы увидим приветствие от нашего приложения.
Можно посмотреть список запущенных контейнеров:
root@docker:~/docker-perl# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4548826053f5 perl-app "/app/app.pl" 4 seconds ago Up 3 seconds 0.0.0.0:3000->3000/tcp gloomy_turing
Ну и остановить контейнер:
root@docker:~/docker-perl# docker stop 4548826053f5
Контейнеру сначала будет послан сигнал SIGTERM
, а через 15 секунд SIGKILL
, так что если вашему приложению нужно время на завершение — учитывайте это.
Теперь у вас есть рабочий контейнер с Perl и вашим приложением, можно дальше углубиться в волшебный мир DevOps с его контейнерами, облачными серверами и прочими серебрянными пулями в промышленных масштабах ;)
А теперь о самом веселом — о граблях!
Что не может не радовать — тут практически нет Perl
-специфичных граблей, со всеми ними вы столкнетесь при сборке любого контейнера.
Изначальная грабля из мануала докера, запускаем контейнер так:
docker run -it -p 3000:3000 perl-app
И все, терминал потерян. Контейнер с этого терминала не остановить, после остановки контейнера его корежит неимоверно, приходится сбрасывать через reset. Бага из серии первого входа в vim, ничего критичного, но нериятно.
Дальше, т.к. контейнер это минимальное окружение, то этого самого окружения там реально по минимуму.
root@docker:~/docker-app# docker run -it --rm docker-perl bash
root@0fe4d45827d0:/tmp/perl-5.18.4# env
HOSTNAME=0fe4d45827d0
TERM=xterm
LS_COLORS= ...
PATH=/perl5/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/tmp/perl-5.18.4
SHLVL=1
HOME=/root
LESSOPEN=| /usr/bin/lesspipe %s
LESSCLOSE=/usr/bin/lesspipe %s %s
_=/usr/bin/env
Если ваше приложение рассчитывает на какие-то переменные окружения, то за этим надо следить и не рассчитывать на то что они выставлены по умолчанию как в настоящих операционках. Особенно весело становится когда используется большое кол-во зависимостей и приложение только что работавшее на хосте падает в контейнере.
Еще небольшая грабелька — Docker
активно использует кэширование операций. Что это значит? Рассмотрим наш Dockerfile
от базового образа:
FROM ubuntu:14.04
RUN apt-get -y update && apt-get install -y build-essential
ADD http://www.cpan.org/src/5.0/perl-5.18.4.tar.gz /tmp/
RUN cd /tmp && tar -zxf /tmp/perl-5.18.4.tar.gz
WORKDIR /tmp/perl-5.18.4
RUN ./Configure -des -Dprefix=/perl5 && make && make test && make install
ENV PATH /perl5/bin:$PATH
Допустим, нам надо установить что-то из пакетов, пусть это будут заголовочные файлы psql. Мы можем сделать так:
RUN apt-get -y update && apt-get install -y build-essential libpq-dev
К чему это приведет? Это приведет к инвалидации ВСЕХ кэшей операций после этой операции. Т.е. в данном случае у нас будет запущена сборка образа с нуля. Это правильный вариант для подготовки финального продакшен образа, но раздражающий при активной разработке приложения. Так что в данном случае правильнее сделать так:
....
ENV PATH /perl5/bin:$PATH
RUN apt-get install -y libpq-dev
Тогда мы используем кэши предыдущих операций и быстро доустанавливаем все что нам надо.
Самая жесткая фича — по умолчанию докер демон запускается от рута и пользователь внутри контейнера тоже root, а теперь делаем так:
VOLUME ["/"]
CMD rm -rf /* && echo Ooops!
Пример немножко надуманный, но идея понятна. Docker это не средство изоляции хоста от гостевой системы, так что следует быть внимательным при работе с внешними разделами и образами из недоверенных источников.
Далее, Docker склонен к замусориванию. После написания этой статьи его состояние примерно таково:
root@docker:~/docker-app# docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
perl-app latest 7ff215a87f66 31 minutes ago 610.5 MB
docker-perl latest 987aba3529c5 About an hour ago 541.4 MB
<none> <none> e0fdf25a2c4d 10 hours ago 524.1 MB
<none> <none> 33a0c1f4d5da 10 hours ago 524.1 MB
<none> <none> aca6ab415167 10 hours ago 389.7 MB
<none> <none> 38e530df4806 10 hours ago 558.6 MB
<none> <none> f7f8d000dec5 11 hours ago 428 MB
<none> <none> fe01b30beff4 11 hours ago 428 MB
<none> <none> f24de2f3ca26 11 hours ago 428 MB
<none> <none> 5b24e9b06ca2 12 hours ago 428 MB
<none> <none> 66ff9778c629 12 hours ago 428 MB
<none> <none> 78f405da27fc 12 hours ago 428 MB
ubuntu 14.04 1d073211c498 7 days ago 187.9 MB
hello-world latest 0a6ba66e537a 2 weeks ago 960 B
root@docker:~/docker-app# du -sh /var/lib/docker/*
2.2G /var/lib/docker/aufs
836K /var/lib/docker/containers
6.0M /var/lib/docker/graph
16K /var/lib/docker/linkgraph.db
4.0K /var/lib/docker/repositories-aufs
4.0K /var/lib/docker/tmp
4.0K /var/lib/docker/trust
4.0K /var/lib/docker/volumes
2.2Gb после десятка пересборок образов, неплохо.
← Как стать Perl-автором | Содержание | Обзор CPAN за сентябрь 2015 г. →