Кастомизация GitLab: опыт Совкомбанк Технологий в написании компонентов для типовых банковских проектов

от автора

Вступление

Хабр, привет! На связи Владимир, 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 в банковских проектах. Есть идеи, как можно развить финтех? Добро пожаловать к нам в команду.

Буду рад ответить на ваши вопросы в комментариях!


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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *