Як динамічно генерувати імена job в GitLab. Робимо темплейт без хардкоду
Вам не набридло користуватись античнокопіпастською технікою CtrlC + CtrlV кожного разу, коли ви додаєте ще один енв в GitLab-пайплайнах?
"deploy_dev": stage: build rules: - if: ‘$CI_COMMIT_BRANCH == $DEV_BRANCH && $CI_PIPELINE_SOURCE == “web”’ when: always - when: never "deploy_prod": stage: build rules: - if: ‘$CI_COMMIT_BRANCH == $PROD_BRANCH && $CI_PIPELINE_SOURCE == “web”’ when: manual - when: never
Це є прикладом «псевдокоду» (лише для демонстрації принципу). Насправді реальні job займають набагато більше місця, з описом всіх змінних оточення, рулів, та самих скриптів. Я навіть бачив пайплайни довжиною в
А як же патерн «Don’t repeat yourself» !?
На щастя, починаючи з Gitlab 17.0 (ба, навіть з 15.11 в бета-версії) ми маємо змогу створювати «shared» темплейти для наших job за допомогою «spec:inputs», щоб потім використовувати їх для динамічної генерації пайплайн. А якщо додати використання CI/CD components на пару з CI/CD Catalog, то можна ще й шерити ці темплейти з усім світом (чи використовувати вже готові).
Gitlab наполегливо рекомендує переходити на Components, називаючи їх «the next generation of the traditional CI/CD templates». А я підкину вам на додачу ще одну мотивацію відредагувати ваші пайплани.
Чи все так райдужно з тими «spec:inputs»
А що там з генерацією безпосередньо самого імені job? Ми можемо їх генерувати динамічно? Хм....
В документації є приклад:
Але, як бачте, для цієї «генерації» необхідно спочатку передати вхідне значення «inputs» десь в основному «.gitlab-ci.yml» (чи вказати дефолтне):
include: - component: “$CI_SERVER_FQDN/project_name/group/repository/file_name@<template_version>” inputs: job-prefix: dev
Стоп! А як мені тепер задеплоїтись на інший енв? Запускати пайплайн з іншої гілки, де захардкоджено «job-prefix: prod» (адже «inputs» — винятково статичні значення)? Як робити merge request в «prod» гілку з «dev» гілки? Кожного разу міняти значення «inputs», і потім назад? Чи відбренчовуватись від «dev», щоб в новій гілці мануально переставляти значення «job-prefix», і вже після того створювати merge request? Це точно DevOps-підхід з автоматизацією рутинної роботи? Тобто з однієї гілки я можу деплоїти лише на один енв? Не дуже то й «динамічно», чи не так?
З розв’язанням цієї проблеми нам допоможе Gitlab «extends».
З «extends» ми можемо створити «універсальний» темплейт, в якому використовуватимемо «spec:inputs» для підставки значень в нього. Наприклад, такий:
spec: inputs: stage: default: deploy image: default: docker:latest --- .deploy: stage: $[[ inputs.stage ]] image: $[[ inputs.image ]] variables: RELEASE_NAME: $APP_NAME DOCKER_HOST: "tcp://docker:2375" script: - echo "This script will deploy our app to $RELEASE_NAME"
Залишається лише передати значення для пропертів темплейту (або мати дефолтні), посилаючись на його назву:
deploy_dev: extends: .deploy # This is the name of our "template" rules: - if: '$CI_COMMIT_BRANCH == $DEV_BRANCH && $CI_PIPELINE_SOURCE == "web"' when: always - when: never deploy_prod: extends: .deploy # This is the name of our "template" rules: - if: '$CI_COMMIT_BRANCH == $PROD_BRANCH && $CI_PIPELINE_SOURCE == "web"' when: manual - when: never
Не помітили нічого дивного? Все правильно! Ми знову користуємось CtrlC + CtrlV, постійно об’являючи майже однакові job, але з різними іменами для різних енвів. І чим це відрізняється від початкового копіпасту? Ну... Ми значно скоротили кількість рядків, які копіпастимо. Але ж ми не хочемо нічого копіпастити! Ми DevOps ! Ми хочемо генерувати все динамічно!
Настав час показати, як би я це зробив
До речі, мій улюблений ChatGPT4.0 сказав, що динамічно генерувати не вийде:
Тож, давайте по черзі...
- Визначаємо стартовий «.gitlab-ci.yml», де вказуємо бажані постфікси для job:
variables: JOB_POSTFIX: description: "job postfix for dynamic generation" value: "dev" options: - "dev" - "feature" - "stage" - "prod" include: - component: "$CI_SERVER_FQDN/project_name/group/templates_repo_name/file_name@<version>" inputs: job_postfix: $JOB_POSTFIX
- Робимо темплейт «deploy.yml» всередині «templates_repo_name/templates/». Код:
spec: inputs: stage: default: deploy image: default: docker:latest --- .deploy: stage: $[[ inputs.stage ]] image: $[[ inputs.image ]] variables: RELEASE_NAME: $JOB_POSTFIX DOCKER_HOST: "tcp://docker:2375" script: - echo "This script will deploy our app to $RELEASE_NAME"
- Додаємо «expand_vars» фічу в «/templates/general_template.yml», щоб передати значення «inputs.job_postfix» для генерації імені job. (Це той «file_name», на який ми посилались в «include» стартового «.gitlab-ci.yml»). Код:
spec: inputs: job_postfix: --- include: - component: $CI_SERVER_FQDN/project_name/group/templates_repo_name/deploy.yml@<version> "deploy_$[[ inputs.job_postfix | expand_vars ]]": extends: .deploy rules: - if: '$CI_COMMIT_BRANCH == $PROD_BRANCH' when: manual - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $DEV_BRANCH' when: always - if: '$CI_PIPELINE_SOURCE == "web" || $CI_PIPELINE_SOURCE == "api"' when: always - when: never
- В Gitlab-репозиторії заходимо в Build → Pipelines → New pipeline → обираємо git branch та бажане значення для $JOB_POSTFIX змінної → Run pipeline
І бачимо ось таку картину без копіпасту зайвого коду та хардкоду імен:
І все було б чудово, якби не одне «але»
Якщо тригером пайплайни є merge request, то замість назви енву в кінці імені job ми побачимо ось це:
Чому це відбувається? Як сказано у документації GitLab:
By default, pipelines from forked projects can’t access the CI/CD variables available to the parent project.
Це означає, що при merge request наш пайплайн не має доступу до значень CI/CD змінних, що вказані в parent гілці, в яку вливатиметься MR. Навіщо так зроблено? Як казав якийсь класик:
Сек’юріті має бути сек’юрним!
Уявімо, що в нас є «sensitive» CI/CD змінна, що тримає пароль від бази даних. Одночасно з цим ми заборонили прямий «git push» в «prod» бренчу без мердж реквесту. Також ми маємо тести, що налаштовані запускатись автоматично після створення цього мердж реквесту (ми хочемо знати, що код перевірений, перед тим, як заливати його). Начебто ми захищені з усіх сторін...
Тепер припустимо, що хакер «проник» всередину нашої системи. У нього немає доступу до значень «sensitive» CI/CD змінних, а йому прям дуже треба. Тому він створює свою «feature» бренчу, описує в ній скрипт для виведення змінної з паролем та отримує його значення, щойно створить merge request (автоматично запускаються тести для MR, в яких лежить цей скрипт).
Саме тому merge request pipeline має доступ до значень CI/CD змінних в «parent» бренчі (в яку робиться merge request) лише коли цей merge request запускається з тієї ж «parent» бренчі. Ось документація, як це реалізувати: Run pipelines in the parent project.
Такий варіант буде корисним, наприклад, щоб в «бойовому» режимі перевірити, як pipeline відпрацьовує перед остаточним заливанням змін в «prod».
З цим розібрались. Але що робити з «$JOB_POSTFIX» в імені job для тестів при MR? Та нічого не робити! Не будемо ж ми через відсутність елегантності повертатись назад до копіпасту або відмовлятись від автоматичного запуску тестів при merge request.
We want systems that are automatic, not just automated. — SRE Book by Google
6 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів