Привет, меня зовут Ярослав, я Backend‑разработчик в отделе Битрикс24 CRM Корус консалтинг. Не так давно я впервые занимался настройкой CI/CD для Битрикс‑проектов, поэтому сегодня хочу поделиться шагами, которые помогут запустить свой первый пайплайн. Статья подойдёт для полных новичков в теме поставки кода.

Содержание
Введение
Подготовка сервера
Настройка переменных окружения
Написание скрипта пайплайна
Запуск скрипта
Ошибки и способы их решений
Подведение итогов
Введение
Описание понятия CI и пользу от его применения найти несложно, здесь я опишу процесс работы системы с использованием непрерывной интеграции.
В ходе описания процесса столкнёмся с определениями:
Задача (job) — конкретная операция, описанная с помощью скрипта.
Этап (stage) — набор задач, которые можно сгруппировать по общей цели.
Пайплайн (pipeline) — последовательность этапов, каждый из которых объединяет несколько задач. Выполняется последовательно.
Раннер (runner) — приложение (агент), которое автоматически выполняет процессы CI/CD.

Разработчик разрабатывает новый функционал или исправляет ошибки в коде.
Коммитит изменения, пушит их в репозиторий, и создаёт Merge Request.
Запускается пайплайн. Исходя из его конфигурации, запускаются этапы. Этапы выполняются по очереди, и содержат задачи, которые выполняются параллельно.
При возникновении ошибки на каком‑либо из этапов, пайплайн завершается с ошибкой.
При успешном прохождении всех этапов, пайплайн завершается успешно.
В процессе выполнения пайплайна могут выполняться любые сценарии, мой опыт подсказывает выделить такие:
сборка;
тестирование (автотесты и проверка линтерами);
развёртывание кода на сервер.
Сегодня рассмотрим только последний этап.
Почему GitLab CI?
Тут всё просто — в компании используется GitLab, поэтому выбираем готовый инструмент.
Почему Shared Runner?
Есть два способа настройки CI/CD: с установкой GitLab Runner на сервер с проектом, и с использованием Shared Runner.
Сегодня опишем способ с использованием Shared Runner — в таком случае job'ы выполняются на сервере с GitLab, либо на отдельном сервере для пайплайна. Он не требует доступа root к серверу с репозиторием, доступен для всех проектов, не нуждается в ручной установке и может использоваться без дополнительных настроек.
Есть ситуации, когда кажется, что Shared Runner не подойдёт — например, когда сервер находится за VPN или внутри клиентской корпоративной сети. Как быть в таком случае также разберём.
Подготавливаем сервер
Мы используем self‑hosted GitLab на своём сервере с CentOS Stream 9, дальнейшие команды будут выполняться на нём.
Шаг 1: Регистрируем раннер (подробно)
Скачиваем бинарник GitLab Runner и кладём его в директорию для общесистемных доступных скриптов (/usr/local/bin
):
sudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
Делаем файл исполняемым:
sudo chmod +x /usr/local/bin/gitlab-runner
Создаём пользователя для раннера:
sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash
Устанавливаем GitLab Runner как службу, которая будет работать от имени созданного пользователя:
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
Регистрируем раннер в GitLab:
Предварительно, нужно получить токен регистрации через веб‑интерфейс GitLab (Settings → CI/CD → Runners).
Переходим на страницу настроек CI/CD для репозитория:

Находим раздел Runners и нажимаем New project runner:

Указываем тег раннера. В нашем случае shared
, но можно и любой другой:

Обратим внимание на опции:
Run untagget jobs — раннер будет использоваться по умолчанию, без указания тега в скрипте.
Paused — Остановит раннер после создания.
Protected — раннер будет выполнять задачи пайплайна только в защищённых ветхах.
Lock to current projects — раннер нельзя будет использовать в других проектах.
Maximum job timeout — максимальное время выполнения раннера в секундах, после которого он остановится.
GitLab выдаст токен регистрации и команду для установки раннера в зависимости от выбранной платформы:

Далее регистрируем раннер командой, где:
{url}
— URL вашего GitLab
{token}
— полученный токен
sudo gitlab-runner register --non-interactive --url "{url}" --registration-token "{token}" --executor "shell" --description "Global Runner on Server" --tag-list "SharedRunner" --run-untagged="true"
Проверяем, что раннер зарегистрирован правильно:
sudo gitlab-runner verify
Запускаем раннер:
sudo gitlab-runner start
sudo gitlab-runner list
Разрешаем пользователю gitlab-runner выполнять sudo без пароля (иначе могут быть проблемы с правами)
echo 'gitlab-runner ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/gitlab-runner
В нашем примере пайплайн будет устанавливать VPN‑соединение перед тем, как подключаться к серверу, поэтому дополнительно установим SSTP‑клиент:
Скачиваем SSTP‑клиент и выполняем команды для настройки и установки:
sudo curl -LO https://sourceforge.net/projects/sstp-client/files/latest/download
cd sstp-client-*
./configure && make && make install
Проверяем, что SSTP-клиент установлен:
sstpc --version
Настраиваем переменные окружения
В скрипте пайплайна будут использоваться данные, которые будут меняться в зависимости от проекта. Их стоит вынести в переменные, которые можно добавить через веб‑интерфейс. Создадим переменные (Settings → CI/CD → Variables):
Переходим на страницу настроек CI/CD для репозитория (см. выше).
Находим раздел Variables и нажимаем Add variable:

Указываем ключ переменной, по которому будем обращаться к ней в скрипте, и значение:

Создаём следующие переменные:

DEP_KEY — ключ для подключения по ssh.
DEP_USER — имя пользователя для подключения по ssh.
DIR_TO_DEPLOY — директория с репозиторием проекта на сервере.
MAIN_HOST — адрес сервера продуктив.
STAGE_HOST — адрес тестового сервера.
REPOSITORY_URL — ссылка для выгрузки изменений в репозитории (в нашем случае по ssh)
SSTP_HOST — адрес подключения VPN.
SSTP_USER — имя пользователя для подключения VPN.
SSTP_PASSWORD — пароль для подключения VPN.
Добавляем ключи в Deploy Keys
Для того, чтобы можно было обновлять репозиторий по ssh на удалённых серверах, нужно добавить ssh‑ключ сервера в Deploy Keys. Для этого:
Подключаемся к серверу с проектом, создаём пару ключей:
ssh-keygen -t ed25519 -C your_email@example.com
cat ~/.ssh/id_ed25519.pub
Команда гененрирует приватный и публичный ключ используя алгоритм ed25 519, а флаг ‑C позволяет указать имя или почту пользователя.
Копируем публичный ключ и добавляем его в Deploy Keys в настройках репозитория (Settings → Repository → Deploy keys).
Переходим на страницу настроек репозитория:

Находим раздел Deploy Keys и нажимаем Add new key:

Указываем название и ключ, после чего нажимаем Add key:

Пишем скрипт пайплайна
Для конфигурации GitLab CI используется файл .gitlab-ci.yml
в корне репозитория. Подробнее о формате YAML, синтаксисе и основных возможностях можно прочитать тут. Начнём со структуры:
stages:
- deploy
variables:
GIT_STRATEGY: none
deploy-job:
stage: deploy
tags:
- SharedRunner
rules:
- if: $CI_COMMIT_REF_NAME == "main"
- if: $CI_COMMIT_REF_NAME == "stage"
script:
stages
— определяем список этапов, которые будут выполняться в пайплайне. Здесь всего один этап — развёртывание (deploy).
variables
— устанавливаем переменные окружения для пайплайна. Переменная GIT_STRATEGY: none
отключает клонирование репозитория перед запуском задачи. Помимо GIT_STRATEGY, скрипт поддерживает и другие предопределённые переменные.
deploy-job
— наша единственная задача, которая будет выполняться на этапе deploy.
tags
— указываем, что задача должна выполняться на раннере с тегом SharedRunner. В нашем случае это необязательно, так как при регистрации раннера мы указали ‑run‑untagged=»true». В этом случае, раннер будет выполнять задачи даже без тегов.
rules
— указываем, что задача должна выполняться только для веток stage
или main
.
script
— здесь опишем список команд в рамках задачи.
Добавим блоки для установки VPN‑соединения и подключения к серверу по ssh:
Первый блок можно пропустить, если можем подключиться сразу по ssh.
.setup_pipeline_vpn: &setup_pipeline_vpn
- sudo nohup timeout 60s sstpc --log-level 2 --log-stderr "$SSTP_HOST" user "$SSTP_USER" password "$SSTP_PASSWORD" noauth --cert-warn &
- sleep 5
- "if ! ip a | grep 'ppp0'; then echo 'Error: VPN connection is not established!'; exit 1; fi"
- sudo ip route add "$DEP_HOST" dev ppp0
В этом блоке:
Запускаем команду подключения к VPN в фоновом режиме.
Ждём 5 секунд, чтобы VPN успел подняться.
Проверяем, что соединение установлено.
Добавляем VPN‑маршрут к хосту, с которым будем работать.
.setup_pipeline_ssh_key: &setup_pipeline_ssh_key
- "which ssh-agent || ( dnf install -y openssh-clients )"
- eval $(ssh-agent -s)
- '[ -z "$DEP_KEY" ] && echo "ERROR: SSH key is missing!" && exit 1'
- echo "$DEP_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo "StrictHostKeyChecking no" > ~/.ssh/config
- ssh-keyscan -H "$DEP_HOST" >> ~/.ssh/known_hosts
В этом блоке:
Устанавливаем ssh‑agent, если не установлен.
Добавляем приватный ключ в ssh‑agent.
Отключаем проверку подлинности хостов (чтобы не ждать подтверждения при первом подключении).
Добавляем удалённый сервер в known_hosts.
.setup_pipeline_vpn: &setup_pipeline_vpn
и setup_pipeline_ssh_key: &setup_pipeline_ssh_key
— это «якоря» в формате YAML. Их можно использовать в разных участках скрипта с помощью *setup_pipeline_vpn
и *setup_pipeline_ssh_key
.
Наконец, напишем скрипт задачи:
script:
- |
if [ "$CI_COMMIT_REF_NAME" == "stage" ]; then
export DEP_HOST="$STAGE_HOST"
elif [ "$CI_COMMIT_REF_NAME" == "main" ]; then
export DEP_HOST="$MAIN_HOST"
else
echo "Branch $CI_COMMIT_REF_NAME is not handled"
exit 1
fi
- *setup_pipeline_vpn
- *setup_pipeline_ssh_key
- ssh $DEP_USER@$DEP_HOST 'ssh-keyscan -H {Хост сервера с GitLab} >> ~/.ssh/known_hosts'
- ssh $DEP_USER@$DEP_HOST "cd ${DIR_TO_DEPLOY} && git remote set-url origin $REPOSITORY_URL"
- ssh $DEP_USER@$DEP_HOST "cd ${DIR_TO_DEPLOY} && git fetch origin && git reset --hard origin/$CI_COMMIT_REF_NAME"
- sudo pkill -SIGINT sstpc || echo "The VPN has been disabled"
В скрипте:
Выбираем хост (тест/прод) в зависимости от ветки (stage/main).
Запускаем блок с установкой VPN‑соединения.
Запускаем блок с настройкой ssh.
Добавляем удалённый хост GitLab в known_hosts на сервере.
Переходим в директорию с проектом и устанавливаем URL для удалённого репозитория.
Обновляем репозиторий изменениями из нужной ветки.
Останавливаем VPN‑соединение.
Хост сервера с GitLab — заменить на свой сервер.
Полный код .gitlab-ci.yml
:
stages:
- deploy
variables:
GIT_STRATEGY: none
.setup_pipeline_vpn: &setup_pipeline_vpn
- sudo nohup timeout 60s sstpc --log-level 2 --log-stderr "$SSTP_HOST" user "$SSTP_USER" password "$SSTP_PASSWORD" noauth --cert-warn &
- sleep 5
- "if ! ip a | grep 'ppp0'; then echo 'Error: VPN connection is not established!'; exit 1; fi"
- sudo ip route add "$DEP_HOST" dev ppp0
.setup_pipeline_ssh_key: &setup_pipeline_ssh_key
- "which ssh-agent || ( dnf install -y openssh-clients )"
- eval $(ssh-agent -s)
- '[ -z "$DEP_KEY" ] && echo "ERROR: SSH key is missing!" && exit 1'
- echo "$DEP_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo "StrictHostKeyChecking no" > ~/.ssh/config
- ssh-keyscan -H "$DEP_HOST" >> ~/.ssh/known_hosts
deploy-job:
stage: deploy
tags:
- SharedRunner
rules:
- if: $CI_COMMIT_REF_NAME == "main"
- if: $CI_COMMIT_REF_NAME == "stage"
script:
- |
if [ "$CI_COMMIT_REF_NAME" == "stage" ]; then
export DEP_HOST="$STAGE_HOST"
elif [ "$CI_COMMIT_REF_NAME" == "main" ]; then
export DEP_HOST="$MAIN_HOST"
else
echo "Branch $CI_COMMIT_REF_NAME is not handled"
exit 1
fi
- *setup_pipeline_vpn
- *setup_pipeline_ssh_key
- ssh $DEP_USER@$DEP_HOST 'ssh-keyscan -H {Хост сервера с GitLab} >> ~/.ssh/known_hosts'
- ssh $DEP_USER@$DEP_HOST "cd ${DIR_TO_DEPLOY} && git remote set-url origin $REPOSITORY_URL"
- ssh $DEP_USER@$DEP_HOST "cd ${DIR_TO_DEPLOY} && git fetch origin && git reset --hard origin/$CI_COMMIT_REF_NAME"
- sudo pkill -SIGINT sstpc || echo "The VPN has been disabled"
Запускаем пайплайн
Создадим ветку gitlab‑ci, закоммитим .gitlab-ci.yml
, и создадим Merge request в ветку stage. Ждём, пока отработает пайплайн.
Посмотреть лог выполнения можно во вкладке Build→ Pipelines, или нажав на блок с пайплайном на странице Merge request:

На открывшейся странице выберем единственную выполненную задачу:

Наблюдаем лог выполнения задачи:

В логе запуска виды промежуточные результаты выполнения пайплайна. Всё горит зелёным, пайплайн успешно завершён!
Ради эксперимента, намеренно добавим в скрипт ошибку.

Получаем сообщение в логе выполнения. Подробнее причины ошибки можно изучить развернув блок с задачей.
Ошибки и способы их решения
Значения переменных недоступны
При создании переменных, используемых в пайплайнах для незащищённых веток (stage, dev, test), нужно снимать флаг Protect variable. Это позволит использовать её во всех ветках, а не только в main.

При запуске Runner зависает на Initializing executor providers...
Ошибка может заключаться в недостаточных правах для пользователя gitlab‑runner.
Для решения нужно авторизоваться под root, и изменить владельца директории gitlab‑runner.
cd /home
chown -R gitlab-runner: gitlab-runner
Итого
В этой статье мы написали просто пайплайн для GitLab CI и настроили Shared Runner на сервере, тем самым исключив необходимость установки и настройки раннера для каждого проекта. Пайплайн позволяет экономить время для поставки изменений на проект, и значительно упрощает жизнь разработчику.
В будущем, скрипт можно улучить, добавив в него:
Обновление изменений в подмодулях.
Выполнение unit‑тестов.
Подключение к VPN с использованием протоколов, помимо SSTP.