Docker Workflow

Перевод инфраструктуры hexlet.io на Docker потребовал от нас определенных усилий. Мы отказались от многих старых подходов и инструментов, переосмыслили значение многих привычных вещей. То, что получилось в итоге, нам нравится. Самое главное — этот переход позволил сильно все упростить, унифицировать и сделать гораздо более поддерживаемым. В этой статье мы расскажем о той схеме для разворачивания инфраструктуры и деплоя, к которой в итоге пришли, а так же опишем плюсы и минусы данного подхода.ПредысторияИзначально Docker нам понадобился для запуска недоверенного кода в изолированном окружении. Задача чем то похожая на то, чем занимаются хостеры. Мы прямо в продакшене собираем образы, которые потом используются для запуска практики. Это, кстати, тот редкий случай, когда нельзя делать по принципу «один контейнер — один сервис». Нам нужно чтобы все сервисы и весь код конкретного задания были в одном окружении. Минимально, в каждом таком контейнере, поднимается supervisord и наша браузерная иде. Дальше все в зависимости от самого задания: автор может туда добавить и развернуть хоть редис, хоть хадуп.А еще оказалось, что докер позволил создать простой способ сборки практических заданий. Во-первых, потому что если практика собралась и заработала на локальной машине у автора, то гарантированно (почти) она запустится и в продакшене. Ибо изоляция. А во-вторых, несмотря на то, что многие считают докер файл «обычным башем» со всеми вытекающими — это не так. Докер это яркий пример использования функциональной парадигмы в правильных местах. Он обеспечивает идемпотентность, но не так, как системы управления конфигурации, за счет внутренних механизмов проверок, а за счет неизменяемости. Поэтому в dockerfile обычный баш, но накатывается он так, словно это всегда происходит на свежий базовый образ, и вам не нужно учитывать предыдущее состояние при изменении образа. А кеширование убирает (почти) проблему ожидания пересборки.

На текущий момент эта подсистема по сути представляет собой continuous delivery для практических заданий. Возможно мы сделаем отдельную статью на эту тему, если у аудитории будет интерес.

Докер в инфраструктуре После этого мы задумались о том, чтобы перевести на Docker и остальную часть нашей системы. Было несколько причин. Понятно, что таким образом мы бы достигли большей унификации нашей системы, ведь докер уже занял серьезную (и весьма не тривиальную) часть инфраструктуры.На самом деле есть еще один интересный случай. Много лет назад я использовал chef, после этого ansible, который значительно проще. При этом всегда сталкивался с такой историей: если у вас нет собственных админов, и вы не занимаетесь инфраструктурой и плейбуками/кукбуками регулярно, то часто возникают неприятные ситуации в случаях вроде:

Обновилась система управления конфигурации (особенно с шефом было), и вы два дня тратите на то, чтобы все под это дело подвести. Вы забыли, что на сервере стоял какой то софт, и при новой накатке начинаются конфликты, или все падает. Нужны переходные состояния. Ну или как делают те кто набил шишек: «каждый раз на новый сервер». Перераспределение сервисов по серверам это боль, все влияют друг на друга. Здесь еще тысяча более мелких причин, в основном все из-за отсутствия изоляции. В связи с этим мы смотрели на Docker как на чудо, которое избавит нас от этих проблем. Так оно и вышло, в общем-то. Сервера при этом все равно приходится периодически перераскатывать с нуля, но значительно реже и, самое главное, мы вышли на новый уровень абстракции. Работая на уровне системы управления конфигурации, мы мыслим и управляем сервисами, а не частями из которых они состоят. То есть единица управления это сервис, а не пакет.

Так же ключевой историей безболезненного деплоя является быстрый, и, что важно, простой откат. В случае с Docker это почти всегда фиксация предыдущей версии и перезапуск сервисов.

И последнее, но не менее важное. Сборка хекслета стала чуть сложнее, чем просто компиляция assets (мы на рельсах, да). У нас есть массивная js-инфраструктура, которая собирается с помощью webpack. Естественно все это хозяйство надо собирать на одном сервере и дальше уже просто раскидывать. Capistrano этого не позволяет.

Разворачивание инфраструктуры Почти все, что нам нужно от систем configuration management, это создание пользователей, доставка ключей, конфигов и образов. После перехода на docker, плейбуки стали однообразными и простыми: создали пользователей, добавили конфигов, иногда немного крона.Еще очень важным моментом является способ запуска контейнеров. Несмотря на то, что Docker из коробки идет со своим супервизором, а Ansible поставляется с модулем для запуска Docker контейнеров, мы все же решили не использовать эти подходы (хотя пробовали). Docker модуль в Ansible имеет множество проблем, часть из которых вообще не понятно как решать. Во многом это связано с разделением понятий создания и старта контейнера, и конфигурация размазана между этими стадиями.

В конечном итоге мы остановились на upstart. Понятно, что скоро все равно придется уходить на systemd, но так сложилось, что мы используем ubuntu той версии, где пока по умолчанию идет upstart. Заодно мы решили вопрос универсального логирования. Ну, и upstart позволяет гибко настраивать способ запуска перезапуска сервиса, в отличие от докеровского restart_always: true.

upstart.unicorn.conf.j2 description «Unicorn» start on filesystem or runlevel [2345] stop on runlevel [!2345] env HOME=/home/{{ run_user }} # change to match your deployment user setuid {{ run_user }} setgid team respawn respawn limit 3 30 pre-start script . /etc/environment export HEXLET_VERSION /usr/bin/docker pull hexlet/hexlet-{{ rails_env }}:$HEXLET_VERSION /usr/bin/docker rm -f unicorn || true end script pre-stop script /usr/bin/docker rm -f unicorn || true end script script . /etc/environment export HEXLET_VERSION RUN_ARGS='--name unicorn' ~/apprunner.sh bundle exec unicorn_rails -p {{ unicorn_port }} end script Самое интересное тут, это строка запуска сервиса:

RUN_ARGS='--name unicorn' ~/apprunner.sh bundle exec unicorn_rails -p {{ unicorn_port }} Это сделано для того, чтобы иметь возможность запускать контейнер с сервера, без необходимости руками прописывать все параметры. Например, так мы можем войти в рельсовую консоль:

RUN_ARGS=»-it» ~./apprunner.sh bundle exec rails c apprunner.sh.j2 #!/usr/bin/env bash . /etc/environment export HEXLET_VERSION ${RUN_ARGS:=''} COMMAND=»/usr/bin/docker run --read-only --rm \ $RUN_ARGS \ -v /tmp:/tmp \ -v /var/tmp:/var/tmp \ -p {{ unicorn_port }}:{{ unicorn_port }} \ -e AWS_REGION={{ aws_region }} \ -e SECRET_KEY_BASE={{ secret_key_base }} \ -e DATABASE_URL={{ database_url }} \ -e RAILS_ENV={{ rails_env }} \ -e SMTP_USER_NAME={{ smtp_user_name }} \ -e SMTP_PASSWORD={{ smtp_password }} \ -e SMTP_ADDRESS={{ smtp_address }} \ -e SMTP_PORT={{ smtp_port }} \ -e SMTP_AUTHENTICATION={{ smtp_authentication }} \ -e DOCKER_IP={{ docker_ip }} \ -e STATSD_PORT={{ statsd_port }} \ -e DOCKER_HUB_USERNAME={{ docker_hub_username }} \ -e DOCKER_HUB_PASSWORD={{ docker_hub_password }} \ -e DOCKER_HUB_EMAIL={{ docker_hub_email }} \ -e DOCKER_EXERCISE_PREFIX={{ docker_exercise_prefix }} \ -e FACEBOOK_CLIENT_ID={{ facebook_client_id }} \ -e FACEBOOK_CLIENT_SECRET={{ facebook_client_secret }} \ -e HEXLET_IDE_VERSION={{ hexlet_ide_image_tag }} \ -e CDN_HOST={{ cdn_host }} \ -e REFILE_CACHE_DIR={{ refile_cache_dir }} \ -e CONTAINER_SERVER={{ container_server }} \ -e CONTAINER_PORT={{ container_port }} \ -e DOCKER_API_VERSION={{ docker_api_version }} \ hexlet/hexlet-{{ rails_env }}:$HEXLET_VERSION $@» eval $COMMAND Здесь есть один тонкий момент. К сожалению, теряется история команд. Для восстановления работоспособности надо прокидывать соответствующие файлы, но, честно говоря, мы так и не занялись этим.

Кстати здесь видно еще одно преимущество докера: все внешние зависимости указаны явно и в одном месте. Если вы не знакомы с таким подходом к конфигурации, то рекомендую обратиться вот к этому документу от компании heroku.

Докеризация Dockerfile Dockerfile FROM ruby:2.2.1 RUN mkdir -p /usr/src/app WORKDIR /usr/src/app ENV RAILS_ENV production ENV REFILE_CACHE_DIR /var/tmp/uploads RUN curl -sL https://deb.nodesource.com/setup | bash - RUN apt-get update -qq \ && apt-get install -yqq apt-transport-https libxslt-dev libxml2-dev nodejs imagemagick RUN echo deb https://get.docker.com/ubuntu docker main > /etc/apt/sources.list.d/docker.list \ && apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9 \ && apt-get update -qq \ && apt-get install -qqy lxc-docker-1.5.0 # bundle config build.rugged --use-system-libraries # bundle config build.nokogiri --use-system-libraries COPY Gemfile /usr/src/app/ COPY Gemfile.lock /usr/src/app/ COPY package.json /usr/src/app/ # without development test RUN npm install RUN bundle install --without development test COPY. /usr/src/app RUN ./node_modules/gulp/bin/gulp.js webpack_production RUN bin/rake assets: precompile VOLUME /usr/src/app/tmp VOLUME /var/folders В первой строчке видно, что нам не нужно париться об установке ruby, мы просто указываем ту версию, которую хотим использовать (и для которой есть image, естественно).

Запуск контейнеров происходит с флагом --read-only, который позволяет контролировать запись на диск. Практика показывает, что писать пытаются всё подряд, в совершенно неожиданные места. Внизу видно, что мы создали volume /var/folders, туда пишет руби при создании временной директории. Но некоторые разделы мы прокидываем снаружи, например /var/tmp, чтобы шарить данные между разными версиями. Это необязательно, но просто экономит нам ресурсы.

Также внутрь мы ставим докер, для того чтобы из докера управлять докером. Это нужно, как раз, для управления образами с практикой.

Дальше, буквально четырьмя строчками, описываем все, что делает capistrano как средство сборки приложения.

Хостинг образов Можно поднимать свой собственный docker distribution (бывший registry), но нас вполне устраивает docker hub, за который мы платим 7$ в месяц и получаем 5 приватных репозиториев. Ему, конечно, далеко до совершенства, и с точки зрения юзабилити, и возможностей. А иногда сборка образов вместо 20 минут затягивается на час. В целом, жить можно, хотя есть и альтернативные облачные решения.Сборка и Деплой Способ сборки приложения отличается в зависимости от среды развертывания.На стейджинге мы используем automated build, который собирается, сразу как только видит изменения в ветке staging.

7c09f9c28f0046c3a30bf09903d4f0c5.png

Как только образ собрался, docker hub через webhook оповещает zapier, который, в свою очередь, отправляет информацию в Slack. К сожалению, docker hub не умеет работать напрямую со Slack (и разработчики не планируют его поддерживать).

Деплой стейджинга выполняется командой:

ansible-playbook deploy.yml -i staging.ini Вот как мы видим это в slack:

e74d59e049ec4f53af58eb6c2ceeebd7.png

В отличие от стедйжинга, продакшен образ не собирается автоматически. В момент готовности он билдится ручным запуском на специальном билд сервере. У нас этот сервер выполняет одновременно роль бастиона.

Еще одно отличие — это активное использование тегов. Если в стейджинге у нас всегда latest, то здесь при сборке мы явно указываем тег (он же версия).

Билд запускается так:

ansible-playbook build.yml -i production.ini -e «hexlet_image_tag=v100» build.yml  — hosts: bastions gather_facts: no vars: clone_dir: /var/tmp/hexlet tasks:  — git: repo: git@github.com: Hexlet/hexlet.git dest: '{{ clone_dir }}' accept_hostkey: yes key_file: /home/{{ run_user }}/.ssh/deploy_rsa become: yes become_user: '{{ run_user }}'  — shell: 'cd {{ clone_dir }} && docker build -t hexlet/hexlet-production:{{ hexlet_image_tag }} .' become: yes become_user: '{{ run_user }}'  — shell: 'docker push hexlet/hexlet-production:{{ hexlet_image_tag }}' become: yes become_user: '{{ run_user }}' Деплой продакшена выполняется командой:

ansible-playbook deploy.yml -i production.ini -e «hexlet_image_tag=v100» deploy.yml  — hosts: localhost gather_facts: no tasks:  — local_action: module: slack domain: hexlet.slack.com token: {{ slack_token }} msg: «deploy started: {{ rails_env }}:{{ hexlet_image_tag }}» channel:»#operation» username:»{{ ansible_ssh_user }}»  — hosts: appservers gather_facts: no tasks:  — shell: docker pull hexlet/hexlet-{{ rails_env }}:{{ hexlet_image_tag }} become: yes become_user: '{{ run_user }}'  — name: update hexlet version become: yes lineinfile: regexp: «HEXLET_VERSION» line: «HEXLET_VERSION={{ hexlet_image_tag }}» dest: /etc/environment backup: yes state: present  — hosts: jobservers gather_facts: no tasks:  — become: yes become_user: '{{ run_user }}' run_once: yes delegate_to: '{{ migration_server }}' shell: > docker run --rm -e 'SECRET_KEY_BASE={{ secret_key_base }}' -e 'DATABASE_URL={{ database_url }}' -e 'RAILS_ENV={{ rails_env }}' hexlet/hexlet-{{ rails_env }}:{{ hexlet_image_tag }} rake db: migrate

— hosts: webservers gather_facts: no tasks:  — service: name=nginx state=running become: yes tags: nginx  — service: name=unicorn state=restarted become: yes tags: [unicorn, app]  — hosts: jobservers gather_facts: no tasks:  — service: name=activejob state=restarted become: yes tags: [activejob, app]  — hosts: localhost gather_facts: no tasks:  — name: «Send deploy hook to honeybadger» local_action: shell cd … && bundle exec honeybadger deploy --environment={{ rails_env }}  — local_action: module: slack domain: hexlet.slack.com token: {{ slack_token }} msg: «deploy completed ({{ rails_env }})» channel:»#operation» username:»{{ ansible_ssh_user }}» # link_names: 0 # parse: 'none' В целом, сам деплой это подгрузка необходимых образов на сервера, выполнение миграций и перезапуск сервисов. Внезапно оказалось что вся капистрана заменилась на десяток строк прямолинейного кода. А заодно десяток гемов интеграции с капистраной, внезапно, оказались просто не нужны. Задачи которые они выполняли, чаще всего, превращаются в одну таску на ansible.

Разработка Первое, от чего придется отказаться, работая с докером, это от разработки в Mac OS. Для нормальной работы нужен Vagrant. Для настройки окружения у нас написан специальный плейбук vagrant.yml. Например, в нем мы устанавливаем и настраиваем базу, хотя в продакшене у нас используется RDS.К сожалению (а может и к счастью) у нас так и не получилось настроить нормальный workflow разработки через докер. Слишком много компромиссов и сложностей. При этом сервисы типа postgresql, redis и им подобные, мы все равно запускаем через него даже при разработке. И все это добро продолжает управляться через upstart.

Мониторинг Из интересного мы ставили гугловый cadvisor, который, в свою очередь, отправлял собранные данные в influxdb. Периодически cadvisor начинал жрать какое то дикое количество памяти и приходилось его руками перезапускать. А дальше оказалось, что influxdb это хорошо, но алертинга поверх нее просто не существует. Все это привело к тому, что мы отказались от любого самопала. Сейчас у нас крутится datadog с соответствующими подключенными плагинами, и мы очень довольны.Проблемы После перехода на докер сразу пришлось отказаться от быстрофиксов. Сборка образа может занимать до 1 часа. И это вас толкает к более правильному флоу, к возможности быстро и безболезненно откатываться на предыдущую версию.Иногда мы натыкаемся на баги в самом докере (чаще чем хотелось бы), например прямо сейчас мы не можем с 1.5 перейти на 1.6.2 потому что у них до сих пор несколько не закрытых тикетов с проблемами на которые натыкаются много кто.

Итог Изменяемое состояние сервера при разворачивании софта это болевая точка любой системы конфигурации. Докер забирает на себя большую часть этой работы, что позволяет серверам долго находится в очень чистом состоянии, а нам не беспокоиться о переходных периодах. Поменять версию того же руби стало не только простой задачей, но и полностью независимой от администратора. А унификация запуска, разворачивания, деплоя, сборки и эксплуатации позволяет нам гораздо меньше тратить времени на обслуживании системы. Да, нам конечно же еще здорово помогает aws, но это не отменяет плюсов простоты использования docker/ansible.Планы Следующим шагом мы хотим внедрить continuous delivery и полностью отказаться от стейджинга. Идея в том что раскатка сначала будет вестись на продакшен сервера доступные только изнутри компании.P.S. Ну, а для тех кто еще не знаком с ansible, вчера мы выпустили базовый курс.

© Habrahabr.ru