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.



    Как только образ собрался, 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, вчера мы выпустили базовый курс.
    Hexlet 55,96
    Практические уроки по программированию
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Похожие публикации
    Комментарии 17
    • +2
      Расскажите про безопасность. Вы качаете имаджи без проверки подписей? Или на http нет подписей?
      • +3
        Мы пользуемся только официальными образами и иногда от них наследуемся. На текущий момент этого достаточно.
      • 0
        Следующий шаг — поставить Kubernetes и окончательно отвязать сервисы от машин?
        • 0
          Мы начинали наши исследования с coreos, kubernetes и многих других модных штук. Они клевые, но для нас не несут никакого бизнес value. А вот непрерывное развертывания влияет и несет добро.
          • 0
            А возможность штатно отключать машины без прерывания сервиса? Ядро там обновить, или жёсткий диск заменить.
            • 0
              «обновить ядро» — в случае облаков это часто невозможно, а на самом деле не нужно. У нас машины живут около месяца, и в процессе постоянно меняются. В принципе такая же история с жесткими дисками.

              В общем случае, для веб серверов, эта проблема (zero downtime) решается тем что бекенд отключается от балансера, а потом снова подключается (после всех нужных изменений), либо подключается новый.
        • +4
          Если вы не знакомы с таким подходом к конфигурации, то рекомендую обратиться вот к этому документу от компании heroku.

          Теперь этот документ есть и на русском: habrahabr.ru/post/258739/#config
          • +2
            Почему не используете штатный модуль docker от ansible?

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

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

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

              Посмотрите содержимое upstart скрипта, там видно и остановка и старт.
              • 0
                Спасибо, проглядел видимо.
              • +2
                Целый час на сборку? Что то вы делаете не так. у вас видимо копирования образов не происходит. Кроме того, зачем вы отдельно собираете образ для прода? Выкладывайте протестированный образ со стейджинга прямо на прод. Для этого он конечно должен быть отвязан от конкретных машин.
                • 0
                  vintage прав. Адепты Continuous Delivery будут негодовать: собираться должен билд один раз.
                  • 0
                    у вас видимо копирования образов не происходит.
                    кэширования конечно же.
                    • 0
                      Из статьи видно что не у нас, а у докер хаба.

                      Стейджинг это autobuild репозиторий на докер хаба. На тот момент когда мы это делали, нельзя было одновременно с ним работать как с обычным репозиторием и autobuild. Поэтому у нас два разных репозитория. В будущем мы конечно уйдем от автосборки прямо на хабе, пустив все это дело через нормальное cd.
                  • 0
                    Интересно узнать про локальную разработку рельсового приложения. Про деплой расписано подробно, а про то как вы с такой структурой работаете ежедневно локально непонятно. Какие инструменты облегчают в этом деле? Используете ли IDE в работе?
                    • 0
                      Конкретно мы в своей команде используем vim, но это вопрос личных предпочтений. Главное что мы используем в локальной разработке это vagrant, а докер только для сервисов, таких как, база данных. Вести непосредственно разработку внутри докера теоретически можно, но я не уверен что это вам что то даст, особенно если вы только в начале пути.

                      Ну и конечно обязательно ansible.
                      • 0
                        toxicmt, интересно, сильно изменился ваш workflow спустя год и более?

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

                    Самое читаемое