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

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

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

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-менеджеров.

Комментарии (6)


  1. ashkraba
    12.07.2024 13:57
    +1

    Статья крутая, но я по прежнему буду топить за Jenkins - вот там возможностей действительно море!


  1. slonopotamus
    12.07.2024 13:57
    +6

    Почти все ваши проблемы от впихивания в пайплайны портянок баша. У меня возникает вопрос, а как вы их проверяете вне гитлаба, и не лучше ли вынести всё в отдельные sh-файлы?


    1. m-evgen Автор
      12.07.2024 13:57
      +2

      А в какой CI-системе нет портянок баша?) Хотя не, в TFS я видел портянки повершелла, лучше ли это?..

      не лучше ли вынести всё в отдельные sh-файлы?

      В какой-то момент нужно ограничить количество уровней вложенности кода и на мой вкус, когда у меня уже есть какой-то include-файл, в котором хранится только код конкретной джобы, то вырезать скрипты оттуда в отдельные файлы это уже перебор. Они все равно нигде больше не переиспользуются. KISS, не?

      как вы их проверяете вне гитлаба?

      А как их проверить вне гитлаба, если они созданы именно для применения в гитлабе? Эти скрипты активно используют внутренние переменные гитлаба и на входе у них, помимо кода, очень много того, что локально замокать проблематично. Да и результат выполнения тоже может требовать специфического окружения, например, токенов для выгрузки результатов работы джобы.

      То, что должно работать на машине у пользователя, конечно разумно выносить из гитлаба. Например, локально у нас работают pre-commit хуки для проверки соответствия коммита стандартам.


      1. gimntut
        12.07.2024 13:57
        +2

        Мне кажется, что слово stupid, в аббревиатуре KISS, говорит о том что решения должны избегать тайных знаний так, чтобы даже джун мог внести правки, ничего не сломав. В данном случае, вся статья про тайные знания. Я и не подозревал обо всех этих проблемах, т.к. у меня все сложные вещи сложены в файлы скриптов. Единственное моё тайное знание: скрипты нужно вызывать с использованием команды source. Так: source my-script.sh.
        За статью спасибо. Полезно узнать какие грабли лежат в тех краях, в которые ещё не ходил.


  1. trabl
    12.07.2024 13:57
    +1

    Как минимум узнал из статьи что за непонятное приложение у меня на телефоне с недавних пор появилось.


  1. Blacker
    12.07.2024 13:57

    Соотношение количества полезной информации к количеству упоминаний Купера (который не агент, к сожалению) наводит на мысль, что статья задумана как маркетинговая.