Настройка CI/CD для самых маленьких разработчиков

Считается, что построение CI/CD — задача для DevOps. Глобально это действительно так, особенно если речь идет о первоначальной настройке. Но часто с докручиванием отдельных этапов процесса сталкиваются и разработчики. Умение поправить что-то незначительное своими силами позволяет не тратить время на поход к коллегам (и ожидание их реакции), т.е. в целом повышает комфорт работы и дает понимание, почему все происходит именно так.

Настроек для пайплайна Gitlab очень много. В этой статье, не вдаваясь в недра тюнинга, поговорим о том, как выглядит скрипт пайплайна, из каких блоков он состоит и что может содержать.

(картинка из документации CI/CD Gitlab)

(картинка из документации CI/CD Gitlab)

Статья написана по материалам внутреннего ознакомительного митапа для разработчиков.

Всем привет! Меня зовут Денис, я 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 помещают задачи, которые зачищают за собой чувствительные данные. В примере ниже это ключ, который используется для доступа. Также встречаются скрипты, которые подготавливают данные для тестов или выполняют другие задания, которые необходимо сделать в любом случае.
В скриптах можно использовать длинные команды…
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 за помощь в подготовке и комментарии к статье.

Наши статьи по теме:

Все статьи

Связаться с нами

Мы свяжемся с вами в течение 24 часов.