Вступление
Хабр, привет! На связи Владимир, DevOps-инженер компании Совкомбанк Технологии. В этой статье расскажу о компонентах GitLab, способах их применения и том, как они помогли нам с настройкой CI/CD на проектах.
Как мы настраивали CI/CD раньше
Во времена, когда DevOps практики только начали зарождаться в нашей компании, сформировалась определенная структура файлов и каталогов, где были описаны необходимые пайплайны.
Как обычно в корне проекта находился файл .gitlab-ci.yml, в котором мы описывали стадии, шаблоны с переменными для каждого стенда, шаблоны с самими скриптами для различных тестов, сборки образов, сканирования собранных образов на уязвимости, деплоем в K8S или на docker-хосты и удалением текущего деплоя. Отдельно в каталоге ci-cd-files мы храним Dockerfile и yml-файлы для пайплайнов, содержащих в себе описание заданий, в которые подключаются шаблоны из .gitlab-ci.yml. Также в корне проекта есть отдельный каталог helm, в котором по началу находился чарт с values.yaml для него, а, в последствии, когда мы написали собственный «универсальный» чарт, только values.yaml для каждого стенда. Но сейчас не про чарт и его конфиг.
Из проекта в проект конфигурации пайплайнов могли получать изменения. В основном изменения касались именно скриптов, а набор заданий был стандартным для большей части проектов. И каждый раз, при настройке нового проекта или изменении старого в части скриптов, по «вине» изменений в работе инфраструктурных сервисов, нам приходилось вносить изменения в каждый скрипт для каждого проекта отдельно. Например, в какой-то момент произошли изменения в нашем хранилище Docker-образов - скрипт по сканированию образов на уязвимости и выгрузке этих уязвимостей сломался и ничего не выводил. Путем долгих поисков вместе с «соседней» командой девопсов мы обнаружили причину в изменении запросов к АПИ. Теперь дополнительно требовалось передавать определенный заголовок. После изменения скрипта мы снова начали получать результаты сканирования, но появилась задача распространить изменения на все проекты. После того, как мы с этим успешно справились, зародилась мысль написать общие шаблоны, которые могли бы использоваться если не во всех, то хотя бы в большей части банковских проектов.
Сначала это были локальные шаблоны для самых крупных проектов или для отдельных команд, у которых процесс настройки CI/CD отличался парочкой переменных.
Результаты радовали, а одна из команд самостоятельно развила эту идею и практически реализовала GitOps прямо в GitLab. Впоследствии мы виделись с этой командой только по каким-нибудь проблемам в самом K8S или других инфраструктурных сервисах.
Но все равно оставалась другая бо́льшая часть проектов, в которых использовался старый формат CI/CD. Пусть и немного обновленный, приведенный к более понятному и красивому формату описания пайплайнов.
Прежде, чем я расскажу о способах решения этой проблемы, раскрою описание компонентов, чтобы вы лучше понимали, о чем пойдет речь дальше.
Описание компонентов
Компоненты в виде экспериментальной функции были добавлены в GitLab 16.0
В Совкомбанк Технологиях используется GitLab 17-й версии. Если вы решили «потрогать» компоненты собственными руками – рекомендую использовать версию не ниже 17. Начиная с этой версии компоненты стали общедоступными, а их функционал неплохо расширился, по-сравнению с версией 16.
Заглянем на страницу документации компонентов.
Компоненты CI/CD – это блок конфигурации пайплайна, который можно повторно использовать.
И тут может возникнуть вопрос:
– Чем это отличается от шаблонов, которые мы подключаем через include? Например, template, remote или project.
Во-первых, у нас появился новый блок в include – component. Дальше больше, вернемся к описанию компонентов:
Компоненты можно настроить с помощью входных параметров для более динамичного поведения.
А вот тут уже становится интереснее: для компонентов появляется блок inputs, благодаря которому можно передать в подключаемый компонент значения определенных параметров. Но об этом позже, а пока снова обратимся к документации:
Компоненты CI/CD похожи на другие виды конфигураций, подключаемых с помощью include, но имеют ряд преимуществ.
Посмотрим, что это за преимущества:
– Компоненты могут быть перечислены в каталоге CI/CD;
– Компоненты могут быть выпущены и использованы с определенной версией;
– В одном проекте можно определить несколько компонентов и создавать для них версии одновременно.
По второму и третьему пунктам все более-менее понятно, а вот обсудить, что такое каталог CI/CD будет не лишним.
Каталог CI/CD – это список проектов с опубликованными компонентами CI/CD, которые вы можете использовать для расширения своего рабочего процесса CI/CD
Все созданные проекты компонентов и сами компоненты можно посмотреть на странице <your-instance>/explore/catalog. Там будут перечислены все проекты, а внутри них все компоненты определенного проекта с описанием параметров и документацией к проекту.
Немного практики
Начнем с простого: создадим проект для наших компонентов, сделаем из него элемент каталога CI/CD, добавим туда базовый компонент и попробуем подключить его к нашему проекту.
Создаем наш новый проект, выдаем ему красивое имя и обязательно просим GitLab создать в проекте файл README.

После создания проекта нужно перейти в Settings -> General -> Naming, description, topics и заполнить описание проекта. Этого требует GitLab для каталогов CI/CD.

Дальше идем в Settings -> General -> Visibility, project features, permissions, находим пункт CI/CD Catalog project и активируем его

В проекте создаем каталог templates, в который и будем складывать наши компоненты. Я назову первый компонент example.yml. Его содержимое:
Скрытый текст
spec:
inputs:
message:
default: "Сообщение по умолчанию."
description: 'Сообщение для джобы.'
extra-message:
default: false
type: boolean
description: 'Включает вывод дополнительного сообщения.'
array-script:
default:
- echo "Скрипт №1"
- echo "Скрипт №2"
type: array
description: 'Массив скриптов для джобы.'
port:
default: 8080
options:
- 8080
- 9000
- 3000
type: number
description: 'Номер порта.'
version:
regex: ^v\d\.\d+(\.\d+)$
description: 'Номер версии.'
---
TEST:
stage: test
image: $CI_REGISTRY/common-alpine:6.13
script:
- echo $[[ inputs.message ]]
- |
if $[[ inputs.extra-message ]]; then
echo "Дополнительное сообщение"
fi
- |
echo "Порт: $[[ inputs.port ]]"
echo "Версия: $[[ inputs.version ]]"
after_script: $[[ inputs.array-script ]]
tags:
- public
Описание компонентов следует начинать с блока spec:inputs. В этом блоке необходимо описать параметры компонента, которые мы сможем определять/переопределять. Все возможные параметры должны быть определены в spec:inputs.
По умолчанию для параметра используется тип string. На выбор нам дается четыре типа:
– string – тип по умолчанию, принимает на вход строковое значение
– number – принимает на вход числовое значение
– array – принимает на вход допустимый синтаксисом YAML массив. Более сложные функции, такие как !reference
, не могут быть использованы
– boolean – принимает на вход true/false
Обязательность параметра определяется наличием поля default – при его отсутствии поле является обязательным и должно быть определено при подключении компонента.
Также параметры могут иметь следующие поля:
– options – задает список из вариантов, которые можно передать в параметре. Значение, которое не совпадает со списком, приведет к ошибке в работе пайплайна. Применяется ко всем типам, кроме boolean.
– regex – задает регулярное выражение, которому должно следовать значение. Шаг влево/вправо – ошибка в работе пайплайна. Применяется только к типу string.
Как только закончили описание всех параметров – необходимо в новой строке написать ”---“. Это требуется для разделения блоков с параметрами и основным кодом. После этого можно приступить к описанию заданий/шаблонов.
Чтобы использовать описанные параметры, их нужно записать в таком виде:
$[[ inputs.<parameter-name> ]]
Но, перед тем как подключать наш новый компонент, нужно подготовить релиз. Для этого в корне проекта создаем файл .gitlab-ci.yml со следующим содержимым:
Скрытый текст
include:
- component: [ДАННЫЕ УДАЛЕНЫ]/components/templates/default@1.18.3
stages: [release]
create-release:
stage: release
script: echo "Creating release $CI_COMMIT_TAG"
rules:
- if: $CI_COMMIT_TAG
when: always
release:
tag_name: $CI_COMMIT_TAG
description: "Release $CI_COMMIT_TAG of components in $CI_PROJECT_PATH"
Здесь я подключаю собственный компонент, в котором описал образ по умолчанию для запуска заданий и тег раннера. Но это не важно. Важно то, что ниже. А ниже у нас стандартное задание по выпуску релиза, которое запустится только при создании тега из ветки.
Сохраняем изменения и идем выпускать тег:


Для создания релизов следует использовать семантическое версионирование https://semver.org/. Например, 1.0.0, 2.3.4 или 1.2.2-alpha. При этом, если при подключении компонента указать @~latest, будет использована последняя версия, соответствующая маске X.Y.Z. Делать так, конечно же, не рекомендуется.
Создаем тег и ждем выполнения задания по выпуску релиза.
Теперь займемся подключением компонента в проект.
Создаем новый проект, если его еще нет и добавляем туда файл .gitlab-ci.yml со следующим содержимым:
Скрытый текст
include:
- component: [ДАННЫЕ УДАЛЕНЫ]/habr-example-component/example@0.0.1
inputs:
version: v1.2.3
stages: [test]
Для подключения компонента используем include:component. Адрес подключения указываем следующим образом: <your-gitlab-instance>/<path/to/project>/<component-name>@<version>, где:
<your-gitlab-instance> - адрес GitLab
<path/to/project> - путь до проекта с компонентами
<component-name> - название компонента без указания расширения файла
<version> - версия релиза
Так как в компоненте параметр version определен без поля default, он является обязательным, указываем его через inputs:<parameter-name>. Не забываем, что на этот параметр мы «навесили» regex, а значит нужно указать версию, которая подойдет нашему регулярному выражению.
Сохраняем изменения и идем смотреть на логи нашего задания:

Как видим, все прекрасно работает. Версия соответствует значению параметра.
Но есть и другие параметры, попробуем поменять все:
Скрытый текст
include:
- component: [ДАННЫЕ УДАЛЕНЫ]/habr-example-component/example@0.0.1
inputs:
version: v1.2.3
message: "Хабр, вам тут сообщение!"
extra-message: true
array-script:
- echo "Теперь этот параметр содержит только один скрипт."
port: 9000
Отлично, поменяли значение параметра message, переключили extra-message на true, переопределили массив array-script и указали новый порт для параметра port из списка options.
Смотрим результат:

Все работает!
Давайте сломаем наш пайплайн. Для этого изменим version, чтобы значение не соответствовало regex или укажем новый порт, который отсутствует в options. Я поменяю порт:
Скрытый текст
include:
- component: [ДАННЫЕ УДАЛЕНЫ]/habr-example-component/example@0.0.1
inputs:
version: v1.2.3
port: 80
Смотрим: Unable to create pipeline
[ДАННЫЕ УДАЛЕНЫ]/habr-example-component/example@0.0.1
:port
input:80
cannot be used because it is not in the list of the allowed options
Ну что же… Это было ожидаемо.
Наш краткий экскурс по самим компонентам подошел к концу. Все остальное вы сможете найти в документации GitLab.
Как мы внедрили компоненты в проекты Совкомбанк Технологий и что изменилось после этого
Я начал активно изучать тему в начале 2024 года, тогда компоненты казались мне чем-то магическим. Путем скрупулезного чтения документации и выпуска сотни релизов, пришел к оптимальному решению. После чего предоставил команде первую стабильную версию своих компонентов.
Итого получилось 3 проекта:
– stands – проект с компонентами, описывающими каждый стенд. Dev, test, stage, prod и дополнительный компонент custom для настраиваемого окружения, например, preprod;
– jobs – проект с компонентами описывающим необходимые задания для каждого стенда. Напомню, что по умолчанию их 4: сборка образа, сканирование образа на уязвимости, деплой и удаление релиза;
– templates – «основной» проект компонентов, описывающий шаблоны и скрипты.
Структура следующая:
STANDS
├─test - шаблон с переменными для test среды
├─dev - шаблон с переменными для develop среды
├─stage - шаблон с переменными для stage среды
├─prod - шаблон с переменными для prod среды
└─custom - шаблон с переменными для настраиваемой среды
JOBS
├─test - стандартные задания для test среды
├─dev - стандартные задания для develop среды
├─stage - стандартные задания для stage среды
├─prod - стандартные задания для prod среды
└─custom - стандартные задания для настраиваемой среды
TEMPLATES
├─default - блок default с образом и тегом для раннеров по умолчанию, а также скрипты для уведомлений в мессенджер и некоторые функции
├─dast – элемент сканирования DAST
├─build - скрипт для сборки образа. Также включает в себя функции по скачиванию секретов из хранилища секретов, необходимых на стадии сборки
├─docker-compose - скрипты для деплоя и удаления релизов через docker-compose. Также включает в себя функции по скачиванию секретов из хранилища секретов, необходимых на стадии деплоя
├─docker - скрипты для деплоя и удаления релизов через docker. Также включает в себя функции по скачиванию секретов из хранилища секретов, необходимых на стадии деплоя
├─k8s - скрипты для деплоя и удаления релизов через helm. Также включает в себя функции по скачиванию секретов из хранилища секретов, необходимых на стадии деплоя
├─sast – элемент сканирования SAST
├─sca – элемент сканирования SCA
└─scan - скрипт сканирования образов на наличие уязвимостей
Структура может показаться не очень удобной, но так было нужно, поскольку на момент составления шаблонов, в проекте могло быть не больше 10 компонентов. Из-за этого пришлось выкручиваться с несколькими проектами. В будущем проведем рефакторинг и сформируем единый проект.
Покажу примеры текущей реализации из каждого проекта.
stands/test:
Скрытый текст
spec:
inputs:
environment:
default: test
description: 'Название окружения GitLab.'
stand:
default: test
description: 'Название стенда.'
context:
default: NOT_REQUIRED
description: 'Имя кластера.'
vr-lvl:
default: Pofig
options:
- Pofig
- Negligible
- Low
- Medium
- High
- Critical
description: 'Минимальный недопустимый уровень уязвимостей.'
vr-exit-code:
default: 0
type: number
options:
- 0
- 1
description: 'Exit Code для джобы сканирования при нахождении уязвимостей.'
node-env:
default: ''
description: 'Переменная окружения для Node.'
---
.test_template:
environment: $[[ inputs.environment ]]
variables:
ENVRM: $[[ inputs.stand ]]
VR_LVL: $[[ inputs.vr-lvl ]]
VULN_EXIT_CODE: $[[ inputs.vr-exit-code ]]
K8S_TOKEN: $K8S_TOKEN_$[[ inputs.context ]]
K8S_URL: $K8S_URL_$[[ inputs.context ]]
K8S_CLUSTER: $K8S_CLUSTER_$[[ inputs.context ]]
NODE_ENV: $[[ inputs.node-env ]]
jobs/test:
Скрытый текст
spec:
inputs:
build-extends:
type: array
default:
- .test_template
- .build_template
description: 'Набор подключаемых шаблонов для джобы сборки образа.'
build-rules:
type: array
default:
- if: $CI_COMMIT_BRANCH == "test"
when: always
- if: "$CI_COMMIT_BRANCH =~ /^devops.*$/"
when: always
- when: never
description: 'Набор правил для триггера джобы сборки образа.'
build-needs:
type: array
default:
- job: EMPTY_NEEDS
optional: true
description: 'Набор needs для джобы сборки образа.'
build-interruptible:
type: boolean
default: false
description: 'Помечает задание как прерываемое.'
scan-extends:
type: array
default:
- .test_template
- .scan_template
description: 'Набор подключаемых шаблонов для джобы сканирования образа.'
scan-rules:
type: array
default:
- if: $CI_COMMIT_BRANCH == "test"
when: on_success
- if: "$CI_COMMIT_BRANCH =~ /^devops.*$/"
when: on_success
- when: never
description: 'Набор правил для триггера джобы сканирования.'
scan-needs:
type: array
default:
- job: Build App Test
optional: true
description: 'Набор needs для джобы сканирования образа.'
scan-interruptible:
type: boolean
default: false
description: 'Помечает задание как прерываемое.'
deploy-extends:
type: array
default:
- .test_template
- .deploy
description: 'Набор подключаемых шаблонов для джобы деплоя.'
deploy-rules:
type: array
default:
- if: $CI_COMMIT_BRANCH == "test"
when: manual
- if: "$CI_COMMIT_BRANCH =~ /^devops.*$/"
when: manual
- when: never
description: 'Набор правил для триггера джобы деплоя.'
deploy-needs:
type: array
default:
- job: Scan App Test
optional: true
description: 'Набор needs для джобы деплоя.'
deploy-interruptible:
type: boolean
default: false
description: 'Помечает задание как прерываемое.'
cleanup-extends:
type: array
default:
- .test_template
- .delete_command
description: 'Набор подключаемых шаблонов для джобы очистки деплоя.'
cleanup-rules:
type: array
default:
- if: $CI_COMMIT_BRANCH == "test"
when: manual
- if: "$CI_COMMIT_BRANCH =~ /^devops.*$/"
when: manual
- when: never
description: 'Набор правил для триггера джобы очистки деплоя.'
cleanup-needs:
type: array
default:
- job: EMPTY_NEEDS
optional: true
description: 'Набор needs для джобы очистки деплоя.'
cleanup-interruptible:
type: boolean
default: false
description: 'Помечает задание как прерываемое.'
---
Build App Test:
extends: $[[ inputs.build-extends ]]
rules: $[[ inputs.build-rules ]]
needs: $[[ inputs.build-needs ]]
interruptible: $[[ inputs.build-interruptible ]]
Scan App Test:
extends: $[[ inputs.scan-extends ]]
rules: $[[ inputs.scan-rules ]]
needs: $[[ inputs.scan-needs ]]
interruptible: $[[ inputs.scan-interruptible ]]
Deploy App Test:
extends: $[[ inputs.deploy-extends ]]
rules: $[[ inputs.deploy-rules ]]
needs: $[[ inputs.deploy-needs ]]
interruptible: $[[ inputs.deploy-interruptible ]]
Cleanup App Test:
extends: $[[ inputs.cleanup-extends ]]
rules: $[[ inputs.cleanup-rules ]]
needs: $[[ inputs.cleanup-needs ]]
interruptible: $[[ inputs.cleanup-interruptible ]]
templates/scan:
Скрытый текст
spec:
inputs:
stage:
default: scan
description: 'Название стадии.'
notify:
default: false
type: boolean
description: 'Включает уведомления о статусе задания сканирования образа.'
---
.scan_template:
stage: $[[ inputs.stage ]]
variables:
GREP_ID: '"id":*"[^"]*"'
GREP_PACKAGE: '"package":*"[^"]*"'
GREP_VERSION: '"version":*"[^"]*"'
GREP_FIX_VERSION: '"fix_version":*"[^"]*"'
GREP_SEVERITY: '"severity":*"\(Low\|Medium\|High\|Critical\)"'
before_script:
- !reference [.border]
- !reference [.log]
script:
- |
DEBUG=${DEBUG:-false}
if $DEBUG; then
set -x
fi
if [ -z ${SCAN_PROJECT_NAME+x} ]; then
log "INFO" "Переменная SCAN_PROJECT_NAME не установлена. Вместо нее будет использована переменная PROJECT_NAME = ${PROJECT_NAME}."
else
log "INFO" "Переменная SCAN_PROJECT_NAME установлена. SCAN_PROJECT_NAME = ${SCAN_PROJECT_NAME}."
SCAN_PROJECT_NAME=$(echo ${SCAN_PROJECT_NAME} | sed 's,/,%252F,g');
fi
URL=”Путь к API-методу для сканирования образа.”
log "INFO" "Адрес сканирования - ${URL}."
- curl -s -X POST -u "$CI_REGISTRY_USER":"$CI_REGISTRY_PASSWORD" "$URL"
- count=16
- |
while [[ "$(curl -s -u "$CI_REGISTRY_USER":"$CI_REGISTRY_PASSWORD" -H "$HEADER" $URL)" != *"severity"* ]] && [[ 0 -lt $count ]]; do
(( count-- ));
log "INFO" "Ожидание завершения сканирования.... Осталось $count попыток"
if [ $count = 0 ]; then
log "ERROR" "Не удалось получить статус сканирования. Попробуйте позже."
exit 1;
fi;
sleep 20;
done
- |
curl -s -u "$CI_REGISTRY_USER":"$CI_REGISTRY_PASSWORD" -H "$HEADER" "$URL" > all_vuln.txt
cat all_vuln.txt | grep --color=always -o ${GREP_ID},${GREP_PACKAGE},${GREP_VERSION},${GREP_FIX_VERSION},${GREP_SEVERITY} || log "DONE" "Уязвимостей не найдено."
cat all_vuln.txt | grep -o ${GREP_ID},${GREP_PACKAGE},${GREP_VERSION},${GREP_FIX_VERSION},${GREP_SEVERITY} > vulnerability.txt || z=1;
- |
cat all_vuln.txt | grep -o '"severity":"'${VR_LVL}'"' > /dev/null || s=1;
if [[ "$s" -eq "1" ]]; then
log "DONE" "Не найдено уязвимостей уровня ${VR_LVL}."
else
log "ERROR" "Обнаружены уязвимости уровня ${VR_LVL}."
exit 1;
fi
- NOTIFY_BY_COMPONENT=$[[ inputs.notify ]]
- !reference [.notify, success]
allow_failure: true
after_script:
- NOTIFY_BY_COMPONENT=$[[ inputs.notify ]]
- if [ $CI_JOB_STATUS == 'success' ]; then exit 0; fi
- !reference [.notify, error]
Выглядит не так хорошо, как хотелось бы, но моя команда постоянно вносит изменения в компоненты и постепенно их улучшает.
На текущий момент статистика следующая:

Внедрили компоненты в ~700 проектах и планируем масштабироваться
Значения постоянно скачут из-за количества самих проектов. Планируем увеличивать число и расти еще. Такое различие в числах между тремя проектами на скриншоте вызвано тем, что проект templates так же содержит в себе компоненты DAST, SAST, SCA, которые могут подключаться ко всем банковским проектам, независимо от того используются остальные компоненты или нет.
Большую часть новых проектов Совкомбанк Технологий переводим на компоненты и актуализируем действующие проекты на их основе. Если сравнивать скорость настройки пайплайна на основе компонентов со старым, заметны улучшения. При знакомстве с новым шаблоном, командам нужно время, чтобы разобраться в механиках, зато потом скорость написания пайплайнов только растет. Улучшился и процесс обновления скриптов. Теперь достаточно просто выпустить новый релиз и сообщить командам о том, в каких компонентах необходимо поменять версию. Latest мы используем только в компонентах sast, sca и dast, так как они не влияют на процесс CI/CD и их поломка не тормозит выкатку релиза.
Очень полезной оказалась и возможность переопределять шаблоны, скрипты и задания. Это позволяет гибко перенастроить определенный элемент пайплайна в случае чего.
Плюсы и минусы данного решения
Плюсы
– Основные настройки пайплайнов теперь хранятся в одном месте, что позволяет один раз внести необходимые изменения, а разработчикам только поменять версию определенного компонента в сбственных проектах.
– Настройка пайплайна в собственных компонентах гибкая: любое задание можно встроить в любое место пайплайна и настроить так, что все будет работать корректно и без поломок.
– Весь цикл от CI до CD можно реализовать только с помощью компонентов.
Минусы
В течение всего процесса написания мне, конечно же, пришлось столкнуться со многими проблемами.
– Из самых серьезных хочу выделить отсутствие возможности использовать проверки IF/ELSE, как это реализовано в helm-templates. Понятное дело, что helm и GitLab совершенно разные инструменты, но функционала, аналогичного helm’у в плане рендера готовой конфигурации очень не хватало. Но и к этому можно адаптироваться.
– Еще одним минусом считаю ограничения по числу компонентов на проект. Сейчас лимит подняли до 30, но хотелось бы, чтобы его вообще не было.
– Наконец, не самая удобная настройка: начиная от инициализации самого проекта компонентов, где без документации первое время ничего не будет понятно, до процесса внедрения компонентов в проекты, особенно для новичков.
Вот таким нехитрым образом мы решили проблему комплексного обновления процессов CI/CD в банковских проектах. Есть идеи, как можно развить финтех? Добро пожаловать к нам в команду.
Буду рад ответить на ваши вопросы в комментариях!