GitLab CI и микросервисы

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

  1. Мы имеем 2 инстанса GitLab-а:

    • один для разработки продуктов нашей команды (соответствует закону Конвея).
    • второй общий для всех команд.
  2. Репозитории микросервисов делятся на 2 типа:

    • с одним микросервисом, расположенным в корне.
    • с несколькими микросервисами, в таком случае для каждого из них мы создаем директорию соответствующую названию сервиса в корне репозитория.

Мы работаем только с одним языком - golang и поэтому для всех микросервисов этапы CI одинаковы. Если рассматривать высокоуровнево, то это:

Далее с помощью Jenkins образы доставляются на продакшн, но это уже совсем другая история. Мы пока ограничимся рассмотрением работа с GitLab.

Проблема №1: Дублирование, иногда даже чрезмерное дублирование.

Изначально каждый репозиторий содержал:

Все файлы, за исключением 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.

Таким образом мы смогли:

Но это работало только в рамках одного инстанса 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 файл со всеми стадиями:

Желательно чтобы это было бы динамически, т.к. позволило бы добавлять дополнительные этапы сборки, не затрагивая репозитории с микросервисами. Т.е. если в будущем мы захотим добавить в наш 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.