GitLab CI вместо Jenkins: .gitlab-ci.yml, Vault и kubectl в одном пайплайне
Перенесли сборку и деплой микросервисов с Jenkins на GitLab CI. Первые два месяца: что сработало, что скрипело, где были грабли с Vault и kubectl.
GitLab 9.0 (март 2017) - встроенный CI/CD pipeline, Review Apps, Auto DevOps preview
Jenkins у нас работал давно. Настроен аккуратно, с плагинами, с Groovy-пайплайнами в Jenkinsfile. И всё равно каждый раз когда надо было добавить новый сервис, кто-то тратил час на копипаст конфига и отладку очередного «почему не запускается агент». Когда в одном из проектов клиент переехал на GitLab, мы решили не тащить Jenkins следом, а попробовать встроенный CI.
Это был январь. С тех пор прошло около двух месяцев эксплуатации - достаточно чтобы составить мнение, но не достаточно чтобы считать что всё выяснено.
Почему вообще переезжать
Jenkins сам по себе не плохой. Проблема в том, что он живёт отдельно от кода. Конфиг пайплайна либо в веб-интерфейсе (значит не в Git, значит «кто менял - непонятно»), либо в Jenkinsfile (значит в репозитории, но и сам Jenkins всё равно требует настройки снаружи - агенты, credentials, плагины). Когда микросервисов становится больше пяти-семи, это начинает давить.
.gitlab-ci.yml лежит в репозитории вместе с кодом. Это единственное место где описан пайплайн. Ревью пайплайна - обычный MR. История изменений - обычный git log. Звучит просто, но на практике это меняет отношение к пайплайну: он перестаёт быть «ещё одной системой» и становится частью кода сервиса.
Структура .gitlab-ci.yml
На проекте четыре микросервиса - Java-бекенд, Python-worker, фронтенд на React и отдельный nginx-proxy. У каждого свой репозиторий, у каждого свой .gitlab-ci.yml. Базовая структура одинаковая:
stages:
- build
- test
- publish
- deploy
variables:
REGISTRY: registry.example.com
IMAGE_NAME: $REGISTRY/$CI_PROJECT_NAME
build:
stage: build
image: docker:1.13
services:
- docker:dind
script:
- docker build -t $IMAGE_NAME:$CI_COMMIT_SHA .
- docker push $IMAGE_NAME:$CI_COMMIT_SHA
deploy_staging:
stage: deploy
image: kubectl-runner:1.5
environment: staging
only:
- develop
script:
- kubectl set image deployment/$CI_PROJECT_NAME app=$IMAGE_NAME:$CI_COMMIT_SHA -n staging
Gitlab-runner запускает стадии в Docker-контейнерах. docker:dind - Docker-in-Docker для сборки образов. kubectl-runner:1.5 - самосборный образ с kubectl нужной версии и kubeconfig для нашего кластера.
Vault вместо GitLab variables
Первый инстинкт - хранить секреты в GitLab CI/CD Variables (Settings -> CI/CD -> Secret Variables). Это работает, но на десяти-пятнадцати сервисах превращается в хаос: нет единого места где видно все секреты, ротация ключа - ручная работа в каждом репозитории.
Решение - Vault от HashiCorp. Runner перед деплоем логинится в Vault через AppRole, получает нужные секреты и пишет их в переменные окружения или в файлы конфигурации. GitLab Variables используем только для двух вещей: VAULT_ADDR (адрес Vault) и VAULT_ROLE_ID/VAULT_SECRET_ID (credentials для AppRole, которые сами по себе не секретны без наличия Vault).
# В секции before_script
- export VAULT_TOKEN=$(vault write -field=token auth/approle/login \
role_id=$VAULT_ROLE_ID secret_id=$VAULT_SECRET_ID)
- export DB_PASSWORD=$(vault read -field=password secret/prod/db)
Это усложняет пайплайн, но зато секреты ротируются в одном месте. И видно через audit log Vault-а кто и когда что читал.
Деплой в Kubernetes: что скрипит
kubectl set image - самый простой способ обновить образ в деплойменте. Работает. Но у него есть особенность: если деплоймент не существует, команда упадёт с ошибкой. Первый деплой нового сервиса нужно делать отдельно - либо через kubectl apply манифеста, либо руками. Мы остановились на том что манифесты Kubernetes лежат рядом с кодом в папке k8s/, и первый деплой выполняется через kubectl apply -f k8s/ вручную, последующие - через set image в пайплайне.
Rollback - отдельная история. GitLab позволяет запустить деплой предыдущего пайплайна через UI (кнопка Retry на нужном джобе), что в нашем случае прогоняет kubectl set image со старым SHA. Это работает, но занимает время - пайплайн прогоняется заново от начала. kubectl rollout undo быстрее, но его надо запускать вручную. Идеального решения тут нет, выбрали «быстрый undo руками» для критичных инцидентов.
Kubeconfig в runner-е - неочевидная точка риска. Если kubeconfig зашит в образ runner-а, при ротации credentials кластера надо пересобирать образ. Сейчас делаем иначе: kubeconfig передаётся через GitLab Variable как base64-строка, runner декодирует его в ~/.kube/config в before_script. Громоздко, но ротируется без пересборки образа.
Что не решили
Параллельный деплой нескольких сервисов с зависимостями друг от друга - пока руками в нужном порядке. GitLab CI позволяет запускать пайплайны в нескольких репозиториях через triggers API, но это ещё не настроено.
Review Apps - GitLab умеет поднимать временное окружение для каждого MR. Выглядит полезно, особенно для фронтенда. Поставили себе задачу попробовать на следующем спринте.
Итог двух месяцев
Jenkins больше не нужен на этом проекте. Пайплайн лежит в Git, виден в MR, меняется через ревью. Vault за секреты - хорошее решение, хотя настройка AppRole заняла день. Деплой через kubectl работает, с ограничениями о которых надо знать заранее.
Сопровождение инфраструктуры включает и поддержку таких пайплайнов - когда пять сервисов превращаются в пятнадцать, хочется чтобы кто-то знал где что лежит.
GitLab 9.0 выходит в марте, там обещают Review Apps в стабильном виде и улучшенный деплой-дашборд. Посмотрим насколько это меняет картину по сравнению с тем что сделали руками.