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

Evgenym
20.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.

berendiaev
20.09.2024 05:56Это кстати странно, можно external secrets оператор было продолжить использовать с волтом, а не устанавливать волтовый оператор

AzamatKomaev Автор
20.09.2024 05:56Хотел быстро вкатиться в Vault, в документации использовали именно их собственный оператор. Про интеграцию ExternalSecrets с Vault узнал уже чуть позже =)
Hamletghost
Помимо штатного подхода app of apps доя Argo есть еще надстройка applicationset, которая позволяет генерировать application ресурсы по шаблону и это на самом деле очень удобно (если у вас хорошо структурированный репозиторий со спеками)- избавляет от написания ненужного бойлерплейта
AzamatKomaev Автор
Слышал и читал о таком, но честно признаюсь, что не осилил =)
Показалось, что такой ресурс мне пока не нужен. Но возможно стоит попробовать, а не просто прочитать об этом в документации