Два года назад мы публиковали статью «Сборка проектов с GitLab CI: один .gitlab-ci.yml для сотни приложений», а теперь расскажем о решении схожей задачи сегодня. Новый материал — о том, как можно построить CI/CD-процессы для большого количества однотипных приложений с появлением
include
в .gitlab-ci.yml
и приходом werf на замену dapp.Вводные
В дальнейших инструкциях, приведенных в статье, рассматривается следующая ситуация:
- Есть большое клиентское приложение, которое разбито на множество репозиториев.
- Каждый репозиторий представляет собой отдельное приложение, которое необходимо запускать в Kubernetes-кластере.
- В качестве CI-системы используется GitLab CI.
- Деплой (инфраструктура, в которую разворачивается код) описывается Helm-чартами.
- Сборка образов и деплой в Kubernetes осуществляется с помощью werf.
Для простоты и удобства (и как дань моде) мы будем в дальнейшем называть эти приложения микросервисами. Все эти микросервисы собираются, деплоятся и запускаются одинаково, а специфические настройки конфигурируются с помощью переменных окружения.
Понятно, что копирование
.gitlab-ci.yml
, werf.yaml
и .helm
приносит множество проблем. Ведь любая правка в CI, сборочный процесс или описание Helm-чарта должна быть добавлена и в остальные репозитории…Подключение шаблонов в .gitlab-ci.yml
С появлением в GitLab CE директивы
include:file
(с версии 11.7) стало возможным делать общий CI. Сам include
появился несколько ранее (в 11.4), но он позволял подключать шаблоны только с публичных URL, что несколько ограничивало его функциональность. В документации GitLab прекрасно описаны все возможности и примеры использования.Таким образом удалось отказаться от копирования
.gitlab-ci.yml
между репозиториями и поддержки его актуальности. Вот пример .gitlab-ci.yml
с include
:include:
- project: 'infra/gitlab-ci'
ref: 1.0.0
file: base-gitlab-ci.yaml
- project: 'infra/gitlab-ci'
ref: 1.0.0
file: cleanup.yaml
Настоятельно рекомендуем с осторожностью использовать имена веток в
ref
. Include’ы вычисляются на момент создания pipeline, поэтому ваши изменения по CI могут автоматически попасть в production pipeline в самый неподходящий момент. А вот использование в ref
тегов позволяет легко версионировать описание CI/CD-процессов. При обновлении всё выглядит максимально прозрачно и можно с легкостью отследить историю изменения версий pipeline, если использовать семантическое версионирование для тегов.Подключение .helm из отдельного репозитория
Поскольку рассматриваемые микросервисы деплоятся и запускаются одинаково, необходим одинаковый набор Helm-шаблонов. Чтобы избежать копирования каталога
.helm
между репозиториями, раньше мы выполняли клонирование репозитория, в котором хранились Helm-шаблоны и делали checkout на нужный тег. Выглядело это примерно так: - git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.example.com/infra/helm.git .helm
- cd .helm && git checkout tags/1.0.0
- type multiwerf && source <(multiwerf use 1.0 beta)
- type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
- werf deploy --stages-storage :local
Были также вариации с использованием git submodules, но всё это больше похоже на обходной путь…
И вот с недавним релизом werf у него появилась возможность подключать чарты из внешних репозиториев. Полноценная поддержка функций пакетного менеджера в свою очередь позволила прозрачно описать зависимости для деплоя приложения.
Последовательность действий
Вернёмся к решению нашей задачи с микросервисами. Поднимем свой репозиторий для хранения Helm-чартов — например, ChartMuseum. Он легко разворачивается в кластере Kubernetes:
helm repo add stable https://kubernetes-charts.storage.googleapis.com
helm install stable/chartmuseum --name flant-chartmuseum
Добавим ingress:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/force-ssl-redirect: "false"
nginx.ingress.kubernetes.io/proxy-body-size: 10m
nginx.ingress.kubernetes.io/ssl-redirect: "false"
name: chart-museum
spec:
rules:
- host: flant-chartmuseum.example.net
http:
paths:
- backend:
serviceName: flant-chartmuseum
servicePort: 8080
path: /
status:
loadBalancer: {}
Deployment'у
flant-chartmuseum
необходимо поменять переменную окружения DISABLE_API
на значение false
. В ином случае (по умолчанию) запросы к API ChartMuseum не будут работать и нельзя будет создавать новые чарты.Теперь опишем репозиторий, в котором будут храниться общие Helm-чарты. Структура его каталогов — следующая:
.
+-- charts
¦ L-- yii2-microservice
¦ +-- Chart.yaml
¦ L-- templates
¦ +-- app.yaml
L-- README.md
Chart.yaml
может выглядеть следующим образом:name: yii2-microservice
version: 1.0.4
В каталоге
templates
должны находиться все необходимые примитивы Kubernetes’а, которые понадобятся для деплоя приложения в кластер. Как вы уже, возможно, догадались, в данном случае микросервис представляет собой PHP-приложение на основе фреймворка yii2. Опишем его минимальный Deployment с двумя контейнерами nginx и php-fpm, которые собираются с помощью werf:---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.global.werf.name }}
spec:
replicas: 1
revisionHistoryLimit: 3
template:
metadata:
labels:
service: {{ .Values.global.werf.name }}
spec:
imagePullSecrets:
- name: registrysecret
containers:
- name: backend
{{ tuple "backend" . | include "werf_container_image" | indent 8 }}
command: [ '/usr/sbin/php-fpm7', "-F" ]
ports:
- containerPort: 9000
protocol: TCP
name: http
env:
{{ tuple "backend" . | include "werf_container_env" | indent 8 }}
- name: frontend
command: ['/usr/sbin/nginx']
{{ tuple "frontend" . | include "werf_container_image" | indent 8 }}
ports:
- containerPort: 80
name: http
lifecycle:
preStop:
exec:
command: ["/usr/sbin/nginx", "-s", "quit"]
env:
{{ tuple "frontend" . | include "werf_container_env" | indent 8 }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.global.werf.name }}
spec:
selector:
service: {{ .Values.global.werf.name }}
ports:
- name: http
port: 80
protocol: TCP
Переменная
.Values.global.werf.name
содержит название проекта из файла werf.yaml
, что позволяет получить необходимые имена сервисов и Deployment’ов.Сделаем простейшую автоматизацию для push’а в ChartMuseum наших чартов при коммите в master-ветку. Для этого опишем
.gitlab-ci.yml
:Build and push to chartmuseum:
script:
- for i in $(ls charts); do helm package "charts/$i"; done;
- for i in $(find . -type f -name "*.tgz" -printf "%f\n"); do curl --data-binary "@$i" http://flant-chartmuseum.example.net/api/charts; done;
stage: build
environment:
name: infra
only:
- master
tags:
- my-shell-runner-tag
Версионирование чартов осуществляется с помощью изменения
version
в Chart.yaml
. Все новые чарты автоматически будут добавлены в ChartMuseum.Выходим на финишную прямую! В репозитории проекта в
.helm/requirements.yaml
прописываем зависимости для чарта:dependencies:
- name: yii2-microservice
version: "1.0.4"
repository: "@flant"
… и выполняем в директории с репозиторием:
werf helm repo init
werf helm repo add flant http://flant-chartmuseum.example.net
werf helm dependency update
Получаем в ней
.helm/requirements.lock
. Теперь для деплоя приложения в кластер достаточно выполнять команду werf helm dependency build
перед запуском werf deploy
.Для обновления описания деплоя приложения теперь необходимо пройтись по репозиториям с микросервисами и наложить небольшие патчи c изменениями хешей и тегов в
requirements.yaml
и requirements.lock
. Данную операцию при желании также можно автоматизировать через CI: как это сделать, мы уже рассказывали в упомянутой статье.Заключение
Надеюсь, описанная последовательность действий для обслуживания однотипных приложений окажется полезной инженерам, что сталкиваются с похожими проблемами. А мы будем рады поделиться и другими практическими рецептами использования werf. Поэтому, если у вас есть сложности, кажущиеся непреодолимыми или просто непонятными в реализации, — смело выходите на связь в Telegram или оставляйте здесь в комментариях запросы на будущие материалы.
P.S.
Читайте также в нашем блоге:
- «Сборка проектов с GitLab CI: один .gitlab-ci.yml для сотни приложений»;
- «GitLab CI для непрерывной интеграции и доставки в production. Часть 1: наш пайплайн»;
- Цикл заметок о нововведениях в werf:
onegreyonewhite
А что мешало использовать git submodules? Gitlab этот функционал неплохо поддерживает.
konstantin_axenov Автор
onegreyonewhite
Я наверное коряво задал вопрос. Чем уступают? Точнее что конкретно вам мешало в подходе с подмодулями? Вы просто написали, что это было похоже на обходной путь, но без пояснений, поэтому и спрашиваю опыта ради.
konstantin_axenov Автор
Git submodule, является частью git, что делает его инструментом для управления исходным кодом. Если есть необходимость работать с исходниками нескольких репозиториев в рамках одного, то git submodule — это отличный выбор. В таком формате, конечно же, удобнее вести разработку.
Git submodule однозначно начинают проигрывать, когда исходники не нужны, а нужно просто стянуть все необходимые пакеты. Например, нужны не только helm шаблоны описанные в собственных репозиториях, но и чарты с hub.helm.sh. Да, можно использовать git submodule, так как большинство чартов имеют открытые исходники, но гораздо проще, используя менеджер пакетов, сказать, что нужен пакет следующей версии.
С сабмодулями есть ещё одна небольшая проблема, многие разработчики используют их в своих проектах. Поэтому периодический вызов
git submodule update --init --recursive --remote --merge
приводит к тому, что в репозитории проекта вместе с библиотеками проекта обновляется ещё и инфраструктура. Но это скорее человеческий фактор.