Отладка пайплайна: типичные ошибки

Разбираем самые частые причины красных пайплайнов и как их быстро находить.

Отладка пайплайна — поиск причины, по которой джоба не запустилась, зависла или упала, по статусу, логам и конфигурации.

Рано или поздно красный пайплайн встретит каждый: это штатная часть работы с CI. Важно не то, что пайплайн падает, а сколько времени уходит на поиск причины. Опытный инженер не «всматривается» в полотно лога — он действует по протоколу: сначала смотрит статус джобы, потом этап, на котором всё сломалось, и только затем читает строки. Эта дисциплина экономит часы, потому что 90% инцидентов укладываются в несколько повторяющихся сценариев.

Главная ловушка новичка — начинать отладку с конца, с последней красной строки лога, тогда как реальная причина может лежать в самом начале: джоба вообще не получила раннер, или пайплайн не создался из-за YAML. Поэтому правило номер ноль звучит так: сначала пойми, на каком уровне отказ — пайплайн не создан, джоба не стартовала или джоба упала в процессе. Каждому уровню соответствует свой набор причин и свой инструмент диагностики; ниже разберём их по порядку, от самых «ранних» к самым «поздним».

Джоба висит в pending

Если джоба не стартует, дело почти всегда в раннере: нет онлайн-раннера, не совпали tags, исчерпаны минуты shared-раннеров или раннер не принимает джобы без тегов. Проверьте Settings → CI/CD → Runners: есть ли активный раннер с нужными тегами. Это причина номер один зависших пайплайнов.

Разберём механику теговой системы, потому что именно она генерирует львиную долю pending-инцидентов. Когда вы пишете джобе tags: [docker, linux], GitLab ищет раннер, у которого есть оба этих тега. Если ни один раннер не подходит, джоба будет ждать вечно — никакой ошибки не возникнет, статус просто застынет на pending. Зеркальная проблема: специфичные раннеры по умолчанию настроены брать только тегированные джобы, поэтому джоба без тегов может зависнуть рядом с вполне живым раннером. Включите для раннера опцию «Run untagged jobs» или явно проставьте теги — и затор рассосётся.

Вторая частая причина — исчерпание минут: на SaaS-инстансе у бесплатного тарифа есть месячный лимит compute-минут, и когда он кончается, джобы встают в pending без понятного объяснения (признак — пайплайн зелёный в начале месяца и стабильно pending к концу). Полезно держать в голове короткий чек-лист pending-диагностики.

pending? проверь по порядку:
  1) есть ли вообще онлайн-раннер?      (Settings -> CI/CD -> Runners)
  2) совпадают ли tags джобы и раннера?
  3) берёт ли раннер untagged-джобы?
  4) не кончились ли compute-минуты?
  5) не упёрлись ли в лимит concurrent?

Ошибка синтаксиса YAML

YAML чувствителен к отступам (только пробелы, не табы) и структуре. Один лишний пробел — и пайплайн вообще не создастся с ошибкой invalid YAML. Лучшая профилактика — встроенный редактор CI/CD → Editor: он валидирует файл и показывает merged YAML. Не редактируйте сложный пайплайн вслепую.

Отдельно стоит понимать разницу между двумя классами YAML-ошибок. Первый — синтаксический: сломан сам формат (таб вместо пробела, незакрытая кавычка, неверный отступ списка). Такой файл GitLab отвергает целиком, и в интерфейсе вы увидите красную плашку «pipeline failed to create» ещё до запуска любой джобы. Второй класс — семантический: формат корректен, но конфигурация бессмысленна (джоба ссылается через needs на несуществующую джобу, stage не объявлен в списке stages, два ключа конфликтуют). Эти ошибки ловит именно линтер редактора, и они коварнее, потому что глазами файл выглядит «нормальным».

Практический приём: для сложных пайплайнов используйте вкладку Full configuration (merged YAML) — она разворачивает все include, extends и якоря и показывает, как джоба выглядит после всех наследований. Очень часто баг прячется в том, что унаследованное значение перекрыло ваше или, наоборот, не применилось.

Ошибки прав и доступа

Деплой-джоба упала с permission denied? Частые причины: protected-переменная недоступна на незащищённой ветке, у токена недостаточно прав, неверный kubeconfig. Помните, что protected-секреты приходят только в пайплайны защищённых веток — на feature-ветке их просто нет, и команда логина падает.

Этот сценарий стоит прочувствовать, потому что он порождает самый запутанный симптом: «джоба падает в feature-ветке, но работает в main». Причина не в коде и не в раннере, а в модели безопасности GitLab. Protected-переменные умышленно не передаются в пайплайны незащищённых веток — это защита от того, чтобы автор форка не вытащил продакшен-секрет через echo $SECRET. Поэтому «точно заданная» переменная в feature-ветке оказывается пустой строкой, и команда логина в реестр или кластер падает с отказом доступа.

Диагностика проста: напечатайте длину секрета (echo ${#TOKEN}), но никогда само значение. Длина ноль на feature-ветке и ненулевая в main — диагноз поставлен, дело в protected. Лечение зависит от намерения: либо джоба и не должна деплоить с feature-веток (ограничьте через rules), либо переменную надо открыть шире (снять флаг protected, осознавая риск).

Проблемы кеша

Симптомы: «работает локально, падает на CI» или старые версии зависимостей. Часто виноват кеш с плохим ключом — он не инвалидируется при смене пакетов. Способ диагностики: временно отключить кеш и проверить, что джоба зеленеет на чистом окружении. Если да — проблема в ключе кеша.

Кеш — двуликий инструмент: он ускоряет пайплайн, но именно он чаще всего делает падения невоспроизводимыми. Корень зла — статический ключ. Если ключ кеша не зависит от lock-файла, то при обновлении зависимостей GitLab подложит в джобу старый node_modules, в котором уже нет новых пакетов или, наоборот, остались удалённые. Получается «призрачное» состояние: ни ваш коммит, ни чистая установка такого не дадут, а CI стабильно падает. Поэтому правильный ключ — это хеш lock-файла: меняется зависимость → меняется ключ → кеш пересобирается с нуля.

Золотое правило отладки кеша: кеш не должен быть источником истины. Любая джоба обязана работать и при полностью пустом кеше — он лишь ускоряет, но не «дополняет» окружение. Быстрый тест — кнопка Clear runner caches или временное переименование ключа: если после очистки джоба упала, значит, она нелегально зависела от мусора в кеше.

Чтение логов

Лог джобы — главный источник истины. Ищите первую красную строку: остальное обычно следствие. Полезные приёмы: добавить set -x в начало script, чтобы видеть каждую выполняемую команду, и временно вставить echo переменных (не секретных!), чтобы проверить, какие значения реально пришли.

debug-job:
  script:
    - set -x                    # печатать каждую команду
    - echo "branch=$CI_COMMIT_BRANCH source=$CI_PIPELINE_SOURCE"
    - ./build.sh

При чтении лога держите в голове разницу между симптомом и причиной. Последние строки лога — это почти всегда симптом: упавший процесс, ненулевой код возврата, оборванный вывод. Причина же — первая аномалия сверху: команда, которая не нашла файл, переменная, которая пришла пустой, шаг установки, который тихо предупредил об ошибке и поехал дальше. Привычка читать лог сверху вниз, а не снизу вверх, отделяет десятиминутную отладку от двухчасовой. Когда лог слишком многословен, помогает временно поднять детализацию точечно: обернуть в трассировку только подозрительный кусок, а для переменных печатать не значение, а его признаки — длину, факт пустоты, — чтобы не утопить секрет в логе.

Как работает под капотом

GitLab разделяет два момента отказа: создание пайплайна (ошибки YAML, правила, отсутствие джоб) и выполнение джобы (код, права, окружение). Первый виден сразу при push как «pipeline failed to create». Второй — это ненулевой код возврата команды внутри джобы. Понимание, на каком этапе проблема, сужает поиск вдвое: либо чините конфиг, либо — содержимое команд.

Углубим эту модель до трёх стадий, потому что между «созданием» и «выполнением» есть промежуточная — постановка в очередь. Сначала GitLab валидирует YAML и решает, какие джобы создавать (тут отрабатывают rules). Затем созданные джобы ждут раннера — это стадия pending, где живут теги и минуты. И только потом раннер разворачивает контейнер, восстанавливает кеш, клонирует репозиторий и запускает script. Каждая красная джоба «сломалась» ровно на одной из этих стадий, и определение стадии — половина отладки.

Частые ошибки

  • Долго искать баг в логах, тогда как джоба вообще не стартовала (pending) — сначала смотрите статус и раннеры.
  • Редактировать YAML без валидатора и ловить ошибки структуры на проде.
  • Печатать секреты в echo для отладки — они утекут в лог (даже masking не панацея).
  • Читать лог снизу вверх и чинить симптом вместо первой красной строки сверху.

Итоги

  • Pending → проблема с раннером (онлайн, теги, минуты); failed → проблема в команде или правах.
  • YAML чините через встроенный редактор с валидацией и merged-просмотром.
  • set -x и аккуратные echo (без секретов) ускоряют диагностику логов.
  • «Падает на feature, работает на main» — почти всегда protected-переменные; кеш только ускоряет.
Проверьте себя
1. Джоба висит в pending. Куда смотреть в первую очередь?
AВ логи команды
BНа раннеры: онлайн ли, совпадают ли теги, есть ли минуты
CВ отчёт о покрытии
DВ Container Registry
2. Почему protected-секрет может быть недоступен и вызвать permission denied?
AОн истёк
BПайплайн идёт по незащищённой ветке, куда protected-переменные не передаются
CGitLab всегда скрывает секреты
DРаннер выключен