Привет, Хабр! Я Евгений Малышев, 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)
slonopotamus
12.07.2024 13:57+6Почти все ваши проблемы от впихивания в пайплайны портянок баша. У меня возникает вопрос, а как вы их проверяете вне гитлаба, и не лучше ли вынести всё в отдельные sh-файлы?
m-evgen Автор
12.07.2024 13:57+2А в какой CI-системе нет портянок баша?) Хотя не, в TFS я видел портянки повершелла, лучше ли это?..
не лучше ли вынести всё в отдельные sh-файлы?
В какой-то момент нужно ограничить количество уровней вложенности кода и на мой вкус, когда у меня уже есть какой-то include-файл, в котором хранится только код конкретной джобы, то вырезать скрипты оттуда в отдельные файлы это уже перебор. Они все равно нигде больше не переиспользуются. KISS, не?
как вы их проверяете вне гитлаба?
А как их проверить вне гитлаба, если они созданы именно для применения в гитлабе? Эти скрипты активно используют внутренние переменные гитлаба и на входе у них, помимо кода, очень много того, что локально замокать проблематично. Да и результат выполнения тоже может требовать специфического окружения, например, токенов для выгрузки результатов работы джобы.
То, что должно работать на машине у пользователя, конечно разумно выносить из гитлаба. Например, локально у нас работают pre-commit хуки для проверки соответствия коммита стандартам.
gimntut
12.07.2024 13:57+2Мне кажется, что слово stupid, в аббревиатуре KISS, говорит о том что решения должны избегать тайных знаний так, чтобы даже джун мог внести правки, ничего не сломав. В данном случае, вся статья про тайные знания. Я и не подозревал обо всех этих проблемах, т.к. у меня все сложные вещи сложены в файлы скриптов. Единственное моё тайное знание: скрипты нужно вызывать с использованием команды source. Так:
source my-script.sh
.
За статью спасибо. Полезно узнать какие грабли лежат в тех краях, в которые ещё не ходил.
trabl
12.07.2024 13:57+1Как минимум узнал из статьи что за непонятное приложение у меня на телефоне с недавних пор появилось.
Blacker
12.07.2024 13:57Соотношение количества полезной информации к количеству упоминаний Купера (который не агент, к сожалению) наводит на мысль, что статья задумана как маркетинговая.
ashkraba
Статья крутая, но я по прежнему буду топить за Jenkins - вот там возможностей действительно море!