Вступление
Если вы работаете с 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 — это круто!
