Выпуск 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 г.
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье