ArgoCD: CIOps vs GitOps и деплой приложения из TravisCI

Автор: | 28/11/2020

Штош, пришло время подумать о том, как мы будем деплоить наши приложения.

Сейчас у нас используются Github-репозитории с кодом и Helm-шаблонами, и Jenkins.

Билд в Jenkins в большинстве проектов запускается вручную, после чего:

  1. Jenkins-джоба клонирует репозиторий с кодом и манифестами, билдит Docker-образ
  2. пушит его в Docker Hub
  3. вызывает helm upgrade --install, которому через --set передаёт тег собранного образа

См. Helm: пошаговое создание чарта и деплоймента из Jenkins.

В другом нашем проекте используется более адекватный подход:

  1. при пул-реквесте в репозиторий триггерится Github Action, который собирает Docker-образ и пушит его в Docker Hub
  2. затем выполняется вебхук из 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-репозитории, и:

  1. при создании пул-реквеста, или после мержа, или создания Git release – триггерим CI-джобу, которая билдит Докер-образ, и пушит его в Docker Hub
  2. эта же 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

Попробуем.

Что надо сделать:

  1. добавить в ArgoCD новый репозиторий с тестовым Хельм-чартом, в values которого будет задан image.tag=latest
  2. добавить в ArgoCD новое приложение
  3. обновить image.tag
  4. выполнить 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.

Что надо сделать:

  1. добавить в репозиторий Dockerfile, из которого будем билдить тестовый образ
  2. добавить в репозиторий .travis.yml, который будет выполнять:
    1. сборку Docker-образа
    2. его пуш в Docker Hub
    3. выполнять argocd app set --helm-set image.tag=$DOCKER_TAG
    4. выполнять 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

Пушим, и проверяем:

Готово.