Вступление

Если вы работаете с 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
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   <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. Всё работает!

admin/prom-operator для логина
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 — это круто!

Комментарии (8)


  1. Hamletghost
    20.09.2024 05:56
    +3

    Помимо штатного подхода app of apps доя Argo есть еще надстройка applicationset, которая позволяет генерировать application ресурсы по шаблону и это на самом деле очень удобно (если у вас хорошо структурированный репозиторий со спеками)- избавляет от написания ненужного бойлерплейта


    1. AzamatKomaev Автор
      20.09.2024 05:56

      Слышал и читал о таком, но честно признаюсь, что не осилил =)

      Показалось, что такой ресурс мне пока не нужен. Но возможно стоит попробовать, а не просто прочитать об этом в документации


  1. Evgenym
    20.09.2024 05:56

    А как решаете задачу деплоя в кластер разных секретов, которые нужны приложениям?


    1. AzamatKomaev Автор
      20.09.2024 05:56

      На прошлом месте работы использовали Yandex Lockbox (облачный сервис для хранения секретов) + External Secrets для интеграции с Yandex.Cloud. В коде описывали kind: ExternalSecret, в котором указывали id секрета в облаке, namespace и название k8s секрета.

      На новом месте решили не привязываться к сервисам облака и поэтому раскатали Vault (для каждого кластера свой собственный). Интегрируем с кубом посредством Vault Secrets Operator, в репозиториях храним ресурсы kind: VaultStaticSecret по аналогии с ExternalSecret.


      1. berendiaev
        20.09.2024 05:56

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


        1. AzamatKomaev Автор
          20.09.2024 05:56

          Хотел быстро вкатиться в Vault, в документации использовали именно их собственный оператор. Про интеграцию ExternalSecrets с Vault узнал уже чуть позже =)


  1. Evgenym
    20.09.2024 05:56

    А еще вопрос, в ArgoCD есть компонент Redis HA, вы его используете?


    1. AzamatKomaev Автор
      20.09.2024 05:56

      Нет, его не используем