GitLab CI: 6 фич из последних релизов, которых мы так ждали

gws2_lpk_xse3zwysoxi7yhgedi.png

В эпоху повсеместного CI/CD мы сталкиваемся с большим спектром сопутствующих инструментов, в том числе и CI-систем. Однако именно GitLab стал для нас самым близким, по-настоящему «родным». Заметную популярность он снискал и в индустрии в целом*. Разработчики продукта не отставали от роста интереса к его использованию, регулярно радуя сообщество разработчиков и DevOps-инженеров новыми версиями.

lelpfl_26qkmpjz5qf5cjq4bbd0.png
Агрегация по месяцам и тегам репозитория GitLab

GitLab — тот случай, когда активное развитие приносит множество новых и интересных возможностей. Если для потенциальных пользователей это просто один из факторов выбора инструмента, то для действующих — ситуация такова: если вы не обновляли свою инсталляцию GitLab последний месяц, то с большой вероятностью пропустили что-то интересное. В том числе и регулярно выходящие security updates.

О наиболее значимых — т.е. востребованных нашими DevOps-инженерами и клиентами — нововведениях в последних релизах Community-редакции GitLab и пойдет речь в статье.

* Ещё лет 5 назад многие, услышав «GitLab», могли бы переспросить: «Наверное, речь про GitHub?». Сегодня же ситуация иная — например, анализ Google Trends указывает на 5-кратный рост популярности запроса «gitlab» за этот период. Не миновали проект и «чёрные» маркетинговые истории, случившиеся совсем недавно. Однако эта статья про технику, а не политику, поэтому останавливаться на таких «фичах» подробнее не будем.

№1: needs


  • Явное указание зависимостей для job’а.
  • Появилось с версии GitLab: 12.2.
  • Документация.


Думали, dependencies — это то, что вам надо? Вероятно, не мы одни ошибались в назначении этой директивы… Она нужна для перечисления списка предыдущих job’ов, артефакты из которых потребуются. Именно артефакты, а не зависимость от выполнения предыдущего задания.

Допустим, так случилось, что в одной стадии есть job«ы, которые не обязательно выполнять, но выносить их в отдельную стадию по каким-то причинам нет возможности или просто желания (лень — двигатель прогресса, но не увлекайтесь).

Ситуация:

_usak1lrpdqpjtxln69r3ktc5ag.png

Как видно, stage Deploy содержит кнопки для выката и на production, и на stage, а job Selenium tests почему-то не выполняется. Всё просто: он ждёт, пока успешно завершатся все job«ы из предыдущей стадии. Однако в рамках этого же пайплайна нам не нужно деплоить stage сейчас, чтобы запустить тесты (он выкачен ранее не в рамках тега). Что же делать? Тут и приходить на помощь needs!

Мы перечисляем только необходимые предыдущие job«ы для запуска наших тестов:

  needs:
    - To production (Cluster 1)
    - To production (Cluster 2)


… и получаем job, что автоматически вызывается после выполнения только перечисленных job’ов:

jm9y8zesapj3neflq_xmn7er70i.png

Удобно, правда? А ведь когда-то я ожидал, что примерно так и будет работать директива dependencies

№2: 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 и для всех, у кого много однотипных проектов, можно:


Десяток однотипных проектов с разным кодом, но деплоящихся одинаково — легко и без поддержания в актуальном виде 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


Эта директива очень похожа на предыдущие only:*, но с важным отличием: она позволяет управлять параметром when. Например, если вы хотите не убирать совсем возможность запуска job«а. Можно просто оставить кнопку, которую при желании будут вызывать самостоятельно, не производя запуск нового пайплайна или не делая commit.

№6: environment: auto_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.


Читайте также в нашем блоге:

© Habrahabr.ru