Переводим проекты с Jenkins на GitLab CI: .gitlab-ci.yml рядом с кодом и docker executor
GitLab CI Runner с docker executor убрал необходимость в отдельном Jenkins-мастере. Переносим несколько проектов и разбираемся, что реально изменилось.
GitLab CI Runner в 2016 году стал полноценной CI/CD-альтернативой Jenkins с нативной интеграцией в GitLab и поддержкой Docker executor
Несколько недель назад мы писали про Jenkinsfile и Pipeline DSL - CI-конфиг наконец-то рядом с кодом, всё хорошо. И буквально в процессе переноса одного из проектов на Jenkinsfile возник вопрос: а зачем нам вообще Jenkins, если репозиторий уже на GitLab?
Вопрос не праздный. GitLab CI Runner - это не какой-то сторонний плагин, а часть GitLab, которая за последний год существенно повзрослела. Docker executor, параллельные джобы, кеш артефактов - выглядит как полноценный CI. Решили проверить на деле.
Как выглядит исходная ситуация
На managed-сопровождении у нас несколько проектов с похожей схемой: Jenkins-мастер на отдельной виртуалке, один-два агента, freestyle-джобы или уже Jenkinsfile. Репозитории при этом - в GitLab. То есть на каждый push GitLab дёргает webhook, Jenkins забирает изменения, что-то делает. Два инструмента, которые надо синхронизировать, обновлять и мониторить.
GitLab Runner - это отдельный процесс (бинарник на Go), который регистрируется в GitLab и забирает джобы. Его можно поставить на любую машину, в том числе на ту же, где уже что-то крутится. Конфигурация CI описывается в .gitlab-ci.yml в корне репозитория - аналог Jenkinsfile, только синтаксис YAML, а не Groovy.
Первый перенос: что пошло не так
Взяли небольшой Python-проект: сборка пакета, тесты, деплой на staging. На Jenkins было три freestyle-джоба.
.gitlab-ci.yml написался быстро:
stages:
- test
- build
- deploy
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
test:
stage: test
image: python:3.5
cache:
paths:
- .pip-cache/
script:
- pip install -r requirements.txt
- pytest tests/
build:
stage: build
image: python:3.5
script:
- python setup.py bdist_wheel
artifacts:
paths:
- dist/*.whl
expire_in: 1 week
deploy_staging:
stage: deploy
script:
- scp dist/*.whl deploy@staging:/opt/releases/
- ssh deploy@staging "cd /opt && pip install --upgrade releases/*.whl && systemctl restart myapp"
only:
- develop
Проблема первая: SSH-ключи для деплоя. В Jenkins они лежали в Jenkins Credentials Store и инжектировались через SSH Agent Plugin. В GitLab CI секреты хранятся в Settings - CI/CD Variables. Переложить ключ - дело пяти минут, но нужно помнить, что переменная с типом File сохраняется как временный файл, а не в переменную окружения. Несколько минут на понимание разницы.
Проблема вторая: docker executor запускает каждый джоб в чистом контейнере. Это хорошо для изоляции, но pip-зависимости надо кешировать явно через cache: - иначе каждый запуск тянет пакеты заново. Прописали кеш, стало терпимо.
Проблема третья - и главная: деплой через SSH из docker-контейнера. Контейнер не знает про known_hosts машины назначения, SSH при первом подключении ждёт подтверждения. В скрипте это выглядит как зависание. Решение - добавить хост в переменную SSH_KNOWN_HOSTS и прописать её в ~/.ssh/known_hosts в начале джоба. Не сложно, но надо знать.
Второй перенос: Java-проект с Gradle
Здесь уже было интереснее - мы только что написали Jenkinsfile для этого проекта, и сравнение получилось прямым.
stages:
- test
- deploy
variables:
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
test:
stage: test
image: gradle:3.2-jdk8
cache:
key: "$CI_PROJECT_ID"
paths:
- .gradle/
script:
- gradle test integrationTest
artifacts:
paths:
- build/test-results/
deploy_staging:
stage: deploy
script:
- ./deploy.sh staging
only:
- develop
environment:
name: staging
url: https://staging.example.com
Gradle-демон в Docker отключаем через GRADLE_OPTS - внутри контейнера демон только тратит память и не даёт никакого ускорения. Кеш gradle-зависимостей - через cache: по ключу проекта.
Что понравилось в сравнении с Jenkins: раздел environment: в GitLab создаёт в интерфейсе страницу Environments с историей деплоев и ссылкой на URL окружения. В Jenkins это надо было строить отдельно или смотреть в логах.
Что реально изменилось в инфраструктуре
Jenkins-мастер для этих двух проектов мы остановили. Runner зарегистрирован на той же виртуалке, где раньше был Jenkins-агент - с docker executor. Весь CI теперь живёт в .gitlab-ci.yml в репозиториях, без отдельного UI для управления джобами.
Несколько наблюдений после двух недель:
- Видимость статуса. Статус CI прямо в интерфейсе GitLab рядом с merge request и коммитами. Не нужно открывать отдельный Jenkins - разработчики видят зелёную или красную метку прямо там, где смотрят код.
- Конфигурация в репозитории.
.gitlab-ci.ymlв git-истории, ревьюируется вместе с кодом. То же что Jenkinsfile, но без Groovy - YAML читается проще для тех, кто не пишет на Java. - Docker-изоляция. Каждый джоб - чистый контейнер с нужным образом. Нет проблемы «на Jenkins-агенте установлена другая версия Python/Java». Образ указан в yaml, все всё знают.
- Нет Jenkins-мастера. Одна виртуалка вместо двух (или трёх). Меньше что обновлять, меньше что ломается.
Что не так гладко
Отладка: когда джоб падает, ты смотришь в лог в GitLab UI. Это нормально, но воспроизвести локально чуть сложнее - Runner умеет запускать джоб локально (gitlab-runner exec docker test), только переменные и кеш работают иначе - не один в один с CI. В Jenkins Replay (переиграть билд с правками скрипта прямо в UI) - удобнее для итерации.
Параллельные джобы в рамках одного stage - поддерживаются, но кеш между параллельными джобами не шарится. Нужно аккуратно думать про структуру, если хочется одновременно гонять разные тест-сьюты.
Управление несколькими проектами и Runners - пока разбираемся. Можно регистрировать Runner как specific (только для одного проекта) или как shared (для группы/инстанса). Для наших задач сейчас хватает specific Runner на каждый проект, но когда проектов станет больше - придётся выстраивать.
Где сейчас
Два проекта живут на GitLab CI. Jenkins на этих проектах остановлен. Ещё три проекта - на Jenkins, там откладываем перевод на потом: один с нетривиальными плагинами, два - на Windows-агентах, где docker executor не тот же опыт.
Вывод пока предварительный: для проектов в GitLab с типовыми сборками (test - build - deploy) GitLab CI Runner убирает лишний уровень инфраструктуры и держит всё в одном месте. Платить за это приходится привычкой к Jenkins-экосистеме плагинов - там их заметно больше.