Вступление
Если вы работаете с Kubernetes, то, скорее всего, используете kubectl, kustomize или Helm для развёртывания сервисов в кластере. Про последнюю утилиту я уже писал статью — можно посмотреть тут. Тогда я рассказал о своём опыте внедрения этого инструмента для собственных нагрузок и сравнил подходы kubectl apply и helm install.
Управление конфигурацией в Kubernetes может осуществляться с помощью различных инструментов. Помимо Helm, можно использовать просто YAML-манифесты или же kustomize. Для каждого из этих инструментов предусмотрена своя команда.
В одном git репозитории вы можете хранить:
- yaml манифесты для kubectl; 
- kustomization.yaml, yaml манифесты и патчи для kustomize; 
- values.yaml для helm. 
Такой подход называется GitOps. Он подразумевает, что вся конфигурация хранится декларативно в едином репозитории. Однако есть и недостатки: нужно вручную создавать и обновлять манифесты. Если кластером управляет не один сотрудник, важно убедиться, что все разработчики согласовывают изменения и вносят их в git-репозиторий. В таком случае мы не можем обеспечить концепцию единого источника истины (SSOT), которого требует GitOps подход.
Оглавление
Скрытый текст
Немного теории об Argo CD
Argo CD — инструмент непрерывной доставки ПО в Kubernetes. Argo CD полностью берет на себя задачи по синхронизации Git репозитория и кластера Kubernetes. Он сам отслеживает все изменения в коде и затем автоматически обновляет ресурсы в кластере.

Argo CD реализован в виде контроллера Kubernetes, который постоянно отслеживает запущенные приложения и сравнивает текущее состояние (live state) с целевым состоянием (desired state). Он имеет замечательный UI, с помощью которого можно управлять процессом синхронизации, просматривать разницу между состояниями, следить за ресурсами приложений.
Argo CD добавляет в кластер кастомные ресурсы (CRD), при помощи которых можно описывать его конфигурацию. Мы можем взаимодействовать с Argo CD при помощи консольной утилиты или через графический интерфейс. В данной статье будет использоваться второй способ.
Установка
Установим Argo CD в отдельное пространство имён:
# kubectl create namespace argocd
namespace/argocd created
# kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
customresourcedefinition.apiextensions.k8s.io/applications.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/applicationsets.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/appprojects.argoproj.io created
<...>Проверим, что все поды перешли в статус Running:
# kubectl -n argocd get pods
NAME                                                READY   STATUS    RESTARTS   AGE
argocd-application-controller-0                     1/1     Running   0          66s
argocd-applicationset-controller-744b76d7fd-nfl66   1/1     Running   0          67s
argocd-dex-server-5bf5dbc64d-tp9ms                  1/1     Running   0          67s
argocd-notifications-controller-84f5bf6896-h48pk    1/1     Running   0          67s
argocd-redis-74b8999f94-m6vsj                       1/1     Running   0          67s
argocd-repo-server-57f4899557-bnz46                 1/1     Running   0          66s
argocd-server-7bc7b97977-8wdxx                      1/1     Running   0          66sУ нас так же должен был появиться сервис argocd-server, при помощи которого мы можем получить доступ к API или UI Argo CD. По умолчанию его type: ClusterIP, но при необходимости (не советую) можно изменить на LoadBalancer или NodePort. В этой статье я буду открывать доступ посредством kubectl port-forward:
# kubectl -n argocd port-forward svc/argocd-server 8080:443
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080Теперь перейдем по адресу http://localhost:8080:

Получим пароль для пользователя admin из Kubernetes Secret:
# kubectl -n argocd get secret/argocd-initial-admin-secret -o json | jq .data.password -r | base64 -d
IKFWGsjONnt5hLV1Успешно залогинимся и увидим, что у нас всё пусто:

Подключаем репозиторий
Создадим секрет с информацией о подключении к GitHub репозиторию. Так как репозитории публичный, нам понадобится только ссылка:
apiVersion: v1
kind: Secret
metadata:
  name: github-repo
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  type: git
  url: https://github.com/AzamatKomaev/argo-demo-habr# kubectl apply -f https://raw.githubusercontent.com/AzamatKomaev/argo-demo-habr/main/repo.yaml
secret/github-repo сreatedУбедимся, что репозитории успешно подключился:

Разворачиваем nginx
Начнём с простого: развернем три реплики с Nginx с сервисом ClusterIP. Сейчас у нас следующая структура репозитория:

В директории apps мы будем хранить все наши приложения. У каждой поддиректории будет app.yaml, который содержит ресурс Application. В manifests будут привычные нам YAML-манифесты.
Прежде чем создать app.yaml, взглянем на его содержимое:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: nginx
  namespace: argocd  # тот же самый, где установлен ArgoCD
spec:
  project: default  # проект по-умолчанию
  destination:
    server: "https://kubernetes.default.svc"  # Kubernetes API адрес. Т.к ArgoCD запущен в тот же кластере, то путь до ClusterIP 
    namespace: nginx-demo  # пространство имен, где будут созданы ресурсы
  sources:
  - repoURL: https://github.com/AzamatKomaev/argo-demo-habr.git  # ссылка на Git-репозиторий
    targetRevision: HEAD  # указание на ветку, с котрой стоит синхронизировать состояние репозитория
    path: apps/nginx/manifests  # абсолютный путь до директории с манифестами
      
  syncPolicy:
    automated:
      prune: true  # разрешает удаление ресурса
      selfHeal: true  # разрешает ArgoCD самому приводить состояние кластера в соответствии с Git-репозиторием 
    syncOptions:
    - CreateNamespace=true  # создавать пространство имён, если оно не существуетПрименим манифест:
# kubectl apply -f https://raw.githubusercontent.com/AzamatKomaev/argo-demo-habr/main/apps/nginx/app.yaml
application.argoproj.io/nginx createdТеперь взглянем на UI Argo CD. Там должно было появиться приложение nginx:


Тут же мы можем увидеть все развёрнутые ресурсы Kubernetes и их статус. Sync OK означает, что ресурсы приложения синхронизированы с Git-репозиторием. Healthy показывает, что все ресурсы развёрнуты успешно. Давайте убедимся, что все описанные ресурсы есть в пространстве имён:
# kubectl -n nginx-demo get all
NAME                                   READY   STATUS    RESTARTS   AGE
pod/nginx-deployment-576c6b7b6-227dc   1/1     Running   0          8m33s
pod/nginx-deployment-576c6b7b6-27p4r   1/1     Running   0          8m33s
pod/nginx-deployment-576c6b7b6-gl24h   1/1     Running   0          8m33s
NAME                    TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
service/nginx-service   ClusterIP   10.43.25.85   <none>        80/TCP    8m33s
NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-deployment   3/3     3            3           8m33s
NAME                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-deployment-576c6b7b6   3         3         3       8m33sРазворачиваем Helm-чарт
Давайте теперь развернём Helm-чарт kube-prometheus-stack. С его помощью мы можем развернуть все необходимые компоненты для мониторинга кластера: kube-state-metrics для генерации метрик о состоянии Kubernetes кластера, Prometheus для сбора метрик, а также Grafana для визуализации собранных данных.
Создадим директорию monitoring внутри apps. Еще чуть глубже создадим директорию с названием Helm-чарта и там разместим файл app.yaml со следующим содержимым:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: prometheus
  namespace: argocd
spec:
  project: default
  destination:
    server: "https://kubernetes.default.svc"
    namespace: monitoring
  source:
    chart: kube-prometheus-stack
    repoURL: https://prometheus-community.github.io/helm-charts
    targetRevision: 60.1.0
    helm:
      releaseName: prometheus
      values: |
        grafana:
          enabled: true
          service:
            type: NodePort
            nodePort: 31234
          persistence:
            enabled: true
            accessModes:
              - ReadWriteOnce
            size: 5Gi
            finalizers:
              - kubernetes.io/pvc-protection
        defaultRules:
          create: false
        alertmanager:
          enabled: false
        prometheus:
          enabled: true
          prometheusSpec:
            storageSpec: 
              volumeClaimTemplate:
                spec:
                  accessModes: ["ReadWriteOnce"]
                  resources:
                    requests:
                      storage: 10Gi
          
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true
    - ServerSideApply=true
    Теперь дерево нашего репозитория выглядит следующим образом:

На этот раз нам необходимо указать название и версию чарта, название релиза и актуальные значения (values.yaml). Обратите внимание на последний элемент в списке syncOptions. Если чарт содержит CRD, то у вас может появиться ошибка, связанная с большим размером данных ресурсов. Чтобы такой ошибки не возникло, необходимо добавить параметр ServerSideApply=true. Подробнее об этом тут. 
Еще важно отметить, что Argo CD не использует helm install для установки чарта. Вместо этого он принимает манифесты, генерируемые командой helm template. Таким образом, Argo CD берёт на себя весь жизненный цикл приложения.  
Применим манифест:
# kubectl apply -f https://raw.githubusercontent.com/AzamatKomaev/argo-demo-habr/main/apps/monitoring/kube-prometheus-stack/app.yaml
application.argoproj.io/prometheus createdУ нас появилось второе приложение в Argo CD:


Подождем пока состояние приложения перейдёт в Healthy. В values релиза для доступа к Grafana мы указали service: NodePort и nodePort: 31234. 
Я использую сервис с типом NodePort для быстрого доступа к Grafana. Не пренебрегайте безопасностью ваших приложений!
Попробуем перейти по адресу_узла:31234. Всё работает!


App-of-apps паттерн
Сейчас у нас только два приложения. Но ведь кластер может содержать 10, 100, 500, 10000 приложении.... И в таком случае нам нужно будет вручную принимать манифесты с Application. Есть выход - App-of-apps.
Суть заключается в том, что у нас есть корневое приложение, которое берёт под управление другие. С помощью этой схемы мы можем заставить Argo CD самому создавать и удалять добавленные в репозитории приложения.
Опишем такой Application:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-app
  namespace: argocd
spec:
  project: default
  destination:
    server: "https://kubernetes.default.svc"
    namespace: argocd
  sources:
  - repoURL: https://github.com/AzamatKomaev/argo-demo-habr.git
    targetRevision: HEAD
    path: apps/
    directory:
      recurse: true
      include: '**/app.yaml'
      
  syncPolicy:
    automated:
      prune: true
      selfHeal: true# kubectl apply -f https://raw.githubusercontent.com/AzamatKomaev/argo-demo-habr/main/root-app.yaml                                
application.argoproj.io/root-app createdОбратите внимание на элементы directory. recurse: true  указывает на то, чтобы Application искал манифесты рекурсивно по всей директории apps/. С помощью include: '**/app.yaml'  мы указываем приложению принимать файлы только с названием app.yaml. Такимобразом, под управление «родительского» приложения перейдут только другие, «дочерние», а обычные YAML-манифесты будут управляться как раз последними. 
Вам может показаться, что вышеописанная схема достаточно сложна: необходимо для каждой пачки манифестов описывать свой app.yaml, затем указывать destination, source(-s) и другие параметры. Изначально я сделал так: Helm-чарты были отдельными приложениями, а обычные манифесты находились под контролем root-app. После увеличения количества таких ресурсов у root-app, я принял решение о дроблении манифестов на Application, что я считаю более правильным.
Вернёмся в интерфейс Argo CD. Появилось третье приложение. Перейдем в него и увидим, что теперь оно управляет двумя другими:

Переводим уже созданные сервисы под управление Argo CD
Я решил внедрять Argo CD в наш кластер уже тогда, когда в нем было развернуто несколько десятков приложений. Я опасался того, что возникнут проблемы при переезде с императивного подхода на декларативный, который предлагал Argo CD. Были также опасения по поводу того, что Argo CD как‑то «навредит» уже развернутой инфраструктуре. Но всё обошлось.
У меня уже как неделю развернут cnpg-operator в пространстве имён cnpg-system и кластер из трёх реплик в пространстве по умолчанию:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: postgres-db
  namespace: default
spec:
  bootstrap:
    initdb:
      database: db
      owner: db
      secret:
        name: db-creds
  instances: 3
  monitoring:
    enablePodMonitor: true
  storage:
    size: 1Gi
    storageClass: local-path
Сначала опишем Application для оператора (apps/cnpg-operator/app.yaml):
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cnpg-operator
  namespace: argocd
spec:
  project: default
  source:
    chart: cloudnative-pg
    repoURL: https://cloudnative-pg.github.io/charts
    targetRevision: 0.22.0
    helm:
      releaseName: cnpg
  destination:
    server: "https://kubernetes.default.svc"
    namespace: cnpg-system
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true
    - ServerSideApply=trueВажно, чтобы версия чарта, название релиза и пространство имён совпадали с тем, что у нас уже развернуто в кластере. Не будем создавать приложение вручную, так как мы уже настроили App-of-apps паттерн. Просто запушим изменения в удаленный репозитории, немного подождем и увидим, что Argo CD сам подтянет все изменения:

Перейдем в само приложение cnpg-operator и убедимся, что ресурсы остались нетронутыми:

Статус приложения Healthy. Обратите также внимание на дату создания ресурсов: 7 days .
Посмотрим установленные чарты:
# helm -n cnpg-system ls  
NAME	NAMESPACE  	REVISION	UPDATED                                	STATUS  	CHART                	APP VERSION
cnpg	cnpg-system	1       	2024-09-12 15:37:54.390475578 +0300 MSK	deployed	cloudnative-pg-0.22.0	1.24.0Как упоминалось ранее, Argo CD при создании Application не использует утилиту helm. Чтобы чарт больше не управлялся Helm, необходимо удалить секреты с типом helm.sh/release.v1:
# kubectl -n cnpg-system get secret --field-selector type=helm.sh/release.v1
NAME                         TYPE                 DATA   AGE
sh.helm.release.v1.cnpg.v1   helm.sh/release.v1   1      7d6h
# kubectl -n cnpg-system delete secret/sh.helm.release.v1.cnpg.v1
secret "sh.helm.release.v1.cnpg.v1" deleted
# helm -n cnpg-system ls
NAME	NAMESPACE	REVISION	UPDATED	STATUS	CHART	APP VERSIONС оператором разобрались, теперь опишем приложение для cnpg-кластера (apps/cnpg-operator/app.yaml):
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cnpg-cluster
  namespace: argocd
spec:
  project: default
  destination:
    server: "https://kubernetes.default.svc"
    namespace: default
  sources:
  - repoURL: https://github.com/AzamatKomaev/argo-demo-habr.git
    path: apps/cnpg-cluster/manifests
    targetRevision: HEAD
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=trueВ директории apps/cnpg-cluster/manifests создадим cluster.yaml и поместим туда спецификацию ранее описанного Cluster. Получим следующую структуру:

Снова запушим изменения в репозитории и убедимся, что Argo CD подтянул ресурсы:

Вносим изменения в Application
Ранее для PostgreSQL я включал podMonitor. Это ресурс, при помощи которого указывается, как Prometheus должен обнаруживать и мониторить поды. Для того чтобы Prometheus смог их обнаружить, необходимо внести следующие изменения в values.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: prometheus
  namespace: argocd
spec:
  <...>
  source:
    chart: kube-prometheus-stack
    <...>
    helm:
      releaseName: prometheus
      values: |
        <...>
        prometheus:
          enabled: true
          prometheusSpec:
            <...>
            podMonitorSelectorNilUsesHelmValues: false      # +
            serviceMonitorSelectorNilUsesHelmValues: false  # + 
    Зайдём в Grafana и импортируем дашборд для cnpg:
Много скриншотов




Argo CD Image Updater
Отлично, мы поняли как переводить более статические сервисы под управление Argo CD. Тот же самый Prometheus или CNPG-кластер вряд ли обновляется каждый день, в отличие от собственных приложений.
Везде, где я имел опыт с CI/CD, выкатка новых версий приложений происходила по модели Push: сначала собирали образ и загружали его в реестр. Затем брали тег образа (номер сборки или COMMIT_SHA) и обновляли образ в спецификации Deployment посредством kubectl apply или helm upgrade. 
Если вы хотите перевести свои нагрузки под управление Argo CD, то тогда вам понадобится Argo CD Image Updater — инструмент для автоматического обновления образов. Он автоматически проверяет новые образы в реестре, которые используются в Kubernetes и сам их обновляет в соответствии с последней версией. Это Pull-подход.
Я не использую этот инструмент, решив оставить классический подход для CD собственных нагрузок.
Конец
GitOps — это круто!
Комментарии (8)
 - Evgenym20.09.2024 05:56- А как решаете задачу деплоя в кластер разных секретов, которые нужны приложениям?  - AzamatKomaev Автор20.09.2024 05:56- На прошлом месте работы использовали Yandex Lockbox (облачный сервис для хранения секретов) + External Secrets для интеграции с Yandex.Cloud. В коде описывали kind: ExternalSecret, в котором указывали id секрета в облаке, namespace и название k8s секрета. - На новом месте решили не привязываться к сервисам облака и поэтому раскатали Vault (для каждого кластера свой собственный). Интегрируем с кубом посредством Vault Secrets Operator, в репозиториях храним ресурсы kind: VaultStaticSecret по аналогии с ExternalSecret.  - berendiaev20.09.2024 05:56- Это кстати странно, можно external secrets оператор было продолжить использовать с волтом, а не устанавливать волтовый оператор  - AzamatKomaev Автор20.09.2024 05:56- Хотел быстро вкатиться в Vault, в документации использовали именно их собственный оператор. Про интеграцию ExternalSecrets с Vault узнал уже чуть позже =) 
 
 
 
 
           
 
Hamletghost
Помимо штатного подхода app of apps доя Argo есть еще надстройка applicationset, которая позволяет генерировать application ресурсы по шаблону и это на самом деле очень удобно (если у вас хорошо структурированный репозиторий со спеками)- избавляет от написания ненужного бойлерплейта
AzamatKomaev Автор
Слышал и читал о таком, но честно признаюсь, что не осилил =)
Показалось, что такой ресурс мне пока не нужен. Но возможно стоит попробовать, а не просто прочитать об этом в документации