Pull to refresh

Gitlab CI «Smart» Pipeline: родители и дети

Reading time5 min
Views8.3K
Раньше даже не мог себе представить, что между Русской классикой и современными CICD инструментами есть связь)
Раньше даже не мог себе представить, что между Русской классикой и современными CICD инструментами есть связь)

С чего все началось

За более 3х летний срок существования продукта у нас собралось более чем 20 репозиториев со spark проектами. Процесс CICD был реализован на Jenkins. С определенного момента у GitLab CI появилась возможность создавать собственные CICD. Но долгое время я совершенно не воспринимал всерьез этот инструмент. Так как мне нравилось, что в Jenkins можно взять и дописать то чего тебе не хватает на Groovy. Настройка WebUI предоставляет широкие возможности для организации параметризованных сборок. Поначалу функционал GitlabCI я воспринимал это как жалкое подобие Jenkins: чтобы реализовать ну что-то очень очевидное и простое, я уже молчу про параметризованную сборку.

Но прошло время и мне показали как возможно шарить между проектами джобы, чтобы реализация под конкретный проект выглядела с наименьшим количеством кода.

Для примера у вас где-то в отдельном репозитории лежат yml, которые выполняют что-то вполне определенное, которое у вас может повторяться не только в одном проекте.

    include:
    - project: 'gitlabci/cicd'
    ref: v1
    file:
      - 'pipelines/product1/.base_pipelines_spark_project.yml'

Выполнять include какого-то джоба у себя в конвейере и прям стало одной из киллер фич. И в какой-то момент перевод пайплайн на GitLabCI уже не выглядело как необходимость, а собственным желанием реализовать интересную задачу.

Что было

  • более 20 репозиториев в gitlab spark проектов;

  • часть из них работают со spark часть из них spark + kafka;

  • CICD реализован на Jenkins

Что хотелось сделать

Выполнить трансформацию CICD с Jenkins на Gitlab CI c наименьшим количеством шагов: чтобы команда разработки, если хотела бы вникнуть то могла это сделать, а если нет то было бы что-нибудь типа создать такой-то файл и скопировать туда такой-то yaml код и чтобы гарантированно заработало причем без активной помощи со стороны devops разработчика.

Начало

В каждом из Spark проектов реализовано было тестирование по одному из 2-х сценариев: с кафка или без. Описать сценарий в одном job было не возможно и поэтому были созданы 2 yaml, которые подключались следующим образом

    include:
      # PRODUCT1
      - project: 'gitlabci/integration-test'
      ref: v2
      file:
        - 'product1/etl/.base_integration_test.yml'
        - 'product1/etl/.base_integration_test_with_kafka.yml'

Для того, чтобы .gitlab-ci.yml выглядел для каждого проекта одинаковым необходимо было придумать логику таким образом, чтобы пайплайн на основании семантического анализа кода в test/fixtures.py мог определить какой сценарий необходим. Решить эту задачу оказалось достаточно тривиальной задачей, первая проблема была дальше. Предполагалось создать job, который в процессе анализа определял переменную CICD_KAFKA_HOST либо в true либо в false

    prepare_test:
      script:
        - export CICD_KAFKA_HOST=$(cat test/fixtures.py | grep KAFKA_HOST)
        - >
          if [ "$CICD_KAFKA_HOST" != "" ]; then
            export CICD_KAFKA_HOST="true"
          else
            export CICD_KAFKA_HOST="false"
          fi
        - echo "CICD_KAFKA_HOST=$CICD_KAFKA_HOST" >> dotenv.env
      artifacts:
        reports:
          dotenv:
            - dotenv.env

и в последующих job нужно запускать тесты либо c кафка либо без. Но по ходу реализации выяснилось, что использовать rules нельзя, потому variables для rules определяются при старте пайплайна и не могут быть переопределены/изменены в процессе работы конвейера и расширения extends должны быть определены в пайплайне однозначным образом.

    integration_test:
      extends: .base_integration_test_with_kafka
      rules:
        - if: '$CICD_KAFKA_HOST == "true"'

Реализация идеи "smart" пайплайна первый раз подверглась сомнению. НО по на помощь должны были прийти триггеры.

Триггер

недавно вышел новый сезон телесериала "Триггер" от продюсерской компании Среда, решение в такой реализации как выход из зоны комфорта, именно этот метод практикует герой Максима Матвеева
недавно вышел новый сезон телесериала "Триггер" от продюсерской компании Среда, решение в такой реализации как выход из зоны комфорта, именно этот метод практикует герой Максима Матвеева

Триггер предоставляет возможность запустить в текущем пайплайне другой пайплайн. Текущий пайплайн становится родительским, а запускаемый другой пайплан ребенком.

Реализация получилась такой

    # --------------- Prepare Test --------------- 
    prepare_test:
      image: platform/docker-images/vault:1.8
      variables:
        CONTEXT_TEST: |
          include:
          # PRODUCT
          - project: 'gitlabci/integration-test'
            ref: v2
            file:
              - 'product1/etl/.base_integration_test.yml'
              - 'product1/etl/.base_integration_test_with_kafka.yml'
          integration_test:
            variables:
              COVERAGE_SOURCE: "./src"
        INTEGRATION_TEST: |
          $CONTEXT_TEST
            extends: .base_integration_test
        INTEGRATION_TEST_WITH_KAFKA: |
          $CONTEXT_TEST
            extends: .base_integration_test_with_kafka

    stage: prepare_test
      script:
        - export CICD_KAFKA_HOST=$(cat test/fixtures.py | grep KAFKA_HOST)
        - >
          if [ "$CICD_KAFKA_HOST" != "" ]; then
            export CICD_KAFKA_HOST="true"
            echo "$INTEGRATION_TEST_WITH_KAFKA" >> test.yml
          else
            export CICD_KAFKA_HOST="false"
            echo "$INTEGRATION_TEST" >> test.yml
          fi
        - env | sort -f
      artifacts:
        paths:
          - test.yml
        expire_in: 7200 seconds

    # --------------- Integration test --------------- 
    integration_test:
      stage: test
      trigger:
        include:
          - artifact: test.yml
            job: prepare_test
        strategy: depend

В такой реализации обычный пайплайн трансформировался в мультипайплайн: родительский пайплайн инициировал запуск пайплайна-ребенка

Gitlab CI: Pipeline родитель, у которого есть pipeline ребенок
Gitlab CI: Pipeline родитель, у которого есть pipeline ребенок

Такие образом появилось smart начало: он умеет определять какой сценарий выбрать и в job с интеграционным тестированием переиспользует именно тот сценарий который необходим: либо с кафка либо без. Начало положено, НО возникла проблема №2: результатом выполнения pipeline ребенка - формирование coverage отчета, который не мультипайплайнах мы далее передаем в job c SonarQube. Решить задачу по передаче между job artifact в виде файлов как раньше было нельзя, вернуть artifact из child в parent оказалось невозможно.

Очевидное решение - добавить upload artifact в наш aftifactory и в job c SonarQube просто его скачать. Но хотелось найти более изящный способ, чтобы исключить дополнительные обращения к Artifactory. И способ был найден: Gitlab CI API

Gitlab CI API: download child artifacts

Чтобы иметь возможность подключаться к Gitlab CI API необходимо для пользователя, который имеет права на репозиторий сгенерировать token. Для того чтобы воспользоваться API скачать artifact из pipeline ребенка необходимо выяснить его CI_JOB_ID.

GET /projects/:id/jobs/:job_id/artifacts

Как это сделать из pipeline родителя?

- определяем ID pipeline ребенка

GET /projects/:id/pipelines/:pipeline_id/bridges

- по id pipeline ребенка определяем id job

GET /projects/:id/pipelines/:pipeline_id/jobs

- после этого уже выполняем скачивание методом /projects/:id/jobs/:job_id/artifacts

Итоговая реализация job по скачиванию artifacts будет выглядеть так: в список переменных группы проектов куда входит и наш репозиторий положили значение token - GITLAB_USER_TOKEN и для разбора json ответа от Gitlab API использовали jq

    get_cicd_artifact:
      image: platform/docker-images/ansible:2.9.24-9
      stage: get_cicd_artifact
      script:
        - >
          export CI_CHILD_PIPELINE_ID=$(curl --header "PRIVATE-TOKEN: $GITLAB_USER_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/pipelines/$CI_PIPELINE_ID/bridges" | jq ".[].downstream_pipeline.id")
        - echo $CI_CHILD_PIPELINE_ID
        - >
          export CI_CHILD_JOB_ID=$(curl --header "PRIVATE-TOKEN: $GITLAB_USER_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/pipelines/$CI_CHILD_PIPELINE_ID/jobs" | jq '.[].id')
        - echo $CI_CHILD_JOB_ID
        - 'curl --output artifacts.zip --header "PRIVATE-TOKEN: $GITLAB_USER_TOKEN" "$CI_API_V4_URL/projects/$CI_PROJECT_ID/jobs/$CI_CHILD_JOB_ID/artifacts"'
        - unzip artifacts.zip
        - ls -las coverage-reports
        - rm -rf artifacts.tar
      dependencies:
        - integration_test
      artifacts:
        paths:
          - coverage-reports/

Таким образом удалось реализовать Multi-project пайплайн имхо со "smart" фичой

Gitlab CI Multi-project pipelines
Gitlab CI Multi-project pipelines
Only registered users can participate in poll. Log in, please.
Какие CI/CD инструменты Вы используете?
33.7% Jenkins31
83.7% Gitlab CI77
15.22% TeamCity14
0% Bamboo CI0
92 users voted. 6 users abstained.
Tags:
Hubs:
Total votes 12: ↑11 and ↓1+10
Comments9

Articles