Раньше даже не мог себе представить, что между Русской классикой и современными 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

Комментарии (9)


  1. gecube
    19.01.2022 20:45

    очень странная статья.

    Первое. Очень странно сравнивать дженкинс и GitlabCI. У первого действительно очень богатые возможности по кастомизации и рисованию UI, которые гитлаб даже не снились. У меня была задача предоставить разработчикам минимальную UI с элементов выбора (например, комбобокс) или валидацией параметров. На гитлабе в текущем виде это сделать попросту невозможно. Для этого надо пилить внешний интерфейс - будь то flask какой, или притаскивать полноценные оркестраторы задач вроде rundeck/polemarch. Увы. Но при этом под капотом легко может быть гитлаб, потому что через АПИ он практически безгранично расширяется

    Второе. Очень странно упоминать мультипайплайн. Позвольте поинтересоваться, у вас там платная редакция гитлаба? Или все-таки бесплатная? Потому что если второе - это постоянный бег с препятствиями. Например, триггернуть пайплайн можно, но в виде единого интерфейса увидеть мультипайплайн нельзя. Может быть сейчас ситуация изменилась, т.к. постоянно какие-то фичи из платной версии переезжают в CE, но буквально недавно это было так. Еще отдельная мякотка - это синхронизация разных пайплайнов. Буквально недавно опять же надо было в цикле ждать выполнение дочернего пайплайна, чтобы вытащить из него артефакты. Тут целый простор для получения race condition...


    1. drno-reg Автор
      19.01.2022 21:07
      -2

      Целью не ставилось сравнивать Jenkins vs Gitlab CI. Была рассмотрена реализация конкретного кейса, которая кстати оказалась не так проста как первоначально ожидалось.


      1. gecube
        19.01.2022 21:54
        +2

        Целью не ставилось сравнивать Jenkins vs Gitlab CI.

        тем не менее, Вы вольно или невольно сравнили эти оба продукта, я только расширил и указал на определенные недостатки гитлаба, что возможно позволит кому-то в дальнейшем принять правильное решение, что внедрять в свой ландшафт

        В остальном решение показалось переусложненным. Зачем надо было разбивать на столько много шагов? Достаточно было в самом шаге теста проверить файл с фикстурами и принять решение для запуска кафки. Но тут непонятно что все эти YML делают - может Вы в какую-то внешнюю систему ходите и там тестовый стенд поднимаете, мало деталей, примеров Вы не выложили, а только конкретные сниппеты

        Еще важный вопрос не поднят - вопрос ролевой модели Гитлаба. Насколько я помню, разработчик должен иметь доступ к репозиторию, в который делается include, иначе пайплайн не сможет нормально стартануть и упадет (но это надо тоже проверить еще раз). Что я могу сказать - хорошо быть админом и не иметь этих проблем...


  1. niyaho8778
    19.01.2022 21:34

    а есть какая хорошая статья на русском как через это дело gitlab cicd работать с docker compose ?


    1. gecube
      19.01.2022 21:53

      зачем? Задача какая? Вообще мне лично хватило официальной доки гитлаба.


    1. AlexMiller001
      20.01.2022 18:38

      А все не оч сложно. Собираете что вам нужно, а потом пушите что хотите в registry. Потом оттуда качаете по токену.


    1. BooooBka
      20.01.2022 18:38

      Пытался реализовать подобное.

      job:
        stage: some-stage
        image: docker/compose
        services:
          - docker:19.03.12-dind
        variables:
          COMPOSE_PROJECT_NAME: docker-compose-demonstrator-$CI_JOB_ID
          DOCKER_DRIVER: overlay2
          DOCKER_HOST: tcp://docker:2375
        before_script:
          - docker-compose -f ....
          - CONTAINER_IP=$(route -n | awk '/UG[ \t]/{print $2}')
          - echo ${COMPOSE_PROJECT_NAME}_default
        	....
          
        script:
          - docker ps
          - docker network list
          - echo $CONTAINER_IP
          ....
        after_script:
          - docker-compose logs || true
          - docker network disconnect ${COMPOSE_PROJECT_NAME}_default $CONTAINER_ID || true
          - docker-compose down || true
          ....

      Я не занимаюсь девопсом, но хотелось попробовать поднять проект и запустить на нем е2е тесты, результат в целом устроил:)


  1. vasyakolobok77
    20.01.2022 18:37

    От прочитанного у меня возникает ощущение, что вы городите костыли:

    – На уровне gitlab-ci не должно быть проверок переменных, сохраненных в коде. Если вам хочется управлять переменными в коде (что странно, но ладно), тогда пусть сама утилита запуска тестов проверяет этот флаг.

    – Если же вы просто хотите формировать разный пайплайн, то вопрос: от чего он зависит? Почему в одном случае мы запускаем тесты с кафкой, а в другом без нее? Может быть дело в стейдж-ветках: дев / тест / релиз? Если так, то посто определите набор джоб и для каждой ветки определи какие джобы выполнять.

    – Для доступа к api из скриптов нет необходимости использвоать свой токен, для джоб есть: $CI_JOB_TOKEN

    – И вообще вопрос: а нужен ли тут вам родительский/дочерний пайплайн? Может быть вы просто хотите запустить джобы параллельно?


    1. drno-reg Автор
      21.01.2022 12:42

      Благодарю за агрументированный разбор.

      • Теоретически если оба сценария завернуть в один то можно обойтись без мультипайплайна, но одним из условий реализации было развести сценарии тестирования по разным yaml файлам

      • Целью было покрыть общим cicd более 20 spark проектов, к которым применяется либо один либо другой сценарий и список сценариев тестирования может расширяться

      • Здесь согласен с вами, собираюсь переключиться на $CI_JOB_TOKEN

      • Параллельно запускать в данной реализации нельзя потому как job в родитеском пайплайне ждет результата тестирования в пайплайне ребенке.