Пайплайны Gitlab CI: моя коллекция граблей

Привет, Хабр! Я Евгений Малышев, SRE-инженер в Купере (так теперь называется СберМаркет). Моя основная задача — это надежная работа сервисов фронтенда, и немалую роль в этом играют правильно построенные пайплайны CI/CD. В этом нам помогает Gitlab CI. В компании мы широко используем этот инструмент для создания общих шаблонов для сервисов на различных языках. На уровне отдельного репозитория легко расширить или настроить шаблонные джобы и добавить свои.

До этого у меня был опыт с Jenkins и Azure Devops, так что Gitlab CI мне показался довольно простым: есть стадии, есть правила запуска джоб с shell-подобным синтаксисом, да и скрипты джоб тоже используют bash-интерпретатор. Но в процессе близкого знакомства не раз возникали ситуации, когда поднимается то одна бровь, то обе, а то и руки в праведном гневе. Заходите посмотреть, какую коллекцию граблей собрал я.

Весь код с примерами граблей можно посмотреть в репозитории.

be793d85b672cf164ddf1baaa683f90a.png

allow_failure это тоже не failure

Начнем с простого. Нам нужно выстроить зависимость двух джоб:

first_job:
  script: ./run_stuff.sh

dependent_job:
  script: ./run_more_stuff.sh
  needs: first_job

Элементарно же? Теперь допустим, что first_job может завершаться неуспешно — пускай это будут нестрогие тесты, которые завершаются как warning и не фейлят весь пайплайн. Для этого мы включаем allow_failure. Есть соблазн использовать это и в более сложных кейсах.

Допустим, нам требуется выстроить какую-то логику запуска зависимой задачи, которая выходит за возможности rules. Казалось бы, вот он механизм, можно просто увести в warning первую джобу и тогда зависимая на запустится!

first_job:
  script: ./check_if_we_need_dependent_job.sh || echo "Nothing to do, skipping dependent job"
  allow_failure: true

dependent_job:
  script: ./run_more_stuff.sh
  needs: first_job

Но нет, это неправильные пчелы и неправильный nёёd. В этом случае зависимая задача все равно запустится. Что делать? Собирать нужные переменные артефактом из первой задачи через dotenv report и проверять их в зависимой задаче. Не очень красиво, но переживём.

Пляски по старинным граблям с set -e

Тот факт, что джобы в Gitlab выполняются в bash, должно помогать разработчикам, освоившим этот универсальный инструмент, который, как суперклей, помогает соединить вместе самые разные инструменты в *nix-системе. Однако, тут можно наткнуться на куда более старые грабли, заложенные уже разработчиками bash:

check_condition:
  script: |
    set -e
    ./check_condition.sh && echo 'Условие проверено, продолжаем!'

Вот казалось бы, у нас есть скрипт check_condition.sh, мы его выполняем и в зависимости от кода возврата либо продолжаем, либо джоба завершается неуспешно. Почему неуспешно? А для этого мы устанавливаем флаг Errexit командой set -e, чтобы любая неуспешная команда в скрипте приводила к ошибке всего скрипта.

Но вот в чем загвоздка, у этого флага есть множество исключений и наличие && или || это одно из них. Поэтому, если логика джобы строится на кодах ошибки, лучше обрабатывать это явным образом, а не расчитывать на поведение шелла. Здесь все тонкости set -e разобраны очень подробно. Также эти грабли описаны в документации по дебагу пайплайнов.

Исправить такую конструкцию можно, поместив конструкцию с && в дочернюю оболочку с помощью скобок:

check_condition:
  script: |
    set -e
    (./check_condition.sh && echo 'Условие проверено, продолжаем!')

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

Кстати, указывать явным образом set -e было необязательно: в шелл-раннерах Gitlab по-умолчанию установлены флаги set -eo pipefail, хотя в документации об этом не упоминается.

Благодаря этому можно подобрать еще одни грабли наподобие таких:

cat application.log | grep 'этого_там_точно_нет' | tee -a found.log

grep не просто ничего не найдет, но еще и уронит джобу. Ой.

Грабли известные

Многие грабли описаны в документации, но все же читают инструкции, когда уже все сломалось, да?

Как пример, шишек можно набить и на оформлении многострочных скриптов. Здесь Gitlab даёт и широкие возможности, чтобы выстрелить себе в ногу:

multiline_script_job:
  script:
    - for i in {1..10}
    - do echo $i   # здесь мы ждем 10 строк с числами 1..10
    - done         # но получаем ошибку

Да, такие конструкции нельзя разносить по элементам YAML-списка (кстати, почему?). Ну да ладно, сделаем другим способом:

multiline_script_job:
  script: >
    for i in {1..10}; do 
      echo $i
    done

Но здесь сработала магия баша при обработке переносов строки внутри цикла. В других случаях можно напороться на такое:

If you use the - > folded YAML multiline block scalar to split long commands, additional indentation causes the lines to be processed as individual commands.

А если по-русски, то:

Если использовать сворачивание многострочного YAML-блока с помощью элемента >, то излишние отступы приведут к тому, что такие линии будут восприняты, как выделенные переносами строк, до и после линии с отступами.

Поиграться и понять, как это работает, поможет сайт https://yaml-multiline.info

Итак, пример:

script: >
  RESULT=$(curl --silent
    --header
      "Authorization: Bearer $CI_JOB_TOKEN"
    "${CI_API_V4_URL}/job"
  )

Что, кстати, ровно то же самое, что:

script:
  RESULT=$(curl --silent
    --header
      "Authorization: Bearer $CI_JOB_TOKEN"
    "${CI_API_V4_URL}/job"
  )

Выведет ошибку из-за лишних переносов строки:

$ RESULT=$(curl --silent # collapsed multi-line command
curl: no URL specified!
curl: try 'curl --help' or 'curl --manual' for more information
/bin/bash: line 149: --header: command not found
/bin/bash: line 150: https://gitlab.example.com/api/v4/job: No such file or directory

Это можно исправить:

script: >
  RESULT=$(curl --silent
  --header
  "Authorization: Bearer $CI_JOB_TOKEN"
  "${CI_API_V4_URL}/job"
script: >
  RESULT=$(curl --silent \
    --header \
      "Authorization: Bearer $CI_JOB_TOKEN" \
    "${CI_API_V4_URL}/job")

А в случае, если все переносы строк нужно сохранить, используется литерал |

script: |
  echo 'это всё'
  echo 'отдельные'
  echo 'команды'

Ну и разумеется, можно просто отделять отдельные команды переносами строки:

script:
  echo 'так'

  echo 'тоже'

  echo 'можно'

Что в итоге?

Gitlab CI неспроста так популярна (возможностей море) и продолжает развиваться, обрастая новыми фичами. Некоторые из которых, возможно, баги, но что поделать. Даже о низкий порог входа можно запнуться. Главное, чтобы можно было быстро освоиться и использовать все эти широкие возможности. Надеюсь, моя коллекция вам в этом немного помогла :)

Tech-команда Купера (ex СберМаркет) ведет соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и на YouTube. А также слушай подкаст «Для tech и этих» от наших it-менеджеров.

© Habrahabr.ru