Считается, что построение CI/CD - задача для DevOps. Глобально это действительно так, особенно если речь идет о первоначальной настройке. Но часто с докручиванием отдельных этапов процесса сталкиваются и разработчики. Умение поправить что-то незначительное своими силами позволяет не тратить время на поход к коллегам (и ожидание их реакции), т.е. в целом повышает комфорт работы и дает понимание, почему все происходит именно так.
Настроек для пайплайна Gitlab очень много. В этой статье, не вдаваясь в недра тюнинга, поговорим о том, как выглядит скрипт пайплайна, из каких блоков он состоит и что может содержать.
![(картинка из документации CI/CD Gitlab) (картинка из документации CI/CD Gitlab)](https://habrastorage.org/getpro/habr/upload_files/148/eb8/fec/148eb8fec92fa6b02cea9926c6d7f6b4.png)
Статья написана по материалам внутреннего ознакомительного митапа для разработчиков.
Всем привет! Меня зовут Денис, я DevOps инженер на внутреннем проекте нашей компании - Mondiad. В этой статье поговорим о том, как писать пайплайны для GitLab CI/CD, который в основном используется у нас на проекте. Поговорим о содержимом этого скрипта - как его читать, понимать и отлаживать.
Скрипт CI пишется на YAML. .gitlab-ci.yml - это единственный файл, который лежит непосредственно в корне проекта. В любых других папках GitLab его просто не прочитает, соответственно пайплайн работать не будет.
Инструменты диагностики
Для отладки и для понимания пайплайна можно использовать средство, встроенное в саму оболочку GitLab: Build -> Pipeline Editor.
В интерфейсе инструмента четыре закладки:
Элементы на закладке Edit позволяют поправить пайплайн. В принципе, его можно даже не коммитить, а работать так.
На закладке Visualize можно посмотреть все стадии и задачи, которые входят в пайплайн.
Validate проводит валидацию - проверяет синтаксис (иначе можно написать такой пайплайн, что GitLab его не поймет и выдаст ошибку).
Последняя вкладка - Full configuration - отображает полный текст скрипта пайплайна. Когда мы используем различные фишки для сокращения кода, Full configuration отображает ситуацию без них. В редакторе удобно накидать скрипт и свериться с Full configuration - увидеть, как он его собрал.
Минимальный скрипт
Минимальный сценарий GitLab CI выглядит следующим образом:
stages:
- build
TASK_NAME:
stage: build
script:
- ./build_script.sh
Здесь указывается стадия пайплайна и имя задания.
Фичи файла .gitlab-ci.yml
Stage
https://docs.gitlab.com/ee/ci/yaml/#stages
Помимо тех stage, которые мы создаем сами, есть два скрытых стейджа:
.pre - выполняется всегда первым;
.post - выполняется всегда последним. Как правило, он используется, чтобы зачистить какие-то данные (например, если разворачивается инфраструктура для тестирования задач). .post проходит, даже если по мере выполнения отдельных стейджей выпадают ошибки.
Данные стейджи не обязательно объявлять. При этом пайплайн не может содержать только эти два стейджа.
stages:
- build
job1:
stage: build
script:
- echo "This job runs in the build stage."
first-job:
stage: .pre
script:
- echo "This job runs in the .pre stage, before all other stages."
last-job:
stage: .post
script:
- echo "This job runs in the .post stage, after all other stages."
Секция default
https://docs.gitlab.com/ee/ci/yaml/#default
В данной секции собран глобальный набор опций задания, которые могут быть переопределены позже.
Самые распространенные:
before_script
after_script
cache
image
services
tags
retry
default:
image: ruby:3.0
retry: 2
В основном данная секция используется, чтобы сократить тело самого скрипта.
Секция variables
https://docs.gitlab.com/ee/ci/yaml/#variables
Чтобы управлять процессом сборки, в скрипте можно использовать переменные самого GitLab. Наиболее используемые:
$CI_PROJECT_DIR - полный путь до проекта внутри контейнера.
$CI_COMMIT_REF_NAME - содержит имя бранчи и имя тега.
$CI_COMMIT_TAG - имя тега. Эта переменная получает данные, только если мы вставляем тег.
$CI_PROJECT_NAMESPACE - namespace проекта.
$CI_PROJECT_NAME - имя проекта.
$CI_REGISTRY - путь для GitLab registry.
$CI_REGISTRY_USER - пользователь для GitLab registry.
$CI_REGISTRY_PASSWORD - пароль для GitLab registry.
Полный перечень предопределенных переменных есть в документации: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
Если этих переменных не хватает, в секции variables можно задать свои и использовать их в заданиях.
variables:
DEPLOY_SITE: "https://example.com/"
Секция workflow
https://docs.gitlab.com/ee/ci/yaml/#workflow
Следующая секция позволяет определить общие правила запуска для всего пайплайна. По сути это также сокращение скрипта - дефолтные правила для всех заданий в пайплайне.
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
variables:
PROJECT1_PIPELINE_NAME: 'MR pipeline: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME'
- if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3/'
variables:
PROJECT1_PIPELINE_NAME: 'Ruby 3 pipeline'
- when: always # Other pipelines can run, but use the default name
Опция image
https://docs.gitlab.com/ee/ci/yaml/#image
У нас на проекте используются в основном раннеры с docker-образами. Image задает имя образа, в котором будет выполняться задание. Например, если в проекте используется Ruby, то нам нужен образ с установленным внутри Ruby.
rspec:
image: registry.example.com/my-group/my-project/ruby:2.7
script: bundle exec rspec
Опции задания
Опция Tags
https://docs.gitlab.com/ee/ci/yaml/#tags
job:
tags:
- ruby
- postgres
Tags определяет на каком раннере должно выполняться задание. Например, нам нужен раннер, который должен стоять в тестовом окружении, где присутствует некая база данных. В этом случае необходимо указать соответствующий тег раннера.
Опции before_script / script / after_script
https://docs.gitlab.com/ee/ci/yaml/#before_script
https://docs.gitlab.com/ee/ci/yaml/#script
https://docs.gitlab.com/ee/ci/yaml/#after_script
Основное тело нашего задания условно можно разделить на три части:
before_script - это набор команд, который выполняется непосредственно перед телом задания, но уже после получения всех артефактов и кэша. Обычно туда заносят команды, которые подготавливают среду для выполнения задания.
script - это тело задания. В основном оно состоит из набор команд, которые мы, грубо говоря, набираем в shell, чтобы выполнить это задание - run, playbook.
after_script - набор команд, который выполняется после тела задания. Важно, что этот набор выполняется всегда, даже если задание завершилось с ошибкой. Единственный вариант, при которому after_script не отработает - это если before_script завалится, поскольку в этом случае задание даже не начнется.
Еще один важный момент - after_script относится только к конкретному заданию (job). Если нужно, чтобы отработал определенный стейдж, есть .post.
Обычно в after_script помещают задачи, которые зачищают за собой чувствительные данные. В примере ниже это ключ, который используется для доступа. Также встречаются скрипты, которые подготавливают данные для тестов или выполняют другие задания, которые необходимо сделать в любом случае.
В скриптах можно использовать длинные команды...
В скриптах можно использовать длинные команды, которые начинаются с I или >. Раннер по-разному интерпретирует эти символы.
I - каждая новая строка является переходом к новой команде (по аналогии с shell);
> - новая команда интерпретируется после новой строки.
В старых версиях YAML доступен только один из символов, но сейчас можно пользоваться обоими.
test-stage:
stage: deploy
image: ansible:2.9.18
before_script:
- echo "$DEPLOY_KEY" > ~/.ssh/id_rsa
- chmod 700 ~/.ssh/id_rsa
script:
- >
echo “run”
ansible-playbook -i invetory.ini playbook.yaml
after_script:
- rm ~/.ssh/id_rsa
Опция artifacts
https://docs.gitlab.com/ee/ci/yaml/#artifacts
Далее идут артефакты. Как правило, они используется для того, чтобы сохранить некоторые данные, которые мы создаём во время выполнения задания. Это могут быть бинарные сборки, логи для дальнейшей обработки, да и все, что угодно (иначе все это мы просто потеряем).
В этом разделе мы прописываем путь до файла (path), где собираем эти данные. Путь можно задавать через маску при помощи *. Для включения папки и ее подпапок стоит использовать конструкцию **/* (именно две звездочки), а исключить часть файлов позволяет exclude.
job:
artifacts:
paths:
- binaries/
- .config
exclude:
- binaries/**/*.o
expire_in: 1 week
Еще одна полезная штука - expire. Она хранит артефакт в течение определенного времени, после чего он автоматически удалится с GitLab, чтобы не занимать место.
Опция cache
https://docs.gitlab.com/ee/ci/yaml/#cache
Следующая полезная опция - кэш. Она позволяет кэшировать данные которые создаются в процессе выполнения задания и которые требуется переиспользовать в дальнейшем, чтобы ускорить пайплайн (например, чтобы каждый раз тот же Gradle не выкачивал свои пакеты). Кеширование очень часто используется для npm пакетов. Path позволяет указать путь до папки с кэшем.
По умолчанию кэш везде используется один и тот же. И здесь может быть полезен Key, с помощью которого можно присвоить кэшу уникальный ключ. Это бывает полезно, когда нужно, чтобы для каждой ветки кэш был свой, например если используются разные версии пакетов.
job:
script:
- echo "This job uses a cache."
cache:
key: binaries-cache-$CI_COMMIT_REF_SLUG
paths:
- .gradle/caches
Опция needs
https://docs.gitlab.com/ee/ci/yaml/#needs
позволяет задать зависимость от какого-либо артефакта или другого задания. Например, некоторый артефакт у нас появляется на определенном этапе, соответственно, задание получается зависимым.
Также опция позволяет ускорить переход к выполнению следующего задания (т.к. с ней не надо ждать выполнения всего стейджа).
test-job1:
stage: test
needs:
- job: build_job1
artifacts: true
test-job2:
stage: test
needs:
- job: build_job2
artifacts: false
test-job3:
needs:
- job: build_job1
artifacts: true
- job: build_job2
- build_job3
Опция services
https://docs.gitlab.com/ee/ci/yaml/#services
Опция позволяет запустить дополнительный docker-контейнер для задания. Например, с базой данных Postgres или docker-dind (контейнер, который обязательно присутствует при сборке docker-образов). Т.к. мы работаем внутри docker, сборка там запрещена. Соответственно, мы можем отправить сборку в этот сервис.
test-job1:
stage: test
services:
- name: docker:dind
- name: postgres:9.6
Опция when
https://docs.gitlab.com/ee/ci/yaml/#when
Следующая опция - when - позволяет задать простейшие условия запуска задания. Например, если мы хотим запускать его только вручную.
deploy_job:
stage: deploy
script:
- make deploy
when: manual
cleanup_build_job:
stage: cleanup_build
script:
- cleanup build when failed
when: on_failure
Rules
https://docs.gitlab.com/ee/ci/yaml/#rules
Более сложные правила запуска можно строить с помощью rules. Здесь можно использовать простое условие if (задача отработает, если, допустим, ее запустили из определенной ветки) или отслеживать изменения в файлах с помощью changes (задача отработает, если изменился определенный файл).
job:
script: echo "Hello, Rules!"
rules:
- if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/
when: never
allow_failure: true
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- Dockerfile
when: manual
allow_failure: true
Как сократить скрипт
Переходим к завершающему этапу - как сократить скрипт, не сильно потеряв в его читабельности. Есть два подхода:
Использовать встроенную опцию Gitlab-а extends https://docs.gitlab.com/ee/ci/yaml/#extends. Она позволяет создать подобие функции, куда мы выносим часто повторяющихся опций. Это удобно, если в скрипте много однотипных заданий. Опции из Extends объединяются с тем, что уже есть в скрипте.
.my_extend: stage: build variables: USERNAME: my_user script: - extend script TASK_NAME: extends: .my_extend variables: VERSION: 123 PASSWORD: my_pwd script: - task script | TASK_NAME: stage: build variables: VERSION: 123 PASSWORD: my_pwd USERNAME: my_user script: - task script |
Использовать референс-ссылки. Это почти то же самое. Мы также вставляем ссылку, но она позволяет не добавить, а перезаписать опции. В некоторых случаях, например когда нужно полностью стереть весь дефолтный набор переменных, удобно использовать именно этот подход. Референсные ссылки могут быть вложенные - каждая следующая будет перезаписывать предыдущую.
.my_extend: &my_extend stage: build variables: USERNAME: my_user script: - extend script TASK_NAME: <<: *my_extend variables: VERSION: 123 PASSWORD: my_pwd script: - task script | TASK_NAME: stage: build variables: VERSION: 123 PASSWORD: my_pwd script: - task script |
Автор: Денис Палагута, Максилект.
Спасибо DevOps-команде Maxilect за помощь в подготовке и комментарии к статье.
P.S. Мы публикуем наши статьи на нескольких площадках Рунета. Подписывайтесь на нашу страницу в VK или на Telegram-канал, чтобы узнавать обо всех публикациях и других новостях компании Maxilect.