С чего все началось
За более 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
В такой реализации обычный пайплайн трансформировался в мультипайплайн: родительский пайплайн инициировал запуск пайплайна-ребенка
Такие образом появилось 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" фичой
Комментарии (9)
niyaho8778
19.01.2022 21:34а есть какая хорошая статья на русском как через это дело gitlab cicd работать с docker compose ?
AlexMiller001
20.01.2022 18:38А все не оч сложно. Собираете что вам нужно, а потом пушите что хотите в registry. Потом оттуда качаете по токену.
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е тесты, результат в целом устроил:)
vasyakolobok77
20.01.2022 18:37От прочитанного у меня возникает ощущение, что вы городите костыли:
– На уровне gitlab-ci не должно быть проверок переменных, сохраненных в коде. Если вам хочется управлять переменными в коде (что странно, но ладно), тогда пусть сама утилита запуска тестов проверяет этот флаг.
– Если же вы просто хотите формировать разный пайплайн, то вопрос: от чего он зависит? Почему в одном случае мы запускаем тесты с кафкой, а в другом без нее? Может быть дело в стейдж-ветках: дев / тест / релиз? Если так, то посто определите набор джоб и для каждой ветки определи какие джобы выполнять.
– Для доступа к api из скриптов нет необходимости использвоать свой токен, для джоб есть: $CI_JOB_TOKEN
– И вообще вопрос: а нужен ли тут вам родительский/дочерний пайплайн? Может быть вы просто хотите запустить джобы параллельно?
drno-reg Автор
21.01.2022 12:42Благодарю за агрументированный разбор.
Теоретически если оба сценария завернуть в один то можно обойтись без мультипайплайна, но одним из условий реализации было развести сценарии тестирования по разным yaml файлам
Целью было покрыть общим cicd более 20 spark проектов, к которым применяется либо один либо другой сценарий и список сценариев тестирования может расширяться
Здесь согласен с вами, собираюсь переключиться на $CI_JOB_TOKEN
Параллельно запускать в данной реализации нельзя потому как job в родитеском пайплайне ждет результата тестирования в пайплайне ребенке.
gecube
очень странная статья.
Первое. Очень странно сравнивать дженкинс и GitlabCI. У первого действительно очень богатые возможности по кастомизации и рисованию UI, которые гитлаб даже не снились. У меня была задача предоставить разработчикам минимальную UI с элементов выбора (например, комбобокс) или валидацией параметров. На гитлабе в текущем виде это сделать попросту невозможно. Для этого надо пилить внешний интерфейс - будь то flask какой, или притаскивать полноценные оркестраторы задач вроде rundeck/polemarch. Увы. Но при этом под капотом легко может быть гитлаб, потому что через АПИ он практически безгранично расширяется
Второе. Очень странно упоминать мультипайплайн. Позвольте поинтересоваться, у вас там платная редакция гитлаба? Или все-таки бесплатная? Потому что если второе - это постоянный бег с препятствиями. Например, триггернуть пайплайн можно, но в виде единого интерфейса увидеть мультипайплайн нельзя. Может быть сейчас ситуация изменилась, т.к. постоянно какие-то фичи из платной версии переезжают в CE, но буквально недавно это было так. Еще отдельная мякотка - это синхронизация разных пайплайнов. Буквально недавно опять же надо было в цикле ждать выполнение дочернего пайплайна, чтобы вытащить из него артефакты. Тут целый простор для получения race condition...
drno-reg Автор
Целью не ставилось сравнивать Jenkins vs Gitlab CI. Была рассмотрена реализация конкретного кейса, которая кстати оказалась не так проста как первоначально ожидалось.
gecube
тем не менее, Вы вольно или невольно сравнили эти оба продукта, я только расширил и указал на определенные недостатки гитлаба, что возможно позволит кому-то в дальнейшем принять правильное решение, что внедрять в свой ландшафт
В остальном решение показалось переусложненным. Зачем надо было разбивать на столько много шагов? Достаточно было в самом шаге теста проверить файл с фикстурами и принять решение для запуска кафки. Но тут непонятно что все эти YML делают - может Вы в какую-то внешнюю систему ходите и там тестовый стенд поднимаете, мало деталей, примеров Вы не выложили, а только конкретные сниппеты
Еще важный вопрос не поднят - вопрос ролевой модели Гитлаба. Насколько я помню, разработчик должен иметь доступ к репозиторию, в который делается include, иначе пайплайн не сможет нормально стартануть и упадет (но это надо тоже проверить еще раз). Что я могу сказать - хорошо быть админом и не иметь этих проблем...