В эпоху повсеместного CI/CD мы сталкиваемся с большим спектром сопутствующих инструментов, в том числе и CI-систем. Однако именно GitLab стал для нас самым близким, по-настоящему «родным». Заметную популярность он снискал и в индустрии в целом*. Разработчики продукта не отставали от роста интереса к его использованию, регулярно радуя сообщество разработчиков и DevOps-инженеров новыми версиями.
Агрегация по месяцам и тегам репозитория GitLab
GitLab — тот случай, когда активное развитие приносит множество новых и интересных возможностей. Если для потенциальных пользователей это просто один из факторов выбора инструмента, то для действующих — ситуация такова: если вы не обновляли свою инсталляцию GitLab последний месяц, то с большой вероятностью пропустили что-то интересное. В том числе и регулярно выходящие security updates.
О наиболее значимых — т.е. востребованных нашими DevOps-инженерами и клиентами — нововведениях в последних релизах Community-редакции GitLab и пойдет речь в статье.
* Ещё лет 5 назад многие, услышав «GitLab», могли бы переспросить: «Наверное, речь про GitHub?». Сегодня же ситуация иная — например, анализ Google Trends указывает на 5-кратный рост популярности запроса «gitlab» за этот период. Не миновали проект и «чёрные» маркетинговые истории, случившиеся совсем недавно. Однако эта статья про технику, а не политику, поэтому останавливаться на таких «фичах» подробнее не будем.
№1: needs
- Явное указание зависимостей для job'а.
- Появилось с версии GitLab: 12.2.
- Документация.
Думали, dependencies
— это то, что вам надо? Вероятно, не мы одни ошибались в назначении этой директивы… Она нужна для перечисления списка предыдущих job'ов, артефакты из которых потребуются. Именно артефакты, а не зависимость от выполнения предыдущего задания.
Допустим, так случилось, что в одной стадии есть job’ы, которые не обязательно выполнять, но выносить их в отдельную стадию по каким-то причинам нет возможности или просто желания (лень — двигатель прогресса, но не увлекайтесь).
Ситуация:
Как видно, stage Deploy содержит кнопки для выката и на production, и на stage, а job Selenium tests почему-то не выполняется. Всё просто: он ждёт, пока успешно завершатся все job’ы из предыдущей стадии. Однако в рамках этого же пайплайна нам не нужно деплоить stage сейчас, чтобы запустить тесты (он выкачен ранее не в рамках тега). Что же делать? Тут и приходить на помощь needs!
Мы перечисляем только необходимые предыдущие job’ы для запуска наших тестов:
needs:
- To production (Cluster 1)
- To production (Cluster 2)
… и получаем job, что автоматически вызывается после выполнения только перечисленных job'ов:
Удобно, правда? А ведь когда-то я ожидал, что примерно так и будет работать директива dependencies
…
№2: extends
- Расширение возможностей YAML anchors и aliases.
- Появилось с версии GitLab 11.3.
- docs.gitlab.com/ce/ci/yaml/#extends
Надоело читать рулоны .gitlab-ci.yaml
? Скучаете по принципу code reuse? Тогда вы уже попытались и наверняка успешно довели свой .gitlab-ci.yaml
до состояния вроде такого:
.base_deploy: &base_deploy
stage: deploy
script:
- my_deploy_command.sh
variables:
CLUSTER: "default-cluster"
MY_VAR: "10"
Deploy Test:
<<: *base_deploy
environment:
url: test.example.com
name: test
Deploy Production:
<<: *base_deploy
environment:
url: production.example.com
name: production
variables:
CLUSTER: "prod-cluster"
MY_VAR: "10"
Вроде бы здорово? Однако, если присмотреться внимательнее, в глаза что-то бросается… Зачем мы в production изменили не только variables.CLUSTER
, но и второй раз прописали variables.MY_VAR=10
? Эта переменная ведь должна взяться из base_deploy
? Оказывается, не должна: YAML работает так, что, переопределяя полученное из anchor’а, не расширяет содержимое совпадающих полей, а заменяет. Поэтому мы вынуждены в совпадающем пункте перечислить уже известные нам переменные.
Да, «расширяет» — подходящее слово: именно так и называется рассматриваемая фича. Extends
позволяют нам не просто перезаписать поле, как это происходит с anchor, а провести умное слияние для него:
.base_deploy:
stage: deploy
script:
- my_deploy_command.sh
variables:
CLUSTER: "default-cluster"
MY_VAR: "10"
Deploy Production:
extends: .base_deploy
environment:
url: production.example.com
name: production
variables:
CLUSTER: "prod-cluster"
Здесь в итоговой job Deploy Production будут и переменная MY_VAR
со значением по умолчанию, и переопределённая CLUSTER
.
Кажется, что это такая мелочь, но представьте: у вас один base_deploy
и 20 контуров, деплоящихся аналогично. Им нужно передать другие cluster
, environment.name
, при этом сохранив какой-то набор переменных или других совпадающих полей… Нам эта маленькая приятность позволила сократить описание деплоя множества dev-контуров в 2-3 раза.
№3: include
- Делим огромный YAML на несколько и повторно используем в других проектах.
- Появилось в Core с версии 11.4.
- docs.gitlab.com/ce/ci/yaml/#include
.gitlab-ci.yaml
всё ещё выглядит как складная инструкция к пылесосу на 20 языках (из которых вам понятен только родной) сложно, когда требуется разобраться с одной его секцией, не меняясь в лице от неведомых job'ов, встречающихся на пути?
Поможет давно знакомый по программированию include
:
stages:
- test
- build
- deploy
variables:
VAR_FOR_ALL: 42
include:
- local: .gitlab/ci/test.yml
- local: .gitlab/ci/build.yml
- local: .gitlab/ci/deploy-base.yml
- local: .gitlab/ci/deploy-production.yml
Т.е. теперь смело занимаемся правками деплоя в production, пока тестировщики заняты модификацией своего файла, на который мы можем даже не смотреть. Вдобавок, это помогает избегать merge conflict'ов: ведь разбираться в чужом коде не всегда в радость.
А что, если мы знаем пайплайн своих 20 проектов вдоль и поперёк, можем объяснить логику каждой job из него? Чем нам это поможет? Для достигших просветления в code reuse и для всех, у кого много однотипных проектов, можно:
- с помощью include:file — подключать файлы GitLab CI из другого репозитория того же инстанса GitLab,
- с include:template — коллекции готовых шаблонов GitLab,
- с include:remote — внешнего файла (доступного по HTTPS).
Десяток однотипных проектов с разным кодом, но деплоящихся одинаково — легко и без поддержания в актуальном виде CI во всех репозиториях!
Пример практического использования include
мы приводили также в этой статье.
№4: only/except refs
- Комплексные условия, включая переменные и изменения файлов.
- Поскольку это целое семейство функций, отдельные части начали появляться в GitLab 10.0, а другие (например,
changes
) — с 11.4. - docs.gitlab.com/ce/ci/yaml/#onlyexcept-advanced
Иногда мне кажется, что это не пайплайн слушается нас, а мы его. Отличным инструментом для управления являются only
/except
— теперь уже комплексные. Что это означает?
В самом простом (и, пожалуй, самом приятном) случае — пропуск стадий:
Tests:
only:
- master
except:
refs:
- schedules
- triggers
variables:
- $CI_COMMIT_MESSAGE =~ /skip tests/
В примере job выполняется только на ветке мастер, но не может быть инициирован расписанием или триггером (вызовы API и триггеров GitLab разделяет, хоть это по сути всё то же API). Job не будет выполняться и в случае, когда в сообщении коммита будет кодовая фраза skip tests. Например, была исправлена опечатка в README.md
проекта или документации — зачем ждать результатов тестирования?
«Эй, 2020 год за окном! Почему это я должен каждый раз объяснять железной коробке, что запускать тесты при изменении документации не нужно?» И действительно: only:changes
позволяет запускать тесты при изменении файлов только в определённых каталогах. Например:
only:
refs:
- master
- merge_requests
changes:
- "front/**/*"
- "jest.config.js"
- "package.json"
А для обратного действия — т.е. не запускать — есть except:changes
.
№5: rules
- Индивидуальные правила заданий.
- Появились в GitLab 12.3.
- docs.gitlab.com/ce/ci/yaml/#rules
Эта директива очень похожа на предыдущие only:*
, но с важным отличием: она позволяет управлять параметром when
. Например, если вы хотите не убирать совсем возможность запуска job’а. Можно просто оставить кнопку, которую при желании будут вызывать самостоятельно, не производя запуск нового пайплайна или не делая commit.
№6: environment:auto_stop_in
- Остановка окружений при отсутствии активности.
- Появилась в GitLab 12.8.
- docs.gitlab.com/ce/ci/yaml/#environmentauto_stop_in
Об этой возможности мы узнали прямо перед публикацией статьи и ещё не успели достаточно опробовать на практике, однако это определённо «то самое», чего так ждали в нескольких проектах.
В GitLab окружениям можно указывать параметр on_stop
— очень полезно, когда хочется создавать и удалять окружения динамически, например, к каждой ветке. Job, помеченный к on_stop
, выполняется, например, при merge'е MR'а в master-ветку или при закрытии MR'а (или даже просто по нажатию на кнопку), благодаря чему ненужное окружение автоматически удаляется.
Всё удобно, логично, работает… если бы не человеческий фактор. Многие разработчики merge'ат MR'ы не нажатием кнопки в GitLab, а локально через git merge
. Их можно понять: ведь это удобно! Но в таком случае логика on_stop
не срабатывает, у нас копятся забытые окружения… Здесь-то и пригодится долгожданный auto_stop_in
.
Бонус: времянки, когда не хватает возможностей
Несмотря на все эти (и многие другие) новые, востребованные функции GitLab, к сожалению, иногда условия выполнения job’а просто невозможно описать в рамках имеющихся на данный момент возможностей.
GitLab не совершенен, зато даёт базовые инструменты для построения пайплайна мечты… если вы готовы выйти за рамки скромного DSL, окунувшись в мир скриптинга. Вот несколько решений из нашего опыта, которые ни в коем случае не претендуют на идеологически верные или рекомендованные, а приведены больше для демонстрирации разных возможностей при нехватке встроенного функционала API.
Workaround №1: запуск двух job одной кнопкой
script:
- >
export CI_PROD_CL1_JOB_ID=`curl -s -H "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}"
"https://gitlab.domain/api/v4/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs" |
jq '[.[] | select(.name == "Deploy (Cluster 1)")][0] | .id'`
- >
export CI_PROD_CL2_JOB_ID=`curl -s -H "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}"
"https://gitlab.domain/api/v4/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs" |
jq '[.[] | select(.name == "Deploy (Cluster 2)")][0] | .id'`
- >
curl -s --request POST -H "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}"
"https://gitlab.domain/api/v4/projects/${CI_PROJECT_ID}/jobs/$CI_PROD_CL1_JOB_ID/play"
- >
curl -s --request POST -H "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}"
"https://gitlab.domain/api/v4/projects/${CI_PROJECT_ID}/jobs/$CI_PROD_CL2_JOB_ID/play"
А почему нет, если очень хочется?
Workaround №2: передача изменившихся в MR rb-файлов для rubocop внутри образа
Rubocop:
stage: test
allow_failure: false
script:
...
- export VARFILE=$(mktemp)
- export MASTERCOMMIT=$(git merge-base origin/master HEAD)
- echo -ne 'CHANGED_FILES=' > ${VARFILE}
- if [ $(git --no-pager diff --name-only ${MASTERCOMMIT} | grep '.rb$' | wc -w |awk '{print $1}') -gt 0 ]; then
git --no-pager diff --name-only ${MASTERCOMMIT} | grep '.rb$' |tr 'n' ' ' >> ${VARFILE} ;
fi
- if [ $(wc -w ${VARFILE} | awk '{print $1}') -gt 1 ]; then
werf --stages-storage :local run rails-dev --docker-options="--rm --user app --env-file=${VARFILE}" -- bash -c /scripts/rubocop.sh ;
fi
- rm ${VARFILE}
Внутри образа нет .git
, поэтому пришлось выкручиваться, чтобы проверять только изменившиеся файлы.
Примечание: Это не совсем стандартная ситуация и отчаянная попытка соблюсти множество условий задачи, описание которой не входит в рамки данной статьи.
Workaround №3: триггер запуска jobs из других репозиториев при выкате
before_script:
- |
echo '### Trigger review: infra'
curl -s -X POST
-F "token=$REVIEW_TOKEN_INFRA"
-F "ref=master"
-F "variables[REVIEW_NS]=$CI_ENVIRONMENT_SLUG"
-F "variables[ACTION]=auto_review_start"
https://gitlab.example.com/api/v4/projects/${INFRA_PROJECT_ID}/trigger/pipeline
Казалось бы, такая простая и необходимая (в мире микросервисов) вещь — выкат другого микросервиса в свежесозданный контур как зависимость. Но её нет, поэтому требуется вызов API и уже знакомая (описанная выше) фича:
only:
refs:
- triggers
variables:
- $ACTION == "auto_review_start"
Примечания:
- Job на trigger создан до возможности завязываться на передачу переменной в API, аналогично примеру №1. Логичнее реализовывать это на API с передачей имени job’а.
- Да, функция есть в коммерческой (EE) версии GitLab, но мы её не рассматриваем.
Заключение
GitLab старается не отставать от трендов, постепенно реализуя приятные и востребованные DevOps-сообществом фичи. Они достаточно просты в использовании, а когда базовых возможностей не хватает, их всегда можно расширить скриптами. И если мы видим, что получается уже не столь изящно и удобно в поддержке… остаётся ждать новых релизов GitLab — или же помочь проекту своим вкладом.
P.S.
Читайте также в нашем блоге:
- «Сборка и деплой однотипных микросервисов с werf и GitLab CI»;
- «JUnit в GitLab CI с Kubernetes»;
- «werf — наш инструмент для CI/CD в Kubernetes (обзор и видео доклада)» (Дмитрий Столяров; 27 мая 2019 на DevOpsConf);
- «Советы по созданию нестандартных рабочих процессов в GitLab CI»;
- «GitLab CI для непрерывной интеграции и доставки в production».
Автор: Михаил Носов