GitLab CI и микросервисы
В нашей компании мы активно используем микросервисы.
В этой статье опишу проблемы, с которыми мы столкнулись при настройке CI для микросервисов, и пути их решения.
Для начала рассмотрим несколько ограничений:
Мы имеем 2 инстанса
GitLab
-а:- один для разработки продуктов нашей команды (соответствует закону Конвея).
- второй общий для всех команд.
Репозитории микросервисов делятся на 2 типа:
- с одним микросервисом, расположенным в корне.
- с несколькими микросервисами, в таком случае для каждого из них мы создаем директорию соответствующую названию сервиса в корне репозитория.
Мы работаем только с одним языком - golang
и поэтому для всех микросервисов этапы CI одинаковы.
Если рассматривать высокоуровнево, то это:
- проверка кода линтером
- запуск тестов
- билд
Docker
образов и их запись вGitLab Container Registry
.
Далее с помощью Jenkins
образы доставляются на продакшн, но это уже совсем другая история.
Мы пока ограничимся рассмотрением работа с GitLab
.
Проблема №1: Дублирование, иногда даже чрезмерное дублирование.
Изначально каждый репозиторий содержал:
gitlab-ci.yml
с полным процессом тестирования и сборки микросервиса.Dockerfile
и.dockerignore
для каждого микросервиса.- 2-3 bash скрипта для настройки и билда образа.
Все файлы, за исключением gitlab-ci.yml
, одинаковы для большинства проектов и они
кочевали из репозитория в репозиторий.
Дублирование было и в рамках репозитория. Например сборка образа для одного мкросервиса, назовем его serviceX, выглядела так:
build-image serviceX:
image: docker:19.03.1
stage: build-image
variables:
PROJECT_PATH: 'serviceX'
script:
- sh build-deploy-container.sh
tags:
- docker
when: manual
only:
- master
- /^v.*$/
Когда в репозитории находилось несколько микросервисов, то эта job-а дублировалась,
менялись только названия и переменная PROJECT_PATH
, и это помимо того,
что существовали и другие стадии CI.
Проблем №2: Когда мы хотели изменить что-то в CI, например, сделать
новый формат версий для Docker
образов, то это делалось в одном проекте,
а потом копировалось в другие. Старые проекты оставались нетронутыми.
Проблема №3: Отсутствие возможности для быстрого применения изменений ко всем проектам. Та же проблема, что и в предыдущем случае, но более критична. Например когда начались проблемы с общедоступными ранерами, нам в срочном порядке нужно было заменить теги для подключения к другим раннерам. Если в предыдущем случае на сборку это никак не влияло, то в данном кейсе это было недопустимо. А когда мы смахивали пыль с давно не разрабатываемых проектов, то снова нужно было искать репозиторий с уже готовым решением и заниматься копипастом.
Extends
Механизм extends
- позволяет наследовать шаблоны,
подробнее можно почитать в официальной документации
Приведу краткий пример использования:
.common-build-image:
image: docker:19.03.1
stage: build-image
script:
- sh build-deploy-container.sh
tags:
- docker
when: manual
only:
- master
- /^v.*$/
build-image service1:
extends: .common-build-image
variables:
PROJECT_PATH: 'service1'
build-image service2:
extends: .common-build-image
variables:
PROJECT_PATH: 'service2'
Как видно можно уменьшить код и вынести общие части в шаблоны, но это не решает наши проблемы, т.к. остается в рамках одного репозитория.
Include
Мы создали общие шаблоны и вынесли их в
один репозиторий, который подключался с помощью include
,
подробности работы также в официальной документации.
Проект получил название Gnome.
Таким образом предыдущий пример стал выглядеть:
include:
- project: 'core/gnome'
file: '.common-templates.yml'
build-image service1:
extends: .common-build-image
variables:
PROJECT_PATH: 'service1'
build-image service2:
extends: .common-build-image
variables:
PROJECT_PATH: 'service1'
Шаблон .common-build-image
был вынесен
в файл .common-templates.yml
в репозиторий core/gnome
В шаблоне у нас использовался bash скрипт (sh build-deploy-container.sh),
который пришлось скопировать в секцию script
.
Таким образом мы смогли:
- Избавиться от дублирующегося описания в
.gitab-ci.yml
. - Удалить bash скрипты, используемые в CI.
- Использовать одни и те же
Dockerfile
и.dockerignore
для всех проектов. - Ну и самое главное - привести сборку к общему виду.
Но это работало только в рамках одного инстанса GitLab
-а.
Подключать удаленные репозитории можно, но с одним условием - они должны быть
публичные, что не удовлетворяло нашим политикам безопасности.
К тому же для каждого из инстансов были свои настройки для работы с ранерами.
К счастью GitLab
имеет механизм зеркалирования репозиториев. Все изменения
вносились в один репозиторий, а при пуше данные попадали в зеркало, расположенное в другом инстансе GitLab
-а.
Таким образом в каждом из инстансов находилось по репозиторию.
Что же касается специфичных моментов, используемых для разных инстансов, то для каждого из них сделали общие шаблон, которые наследовались от еще более общих.
Механизм extends
позволяет указывать несколько шаблонов, но в нашем случае, нужно
подключить нужный шаблон для конкретного инстанса один раз и один раз прописать в extends
.
Для наглядности рассмотрим пример:
.common.yml
(общий шаблон):
.common-build-image:
image: docker:19.03.1
stage: build-image
script:
- step1
- step2
- ...
- stepN
when: manual
only:
- master
- /^v.*$/
.team-templates.yml
(шаблон, специфичный для конкретного иснтасна GitLab
-а):
include:
- local: '.common.yml'
.build-image:
extends: .common-build-image
tags:
- docker
.gitlab-ci.yml
(конечный файл настройки CI):
include:
- project: 'core/gnome'
file: '.team-templates.yml'
build-image service1:
extends: .build-image
variables:
PROJECT_PATH: 'service1'
build-image service2:
extends: .build-image
variables:
PROJECT_PATH: 'service1'
Include remote
Совершенству нет предела. Хотелось все-таки сделать инструмент
принимающий всего один параметр - PROJECT_PATH
и отдающий yaml
файл
со всеми стадиями:
- проверкой линтером
- билдом приложения
- билдом
Docker
образа
Желательно чтобы это было бы динамически, т.к. позволило бы добавлять
дополнительные этапы сборки, не затрагивая репозитории с микросервисами.
Т.е. если в будущем мы захотим добавить в наш pipline
сборку документации для всех микросервисов,
то изменения нужно будет внести только в одном месте.
Как оказалось, такая возможность есть! В рамках R&D был сделан сервис, который
на входе принимал параметры и подставлял их в yml
шаблон. Сам же
сервис подключался через include
.
Теперь для настройки CI в новом репозитории с 3-мя микросервисами
нужно подключить основной шаблон и по одной строчке для каждого микросервиса.
Пример конечного .gitlab-ci.yml
:
include:
- remote: http://127.0.0.1:8000/?template=.team-templates.yml
- remote: http://127.0.0.1:8000/?PROJECT_PATH=servic1&template=single.yml
- remote: http://127.0.0.1:8000/?PROJECT_PATH=servic2&template=single.yml
- remote: http://127.0.0.1:8000/?PROJECT_PATH=servic3&template=single.yml
Пример single.yml
со всеми стадиями CI:
lint {PROJECT_PATH}:
extends: .lint
variables:
PROJECT_PATH: '{PROJECT_PATH}'
# Build application
build {PROJECT_PATH}:
extends: .build
variables:
PROJECT_PATH: '{PROJECT_PATH}'
build-image {PROJECT_PATH}:
extends: .build-image
variables:
PROJECT_PATH: '{PROJECT_PATH}'
dependencies:
- 'build {PROJECT_PATH}'
Сервис выложен на github. Форкайте и адаптируйте под свои нужды.
Статья содержит много специфических особенностей для нашей компании. Надеюсь с помощью индукции вы сможете сделать для себя более общие выводы.
DRY
работает и в CI
.