Хотел бы поделиться опытом настройки CI/CD в нашей компании, плюс, послушать советы, если у вас похожая структура проектов.

Кому, как мне кажется, данная статья может оказаться полезной:

  • ваши проекты содержат несколько отдельных репозиториев с приложениями;
  • вы хотите быть уверены, что каждый репозиторий проходит тесты;
  • вы хотите быть уверены в совместимости версий между репозиториями;
  • вы ещё не успели, но планируете, перевести свои проекты на докер;
  • хотите посмотреть пару playbook'ов Ansible.

Очень рекомендую курс «Continuous Delivery Using Docker And Ansible». Мы оттакливались от него при разработке нашего решения.

Задачи для CI/CD


Один наш проект в среднем — это 4-5 репозиториев, взаимодействующих между собой по rest-api. Считается ли это микросервисной архитектурой или нет, не знаю точно, но, учитывая это, перед CI/CD мы ставили следующие задачи:

  • в каждом репозитории в каждой из основных веток должен быть рабочий (протестированный) код;
  • каждая эта ветка, плюс каждый тэг, должны быть полностью консистены между всеми репозиториями в проекте;
  • должна быть возможность развернуть проект локально, как полноценно, так и отдельно любой репозиторий, для разработки;
  • должна быть возможность развернуть проект на разных окружениях: testing, staging, production.

Итак, приступим.

Настройка CI/CD


Предварительный шаг


  • мы перешли на git-flow. Оказалось, что наша кастомная vcs-workflow, по-сравнению с «классикой», избыточна и сложна, особенно для новичков;
  • наш недельный спринт — это новая версия продукта. Каждая задача, будь то баг или фича, в таск-менеджере прикрепляется к конкретной версии. У каждого репозитория в конце спринта появляется тэг с новой версией, даже если именно в данной репе для данной версии ничего и не делали. Исключение, если ни один репозиторий из проекта за спринт не трогали;
  • запретили напрямую пушить в ветки master, develop и release, только через пул-реквесты;
  • повесили хук на пул-реквесты в вышеуказанные ветки для сборки и тестирования в Jenkins;
  • запретили слияние пул-реквестов без удачного тестирования Jenkins'ом и без одобрения Code Review.

В качестве CI инструмента мы выбрали Jenkins, который запускает юнит тесты и интеграционные тесты api.

В качестве CD инструментов — Ansible + Docker.

Первый шаг, настройка отдельного репозитория


Мы изменили структуру каждого нашего репозитория внутри проекта:

app
  |-src
  |-docker
  |   |-ci
  |   |-develop
  |   |-release
  |-requirements
  |-Jenkinsfile
  |-Makefile

Настроенный хук на пул-реквесте скажет Jenkins'у, что репозиторий необходимо протестировать. Jenkins будет искать и исполнять Jenkinsfile. Последний последовательно вызывает команды Makefile для сборки контейнера и тестирования. Makefile запускает команды docker-compose из каталога ./docker/ci. Почему мы не настроили запуск команд docker-compose сразу из Jenkinsfile? Чтобы сохранить его универсальность для всех репозиториев. Т.е. разным репозиториям для сборки и запуска требуются разные команды docker-compose, и эти различия настроены в Makefile, который для Jenkinsfile всегда имеет одинаковый интерфейс сборки и запуска.

NB. В конце статьи находятся ссылки на репозитории с примерами.

Также в Makefile находятся команды по сборке и запуску репозитория локально в develop-режиме — настроен проброс с исходниками с хостовой машины внутрь докера, и достаточно будет только перезапустить docker-compose, что тоже сделано через make-команду, чтобы увидеть новые изменения. За это отвечает Makefile + ./docker/develop.

В ./docker/release находятся настройки сборки репозитория для среды testing/staging и пр. Они, настройки, будут использоваться позже.

Второй шаг. Настройка дополнительного devops-репозитория


Назначение общего репозитория — сохранение целостности проекта при разворачивании репозиториев, которые в него входят, а также в возможности интеграционного тестирования.

Структура репозитория

devops
  |-ansible
  |   |-plays
  |   |-roles
  |-projects
  |   |-project_1
  |   |   |-apps
  |   |   |   |-app_1
  |   |   |   |-app_2
  |   |   |   |-app_3
  |   |   |   |-...
  |   |   |-docker
  |   |   |   |-ci-api
  |   |   |   |-ci-selenium-gherkin
  |   |   |   |-develop
  |   |   |   |-testing
  |   |   |   |-staging
  |   |   |   |-production
  |   |   |-Makefile
  |-requirements
  |-Jenkinsfile
  |-Makefile

Сперва о том, как этот репозиторий выполняет интеграционное тестирование.
Не самое простое дело, попробую объяснить.
Как и в случае репозитория с приложением, здесь есть файлы Jenkinsfile и Makefile, которые при пул-реквесте запустят команды сборки и тестирования. Настройки сборки располагаются в ./projects/PROJECT/docker/ci-api, где «PROJECT» — название текущего проекта. Сборка включает в себя клонирование каждого репозитория в нужной ветке/тэге, запуск контейнера-тестировщика api.

«Нужная ветка/тэг» — это то, что мы пытается протестировать — либо общая ветка (master, develop, release) для всех репозиториев, либо тэг-версия проекта. Тэг необходимо проставить в каждом репозитории. Затем создать ветку в devops-репозитории с названием, совпадающим с «нужным». После этого можно делать пул-реквест.

Jenkins попытается собрать проект по выбранному тэгу/ветке, если в каком-то репозитории не найдётся такого — тестирование провалено. Если собрать проект удалось, то запустится «тестовый фреймворк», в качестве которого мы используем Postman и его утилиту для командной строки — Newman. Если тесты прошли успешно — на выходе мы сливаем пул-реквест и проставляем тестируемый тэг в devops-репозиторий. Наличие этого тэга говорит, что эта версия проекта протестирована.

Для запуска тестов Постмана, нам требуется ссылка расшаренной коллекции, которую вставляем в command контейнера.

Пока это единственный вид интеграционного тестирования, чуть позже мы добавим тестирование с помощью gherkin'a или selenium'a, по крайней мере, каталог docker/ci-selenium-gherkin уже есть.

Теперь про функции CD в данном репозитории.
Здесь, в ./ansible, находится пульт управления всем проектом по сборке образов и их доставке на разные сервера и окружения, а именно:

  • develop.yml — настройки по разворачиванию всего проекта локально;
  • make-images.yml — создание docker-образов с определённой версией проекта и пуш в докер-registry;
  • deploy-and-run-images.yml — разворачивание проекта на серверах с разным окружением.

В начале каждого пункта указан playbook, который выполняет данный сценарий.

Запускаются они командой:

$ ansible-playbook -i ../testing.ini make-images.yml -e 'project=todo ver=2017.1'
где
- -i ../testing.ini -- файл inventory, в котором указан сервер для данного окружения, куда впоследствии надо будет сделать доставку проекта
- make-images-yml -- playbook
- -e 'project=todo ver=2017.1' -- дополнительные параметры, которые ожидает playbook, в данном случае указывается проект и тэг.

В ./ansible/plays/group_vars/all.yml находятся настройки проекта:
  • какие репозитории относятся к данному проекту;
  • какой docker-registry использовать, какие логин-пароль к нему;
  • индивидуальные настройки для каждого окружения и пр.

Как вы могли заметить, хотя данный репозиторий полностью посвящён только одному проекту, всё равно мы передаём название проекта в параметры playbook'а, а также каталог проекта находится в каталоге projects. Это из-за того, что данный devops-репозиторий — это форк от master-devops-репозитория, от которого точно также форкнуты devops-репозитории других проектов. И подобная структура позволяет обмениваться кодом общих настроек и ansible команд между мастером-форками и между самим форками без угрозы что-то сломать. Точнее, каталог ansible — общий, и его рефакторинг легко можно переносить из мастера в форк и наоборот. А все частные проектные настройки находятся в своём отдельном каталоге в projects. И пул из мастера, либо из соседнего devops-репозитория — не будет конфликтовать с текущим.

Возвращаемся к каталогу docker/release в приложениях, в котором находится Dockerfile, отвечающий за сборку для окружения testing/staging и production, т.е. за всё, кроме develop. Сама по себе релизная сборка одного репозитория ничего полезного не даёт, только в совокупности с остальными репозиториями проекта. Ansible настроен таким образом, что для develop-сборки он возьмёт Dockerfile из каталога docker/develop каждого проекта, а для сборки под релизное окружение — из каталога docker/release.

Итого, у нас получилось сделать:

  • возможность клонирования любого репозитория и запустить develop-версию
  • каждый репозиторий проверяется Jenkins'ом;
  • есть общий репозиторий, которая запускает интеграционные тесты по всем репозиториям по общей версии;
  • ansible playbook: разворачивает локально и запускает все репозитории проекта в develop-режиме;
  • ansible playbook: собирает образы в зависимости от выбранной схемы окружения и отправляет в докер-registry;
  • ansible playbook: на сервере настраивает проект;
  • ansible playbook: на сервере запускает приложение.

Ссылки на приложения для демонстрации системы:

  • todo_todo — форк с проекта todobackend.com. Изменил структуру и добавил тесты. Создаёт todo'шк;
  • todo_crm — создаёт пользователей, отправляет запрос в todo_todo, создаёт todo и привязывает его к пользователю;
  • todo_ops — devops-репозиторий с конфигами

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


  1. alexkuzko
    13.11.2017 21:05

    Спасибо за представление вашего варианта, но без конкретных примеров ценность материала сильно уменьшается. Попробуйте всё-таки сделать какой-то мини проект на вашей базе.


    1. vlfedotov Автор
      13.11.2017 21:11

      Ссылка на минипроект в конце статьи. Там две приложухи и devops.


  1. Fesor
    14.11.2017 00:38

    запретили напрямую пушить в ветки master, develop и release, только через пул-реквесты;

    существует мнение что это противоречит идее CI/CD. Что вы думаете по этому поводу? Я не к тому что "разрешить всем пушить в master/release", остановимся только на develop. Ограничения на остальных ветках пусть останутся для код фриза.


    1. ggo
      14.11.2017 09:52

      Как запрет напрямую пушить в ... влияет на ci/cd?


      1. Fesor
        14.11.2017 11:12

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


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


        1. ggo
          14.11.2017 15:03

          Строго говоря запрет напрямую пушить и ci/cd друг к другу ортогональны.
          В данном случае может быть ci/cd на пул-реквесты, т.е. автосборки автособираются, дают обратную связь по качеству, люди при этом оценивают код не только с точки зрения машинного качества, но и точки зрения человеческого — оформление, понятность и прочее.
          Т.е. ci/cd не про запрет пушить, а про регулярную проверку качества.


          1. Fesor
            14.11.2017 17:15

            Т.е. ci/cd не про запрет пушить, а про регулярную проверку качества.

            Очень интересное определение, не поделитесь источником? Просто обычно когда говорят о continious integration то имеют ввиду "интеграция кода происходит хотя бы раз в день".


            1. ggo
              14.11.2017 20:06

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


              1. Fesor
                15.11.2017 01:11

                Все-равно это не про запрет пушить напрямую.

                Бывает ли у вас так что пул реквест висит несколько дней?


                1. ggo
                  15.11.2017 10:31

                  Предположим, какой-то пул-реквест завис. Это означает, что для данного проекта ci/cd отсутствует?


                  1. Fesor
                    15.11.2017 19:04

                    Не факт конечно, уж точно ничего про CD по этому критерию мы говорить не можем. А вот CI — зависит от того как часто у вас происходят такие вот "зависания" скорее и почему они возникают.


                    В целом если мы можем нажать "Merge" в любой момент времени — это ближе к CI. То есть проверка качества тут важно, дабы быть уверенным что все будет хорошо, но это не цель а средство достижения цели. Цель по сути то в том, что бы команда всегда знала что происходит и усилить коммуникацию.


                    Если у вас те же цели достигаются при вашем варианте процесса — ну чтож, хорошо. В большинстве случаев это не так. Ну и не CI единым. Это далеко не для всех команд будет работать. Да и говорить "у нас нет CI" в целом смысла тоже нет, подумаю что "нет CI сервера".


                    1. ggo
                      16.11.2017 10:29

                      Представьте, у вас все артефакты лежат в репозитариях, все покрыто тестами, вся инфраструктура управляется специальными cfg mgnt тулами, со всего собираются и анализируются логи, и все это завернуто в единый pipeline. И для вас ценно, что изменения не пушатся напрямую в основной код, а ревьюются человеком. Из-за этого у вас не кошерный CI/CD, а какой-то рафинированный? И кошерным он станет, тогда и только тогда, когда всем будет дадено разрешение пушить напрямую?

                      CI/CD — про управляемый повторяемый способ контроля качества и доставки изменений. Не про модель контроля версий.


                      1. Fesor
                        16.11.2017 13:30

                        И для вас ценно, что изменения не пушатся напрямую в основной код, а ревьюются человеком.

                        Если все так как вы описали — какая ценность для в том что код ревью обязательно должен происходить ДО попадания в основной код?


                        И кошерным он станет, тогда и только тогда, когда всем будет дадено разрешение пушить напрямую?

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


                        Я пожалуй все же поясню — суть CI — постоянная интеграция (для чего необходим автоматизированный контроль качества). С пулреквестами это организовать очень сложно, так как подразумевает короткий цикл жизни этих самых пул реквестов. Требует неплохого уровня дисциплины команды. При этом если команда уже настолько хорошо работает — почему бы не пойти дальше? Я не говорю что идти дальше надо, но почему нет?


    1. vlfedotov Автор
      14.11.2017 09:59

      Мы новые версии продукта начинаем из ветки develop, и очень важно, чтобы код в этой ветке не оказался бракованным из-за того, что кто-то решил сделать пуш с «безобидными» правками. Сейчас это невозможно, чтобы что-то попало в master/develop/release необходимо сделать пул-реквест с запуском Jenkins'а, даже если мы пытаемся влить уже протестированный release в develop.


      1. Fesor
        14.11.2017 11:19

        и очень важно, чтобы код в этой ветке не оказался бракованным

        Именно по этому CI как процесс требует наличия автоматической системы "проверки на брак". Далее опять же важный момент это код фризы (например тегами) какой-то версии для стабилизации.


        необходимо сделать пул-реквест с запуском Jenkins'а

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


        даже если мы пытаемся влить уже протестированный release в develop.

        а в каких ситуациях у вас подобное может происходить? У вас часто в release напрямую попадают изменения (я так понимаю какие-то хотфиксы)?


        p.s. Мне просто интересно мнение людей, ибо часто когда я общаюсь с людьми замечаю что под CI/CD имеют ввиду не процесс а инструменты.


        1. vlfedotov Автор
          14.11.2017 12:30

          это код фризы (например тегами) какой-то версии для стабилизации
          Тэги делаем из ветки release в каждом репозитории проекта для дальнейшего интеграционного тестирования данной версии.

          jenkins и так по пушу будет запускаться
          Это так, но упавший билд не привлечёт внимание команды. Он виден только либо из самого Jenkins'a, либо из vcs, но заходить туда лишний раз никто не станет. На данном этапе, Jenkins запускается после пуша, т.е. изменения уже будут в develop'e, и их уже смогут себе спуллить.

          а в каких ситуациях у вас подобное может происходить?
          Да, это хотфиксы к текущей версии продукта, которую пытаемся сдать, а в это время в develop'е разрабатываем функционал для следующей версии. И, несмотря на то, что все хотфиксы также проходят через пул-реквесты и release тестируется, чтобы его влить в develop также делаем пул-реквест. Больше для проформы, но всё-таки.


          1. Fesor
            14.11.2017 17:18

            но упавший билд не привлечёт внимание команды.

            email/slack/telegram/etc или дашборд со статусом сборки. И вроде как все в курсе.


            через пул-реквесты и release тестируется

            а master ветка зачем? В целом можно этот вопрос по разному решать, просто мне кажется противоестественным когда код из release в develop попадает. Мы в этом случае делали просто cherry-pick коммитов. Благо штука не частая.


            Из моего опыта в целом флоу работы через пул реквесты очень сильно замедляет разработку и при этом не сильно добавляет качества. Что до обмена знаниями в команде — есть и другие подходы которые намного эффективнее. Но это только если есть возможность что-то такое делать. Чаще всего просто тесты не пишут, как минимум, что уже отсекает массу вариантов для эксперементов в плане оптимизации процессов.


            1. ggo
              14.11.2017 20:11

              кажется противоестественным когда код из release в develop попадает

              Это одна из классик nvie.com/posts/a-successful-git-branching-model
              И это не исключает того, что могут быть и другие классические модели.


            1. vlfedotov Автор
              14.11.2017 21:05

              email/slack/telegram/etc или дашборд со статусом сборки
              Это всё равно всё посторонние штуки, куда надо зайти и посмотреть, сами по себе они ничего не запретят. У нас настроен хук на слак, туда идут успешные и неуспешные сборки. По всем проектам по всем сборкам. Никто не смотрит.

              а master ветка зачем?
              Master — продакшен, release — потенциальный продакшен. Процесс у нас такой: готовим версию, например, третью, сделали, протегировали как 3.0, CI протестировал, развернули на тестинге. Тестировщики, менеджеры, в некоторых случаях и клиенты, могут зайти посмотреть что и как, если нашли дефекты — мы делаем хотфиксы в этот release, с тэгом 3.1, и т.д. В какой-то момент дефектов найти не смогут — вот этот тэг и будет сливаться в мастер и девелоп.

              Чаще всего просто тесты не пишут
              Это один из моментов, за которыми мы следим в пул реквестах, если есть ветка, то должны быть новые тесты.


              1. Fesor
                15.11.2017 01:16

                сами по себе они ничего не запретят.

                речь не о запрете а об информировании. Надо не запрещать а как раз наоборот, допускать ошибки. Зафэйлился билд — стопорится работа всех но ничего страшного не происходит. Все это видят в слэке и это тригерит анализ ситуации и выводы. А так большинство просто тихо фиксят и идут дальше, не делая никаких выводов что приводит к многократному повторению ситуации и от этого страдает продуктивность команды. ухудшается обмен знаниями.


                Master — продакшен, release — потенциальный продакшен.

                понял, привык что release это самый крайний продакшен.


                вот этот тэг и будет сливаться в мастер и девелоп.

                до этого момента в develop эти баги все еще присутствуют? Или в целом работа над develop на это время приостанавливается?


                Это один из моментов, за которыми мы следим в пул реквестах, если есть ветка, то должны быть новые тесты.

                но это можно автоматизировать + подключить мутационные тесты как показатель качества юнитов (если вы пишите юниты а не только интеграционные).


                1. vlfedotov Автор
                  15.11.2017 11:58

                  Зафэйлился билд — стопорится работа всех
                  Не вижу смысла стопорить работу всех, и даже привлекать к этому какое-то дополнительное внимание. Фейл билда, например, release-ветки касается только людей, которые занимаются этой версией, они видят этот фейл и правят его. И вызван он кодом, который они сами и сделали. Разработчикам, которые в это время разрабатывают следующую версию продукта в develop-ветке, а по факту это просто новый функционал, который чаще всего не связан с функционалом, над которым сейчас работают в release-ветке, нет необходимости знать о фейлах в сборке release-ветки. Главное, они знают, что в итоге к ним смогут влить только «хороший» код. Для этого и нужен именно запрет.

                  до этого момента в develop эти баги все еще присутствуют?
                  Да, они присутствуют в develop'e, но это, в большинстве случаев, ни на что не влияет, от данной ветки мы хотим получить только работающий новый функционал. Проверка функционала по всему прокту происходит в ветке release. Выходит, что текущий релиз хотфиксами поправили, затем влили в девелоп, получили поправленный старый функционал плюс новый, и делаем новый release.

                  но это можно автоматизировать + подключить мутационные тесты как показатель качества юнитов
                  До этого мы ещё не дошли.


                  1. Fesor
                    15.11.2017 19:10
                    +1

                    Фейл билда, например, release-ветки касается только людей, которые занимаются этой версией

                    Надо было пояснить кто все. Все верно, фэйл билда затрагивает только людей, которые с этими билдами работают.


                    Идея не в том что бы "блочить" а в том что бы тригерить общение команды. Еще из бонусов того что все работают с одним и тем же кодом — регрессии которые не выявили тесты проще находить (чем больше команда — тем проще). Но это опять же не для всех будет работать. Я же не агитирую, просто интересно мнение других.


                    До этого мы ещё не дошли.

                    просто интереса ради попробуйте мутационные тесты прогнать. Это не много времени занимает (настройка, запуск бывает весьма длительным) и дает интересную статистику по качеству тестов. Можно просто с командой раз в недельку скажем делать.


                    1. vlfedotov Автор
                      15.11.2017 20:01

                      Спасибо за идеи!