Як динамічно генерувати імена job в GitLab. Робимо темплейт без хардкоду

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

Вам не набридло користуватись античнокопіпастською технікою 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 займають набагато більше місця, з описом всіх змінних оточення, рулів, та самих скриптів. Я навіть бачив пайплайни довжиною в 600-800 строк коду (5-6 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
👍ПодобаєтьсяСподобалось5
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

слухайте ну про шаблони норм, підтримую, а от про джоби на кожний енв.... ну таке це не можна використовувати на кожному проекті, бо вони всі різні, от для прикладу, в мене всі енви динамічні і джоба для деплою одна:

.release_rules:
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
      when: never
    - if: $CI_COMMIT_BRANCH == 'master' || $CI_COMMIT_BRANCH == 'dev' || $CI_COMMIT_BRANCH == 'stage'

.deploy-ecs-service: &deploy-ecs-service
  variables:
    GIT_SUBMODULE_STRATEGY: recursive
    GIT_SUBMODULE_UPDATE_FLAGS: --remote --jobs 4
    GIT_SUBMODULE_FORCE_HTTPS: "true"
    GIT_DEPTH: "3"
  environment:
    name: $CI_COMMIT_BRANCH
  image: public.ecr.aws/tra-la-la
  interruptible: true
  tags:
    - $CI_ENVIRONMENT_NAME

deploy:
  <<: *deploy-ecs-service
  stage: deploy
  script:
    - |
      echo "ECS service deploy starting..."
  rules:
    - !reference [.release_rules, rules]
  needs:
    - job: build-php
    - job: run-tests

Мій підхід суто для того, щоб по назві джоби відразу розуміти куди пішов деплой. Щоб не було необхідності клікати на джобу, і перевіряти назву енву в логах. Він лише для візуалу, тільки і всього.
Якщо такої необхідності немає — ваш варіант буде навіть кращим, адже прибирає зайву складність

то ми ж деплоїмо на якийсь енв з гілки? а там все є, назва гілки, не можу скрін вставити

А якщо ми деплоїмо з гілки «feature-shototam» на «dev2» то що ми зрозуміємо з назви гілки?

ну в мому варіанті не передбачено деплой фіче гілки на дев енв, це треба переробляти пайпу, але все одно це можливо і можна це зробити менш складно. і насправді подивитися яка гілка на якому енві є наразі можна подивитися у енвайрментс, а також нащо нам нотифікації? чи ми їх не використовуємо?

Та не хочу я нікуди заходити, дивитись в енвайронментс, розбиратись в нотифікаціях (це ж як треба заморочитись щоб через нотифікації розібратись яка пайплайна куди деплоїлась тиждень назад). Я хочу відкрити ран і відразу бачити з якої гілки і куди пішов деплой. Що, як в мене 100-200 деплоїв за день? Я не хочу в цьому всьому копатись!
Навіщо нам переробляти пайпу задля деплою з нових гілок (hot-fix, наприклад)? А якщо у вас один СПІЛЬНИЙ «test» енв, куди девелопери деплоять і тестують свої фічі (з власних feature гілок звісно ж)? Вказати гілку і енв — єдине, що треба робити з точки зору best-practices. А все інше, всі конфіги, всі депенденсі.... хай пайплайна підхоплює сама, і робить своє діло. Єдине, що треба, це встановити необхідні рестрікшени для деплою на прод та інших важливих речей.
А з приводу «можна це зробити менш складно» — що може бути простіше, ніж додати «extend_vars $VAR_NAME» в назву job ?

Підписатись на коментарі