Штош, пришло время подумать о том, как мы будем деплоить наши приложения.
Сейчас у нас используются Github-репозитории с кодом и Helm-шаблонами, и Jenkins.
Билд в Jenkins в большинстве проектов запускается вручную, после чего:
- Jenkins-джоба клонирует репозиторий с кодом и манифестами, билдит Docker-образ
- пушит его в Docker Hub
- вызывает
helm upgrade --install
, которому через--set
передаёт тег собранного образа
См. Helm: пошаговое создание чарта и деплоймента из Jenkins.
В другом нашем проекте используется более адекватный подход:
- при пул-реквесте в репозиторий триггерится Github Action, который собирает Docker-образ и пушит его в Docker Hub
- затем выполняется вебхук из Github, который триггерит джобу в Jenkins, передавая ей параметром тег докер-образа, который был собран Github Actions
Тут есть сразу несколько проблем:
- нет нормального версинирования кода и Docker-образов — хотя образ тегается с номером билда и номером коммита репозитория, который использовался для билда, в случае, если надо будет выяснить что именно пошло не так в работе приложения — придётся копаться в истории мержей и ID коммитов в репозитории, и сравнивать их с задеплоенным докер-образом
- Dev, Stage, Production деплоятся из разных Docker-образов, т.е. — в каждой CI/CD джобе выполняется новый билд докер-образа, который потом деплоится, в результате чего на Продакшене образ всегда будет != образу, протестированному на Стейджинге
- откатить неудачный деплой можно только вручную, вызывая
helm rollback
с рабочей машины, что бы откатить релиз, либоhelm install --upgrade
с передачей в--set
докер-тега одой из предыдущих сборок Jenkins-а
Вдобавок, Github Actions не имеет стабильного пула IP-адресов, и вообще хостится на упаси боже Microsoft Azure, с которым у меня связано очень много «приятных» воспоминаний, см. Azure: почему никогда. Microsoft же предлагает каждую неделю качать JSON с обновлёнными пулами IP-адресов, и обновлять свои Security Goups для Jenkins. Юмористы.
Начав внедрять ArgoCD мы столкнулись с тем, что надо выбрать стратегию будущих деплоев, о чём сегодня и попробуем поразмышлять.
Плюс — перенесём сборку образов в TravisCI, который появился в Github задолго до того, как Microsoft его купила, и имеет стабильный блок IP, которые можно добавить в группы безопасности ArgoCD/Jenkins.
От Jenkins хочется отказаться вообще — писать Goovy разработчикам неудобно, да и сам его UI не слишком удобен и информативен, а работа с Github делается через плагины, тогда как TravisCI имеет нативную интеграцию с Github.
Содержание
CIOps vs GitOps vs Handjobs
CIOps
В целом — мне нравится CIOps подход, т.е. примерно, что у нас происходит сейчас — когда приложение и нужный Docker-тег передаются во время деплоя из Jeknins.
У нас есть Helm-шаблон, и версия докер-образа в шаблоне задаётся из values:
... spec: containers: - name: {{ .Chart.Name }} image: {{ .Values.image.repository }}/{{ .Values.image.name }}:{{ .Values.image.tag }} ...
Репозиторий и имя образа тоже заданы в values.yaml
, а тег по умолчанию задан в latest:
... image: registry: 'docker.io' username: 'username' repository: 'projectname' name: 'appname' tag: 'latest' ...
Затем во время билда в одной джобе мы билдим Docker-образ, тегаем его, пушим в Docker Hub, и через --set
просто указываем Helm-у тег Docker-образа, который хотим задеплоить:
[simterm]
$ helm secrets upgrade --install --namespace dev-1-appname --create-namespace --atomic appname appname.tgz --set image.tag=2.ab0b2034 -f appname-frontend/env/dev/values.yaml -f appname-frontend/env/dev/secrets.yaml --debug
[/simterm]
Недостатки CIOps
Основным недостатком тут считается факт того, что CI в виде Jenkins имеет полный доступ к Kubernetes-кластеру.
Но, господи — у нас кластера создаются и настраиваются из Jenkins, так в чём проблема того, что у Jenkins и Helm внутри него есть доступ к кластеру?
Другим недостатком считается отсутствие возможности легко откатиться в случае неудачного деплоя, т.к. только Deployments в Kubernetes имеют ревизии, которые можно использовать.
Но тут снова-таки — мы используем Helm с его собственными версиями релизов, на которые хоть и вручную — но можно откатиться в случае проблем с каким-либо из задеплоенных ресурсов.
Из более-менее валидных (для нас) недостатков — это вероятность race-condition во время деплоев, т.е. если несколько Jenkins-джоб начнут деплоить одно приложение в одно и то же время.
См. Kubernetes anti-patterns: Let’s do GitOps, not CIOps!
GitOps
Что предлагает GitOps? Использовать Git-репозиторий, как source of truth — когда для конкретного приложения конкретная версия и докер-тега, и вообще манифеста хранятся в Git-репозитории, и у нас есть и история всех изменений, и возможность откатиться на любую предыдущую версию приложения.
Т.е. во время CI, после билда докер-образа — предлагается изменить тот же докер-тег прямо в файле манифеста Kubernetes, после чего вкоммитить и запушить изменения в репозиторий, где их увидит GitOps-утилита типа ArgoCD или Weave Flux, и применит изменения к приложению в Kubernetes-кластере, см. Automation from CI Pipelines.
Недостатки GitOps
А вот недостатков у GitOps достаточно много, например:
- вероятность конфликтов Git-репозитории — если одновременно запустятся несколько CI-джоб, каждая выполнит clone репозитория, внесёт свои изменения, и одновременно их пушнут обратно в этот репозиторий
- необходимость создания пачки отдельных репозиториев — и для того, что бы уменьшить вероятность конфликтов, и для того, что бы избежать вероятности зацикливания билдов, что может произойти, если код и манифесты будут храниться в одном репозитории, т.е. — вы пушите изменения кода в репозиторий и создаёте пул-реквест => он триггерит CI-джобу => джоба обновляет манифест с новой версией Docker-тега => пушит изменнения в репозиторий, создаёт новый пул-реквест, и… Решаемо, но.
Разделение репозиториев на config-repo и code-repo на мой взгляд одна из главных проблем при таком подходе, при этом ArgoCD рекомендует (и вполне логично) их всё-таки разделять, см. Best Practices.
И что делать?
Меня наверняка будут бить тапками поклонники ArcgoCD и GitOps за такое решение, но — почему бы не совмещать оба подхода?
Т.е. — мы храним код и манифесты Kubernetes, в общем Github-репозитории, и:
- при создании пул-реквеста, или после мержа, или создания Git release — триггерим CI-джобу, которая билдит Докер-образ, и пушит его в Docker Hub
- эта же CI-джоба триггерит ArgoCO на синхронизацию манифестов из этого репозитория, но нужные нам параметры передаёт аргументами к ArgoCD
Не знаю, как это сработает, и насколько оно зайдёт в Production-окружении — но можно попробовать.
ArgoCD
Parameter Overrides
Итак, что мы можем использовать, так это Parameter Overrides в ArgoCD.
Идея получается такая:
- код и манифесты Kubernetes в виде Helm-шаблонов храним в репозитории
- CI, в нашем случае это будет Travis вместо Jenkins, клонирует код, собирает Docker-образ его в Docker Hub с нужным тегом, например — номер билда+id коммита
- вызывает
argocd app set --image.tag=$номер билда+id коммита
- вызывает
argocd app sync
, что бы применить изменения в манифестах, если они были — при этомimage.tag
останется таким, как мы его задали вapp set
Попробуем.
Что надо сделать:
- добавить в ArgoCD новый репозиторий с тестовым Хельм-чартом, в values которого будет задан
image.tag=latest
- добавить в ArgoCD новое приложение
- обновить
image.tag
- выполнить
app sync
, и убедится, что Docker-образ остался прежним
Потом — попробуем всё это интегрировать с TravisCI.
Добавление репозитория
Создадим Helm-чарт:
[simterm]
$ helm create test-helm-chart Creating test-helm-chart
[/simterm]
Коммитим, пушим в репозитоий:
[simterm]
$ git add -A && git commit -m "test Helm chart init" && git push ... To github.com:rtfmorg/org-repo-1-pub.git * [new branch] master -> master
[/simterm]
Добавляем репозиторий в ArgoCD:
[simterm]
$ argocd repo add --name rtfm-org-repo-1-pub https://github.com/rtfmorg/org-repo-1-pub repository 'https://github.com/rtfmorg/org-repo-1-pub' added
[/simterm]
argocd app create
Создаём приложение:
[simterm]
$ argocd app create test-helm-chart-deployment --repo https://github.com/rtfmorg/org-repo-1-pub --path test-helm-chart --dest-namespace default --dest-server https://kubernetes.default.svc application 'test-helm-chart-deployment' created
[/simterm]
Проверяем:
[simterm]
$ argocd app get test-helm-chart-deployment --show-params Name: test-helm-chart-deployment Project: default Server: https://kubernetes.default.svc Namespace: default URL: https://dev-1.argocg.example.com/applications/test-helm-chart-deployment Repo: https://github.com/rtfmorg/org-repo-1-pub Target: Path: test-helm-chart SyncWindow: Sync Allowed Sync Policy: <none> Sync Status: OutOfSync from (d120d19) Health Status: Missing GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE Service default test-helm-chart-deployment OutOfSync Missing ServiceAccount default test-helm-chart-deployment OutOfSync Missing apps Deployment default test-helm-chart-deployment OutOfSync Missing
[/simterm]
Синхронизируем:
[simterm]
$ argocd app sync test-helm-chart-deployment Name: test-helm-chart-deployment Project: default Server: https://kubernetes.default.svc Namespace: default URL: https://dev-1.argocg.example.com/applications/test-helm-chart-deployment Repo: https://github.com/rtfmorg/org-repo-1-pub Target: Path: test-helm-chart SyncWindow: Sync Allowed Sync Policy: <none> Sync Status: Synced to (d120d19) Health Status: Progressing Operation: Sync Sync Revision: d120d1949a05f5a399365c33413e8457e0480680 Phase: Succeeded Start: 2020-11-28 14:02:14 +0200 EET Finished: 2020-11-28 14:02:15 +0200 EET Duration: 1s Message: successfully synced (all tasks run) GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE ServiceAccount default test-helm-chart-deployment Synced serviceaccount/test-helm-chart-deployment created Service default test-helm-chart-deployment Synced Healthy service/test-helm-chart-deployment created apps Deployment default test-helm-chart-deployment Synced Progressing deployment.apps/test-helm-chart-deployment created
[/simterm]
Редактируем test-helm-chart/values.yaml
— задаём tag=latest
:
... image: repository: nginx pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: "latest" ...
Пушим:
[simterm]
$ git add test-helm-chart/values.yaml && git commit -m "image.tag=latest" && git push
[/simterm]
Проверяем статус приложения — ArgoCD уже видит различия между тем, что задеплоено — и что задано в values.yaml
:
[simterm]
$ argocd app diff test-helm-chart-deployment ===== apps/Deployment default/test-helm-chart-deployment ====== 42c42 < - image: nginx:1.16.0 --- > - image: nginx:latest
[/simterm]
Синхронизируем:
[simterm]
$ argocd app sync test-helm-chart-deployment
[/simterm]
Проверяем текущий образ:
Хорошо — изменения в values.yaml
из репозитория применились к приложению в Kubernetes.
argocd app set
Выполняем app set
, и задаём новый image.tag
:
[simterm]
$ argocd app set --helm-set image.tag=1.19.5-alpine test-helm-chart-deployment
[/simterm]
Проверяем — теперь видим заданный нами параметр image.tag 1.19.5-alpine
и статус OutOfSync
:
[simterm]
$ argocd app get test-helm-chart-deployment --show-params Name: test-helm-chart-deployment Project: default Server: https://kubernetes.default.svc Namespace: default URL: https://dev-1.argocg.example.com/applications/test-helm-chart-deployment Repo: https://github.com/rtfmorg/org-repo-1-pub Target: Path: test-helm-chart SyncWindow: Sync Allowed Sync Policy: <none> Sync Status: OutOfSync from (b3a4daf) Health Status: Healthy NAME VALUE image.tag 1.19.5-alpine GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE ServiceAccount default test-helm-chart-deployment Synced serviceaccount/test-helm-chart-deployment unchanged Service default test-helm-chart-deployment Synced Healthy service/test-helm-chart-deployment unchanged apps Deployment default test-helm-chart-deployment OutOfSync Healthy deployment.apps/test-helm-chart-deployment configured
[/simterm]
Ещё раз выполняем app sync
:
[simterm]
$ argocd app sync test-helm-chart-deployment
[/simterm]
Kubernetes Pod пересоздаётся:
И проверяем параметр в новом поде:
Отлично — всё на месте.
ArgoCD и TravisCI
Добавим работу с CI, в данном случае — TravisCI.
Что надо сделать:
- добавить в репозиторий
Dockerfile
, из которого будем билдить тестовый образ - добавить в репозиторий
.travis.yml
, который будет выполнять:- сборку Docker-образа
- его пуш в Docker Hub
- выполнять
argocd app set --helm-set image.tag=$DOCKER_TAG
- выполнять
argocd app sync
Dockerfile
Набросаем простейший докерфайл:
FROM nginx:latest
Билдим, пушим:
[simterm]
$ docker build -t setevoy/nginx-example:1.0 . $ docker push setevoy/nginx-example:1.0
[/simterm]
Настройка TravisCI
Надо добавить несколько переменных — для логина в Docker Hub и ArgoCD:
.travis.yml
Пишем файл для Travis.
Пример докер-билда берём тут>>>.
Сам файл получается таким:
dist: trusty jobs: include: - stage: Build Docker image script: - echo "$DOCKER_PASS" | docker login -u "$DOCKER_USER" --password-stdin - docker build -t setevoy/nginx-example:${TRAVIS_BUILD_NUMBER}.${TRAVIS_COMMIT} . - docker push setevoy/nginx-example:${TRAVIS_BUILD_NUMBER}.${TRAVIS_COMMIT} - stage: Deploy to ArgoCD before_script: - VERSION=$(curl --silent "https://api.github.com/repos/argoproj/argo-cd/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') - wget https://github.com/argoproj/argo-cd/releases/download/$VERSION/argocd-linux-amd64 -O argocd - chmod +x argocd - PATH=$PATH:$PWD/ script: - argocd login --username "$ARGOCD_USER" --password "$ARGOCD_PASS" "$ARGOCD_HOST" - argocd app set --helm-set image.repository=setevoy/nginx-example --helm-set image.tag=${TRAVIS_BUILD_NUMBER}.${TRAVIS_COMMIT} test-helm-chart-deployment - argocd app sync test-helm-chart-deployment - argocd app wait --sync test-helm-chart-deployment - argocd app get test-helm-chart-deployment --show-params
Пушим, и проверяем:
Готово.