Предыстория
Изначально 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.
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
#!/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
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’
- 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’
- 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)
aml
26.05.2015 19:19Следующий шаг — поставить Kubernetes и окончательно отвязать сервисы от машин?
toxicmt Автор
26.05.2015 19:29Мы начинали наши исследования с coreos, kubernetes и многих других модных штук. Они клевые, но для нас не несут никакого бизнес value. А вот непрерывное развертывания влияет и несет добро.
aml
26.05.2015 23:23А возможность штатно отключать машины без прерывания сервиса? Ядро там обновить, или жёсткий диск заменить.
toxicmt Автор
26.05.2015 23:32«обновить ядро» — в случае облаков это часто невозможно, а на самом деле не нужно. У нас машины живут около месяца, и в процессе постоянно меняются. В принципе такая же история с жесткими дисками.
В общем случае, для веб серверов, эта проблема (zero downtime) решается тем что бекенд отключается от балансера, а потом снова подключается (после всех нужных изменений), либо подключается новый.
mahnunchik
26.05.2015 19:26+4Если вы не знакомы с таким подходом к конфигурации, то рекомендую обратиться вот к этому документу от компании heroku.
Теперь этот документ есть и на русском: habrahabr.ru/post/258739/#config
Glueon
27.05.2015 02:20+2Почему не используете штатный модуль docker от ansible?
По поводу deploy-a — либо не понял, либо не увидел, но каким обрзом вы вводите в работе обновленный контейнер? В deploy-ном скрипте видно, что вы выкачиваете новый образ из docker hub-а и запускаете контейнер с автоудалением после завершения работы. Но не видно, чтобы контейнер где-то тормозился, чтобы подняться уже из нового образа.
toxicmt Автор
27.05.2015 15:27Почему не используете штатный модуль docker от ansible?
В разделе «Разворачивание инфраструктуры» я подробно ответил на этот вопрос.
По поводу deploy-a — либо не понял, либо не увидел, но каким обрзом вы вводите в работе обновленный контейнер?
Посмотрите содержимое upstart скрипта, там видно и остановка и старт.
vintage
28.05.2015 00:21+2Целый час на сборку? Что то вы делаете не так. у вас видимо копирования образов не происходит. Кроме того, зачем вы отдельно собираете образ для прода? Выкладывайте протестированный образ со стейджинга прямо на прод. Для этого он конечно должен быть отвязан от конкретных машин.
vintage
28.05.2015 10:08у вас видимо копирования образов не происходит.
кэширования конечно же.
toxicmt Автор
28.05.2015 11:49Из статьи видно что не у нас, а у докер хаба.
Стейджинг это autobuild репозиторий на докер хаба. На тот момент когда мы это делали, нельзя было одновременно с ним работать как с обычным репозиторием и autobuild. Поэтому у нас два разных репозитория. В будущем мы конечно уйдем от автосборки прямо на хабе, пустив все это дело через нормальное cd.
Wadik
31.05.2015 21:16Интересно узнать про локальную разработку рельсового приложения. Про деплой расписано подробно, а про то как вы с такой структурой работаете ежедневно локально непонятно. Какие инструменты облегчают в этом деле? Используете ли IDE в работе?
toxicmt Автор
05.06.2015 01:30Конкретно мы в своей команде используем vim, но это вопрос личных предпочтений. Главное что мы используем в локальной разработке это vagrant, а докер только для сервисов, таких как, база данных. Вести непосредственно разработку внутри докера теоретически можно, но я не уверен что это вам что то даст, особенно если вы только в начале пути.
Ну и конечно обязательно ansible.
amarao
Расскажите про безопасность. Вы качаете имаджи без проверки подписей? Или на http нет подписей?
toxicmt Автор
Мы пользуемся только официальными образами и иногда от них наследуемся. На текущий момент этого достаточно.