ADG Оставить заявку
Блог DevOps 5 мин чтения

Переводим проекты с 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-экосистеме плагинов - там их заметно больше.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.