Первый пайплайн в GitLab CI
Собираем простой .gitlab-ci.yml с нуля: hello-world, стадии, артефакты, тег по SHA.
Pipeline как код
В GitLab CI весь пайплайн описывается одним файлом .gitlab-ci.yml в
корне репозитория. Отдельный CI-сервер поднимать не нужно: CI встроен в
GitLab, а выполняет работу раннер (машина или контейнер).
Положил файл в репо, сделал push. GitLab сам находит его и запускает пайплайн. Никаких кнопок и веб-форм: конфиг живёт рядом с кодом и ревьюится вместе с ним.
Hello world
Самый маленький рабочий пайплайн:
hello:
script:
- echo "Hello, CI!"
Один job с именем hello, внутри одна команда. На каждый push раннер
возьмёт чистый контейнер, выполнит echo и покажет результат в логе.
Это уже полноценный пайплайн.
Стадии и порядок
Обычно работы больше одной, и часть из них должна идти по порядку. Для этого есть стадии:
stages:
- build
- test
build-job:
stage: build
script:
- echo "Собираем приложение"
- echo "v1" > app.txt
test-job:
stage: test
script:
- echo "Проверяем"
- test -f app.txt # учебный пример: проверяем наличие файла, а не unit-тесты
Стадии идут последовательно: сначала весь build, потом весь test.
Если в одной стадии несколько jobs, они бегут параллельно. Если build
упал, test даже не стартует.
Передать файл между стадиями: artifacts
Каждый job стартует в чистом контейнере, поэтому файл из build сам по
себе в test не попадёт. Чтобы передать, объявляем артефакт:
build-job:
stage: build
script:
- echo "v1" > app.txt
artifacts:
paths:
- app.txt
test-job:
stage: test
script:
- cat app.txt
GitLab сохранит app.txt после build и распакует его в следующих
стадиях. Так результат сборки доезжает до тестов и деплоя.
Запускать только где нужно: rules
Гонять весь пайплайн на каждой ветке не всегда нужно. rules
ограничивают запуск:
build-job:
stage: build
script:
- echo "Собираем"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
CI_COMMIT_BRANCH и CI_DEFAULT_BRANCH — встроенные переменные, их
подставляет сам раннер. Тут job запускается только на дефолтной ветке
(main).
Сборка образа и тег по SHA
Когда дело доходит до Docker-образа, важен тег. Распространённый
антипаттерн — тегать всё как :latest:
# так делать не надо
- docker build -t myapp:latest .
- docker push myapp:latest
latest — это движущийся тег: сегодня под ним один образ, завтра
другой. По нему не понять, что именно крутится в проде, и откат
превращается в гадание. Тегай по SHA коммита:
build-image:
stage: build
image: docker:27
services:
- name: docker:27-dind
alias: docker
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
script:
- docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
- docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
CI_COMMIT_SHORT_SHA — короткий хеш коммита. Тег
registry/app:a1b2c3d жёстко связан с коммитом: всегда видно, из чего
собран образ, а откат сводится к запуску предыдущего тега.
В этом куске две строки, без которых пример не поедет, и обе часто теряются в туториалах.
DOCKER_TLS_CERTDIR: "/certs" нужен потому, что начиная с Docker 19.03
сервис dind поднимает демон по TLS. Клиент и сервис должны договориться,
где лежат сертификаты. Без этой переменной job падает на первой же
docker-команде с «Cannot connect to the Docker daemon».
docker login нужен внутри самого job. Логин, сделанный в соседнем
job или в шаблоне другой стадии, сюда не доедет: у каждого job своё
чистое окружение со своим ~/.docker/config.json. Без логина build
пройдёт, а push упадёт с 401.
Чем dind не нравится
Сервис docker:dind поднимает полноценный демон внутри job, а для
этого раннер должен быть запущен в privileged-режиме. Раннер с
privileged = true фактически даёт любому пайплайну права root на
машине раннера. На своих раннерах это осознанный компромисс, а на общих
или в чужом окружении так себе идея.
Альтернативы, которые собирают образ без привилегированного демона:
- kaniko — собирает из Dockerfile прямо в userspace, популярный выбор в Kubernetes-раннерах.
- buildah — сборщик из экосистемы Podman, умеет работать без демона и без root.
docker buildxс внешним билдером — сборка уезжает на отдельный BuildKit-инстанс, самому job демон не нужен.
Начинать проще с dind: конфиг короче, а поведение один в один как локально. Переезжать имеет смысл, когда privileged-раннер становится проблемой.
Секреты — не в репозитории
Пароль от registry, токены, ключи в .gitlab-ci.yml писать нельзя: файл
лежит в git и виден всем. Секреты живут в Settings -> CI/CD -> Variables
как masked (скрыты в логах) и protected (доступны только на
защищённых ветках). В пайплайне на них ссылаешься по имени:
before_script:
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
Пароль идёт через --password-stdin, а не аргументом команды, чтобы не
засветиться в выводе.
Для встроенного registry свои переменные заводить не нужно: GitLab сам
подставляет CI_REGISTRY_USER и CI_REGISTRY_PASSWORD для текущего пайплайна.
Свои REGISTRY_USER/REGISTRY_PASSWORD нужны только для внешнего реестра
(Docker Hub, приватный).
Что важно запомнить
.gitlab-ci.yml в корне репо — весь пайплайн как код, отдельный сервер не
нужен.
Job — это набор команд в script; стадии (stages) задают порядок,
jobs внутри стадии бегут параллельно.
artifacts передают файлы между стадиями: чистый контейнер сам ничего не
помнит.
Тег образа — по SHA коммита, а не :latest. latest движется, по нему
не отследить прод и не откатиться.
Секреты — в CI-переменных (masked + protected), не в репозитории.
Сборка образа через dind требует DOCKER_TLS_CERTDIR: "/certs",
docker login в самом job и privileged-раннера. Без привилегий берут
kaniko, buildah или docker buildx с внешним билдером.
Дальше короткий квиз, и трек CI/CD закрыт.
Дрилл · вопросы как на собесе
Проверь себя без подсказок
Сформулируй ответ вслух или письменно, потом раскрой и честно отметь: знал или нет.
Почему тег :latest у Docker-образа - антипаттерн в CI?
CICD-052Зачем тегать Docker-образ по SHA коммита?
CICD-051Где описывается пайплайн в GitLab CI?
CICD-050Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс