Путь в GitOps или как мы перевели кластер Kubernetes под управление Argo CD
Вступление
Если вы работаете с 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. Он сам отслеживает все изменения в коде и затем автоматически обновляет ресурсы в кластере.
From official site
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 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. Всё работает!
admin/prom-operator для логина
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 — это круто!