Вступление
Хабр, привет! На связи Владимир, 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:portinput:80cannot 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 в банковских проектах. Есть идеи, как можно развить финтех? Добро пожаловать к нам в команду.
Буду рад ответить на ваши вопросы в комментариях!
ссылка на оригинал статьи https://habr.com/ru/articles/917466/
Добавить комментарий