← CI/CD: практика
14 мин · просто · Урок 6 из 10

Переменные: контекст пайплайна и секреты

Встроенные CI_-переменные, свои значения в .gitlab-ci.yml и masked-секреты в настройках проекта. Плюс двоеточие, которое ломает YAML.

Откуда job знает, где он запущен

Раннер получает от GitLab несколько десятков переменных: кто запустил пайплайн, из какой ветки, какой коммит, номер job. Объявлять их не нужно, они уже есть.

Материалы этого урока лежат в учебном репозитории. Заведите второй удалённый адрес: origin указывает на ваш проект, а обновления уроков приходят из учебного.

git remote add upstream https://gitlab.com/izidevops-learn/ci-start.git
git pull --no-rebase upstream main
cp lesson-06-variables/pipeline.yml .gitlab-ci.yml

Флаг --no-rebase обязателен, и вот почему. К этому моменту ваша история разошлась с учебной: вы уже сделали свои коммиты с пайплайном. На разошедшихся ветках git отказывается угадывать, чего вы хотите:

fatal: Need to specify how to reconcile divergent branches.

--no-rebase говорит «сделай обычный merge-коммит». Это же поведение можно включить один раз навсегда: git config pull.rebase false.

Встроенные переменные

show-context:
  stage: info
  script:
    - echo "Проект $CI_PROJECT_NAME, ветка $CI_COMMIT_BRANCH"
    - echo "Коммит $CI_COMMIT_SHORT_SHA, автор $GITLAB_USER_LOGIN"
    - echo "Пайплайн номер $CI_PIPELINE_ID, job номер $CI_JOB_ID"

В логе получите примерно такое:

Проект my-ci-site, ветка main
Коммит 9f09955b, автор vladislav.makhankov
Пайплайн номер 2782918422, job номер 16052273647

Эти значения и превращают пайплайн из скрипта в инструмент. Тег образа по CI_COMMIT_SHORT_SHA, имя окружения по CI_COMMIT_BRANCH, ссылка на job по CI_JOB_ID в уведомлении.

Полный список открывается по env в любом job. Их правда много, больше сотни, и набор зависит от того, чем запущен пайплайн.

Свои переменные

Объявляются блоком variables в корне файла или внутри job:

variables:
  SITE_TITLE: "Собрано пайплайном"
  BUILD_DIR: "public"

pages:
  stage: build
  script:
    - mkdir -p "$BUILD_DIR"
    - cp style.css "$BUILD_DIR/"
    - sed -e "s|<h1>.*</h1>|<h1>$SITE_TITLE</h1>|"
          -e "s|<dd id=\"commit\">.*</dd>|<dd id=\"commit\">$CI_COMMIT_SHORT_SHA</dd>|"
          -e "s|<dd id=\"branch\">.*</dd>|<dd id=\"branch\">$CI_COMMIT_BRANCH</dd>|"
          index.html > "$BUILD_DIR/index.html"
  artifacts:
    paths:
      - $BUILD_DIR
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Тут переменные впервые дают видимый результат. На странице есть пустые поля «коммит» и «ветка», и sed подставляет в них значения из окружения job. Запушьте, дождитесь деплоя и обновите свой сайт: в карточке появится короткий хеш коммита, из которого она собрана.

Переменные раскрываются и в artifacts.paths, не только в script. Переменная внутри job перекрывает одноимённую из корня.

Кавычки вокруг "$BUILD_DIR" не украшение. Без них путь с пробелом развалится на два аргумента, и mkdir создаст не то, что вы ждали.

Секреты живут в настройках, не в файле

.gitlab-ci.yml лежит в git и виден всем, у кого есть доступ к репозиторию (а у публичного проекта — вообще всем). Токены и пароли туда не пишут.

Идите в Settings -> CI/CD -> Variables -> Add variable. Ключ DEPLOY_TOKEN, значение произвольное, и главное: тип переменной. GitLab предлагает три варианта, причём новые переменные с версии 18.3 создаются сразу как Masked.

Visible — обычная переменная, значение видно в настройках и в логах. Для флагов и путей.

Masked — значение заменяется на [MASKED] везде, где оно попадёт в лог. В настройках его по-прежнему можно подсмотреть.

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

Важная деталь: включить скрытие можно только при создании переменной. Перевести в этот режим уже существующую нельзя, придётся удалить и завести заново.

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

Проверьте на живом job:

show-secret:
  stage: info
  script:
    - 'echo "Длина токена: ${#DEPLOY_TOKEN}"'
    - 'echo "Сам токен: $DEPLOY_TOKEN"'

В логе:

Длина токена: 22
Сам токен: [MASKED]

Значение скрыто, а длина утекла. Маскировка ищет в выводе точное совпадение со строкой секрета и заменяет его. Всё, что вы посчитали от секрета сами (длина, хеш, первые символы через cut), пройдёт в лог спокойно.

Отсюда практическое правило: секрет не печатают вообще, даже частично. Передавайте его сразу в команду, лучше через stdin:

    - echo "$DEPLOY_TOKEN" | docker login -u ci --password-stdin registry.example.com

У маскировки есть требования к значению: минимум 8 символов, одна строка без пробелов, и значение не должно совпадать с именем другой переменной. Если GitLab не даёт включить Masked, причина обычно в этом списке.

Многострочный секрет: приём с base64

Маскировка требует значение без переводов строки, а секреты часто многострочные: kubeconfig, приватный SSH-ключ, JSON сервисного аккаунта. Положить их в переменную как есть не выйдет, Masked включить не дадут.

Обходят это кодированием в base64. На своей машине:

base64 < kubeconfig.yaml | tr -d '\n'

tr -d '\n' обязателен: на Linux base64 переносит строку каждые 76 символов, и без него вы получите то же многострочное значение, от которого уходили.

Полученную строку кладёте в переменную KUBECONFIG_B64 с типом Masked, а в пайплайне разворачиваете обратно в файл:

deploy:
  script:
    - echo "$KUBECONFIG_B64" | base64 -d > /tmp/kubeconfig
    - export KUBECONFIG=/tmp/kubeconfig
    - kubectl get pods

Файл ложится в /tmp, а не в рабочую папку: иначе он рискует уехать в артефакт вместе с остальной сборкой.

Чего base64 не делает

Это кодирование, а не шифрование. base64 -d раскодирует кто угодно, так что смысл приёма ровно один: уложить многострочное значение в одну строку.

И тут есть ловушка, которую видно на живом job:

$ echo "Сырая переменная: $KUBECONFIG_B64"
Сырая переменная: [MASKED]
$ cat /tmp/kubeconfig
apiVersion: v1
clusters:
- cluster:
    server: https://k8s.example.com
  name: prod

Сама переменная замаскирована. А содержимое файла после декодирования вылилось в лог целиком, потому что маскировка сравнивает вывод с base64 -строкой, и раскодированный текст под неё не подходит.

В этом примере строка token: всё-таки скрылась, но по отдельной причине: её значение заведено вторым секретом в настройках проекта. Само по себе декодирование ничего не прячет.

Отсюда правило: раскодировали в файл — файл не печатаем. Ни cat, ни kubectl config view --raw (без флага секреты как раз прячутся, с ним показываются), ни отладочный вывод «на минутку».

Двоеточие, которое ломает пайплайн

Обратите внимание на кавычки вокруг всей строки:

    - 'echo "Длина токена: ${#DEPLOY_TOKEN}"'

Без внешних кавычек GitLab отвечает так:

jobs:show-secret:script config should be a string
or a nested array of strings up to 10 levels deep

Причина в YAML, а не в GitLab. Последовательность : (двоеточие с пробелом) внутри незакавыченной строки читается как разделитель ключа и значения, и элемент списка превращается в словарь. Любой echo с двоеточием в тексте нужно брать в одинарные кавычки целиком.

Что важно запомнить

Встроенные CI_* переменные дают job контекст: проект, ветку, коммит, номера пайплайна и job.

Свои переменные объявляются в variables, работают и в script, и в artifacts.paths; значение внутри job перекрывает корневое.

Секреты идут в Settings -> CI/CD -> Variables с типом Masked, а для боевых токенов Masked and hidden.

Маскировка прячет только точное совпадение. Длину, хеш и обрезок секрета она не поймает, поэтому не печатайте его совсем.

Многострочные секреты кладут в переменную через base64 и разворачивают в файл в /tmp. Это упаковка, а не защита: раскодированное содержимое маскировка уже не узнаёт.

Двоеточие с пробелом внутри команды требует одинарных кавычек вокруг всей строки, иначе YAML разберёт её как словарь.

Дальше собираем сайт настоящим сборщиком в контейнере с Node.

Прошли материал до конца? Отметьте урок пройденным.