Перевод инфраструктуры 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.



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

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

ansible-playbook deploy.yml -i staging.ini


Вот как мы видим это в slack:



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

Еще одно отличие – это активное использование тегов. Если в стейджинге у нас всегда 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, вчера мы выпустили базовый курс.

Комментарии (16)


  1. amarao
    26.05.2015 18:03
    +2

    Расскажите про безопасность. Вы качаете имаджи без проверки подписей? Или на http нет подписей?


    1. toxicmt Автор
      26.05.2015 18:09
      +3

      Мы пользуемся только официальными образами и иногда от них наследуемся. На текущий момент этого достаточно.


  1. aml
    26.05.2015 19:19

    Следующий шаг — поставить Kubernetes и окончательно отвязать сервисы от машин?


    1. toxicmt Автор
      26.05.2015 19:29

      Мы начинали наши исследования с coreos, kubernetes и многих других модных штук. Они клевые, но для нас не несут никакого бизнес value. А вот непрерывное развертывания влияет и несет добро.


      1. aml
        26.05.2015 23:23

        А возможность штатно отключать машины без прерывания сервиса? Ядро там обновить, или жёсткий диск заменить.


        1. toxicmt Автор
          26.05.2015 23:32

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

          В общем случае, для веб серверов, эта проблема (zero downtime) решается тем что бекенд отключается от балансера, а потом снова подключается (после всех нужных изменений), либо подключается новый.


  1. mahnunchik
    26.05.2015 19:26
    +4

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

    Теперь этот документ есть и на русском: habrahabr.ru/post/258739/#config


  1. Glueon
    27.05.2015 02:20
    +2

    Почему не используете штатный модуль docker от ansible?

    По поводу deploy-a — либо не понял, либо не увидел, но каким обрзом вы вводите в работе обновленный контейнер? В deploy-ном скрипте видно, что вы выкачиваете новый образ из docker hub-а и запускаете контейнер с автоудалением после завершения работы. Но не видно, чтобы контейнер где-то тормозился, чтобы подняться уже из нового образа.


  1. toxicmt Автор
    27.05.2015 15:27

    Почему не используете штатный модуль docker от ansible?

    В разделе «Разворачивание инфраструктуры» я подробно ответил на этот вопрос.

    По поводу deploy-a — либо не понял, либо не увидел, но каким обрзом вы вводите в работе обновленный контейнер?

    Посмотрите содержимое upstart скрипта, там видно и остановка и старт.


    1. Glueon
      27.05.2015 16:30

      Спасибо, проглядел видимо.


  1. vintage
    28.05.2015 00:21
    +2

    Целый час на сборку? Что то вы делаете не так. у вас видимо копирования образов не происходит. Кроме того, зачем вы отдельно собираете образ для прода? Выкладывайте протестированный образ со стейджинга прямо на прод. Для этого он конечно должен быть отвязан от конкретных машин.


    1. morkot
      28.05.2015 04:23

      vintage прав. Адепты Continuous Delivery будут негодовать: собираться должен билд один раз.


    1. vintage
      28.05.2015 10:08

      у вас видимо копирования образов не происходит.
      кэширования конечно же.


      1. toxicmt Автор
        28.05.2015 11:49

        Из статьи видно что не у нас, а у докер хаба.

        Стейджинг это autobuild репозиторий на докер хаба. На тот момент когда мы это делали, нельзя было одновременно с ним работать как с обычным репозиторием и autobuild. Поэтому у нас два разных репозитория. В будущем мы конечно уйдем от автосборки прямо на хабе, пустив все это дело через нормальное cd.


  1. Wadik
    31.05.2015 21:16

    Интересно узнать про локальную разработку рельсового приложения. Про деплой расписано подробно, а про то как вы с такой структурой работаете ежедневно локально непонятно. Какие инструменты облегчают в этом деле? Используете ли IDE в работе?


    1. toxicmt Автор
      05.06.2015 01:30

      Конкретно мы в своей команде используем vim, но это вопрос личных предпочтений. Главное что мы используем в локальной разработке это vagrant, а докер только для сервисов, таких как, база данных. Вести непосредственно разработку внутри докера теоретически можно, но я не уверен что это вам что то даст, особенно если вы только в начале пути.

      Ну и конечно обязательно ansible.