Настройка GitLab CI/CD: понимаем принципы работы и запускаем первый pipeline

от автора

Настройка GitLab CI/CD: понимаем принципы работы и запускаем первый pipeline

Все найденные мной русскоязычные гайды не дают базового понимания того, как это работает, по большому счету это просто инструкции по настройке, причем под какой-то конкретный продукт и кейс: .net, Java, Node JS, etc.

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

  • Максимально много функционала, которыми обладает Gitlab (общие абстракции актуальны и для его аналогов).

  • Инструменты которые нужны: Bash, Docker, Kubernetes и другие.

  • Общую теорию и практику с конкретными сценариями.


Материал разбит на 3 части.

  1. [Вы здесь → ] Простейший пример ci-cd

  2. [in progress 80%] Более зрелый CI/CD (выход в продакшн, self-hosted runner и работа с registry)

  3. [todo] Горизонтальное масштабирование CI/CD (высоко-нагруженный продакшн)

Оглавление

  • В чем идея CI/CD?

  • Терминология

  • Простейший CI/CD (разработка, MVP)

    • Простой пример .gitlab-ci.yml

    • Схема работы простейшего CI/CD

    • Практика с простейшим шаблоном CI/CD


В чем идея CI/CD

Даже начинающему разработку интуитивно понятно что такое CI/CD и для чего оно нужно. Отправили коммиты с изменениями в репозиторий — нужно исходный код превратить в собранное приложение и развернуть его на dev/test/prod стенде. Можно зайти на VPS и все сделать вручную: git clone, запустить автотесты, сделать сборку и запустить собранное приложение, при этом старое запущенное приложение, нужно заменить новым билдом. Автоматизацию этого процесса и назвали CI/CD. В эту аббревиатуру навалили сразу несколько значений которые всегда делаются рядом друг с другом и часто эти действия трудно однозначно классифицировать.

Но я все же попробую классифицировать и достичь какой-то однозначности по терминологии.

Терминология

CI (continues integration, непрерывная интеграция) — действия необходимые для интеграции новых изменений в имеющуюся кодовую базу.

Continues integration включает в себя следующее:

  • объедение всех изменений в одну точку

  • установку зависимостей

  • статистические проверки кода (типизация, линтеры)

  • запуск тестов (Unit, интеграционные, e2e)

  • проверка работоспособности сборки (убедиться что сборка возможна)

  • сборка кодовой базы. Это может быть как сборка библиотеки в соответствующий формат (например, npm-модуль, PyPI-пакет, Go-модуль), так и сборка в конечный исполняемый формат (.jar, .whl, bundle, Docker-контейнер, .exe-файл и т. д.) Результат сборки принято называть артефактом.

  • загрузка артефакта в специализированное хранилище для дальнейшего использования и версионирования. Такие хранилища часто называют registry (npm registry, Docker Registry, PyPI) или hub (Docker Hub), а также repository manager (Nexus, Artifactory, GitHub Packages).

CD (continues delivery, непрерывная доставка) — действия необходимые для доставки артефакта в место назначения (сервер где он будет выполняться). На этом этапе определяется куда будет доставлен артефакт, на dev, test, pre-prod(stage), prod.

Continues delivery включает в себя следующее:

  • Получение артефакта на целевом сервере

CD (continues deployment, непрерывное развертывание) — Действия необходимые для запуска уже доставленного артефакта.

Continues deployment включает в себя следующее:

  • Запуск артефакта

  • Миграция БД (если наш артефакт ее использует)

  • Запуск e2e тестов. Именно тех чья функция заключается в том, что бы проверить целостность работы системы. Некоторые e2e могли быть запущены еще на этапе CI.

  • Тут может быть так же механизм отката к предыдущей версии.

Delivery и Deployment на практике часто смешивается в одно целое “доставили и сразу запустили”. А если приблизиться к реальности еще больше и отдалиться от академических определений — часто на практике все 3 части смешиваются в один большой конвеер который и называется единым словом CI/CD или CICD. Таким образом, на практике все может быть свалено в одну большую кучу и например запуск тестов или миграций можно встретить совсем не в той последовательности, которая встречается в учебниках и теоритических статьях.

Pipeline (пайплайн) — это цепочка всех шагов необходимых для реализации CI/CD, каждый шаг это stage (стадия, этап), внутри каждого stage запускаются jobs (джобы, задачи). Если один этап падает, дальнейшие шаги отменяются. Дословно с английского переводиться как “трубопровод”, соответственно для носителя языка это ассоциируется с каким-то промышленным трубопроводом, который построен с целью доставки чего-то из точки A в точку B.

Stage (стадия, этап) — группа конкретных действий (джобов) из которых состоит пайплайн.

job (джоба) — конкретное действие или скрипт.

Runner (раннер) GitLab — runner выполняет джобы в пайплайне. Runner умеет выполнять джобы параллельно друг другу, использовать разные экзекьютеры (executors). Раннер можно установить отдельно на вашу машину и тогда gitlab будет просить его выполнять все инструкции.

Executor (экзекьютер) — способ выполнения скриптов описанных в джобах, например может работать в docker или сразу на целевой машине в bash.

Все варианты экзекьютеров

Всего есть такие варианты:

  • Docker — Самый популярный, все джобы изолировано в одноразовых контейнерах.

  • Docker Machine (deprecated в пользу Autoscaler) — будет удален в 2027-ом.

  • Docker Autoscaler — Замена Docker Machine, автоматически управляет виртуальными машинами с Docker.

  • Custom — Для нестандартных случаев, когда какой-то самописный или малоизвестный механизм оркестрации. Полная кастомизация и контроль, но и все сопустсвующие подводные камни.

  • Instance — Каждая джоба, отдельная преднастроенная виртуалка.

  • Kubernetes — Джобы запускаются как поды в кластере. Можно использовать если уже настроен кластер k8s.

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

  • SSH — Тоже как Shell, но через SSH подключение.

  • Parallels — Адаптация под Parallels Desktop for Mac, это коммерческий гипервизор позволяющий запускать Windows/Linux на маке. Короче решение для тех у кого слишком много лишних денег (возможно полезен тем кто специализируется на iOS/macOS разработке).

  • VirtualBox — Полная виртуализация для каждой джобы в виртуалках VirtualBox.

Более детально и схематично каждый термин будет описан дальше в статье.

Сколько есть специалистов и компаний, столько и разных реализацией CI/CD. Важно не просто следовать какому-то гайду, а делать осознанно именно то, что вам действительно требуется.

Простейший CI/CD (разработка, MVP)

В этом разделе рассмотрим конфигурацию и схему простой реализации CI/CD которая подойдет на этапе разработки или MVP.

Простой пример .gitlab-ci.yml

Все начинается с репозитория, а точнее с описанного в нем файла .gitlab-ci.yml (документация GitLab по gitlab-ci.yml). Этот файл содержит все инструкции, которые нужно выполнить для целей CI/CD.

Рассмотрим простейший пример, который GitLab предлагает в качестве стартового шаблона .gitlab-ci.yml:

Вместо реальных запусков тестов используется команда echo, которая просто выводит сообщение в консоль, а также команда sleep, которая создает искусственную задержку.

stages:          # Список stages (стадий) для jobs и порядок их выполнения  - build  - test  - deploybuild-job:       # Джоба build, выполняемая в первую очередь  stage: build  script:    - echo "Compiling the code..."    - echo "Compile complete."unit-test-job:   # Запуск тестов  stage: test    # Запустится только если build прошел успешно  script:    - echo "Имитация запуска unit-тестов... с задержкой 60 секунд"    - sleep 60    - echo "Покрытие кода в районе 90%"lint-test-job:   # Еще одна джоба, которая относится к стадии "test"  stage: test    # Так как она тоже относится к стадии "test", будет запущена параллельно  script:    - echo "Проверка кода линтером... Задержка 10 секунд."    - sleep 10    - echo "Нет ошибок линтера."deploy-job:      # Само развертывание (deploy)  stage: deploy  # Запустится только если все джобы из стадии "test" завершились успешно  environment: production  script:    - echo "Развертывание приложения..."    - echo "Приложение развернуто."

Когда GitLab отслеживает изменение в репозитории, он передает инструкции из .gitlab-ci.yml раннеру. Раннер, в свою очередь, использует указанный executor для выполнения команд. Подробнее о поддерживаемых executors — в документации GitLab.

Runner (раннер) GitLab — это программа, которая выполняет инструкции в указанном окружении. Executor определяет, в каком именно окружении будут выполнены команды. Так как в примере используются только базовые утилиты echo и sleep, подойдет практически любой executor:

  • Docker — каждая джоба запускается в изолированном Docker-контейнере на основе указанного образа.

  • Shell — команды выполняются напрямую в оболочке хост-системы, где установлен раннер.

  • Kubernetes — джобы выполняются в подах кластера Kubernetes.

  • SSH — команды выполняются на удаленной машине через SSH-соединение.

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

Если вы используете не Self Hosted Gitlab (который поднят в вашей инфраструктуре), а GitLab.com (SaaS-инстанс) по умолчанию будет использован shared runners, который в стандартной конфигурации использует Docker executor — каждая джоба запускается в изолированном контейнере.

Схема работы простейшего CI/CD

Схема на примере когда:

  • Используется gitlab.com (не self hosted)

  • Runner установлен на вашем сервере

  • Используется docker executor

  • Используется вышеописанный шаблон .gitlab-ci.yml (4 джобы)

Если build-job создает какой-то артефакт, его можно расположить в каком-то хранилище. Если довольствоваться самой простой и быстрой реализацией, можно исключить работу с registry (хранилищем артефактов). Часто на начальных этапах разработки так и поступают, вместо дополнительных действий с registry происходит следующее: раннер поднимает контейнер в котором происходит подключение по SSH к целевому серверу, на целевом сервере уже установлен docker и git, по этому в рамках подключения SSH достаточно сделать git pull, docker run, а старый контейнер просто гасят. Это работает когда в репозитории разработчика есть Dockerfile который делает все необходимое для запуска приложения: установка зависимостей, сборка и запуск.

Но этот сценарий имеет сразу множество недостатков и подходит для этапа разработки или MVP, но не для поддержки серьезного продакшена. Недостатки такого подхода по пунктам:

  • Downtime — нужно положить старый контейнер, поднять новый, приложение недоступно пока происходит сборка нового контейнера.

  • Сборка нагружает production сервер

  • Нет истории артефактов, нет возможности откатиться к прошлому артефакту, если с новым что-то пошло не так.

  • Не подходит для горизонтального масштабирования.

  • Уязвимая инфраструктура — доступ к серверу прода попадает в gitlab (ssh ключи), а на сервере прода есть доступ к репозиторию.

  • Исходный код присутствует на сервере прода.

  • Docker хранит свои слои и кэш на сервере прода.

При этом у многих такой подход живет в проде и ничего страшного не случается (до поры до времени).

Практика с простейшим шаблоном CI/CD

Закоммитим и запушим данный .gitlab-ci.yml файл gitlab и посмотрим что произойдет.

Переходим к списку пайплайнов

Переходим к списку пайплайнов
Обзор списка пайплайнов

Обзор списка пайплайнов
Просмотр конкретного пайплайна

Просмотр конкретного пайплайна

При просмотре пайплайна, gitlab визуализирует наши stages и jobs. Всего у нас 4 джобы. 2 параллельных lint-test-job и unit-test-job они находятся в стадии test. Перейдем в unit-test-job и посмотрим что gitlab нам отобразит:

Логи джобы unit-test-job

Логи джобы unit-test-job

В .gitlab-ci.yml это описано так:

unit-test-job:   # Запуск тестов  stage: test    # Запустится только если build прошел успешно  script:    - echo "Имитация запуска unit-тестов... с задержкой 60 секунд"    - sleep 60    - echo "Покрытие кода в районе 90%"

Всего 3 команды, но в логах мы видим много всего. По этому разберем каждую строчку для лучшего понимания.

Running with gitlab-runner 18.10.0~pre.705.ge11dde90 (e11dde90)  on green-4.saas-linux-small-amd64.runners-manager.gitlab.com/default ntHFEtyXQ, system ID: s_8990de21c550  feature flags: FF_USE_GIT_PROACTIVE_AUTH:true
Подробно
Running with gitlab-runner 18.10.0~pre.705.ge11dde90 (e11dde90)

Используется gitlab-runner версии 18.10.0 (pre-release сборка).
e11dde90 — Судя по всему хэш-коммита.

on green-4.saas-linux-small-amd64.runners-manager.gitlab.com/default ntHFEtyXQ

Имя хоста runner’а: green-4.saas-linux-small-amd64.runners-manager.gitlab.com:
green-4 — конкретная нода
saas-linux-small-amd64 — тип машины: SaaS (облако GitLab), Linux, small (малый размер ресурсов), архитектура amd64
ntHFEtyXQ — видимо просто ID раннера (но почему-то в UI мы увидим этот ID без последнего символа).
Исходя из всего этого складывается впечатление, что это shared runner от GitLab (не self-hosted). system ID: s_8990de21c550 — Уникальный системный идентификатор машины, на которой работает runner. Узнал из документации gitab API.
feature flags: FF_USE_GIT_PROACTIVE_AUTH:true — Определяет стратегию авторизации которую использует Gitaly. Gitaly — это программный модуль разработанный gitlab для удаленного управления репозиториями GIT.
Документация по Gitaly.
Документация гитлаб о feature flags.
Документация proactiveAuth GIT.

Нам сообщается версия runner’а который занимался выполнением джобы. Я не нашел прямой ссылки для перехода к нему, по этому просто подставил в url его номер, так мне удалось просмотреть информацию о нем.

Раннер выполнишвий нашу джобу

Раннер выполнишвий нашу джобу

Это один из множества раннеров который запустил gitlab для своих пользователей. Он выполняется где-то на серверах гитлаба. Позже мы настроим свой собственным раннер для своих задач.

Двигаемся по логам дальше и видим:

Preparing the "docker+machine" executorUsing default imageUsing Docker executor with image ruby:3.1 ...Using default imageUsing effective pull policy of [always] for container ruby:3.1Pulling docker image ruby:3.1 ...Using docker image sha256:9981... for ruby:3.1 with digest ruby@sha256:9162...
Подробно
Preparing the "docker+machine" executor

Тип executor’а docker+machine — Он кстати deprecated уже года 4 и gitlab выбрал другое направление развития — Autoscaler. Странно почему до сих пор используется в shared runners. Подробнее docker+machine. В двух словах этот экзекьютор умеет поднимать полноценные виртуальные машины, а внутри уже запускать docker. Зачем гитлабу виртуализация, если есть контейнеризация? Коротко — безопасность, фикс проблемы известной под именем container escape.

Using default imageUsing Docker executor with image ruby:3.1 ...

В .gitlab-ci.yml не указано какой image докера использовать, по этому используется образ по умолчанию из настроек раннера. В UI к сожалению не выводиться информация о конфигурации раннера, она лежит в конфигурационных файлах формата toml и скрыта от нас.

Using effective pull policy of [always] for container ruby:3.1

Политика скачивания образа always — то есть всегда скачивать, даже если есть локально.

Pulling docker image ruby:3.1Using docker image sha256:9981... for ruby:3.1 with digest ruby@sha256:9162...

Информация об успешном скачивании docker образа ruby:3.1 из docker hub.

Нам говориться о подготовке экзекьютора и загрузке docker образа. Читаем дальше:

Preparing environmentUsing effective pull policy of [always] for container sha256:c11...Running on runner-nthfetyxq-project-79632759-concurrent-0 via runner-nthfetyxq-s-l-s-amd64-1775396763-e03183f3...Getting source from Git repositoryGitaly correlation ID: 73b6c9732c8240b7b5a1937a1eaac112Fetching changes with git depth set to 20...Initialized empty Git repository in /builds/webstap/self-server-config/.git/Created fresh repository.Checking out 3575359a as detached HEAD (ref is main)...Skipping Git submodules setup$ git remote set-url origin "${CI_REPOSITORY_URL}" || echo 'Not a git repository; skipping'
Подробно
Preparing environmentUsing effective pull policy of [always] for container sha256:c11...

Runner поднимает вспомогательный контейнер (это уже другой, не тот который ruby:3.1). Контейнер ruby:3.1 имеет другой хэш 9981, он предназначен для наших jobs. Контейнер о котором сейчас идет речь имеет другой хэш c11, он нужен раннеру для клонирования git репозитория, работой с кэшем/артефактами и всем тем что происходит до и после нашего пользовательского скрипта.

Running on runner-nthfetyxq-project-79632759-concurrent-0 via runner-nthfetyxq-s-l-s-amd64-1775396763-e03183f3...

Тут указывается 2 имени:
Первое имя — контейнер который будет запускаться.
Второе имя — виртуальная машина на которой будет запускаться контейнер, в ней есть Docker daemon за счет которого и запуститься контейнер.

Имя контейнера состоит из:

runner-nthfetyxq-project-79632759-concurrent-0       │                 │                   │       │                 │                   └─ Слот конкурентности (первый из N)       │                 └─ ID проекта в GitLab       └─ Токен/ID раннера (короткий хеш)

Имя виртуальной машины состоит из:

via runner-nthfetyxq-s-l-s-amd64-1775396763-e03183f3           │         │ │ │  │        │      │           │         │ │ │  │        │      └─ ID инстанса (VM)           │         │ │ │  │        └─ Похоже на Timestamp или порядковый номер           │         │ │ │  └─ Архитектура: amd64           │         s-l-s = shared/linux/small (тип машины)           └─  Токен/ID раннера (короткий хеш)
Gitaly correlation ID: 73b6c9732c8240b7b5a1937a1eaac112

Ранее мы уже говорили о Gitaly. Тут просто указан ID для трассировки. Простыми словами это конкретный ID операции, который позволит отыскать ее в логах.

Fetching changes with git depth set to 20...

Информация о том, что runner выполнит что-то вроде:

git initgit remote add <наш репозиторий>git fetch origin --depth 20 

Иными словами, вместо загрузки всех коммитов и полной копии репозитория будут подтянуты последние 20 коммитов. Это нужно для экономии трафика и ускорение работы. Это значение можно конфигурировать через переменную GIT_DEPTH в .gitlab-ci.yml, подробнее о других конфигурационных переменных можно найти тут.

Initialized empty Git repository in /builds/webstap/self-server-config/.git/Created fresh repository.

Инициализирован пустой git репозиторий в файловой системе внутри контейнера.

Checking out 3575359a as detached HEAD (ref is main)...

Было выполнено git checkout 3575359a — переключение на конкретный коммит (не на ветку). Переключение именно на коммит важно в условиях CI — пока запускался пайплайн, в ветку могли прилететь сверху еще коммиты, но пайплайн предназначен строго для конкретного коммита, по этому раннер ориентируется строго по коммитам, а не по веткам.

Skipping Git submodules setup

Подмодули не настроены (нет GIT_SUBMODULE_STRATEGY в .gitlab-ci.yml). Подробнее.

git remote set-url origin "${CI_REPOSITORY_URL}" || echo 'Not a git repository; skipping'

CI_REPOSITORY_URL — predefined переменная. Содержит полный путь к репозиторию. Ее значение:

https://gitlab-ci-token:$CI_JOB_TOKEN@gitlab.example.com/my-group/my-project.git

Это команда перезаписывает URL remote origin, чтобы последующие git операции были авторизованы.

|| echo 'Not a git repository; skipping

Если не удалось выполнить git remote будет просто написано “Not a git repository; skipping”, это нужно что бы джоба не упала, на случай если работа не с git репозиторием.

GitLab Runner на виртуальной машине скачал два Docker-образа — ruby:3.1 для выполнения пользовательского кода и gitlab-runner-helper для служебных операций, затем helper-контейнер склонировал Git-репозиторий webstap/self-server-config (shallow clone, глубина 20 коммитов) в директорию /builds/webstap/self-server-config, переключился на конкретный коммит 3575359a из ветки main и подготовил рабочую среду для выполнения скриптов из .gitlab-ci.yml.

Осталось разобрать последнюю часть:

Executing "step_script" stage of the job scriptUsing default imageUsing effective pull policy of [always] for container ruby:3.1Using docker image sha256:9981... for ruby:3.1 with digest ruby@sha256:916... ...$ echo "Имитация запуска unit-тестов... с задержкой 60 секунд"Имитация запуска unit-тестов... с задержкой 60 секунд$ sleep 60$ echo "Покрытие кода в районе 90%"Покрытие кода в районе 90%Cleaning up project directory and file based variablesJob succeeded
Подробно
Using default imageUsing effective pull policy of [always] for container ruby:3.1Using docker image sha256:9981... for ruby:3.1 with digest ruby@sha256:916... ...

Подготовка контейнера для скрипта. Раннер поднимает новый контейнер из образа ruby:3.1, в котором будет выполняться наш скрипт из .gitlab-ci.yml.

$ echo "Имитация запуска unit-тестов... с задержкой 60 секунд"

Далее мы видим строку которая начинается с $ — гитлаб показывает саму команду, которую он запустит в контейнере. А затем stdout это команды (результат ее выполнения):

Имитация запуска unit-тестов... с задержкой 60 секунд

Аналогично далее:

$ sleep 60$ echo "Покрытие кода в районе 90%"Покрытие кода в районе 90%

Завершение:

Cleaning up project directory and file based variablesJob succeeded

“Cleaning up project directory” — Удаляются файлы проекта из рабочей директории /builds/webstap/self-server-config/.

“file based variables” — Удаляются временные файлы, созданные из CI/CD переменных типа File (для безопасности — чтобы секреты не остались на диске).

Job succeeded

Все команды вернули exit code 0 — то есть выполнены без ошибок.

Выполнен блок script из .gitlab-ci.yml джобы unit-test-job:

unit-test-job:   # Запуск тестов  stage: test    # Запустится только если build прошел успешно  script:    - echo "Имитация запуска unit-тестов... с задержкой 60 секунд"    - sleep 60    - echo "Покрытие кода в районе 90%"

Ранее мы говорили об артефактах, в результате выполнения пайплайнов мы не создавали артефакты в скриптах наших джобов, но все же некоторые файлы были созданы и размещены в хранилище артефактов gitlab. Речь идет о файлах с логами. Просмотреть их можно если перейти в Build → Artifacts.

Просмотр артефактов

Просмотр артефактов

Мы рассмотрели как себя ведет шаблон CI-CD предложенный gitlab. Данную реализацию можно доработать таким образом, что одна из наших джобов подключалась через SSH к нашему VPS, заходила в определенную директорию, обновляла git репозиторий и поднимала контейнер. Но как я уже писал: у этого есть ряд недостатков.


С вами был Тимофей. Кто я?

Разрабатываю с 2015 года. Стартовал как front-end разработчик на React, после 6-лет переключился на full-stack, последние годы — чаще DevOps. Мой публичный WakaTime.


Спасибо, что дочитали! Тем, кто уже подписан на мой телеграм-канал — отдельная благодарность, это мотивирует делиться опытом дальше. Остальным — заходите, помимо технических статей поднимаю и социально важные темы.


ссылка на оригинал статьи https://habr.com/ru/articles/1031452/