Kubernetes для самых маленьких. Часть 41: Настраиваем GitOps на Argo CD
Все части серии «Kubernetes для самых маленьких»За 40 частей мы поставили в кластер десятки объектов: Deployment и Service приложения hello, HTTPRoute для Traefik, Certificate для cert-manager, NetworkPolicy, ServiceMonitor, компоненты платформы. Каждый из них попал туда одинаково — командой kubectl apply или helm upgrade с рабочей станции администратора. Kustomize из части 26 и Helm из части 27 навели порядок в манифестах, но не в том, как они доставляются: что сейчас в кластере, знает только сам кластер, а кто и когда это туда положил — в лучшем случае история команд в чьём-то терминале. Последняя основная часть серии — о GitOps: подходе, при котором единственный источник правды о кластере — репозиторий Git, а в кластер изменения приносит контроллер, который сам сравнивает Git с действительностью и устраняет разницу. Это тот же цикл согласования, с которого серия началась в части 1, только поднятый на уровень выше. Инструмент — Argo CD.
Принципы GitOps
GitOps сформулирован как четыре принципа (их описывает рабочая группа OpenGitOps при CNCF — фонде, который развивает Kubernetes и проекты вокруг него):
- Декларативность. Желаемое состояние системы описано целиком — манифестами, чартами, overlays Kustomize, — а не последовательностью команд, которые к нему ведут.
- Версионирование и неизменяемость. Описание хранится там, где у каждого изменения есть автор, время, причина и возможность откатиться: в Git. Изменение кластера — это коммит, проверка изменения — это ревью запроса на слияние, откат —
git revert. - Вытягивание (pull). Изменения в кластер забирает агент внутри кластера, а не толкает снаружи CI-система или человек.
- Постоянное согласование. Агент не применяет изменения один раз, а непрерывно сравнивает желаемое состояние с фактическим и сводит их, в том числе когда расхождение возникло не из Git, а из-за ручной правки в кластере.
Третий пункт важнее, чем кажется. Сравним две схемы доставки:
push: CI толкает в кластер
разработчик ──► Git ──► CI-конвейер ──── kubectl apply ────► kube-apiserver
(держит kubeconfig с правами
на запись в кластер)
pull: агент в кластере тянет из Git
разработчик ──► Git ◄──── git fetch, раз в 3 минуты ──── Argo CD ──► kube-apiserver
(ключ только на чтение) (внутри кластера)
В push-схеме ключи с правом писать в кластер лежат в CI-системе, обычно с правами администратора на все кластеры, куда она что-либо доставляет: взлом CI — это взлом всех кластеров. Кластер к тому же должен быть доступен из CI по сети. В pull-схеме права на запись в кластер есть только у агента внутри него, а наружу кластер ходит сам, причём с ключом только на чтение репозитория. И только pull-агент, который работает постоянно, может заметить, что кто-то поменял объект руками, — CI-конвейер о кластере вспоминает, лишь когда его запускают.
Следствия для работы:
kubectl apply,kubectl editиhelm upgradeруками на кластере под GitOps больше не делают: ручная правка будет откачена или, как минимум, подсвечена как расхождение;- доступ людей к кластеру можно сузить до чтения и диагностики (
logs,describe,execдля отладки), а изменения идут через запросы на слияние; - восстановление кластера после потери — это установка агента и указание ему на репозиторий: всё, что описано в Git, вернётся само. Это хорошо дополняет снапшоты etcd из части 34, но не заменяет их: в Git нет данных, созданных в кластере (PVC, Secret, выпущенные cert-manager, состояние операторов).
Argo CD: из чего состоит
Argo CD — GitOps-контроллер из проекта Argo фонда CNCF. Он хранит описание приложений в своих CRD (о CRD и операторах — часть 28), сам рендерит манифесты из Git и сам применяет их в кластер. Компоненты стандартной установки:
| Компонент | Вид | Что делает |
|---|---|---|
argocd-application-controller |
StatefulSet | сердце системы: следит за объектами Application, сравнивает отрендеренные манифесты с живыми объектами в кластере, выставляет статусы, выполняет синхронизацию |
argocd-repo-server |
Deployment | клонирует репозитории и кеширует их, превращает каталог в манифесты: kustomize build, helm template, обычный YAML или плагин |
argocd-server |
Deployment | API для веб-интерфейса и CLI argocd, аутентификация пользователей, RBAC Argo CD, приём веб-хуков от Git-сервера |
argocd-applicationset-controller |
Deployment | генерирует объекты Application из шаблона ApplicationSet |
argocd-redis |
Deployment | кеш: отрендеренные манифесты, состояние дерева ресурсов; не источник правды, его можно потерять |
argocd-dex-server |
Deployment | Dex — посредник единого входа (SSO): принимает вход через OIDC, LDAP, SAML и выдаёт Argo CD токен OIDC |
argocd-notifications-controller |
Deployment | уведомления о синхронизации и сбоях в мессенджеры и почту |
Git (k8s-config) кластер, неймспейс argocd
┌──────────────────┐ git fetch ┌──────────────────────────────────┐
│ apps/hello/... │◄───────────────│ argocd-repo-server │
│ platform/... │ deploy key │ kustomize build / helm template│
└──────────────────┘ (только └────────────────┬─────────────────┘
чтение) │ манифесты
▼
┌──────────────────────────────────┐
│ argocd-application-controller │
│ желаемое (Git) │
│ сравнить с │
│ живым (watch kube-apiserver) │
│ → Synced / OutOfSync │
│ → apply, prune │
└────────────────┬─────────────────┘
│ server-side apply
▼
┌──────────────────────────────────┐
│ kube-apiserver │
└──────────────────────────────────┘
Контроллер держит в памяти кеш всех объектов кластера, которыми управляет, через watch — так же, как любой контроллер Kubernetes из части 3. Git он перечитывает раз в 120 секунд с добавкой до 60 секунд (чтобы сотни приложений не ходили в Git одновременно), то есть изменение в репозитории доходит до кластера максимум за 3 минуты. Если Git-сервер умеет веб-хуки, его настраивают дёргать argocd-server на каждый push — тогда синхронизация начинается через секунды.
Установка
Argo CD ставится официальным манифестом в неймспейс argocd. Ссылку берут с конкретной версией, а не с ветки stable: так повторная установка через полгода даст то же самое, а обновление станет осознанным шагом. На момент написания актуальна 3.6.1:
$ kubectl create namespace argocd
namespace/argocd created
$ kubectl apply -n argocd --server-side --force-conflicts \
-f https://raw.githubusercontent.com/argoproj/argo-cd/v3.6.1/manifests/install.yaml
customresourcedefinition.apiextensions.k8s.io/applications.argoproj.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/applicationsets.argoproj.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/appprojects.argoproj.io serverside-applied
serviceaccount/argocd-application-controller serverside-applied
...
statefulset.apps/argocd-application-controller serverside-applied
--server-side здесь обязателен: файл CRD ApplicationSet занимает полтора мегабайта, а обычный kubectl apply кладёт копию всего объекта в аннотацию last-applied-configuration, у которой предел — 262 144 байта на все аннотации объекта. Без ключа установка падает на этом CRD:
$ kubectl apply -f applicationset-crd.yaml
The CustomResourceDefinition "applicationsets.argoproj.io" is invalid: metadata.annotations: Too long: may not be more than 262144 bytes
Server-side apply (часть 3) хранит владение полями в managedFields и аннотации не пишет. --force-conflicts забирает поля, которыми владел кто-то другой, — нужно при обновлении поверх прошлой установки.
У Argo CD своя таблица совместимости с версиями Kubernetes (страница Tested Kubernetes Versions в документации): 3.6 проверена с Kubernetes от 1.34 до 1.37, то есть и со стендом, который после части 35 работает на 1.37. Перед обновлением Kubernetes в кластере с Argo CD эту таблицу смотрят так же, как список устаревших API.
Через минуту все компоненты работают:
$ kubectl -n argocd get pods
NAME READY STATUS RESTARTS AGE
argocd-application-controller-0 1/1 Running 0 65s
argocd-applicationset-controller-794bb95c9d-d9ljf 1/1 Running 0 66s
argocd-dex-server-66cf64bfcf-r2w4x 1/1 Running 0 66s
argocd-notifications-controller-5878c8d644-kjppp 1/1 Running 0 66s
argocd-redis-5869b7547-5kxrs 1/1 Running 0 66s
argocd-repo-server-7d55cc466b-blj7h 1/1 Running 0 66s
argocd-server-85b849c688-79l5f 1/1 Running 0 66s
Сами поды Argo CD лёгкие, но application-controller при старте подписывается через watch на все типы объектов кластера, которыми может управлять, а первая синхронизация десятка приложений — это сотни запросов apply и проверок --dry-run=server. Всё это ложится на kube-apiserver и etcd. На стенде мастер с 4 ГБ памяти, где уже жили apiserver, etcd и компоненты мониторинга из прошлых частей, через десять секунд после запуска корневого приложения (о нём ниже) перестал отвечать даже по SSH, и его пришлось выключать. С 8 ГБ та же синхронизация прошла без заметной нагрузки. Мастеру кластера под GitOps нужен запас памяти, а не впритык.
Манифест install.yaml выдаёт контроллеру права администратора кластера: он должен уметь создать любой объект, который описан в Git. Ограничивать, что именно можно доставлять, будем не урезанием этих прав, а проектами Argo CD — ниже.
CLI и вход. Утилита argocd скачивается с той же страницы релизов, версия должна совпадать с сервером:
$ curl -fsSLo argocd https://github.com/argoproj/argo-cd/releases/download/v3.6.1/argocd-linux-amd64
$ curl -fsSLO https://github.com/argoproj/argo-cd/releases/download/v3.6.1/cli_checksums.txt
$ grep argocd-linux-amd64 cli_checksums.txt | sed 's/argocd-linux-amd64/argocd/' | sha256sum -c
argocd: OK
$ sudo install -m 0755 argocd /usr/local/bin/argocd
$ argocd version --client --short
argocd: v3.6.1+94037c4
При установке создаётся пользователь admin со случайным паролем в Secret argocd-initial-admin-secret:
$ argocd admin initial-password -n argocd
Xq7vP2mK9tLw4RzB
This password must be only used for first time login. We strongly recommend you update the password using `argocd account update-password`.
$ kubectl -n argocd port-forward svc/argocd-server 8080:443 &
$ argocd login localhost:8080 --username admin --insecure
Password:
'admin:login' logged in successfully
Context 'localhost:8080' updated
$ argocd account update-password
...
Password updated
Context 'localhost:8080' updated
$ kubectl -n argocd delete secret argocd-initial-admin-secret
secret "argocd-initial-admin-secret" deleted from argocd namespace
--insecure нужен потому, что argocd-server пока работает с самоподписанным сертификатом. Постоянный доступ к веб-интерфейсу и API делают через Traefik: в ConfigMap argocd-cmd-params-cm ставят server.insecure: "true" (TLS завершает Traefik), а сертификат для argocd.apps.example.com выпускает cert-manager, как в части 19. Веб-интерфейс Argo CD показывает дерево объектов каждого приложения с их статусами и разницей с Git — удобно для обзора, но всё, что в нём можно сделать, делается и CLI, и YAML, поэтому дальше работаем в CLI, а интерфейс покажем на двух снимках. Пользователь admin после настройки единого входа через Dex или OIDC отключают (admin.enabled: "false" в ConfigMap argocd-cm): общий пароль на всех — то, от чего GitOps как раз уводит.
Репозиторий конфигурации
Конфигурацию кластера держат в отдельном репозитории, а не рядом с кодом приложения. Причины: у них разные циклы (код меняется десятки раз в день, конфигурация кластера — реже и с другим ревью), разные люди с правом слияния, и Argo CD не нужно реагировать на каждый коммит в код. Конвейер сборки приложения из части 5 собирает образ, кладёт его в реестр и открывает запрос на слияние в репозиторий конфигурации с новым тегом — дальше работает GitOps.
Структура:
k8s-config/
├── clusters/
│ └── stand/
│ ├── root.yaml корневое Application (App of Apps)
│ ├── projects/
│ │ ├── demo.yaml AppProject для приложений
│ │ └── platform.yaml AppProject для платформы
│ └── apps/
│ ├── platform.yaml ApplicationSet по каталогам platform/*
│ └── hello.yaml ApplicationSet: hello в dev и prod
├── platform/
│ ├── cert-manager/
│ ├── metallb/
│ ├── namespaces/
│ └── traefik/
└── apps/
└── hello/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── httproute.yaml
│ ├── networkpolicy.yaml
│ └── servicemonitor.yaml
└── overlays/
├── dev/
│ └── kustomization.yaml
└── prod/
├── kustomization.yaml
├── hpa.yaml
├── pdb.yaml
└── smoke-test.yaml
Окружения — каталогами, а не ветками. Соблазн сделать ветку dev и ветку prod велик, но на практике ветки окружений расходятся: исправление, сделанное в prod, забывают перенести в dev, слияние между ветками приносит чужие изменения, и ответ на вопрос «чем отличается prod от dev» — это git diff двух веток с посторонним шумом. С каталогами вся разница между окружениями — это содержимое overlays/dev и overlays/prod, видимое в одном коммите. Продвижение версии из dev в prod — запрос на слияние, меняющий тег образа в overlays/prod/kustomization.yaml. Все Application смотрят в одну ветку main, защищённую от прямых push: изменения только через ревью.
Приложение hello целиком
Base — то, что одинаково во всех окружениях. Это те же объекты, что мы создавали в частях 7, 15, 18, 20 и 32, собранные вместе. Отдельного Certificate у hello нет: HTTPRoute цепляется к слушателю https шлюза apps, а у слушателя уже есть сертификат на *.apps.example.com из части 19, и шлюз описан в приложении платформы platform/traefik.
apps/hello/base/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- networkpolicy.yaml
- deployment.yaml
- service.yaml
- httproute.yaml
- servicemonitor.yaml
deployment.yaml — с настройками безопасности из части 30 и без поля replicas: им управляют overlays и автомасштабирование.
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
labels: { app.kubernetes.io/name: hello }
spec:
selector:
matchLabels: { app.kubernetes.io/name: hello }
template:
metadata:
labels: { app.kubernetes.io/name: hello }
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile: { type: RuntimeDefault }
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: { app.kubernetes.io/name: hello }
containers:
- name: hello
image: ghcr.io/example/hello
ports:
- { name: http, containerPort: 8080 }
readinessProbe:
httpGet: { path: /healthz, port: http }
livenessProbe:
httpGet: { path: /healthz, port: http }
resources:
requests: { cpu: 50m, memory: 32Mi }
limits: { memory: 64Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
Остальное коротко:
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: hello
labels: { app.kubernetes.io/name: hello }
spec:
selector: { app.kubernetes.io/name: hello }
ports:
- { name: http, port: 80, targetPort: http }
---
# httproute.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: hello
spec:
parentRefs:
- { name: apps, namespace: traefik, sectionName: https }
hostnames: [hello.apps.example.com]
rules:
- backendRefs:
- { name: hello, port: 80 }
---
# networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: hello
annotations:
argocd.argoproj.io/sync-wave: "-1"
spec:
podSelector:
matchLabels: { app.kubernetes.io/name: hello }
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: traefik }
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: monitoring }
- podSelector:
matchLabels: { app.kubernetes.io/component: smoke-test }
ports:
- { port: http }
---
# servicemonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: hello
labels: { release: kube-prometheus-stack }
spec:
selector:
matchLabels: { app.kubernetes.io/name: hello }
endpoints:
- { port: http, path: /metrics, interval: 30s }
Селекторы написаны явно в каждом объекте, а не добавляются трансформером меток Kustomize: у NetworkPolicy он заполняет селекторы только там, где они уже есть, и пустой podSelector: {} остался бы пустым — политика молча применилась бы ко всем подам неймспейса. Метка release на ServiceMonitor — та, по которой Prometheus из kube-prometheus-stack (часть 32) находит ServiceMonitor; NetworkPolicy пропускает к подам только Traefik, Prometheus и проверочный Job, о котором ниже.
Overlay dev — одна реплика, своё имя хоста (его покрывает тот же wildcard-сертификат шлюза) и тег образа, который только что собрал конвейер:
# apps/hello/overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: demo-dev
resources:
- ../../base
images:
- name: ghcr.io/example/hello
newTag: "1.1"
patches:
- target: { kind: Deployment, name: hello }
patch: |
- op: add
path: /spec/replicas
value: 1
- target: { kind: HTTPRoute, name: hello }
patch: |
- op: replace
path: /spec/hostnames/0
value: hello-dev.apps.example.com
Overlay prod — образ, закреплённый по дайджесту (тег можно перезаписать в реестре, дайджест — нет, см. часть 5), автомасштабирование по части 32 и PodDisruptionBudget по части 13:
# apps/hello/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: demo
resources:
- ../../base
- hpa.yaml
- pdb.yaml
- smoke-test.yaml
images:
- name: ghcr.io/example/hello
newTag: "1.0"
digest: sha256:a5066cd40ef37b9b889ddfbe9c1860b3afe231b96b975000e0edca3fdd6f4a4a
# apps/hello/overlays/prod/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: hello
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: hello }
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }
---
# apps/hello/overlays/prod/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: hello
spec:
minAvailable: 1
selector:
matchLabels: { app.kubernetes.io/name: hello }
Почему в prod у Deployment нет replicas: число реплик меняет HPA. Если бы оно было в Git, Argo CD видел бы расхождение после каждого решения HPA и с selfHeal возвращал бы значение из Git, а HPA — своё, и они бы бесконечно спорили. Общее правило GitOps: в Git описывают только поля, которыми владеет человек; поля, которыми владеет контроллер, в Git не пишут. Если убрать поле нельзя (например, его задаёт чужой Helm-чарт), Argo CD можно велеть его не сравнивать — ignoreDifferences в Application.
Проверить, что получится, можно до коммита, той же командой, которую выполнит repo-server:
$ kubectl kustomize apps/hello/overlays/prod | grep -E '^kind:' | sort
kind: Deployment
kind: HorizontalPodAutoscaler
kind: HTTPRoute
kind: Job
kind: NetworkPolicy
kind: PodDisruptionBudget
kind: Service
kind: ServiceMonitor
$ kubectl kustomize apps/hello/overlays/prod | grep 'image:'
- image: ghcr.io/example/hello:1.0@sha256:a5066cd40ef37b9b889ddfbe9c1860b3afe231b96b975000e0edca3fdd6f4a4a
image: busybox:1.37
Kustomize выводит поля объектов по алфавиту, поэтому image у контейнера hello оказался первым полем элемента списка, перед name. Тег и дайджест вместе дают ссылку тег@дайджест: среда выполнения берёт образ по дайджесту, а тег остаётся для людей.
Неймспейсы demo и demo-dev с метками Pod Security описаны отдельно, в приложении платформы platform/namespaces/. Опция синхронизации CreateNamespace=true создала бы неймспейс без меток и без политик, поэтому ею не пользуемся. Если hello при первой установке синхронизируется раньше, чем появился неймспейс или чем приложение traefik поставило CRD Gateway API, синхронизация завершится ошибкой и повторится по retry. На стенде так и вышло: первые попытки hello-dev и hello-prod закончились сообщением failed to discover server resources for group version gateway.networking.k8s.io/v1: the server could not find the requested resource, а пятая, через три минуты, прошла.
Доступ к репозиторию: deploy key
Argo CD нужен доступ к репозиторию только на чтение. Лучший способ — deploy key: SSH-ключ, привязанный не к человеку, а к одному репозиторию, с правом только чтения. Его поддерживает любой Git-сервер. Ключ создаётся без пароля (контроллер не сможет его ввести), открытая часть добавляется в настройки репозитория на Git-сервере, закрытая — в Secret в неймспейсе argocd:
$ ssh-keygen -t ed25519 -N '' -C argocd@stand -f ./argocd-deploy-key
Generating public/private ed25519 key pair.
Your identification has been saved in ./argocd-deploy-key
Your public key has been saved in ./argocd-deploy-key.pub
The key fingerprint is:
SHA256:zw017JzxRc0lI3/P1FIRSxZt0fojC6cGdWB1N7YmRz8 argocd@stand
...
$ cat argocd-deploy-key.pub # → в настройки репозитория k8s-config, только чтение
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHVLI6SNSii+DzwFcVoQ7McrOmwPyjoajlBQ3G9LWe4s argocd@stand
$ kubectl -n argocd create secret generic repo-k8s-config \
--from-literal=type=git \
--from-literal=url=git@git.example.com:platform/k8s-config.git \
--from-file=sshPrivateKey=./argocd-deploy-key
secret/repo-k8s-config created
$ kubectl -n argocd label secret repo-k8s-config argocd.argoproj.io/secret-type=repository
secret/repo-k8s-config labeled
$ shred -u argocd-deploy-key
$ ssh-keyscan git.example.com | argocd cert add-ssh --batch
$ argocd repo list
TYPE NAME REPO INSECURE OCI LFS CREDS STATUS MESSAGE PROJECT
git git@git.example.com:platform/k8s-config.git false false false false Successful
Argo CD находит репозитории по метке argocd.argoproj.io/secret-type: repository на Secret; STATUS Successful в argocd repo list значит, что repo-server подключился по ключу и прочитал репозиторий. Команда с ssh-keyscan кладёт открытые ключи SSH-сервера Git в ConfigMap argocd-ssh-known-hosts-cm: без них Argo CD откажется подключаться, потому что не сможет убедиться, что говорит с настоящим сервером. Ключи после ssh-keyscan стоит сверить с тем, что показывает администратор Git-сервера: ssh-keyscan сам по себе подлинность не проверяет. Для GitHub, GitLab, Bitbucket и Azure DevOps ключи уже лежат в этом ConfigMap после установки, и для них команда ничего не добавит (Successfully created 0 SSH known host entries).
Сам Secret с ключом в Git не кладётся — это одна из немногих вещей, которую создают руками при начальной установке (или доставляют одним из способов из раздела о секретах).
Application
Application — CRD Argo CD, описывающий одно приложение: откуда брать манифесты и куда их применять.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: hello-prod
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: demo
source:
repoURL: git@git.example.com:platform/k8s-config.git
targetRevision: main
path: apps/hello/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: demo
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- ServerSideApply=true
- PruneLast=true
retry:
limit: 5
backoff: { duration: 10s, factor: 2, maxDuration: 3m }
Поля:
source— репозиторий, ветка или тег (targetRevision) и каталог. Как его рендерить, repo-server решает сам: естьkustomization.yaml—kustomize build; естьChart.yaml—helm template; иначе — все YAML-файлы каталога. Вместоpathможет бытьchartс адресом Helm-репозитория — тогда Argo CD ставит чарт без Git, а значения задаются вhelm.valuesObject. Helm-релиза при этом нет: Argo CD использует Helm только как шаблонизатор и применяет результат сам, поэтомуhelm listтакие приложения не показывает;destination— кластер и неймспейс по умолчанию.https://kubernetes.default.svc— тот кластер, где работает Argo CD; другие кластеры регистрируются Secret-ами с их адресом и учётными данными;syncPolicy.automated— синхронизировать автоматически, как только Git и кластер разошлись. Без него Argo CD только показывает расхождение, а синхронизирует человек — командойargocd app syncили кнопкой;prune: true— удалять из кластера объекты, которые удалили из Git. По умолчанию выключено: Argo CD осторожен и удалённое из Git только помечает. Безpruneкластер со временем обрастает объектами, о которых в Git уже ничего нет;selfHeal: true— возвращать состояние из Git, если объект изменили в кластере в обход Git. Первая попытка — сразу после обнаружения, а если объект продолжают менять, следующие идут с паузой от 2 секунд, которая растёт втрое до 5 минут (флаги--self-heal-backoff-*контроллера): так Argo CD не устраивает бесконечную перетяжку с другим контроллером. БезselfHealручная правка остаётся, а приложение просто помечается OutOfSync;ServerSideApply=true— применять server-side apply вместо клиентского: без предела размера аннотации и с честным владением полями. Поля, которые выставляют другие контроллеры (тот же HPA), остаются за ними. С версии 3.6 эта опция включает и сравнение на стороне сервера (Server-Side Diff): Argo CD делает пробный server-side apply с--dry-run=serverи сравнивает результат с живым объектом, поэтому в сравнении участвуют и значения по умолчанию, и вебхуки допуска;PruneLast=true— удалять лишнее в самом конце синхронизации, когда новые объекты уже созданы и здоровы;retry— повторять неудачную синхронизацию с нарастающей паузой: например, если CRD ещё не успел зарегистрироваться;- финализатор
resources-finalizer.argocd.argoproj.io— при удалении Application удалить и все его объекты в кластере. Без него удаление Application оставляет объекты в кластере без присмотра; иногда это и нужно (перенос под другое Application), но по умолчанию приложение и его объекты живут и умирают вместе.
Как Argo CD узнаёт «свои» объекты: при применении он ставит на каждый аннотацию argocd.argoproj.io/tracking-id вида hello-prod:apps/Deployment:demo/hello. По ней контроллер строит дерево приложения и находит объекты, подлежащие удалению. С версии 3.0 это способ по умолчанию; раньше использовалась метка app.kubernetes.io/instance, которая конфликтовала с одноимённой меткой Helm-чартов.
Объекты hello в неймспейсе demo уже существуют — мы создавали их руками в прошлых частях. Argo CD спокойно берёт их под управление: при первой синхронизации он применяет манифесты поверх существующих объектов и ставит аннотацию отслеживания. Поля, которые есть в Git, получают значения из Git, а поля, которых в Git нет (те же replicas), остаются как были. На стенде в demo работал hello:1.2 в двух репликах, оставшийся от прошлых опытов; после синхронизации Deployment раскатил 1.0 по дайджесту новым ReplicaSet, сохранив две реплики, а старые ReplicaSet остались в истории выкатки. Перед тем как отдавать живое приложение под Argo CD, стоит посмотреть argocd app diff — это список всего, что успели поменять руками за 40 частей.
Статусы
У приложения два независимых статуса.
Синхронизация — совпадает ли кластер с Git:
| Статус | Значение |
|---|---|
Synced |
каждый объект из Git есть в кластере и совпадает с отрендеренным манифестом |
OutOfSync |
хотя бы один объект отличается, отсутствует в кластере или есть в кластере, но удалён из Git |
Unknown |
сравнить не удалось: Git недоступен, манифест не рендерится |
Сравнение идёт не по всему объекту, а по полям, заданным в Git: значения по умолчанию, проставленные apiserver, и поля status расхождением не считаются. Поэтому OutOfSync означает именно то, что кто-то или что-то изменило поле, которым владеет Git, — это и называется дрейфом (drift).
Здоровье — работает ли то, что применено:
| Статус | Значение |
|---|---|
Healthy |
объект в рабочем состоянии |
Progressing |
ещё не готов, но движется к готовности: Deployment раскатывается, Certificate выпускается |
Degraded |
сломан: Deployment не может поднять поды за отведённое время, Job упал |
Suspended |
приостановлен: CronJob с suspend: true, Deployment на паузе |
Missing |
объекта нет в кластере |
Для встроенных типов проверки зашиты в Argo CD: Deployment, StatefulSet и DaemonSet здоровы, когда все реплики обновлены и готовы; Service типа LoadBalancer — когда получил адрес от MetalLB; PVC — когда привязан к тому; HPA — когда смог посчитать число реплик по метрикам. Для популярных CRD проверки на языке Lua поставляются вместе с Argo CD: Certificate здоров, когда у него условие Ready: True, HTTPRoute — когда Gateway его принял. У NetworkPolicy и ServiceMonitor статуса нет, и их здоровье не оценивается.
Проверка HPA строгая: сразу после выкатки у новых подов ещё нет метрик в metrics-server, HPA пишет FailedGetResourceMetric, и приложение на 15–30 секунд становится Degraded (событие Updated health status: Healthy -> Degraded (Caused by autoscaling/HorizontalPodAutoscaler:demo/hello)), а потом само возвращается в Healthy. Уведомления о деградации стоит настраивать с задержкой, иначе они будут приходить после каждой выкатки. Для своего CRD проверку пишут на Lua в ConfigMap argocd-cm под ключом resource.customizations.health.<группа>_<вид>. Здоровье приложения — худшее из здоровья его объектов.
Порядок синхронизации: волны и хуки
Синхронизация применяет объекты не в произвольном порядке. Сортировка такая: сначала по фазе (PreSync, Sync, PostSync), внутри фазы — по волне, внутри волны — по виду (неймспейсы и CRD раньше остального, обычные объекты раньше объектов из CRD), затем по имени. Следующая волна начинается, только когда все объекты предыдущей применены и здоровы.
Волна задаётся аннотацией argocd.argoproj.io/sync-wave — целым числом, по умолчанию 0, может быть отрицательным. В base выше NetworkPolicy стоит в волне -1: политика появится и вступит в силу раньше, чем Deployment создаст первый под, и не будет ни секунды, когда под принимает трафик откуда угодно. Ещё типичные случаи — CRD раньше объектов, которые его используют, база данных раньше приложения.
Хуки — объекты, которые не являются частью желаемого состояния, а запускаются в определённый момент синхронизации. Обычно это Job. Тип задаётся аннотацией argocd.argoproj.io/hook:
| Хук | Когда |
|---|---|
PreSync |
до применения манифестов; если упал — синхронизация останавливается. Пример: миграция схемы базы |
Sync |
вместе с манифестами |
PostSync |
после того, как всё применено и здорово. Пример: проверка работоспособности, уведомление |
SyncFail |
если синхронизация завершилась ошибкой |
PreDelete, PostDelete |
до и после удаления всего приложения |
Проверка hello после каждой синхронизации — PostSync-хук, который обращается к /healthz через Service. Он лежит в overlays/prod/smoke-test.yaml и добавлен в resources overlay:
apiVersion: batch/v1
kind: Job
metadata:
name: hello-smoke
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded
spec:
backoffLimit: 2
activeDeadlineSeconds: 120
template:
metadata:
labels: { app.kubernetes.io/component: smoke-test }
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65534
seccompProfile: { type: RuntimeDefault }
containers:
- name: check
image: busybox:1.37
command: ["wget", "-q", "-O-", "http://hello/healthz"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
hook-delete-policy: BeforeHookCreation удаляет Job прошлой синхронизации перед созданием нового (имя то же), HookSucceeded — удаляет успешный Job сразу; упавший останется, и по нему можно прочитать логи. Если Job упадёт, приложение получит статус синхронизации с ошибкой, а уведомления (notifications-controller) сообщат об этом. Метка app.kubernetes.io/component: smoke-test нужна NetworkPolicy из base, которая пропускает к подам hello только перечисленных; если в неймспейсе действует запрет исходящего трафика по умолчанию из части 20, проверочному поду нужно разрешить DNS и обращение к hello.
App of Apps и ApplicationSet
Описывать каждое Application руками и применять его kubectl apply — это снова ручная работа, только на уровень выше. Два способа сделать так, чтобы и сами Application жили в Git.
App of Apps. Одно корневое Application смотрит в каталог, где лежат манифесты других Application, ApplicationSet и AppProject. Его применяют руками один раз — это единственный kubectl apply при установке, всё остальное Argo CD подтянет сам:
# clusters/stand/root.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: argocd
spec:
project: default
source:
repoURL: git@git.example.com:platform/k8s-config.git
targetRevision: main
path: clusters/stand
directory:
recurse: true
exclude: root.yaml
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated: { prune: true, selfHeal: true }
Новое приложение в кластере появляется коммитом файла в clusters/stand/apps/. Тонкость: здоровье дочерних Application для родителя по умолчанию не учитывается, поэтому волны между ними без дополнительной проверки здоровья (Lua-проверки для argoproj.io/Application в argocd-cm) не работают — родитель не ждёт, пока предыдущее приложение станет здоровым.
ApplicationSet — шаблон, из которого контроллер генерирует Application по данным генератора. Основные генераторы:
- list — явный список элементов с параметрами;
- git — по каталогам (
directories) или по файлам конфигурации (files) в репозитории: новый каталог — новое приложение; - cluster — по кластерам, зарегистрированным в Argo CD, с отбором по меткам: одно приложение на каждый кластер с меткой
env: prod; - matrix и merge — комбинации других генераторов, например «каждый компонент платформы на каждый кластер».
hello в двух окружениях — генератор list:
# clusters/stand/apps/hello.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: hello
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- list:
elements:
- { env: dev, namespace: demo-dev }
- { env: prod, namespace: demo }
template:
metadata:
name: 'hello-{{.env}}'
finalizers: [resources-finalizer.argocd.argoproj.io]
spec:
project: demo
source:
repoURL: git@git.example.com:platform/k8s-config.git
targetRevision: main
path: 'apps/hello/overlays/{{.env}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{.namespace}}'
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [ServerSideApply=true, PruneLast=true]
retry:
limit: 5
backoff: { duration: 10s, factor: 2, maxDuration: 3m }
Компоненты платформы — генератор git по каталогам: каждый подкаталог platform/ становится приложением с тем же именем, а добавить компонент — значит добавить каталог:
# clusters/stand/apps/platform.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: platform
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- git:
repoURL: git@git.example.com:platform/k8s-config.git
revision: main
directories:
- path: platform/*
template:
metadata:
name: '{{.path.basename}}'
spec:
project: platform
source:
repoURL: git@git.example.com:platform/k8s-config.git
targetRevision: main
path: '{{.path.path}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{.path.basename}}'
syncPolicy:
automated: { prune: false, selfHeal: true }
syncOptions: [ServerSideApply=true]
retry:
limit: 5
backoff: { duration: 10s, factor: 2, maxDuration: 3m }
У платформы prune: false намеренно: ошибочное удаление каталога platform/cert-manager не должно унести из кластера cert-manager вместе с CRD и всеми Certificate. Удаление компонентов платформы — редкая операция, и её делают руками.
Компоненты, которые мы ставили манифестом, описываются в каталоге platform/ файлом kustomization.yaml со ссылкой на манифест нужной версии. Так cert-manager, установленный в части 19, перешёл под Argo CD без переустановки: в Git та же версия, и синхронизация только проставила аннотации отслеживания.
# platform/cert-manager/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- https://github.com/cert-manager/cert-manager/releases/download/v1.21.3/cert-manager.yaml
- issuers.yaml
Компоненты, которые мы ставили Helm-чартом, описываются через Kustomize с полем helmCharts или отдельным Application с chart вместо path; значения чарта, которые раньше лежали в values.yaml на рабочей станции, теперь лежат в Git рядом. Traefik со шлюзом apps выглядит так:
# platform/traefik/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: traefik
resources:
- https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.3/standard-install.yaml
- namespace.yaml
- certificate.yaml
helmCharts:
- name: traefik
repo: https://traefik.github.io/charts
version: 42.3.1
releaseName: traefik
namespace: traefik
includeCRDs: true
valuesInline:
providers:
kubernetesGateway: { enabled: true }
service:
annotations:
metallb.io/loadBalancerIPs: 192.168.10.200
gateway:
name: apps
listeners:
web:
port: 8000
protocol: HTTP
namespacePolicy: { from: All }
https:
port: 8443
protocol: HTTPS
hostname: "*.apps.example.com"
namespacePolicy: { from: All }
certificateRefs:
- name: apps-example-com-tls
Kustomize запускает Helm только с ключом --enable-helm, и Argo CD по умолчанию его не передаёт. Без него приложение traefik получает статус Unknown с ошибкой trouble configuring builtin HelmChartInflationGenerator. Ключ задаётся один раз для всего Argo CD в ConfigMap argocd-cm:
$ kubectl -n argocd patch cm argocd-cm --type merge \
-p '{"data":{"kustomize.buildOptions":"--enable-helm"}}'
configmap/argocd-cm patched
Вторая тонкость — объекты CRD, которые ставятся в той же синхронизации, что и сам CRD: IPAddressPool рядом с манифестом MetalLB, ClusterIssuer рядом с cert-manager. Перед применением Argo CD проверяет каждый объект пробным запросом --dry-run=server, а пока CRD нет, apiserver не знает такого типа, и вся синхронизация отклоняется. Аннотация argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true на таких объектах отключает для них пробный запрос, а волна argocd.argoproj.io/sync-wave: "1" ставит их после Deployment контроллера и его вебхука, которые должны стать здоровыми раньше:
# platform/metallb/pool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: apps
namespace: metallb-system
annotations:
argocd.argoproj.io/sync-wave: "1"
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
spec:
addresses: [192.168.10.200-192.168.10.203]
Проекты и RBAC
AppProject — граница того, что приложениям разрешено. Проект default, создаваемый при установке, разрешает всё: любой репозиторий, любой кластер, любые объекты, включая ClusterRoleBinding. Приложения команд в него не кладут.
# clusters/stand/projects/demo.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: demo
namespace: argocd
spec:
description: Приложения в неймспейсах demo и demo-dev
sourceRepos:
- git@git.example.com:platform/k8s-config.git
destinations:
- { server: https://kubernetes.default.svc, namespace: demo }
- { server: https://kubernetes.default.svc, namespace: demo-dev }
clusterResourceWhitelist: []
namespaceResourceBlacklist:
- { group: "", kind: ResourceQuota }
- { group: "", kind: LimitRange }
- { group: rbac.authorization.k8s.io, kind: Role }
- { group: rbac.authorization.k8s.io, kind: RoleBinding }
roles:
- name: developers
description: Просмотр и ручная синхронизация
policies:
- p, proj:demo:developers, applications, get, demo/*, allow
- p, proj:demo:developers, applications, sync, demo/hello-dev, allow
groups:
- demo-developers
Что это даёт: приложения проекта demo берут манифесты только из репозитория конфигурации, применяются только в demo и demo-dev, не могут создать ни одного объекта уровня кластера (пустой clusterResourceWhitelist) и не могут в своём неймспейсе поменять квоты, лимиты и права. Даже если в overlay по ошибке или злому умыслу окажется ClusterRole или ClusterRoleBinding, синхронизация откажется его применять. На стенде ClusterRole, закоммиченная в overlays/dev, так и осталась только в Git:
$ kubectl -n argocd get app hello-dev -o jsonpath='{.status.operationState.message}{"\n"}'
one or more synchronization tasks are not valid: resource rbac.authorization.k8s.io:ClusterRole is not permitted in project demo. Retrying attempt #1 at 2026-11-07T12:13:09Z.
$ kubectl get clusterrole hello-dev-reader
Error from server (NotFound): clusterroles.rbac.authorization.k8s.io "hello-dev-reader" not found
Проект platform для компонентов платформы, наоборот, разрешает любые неймспейсы и объекты уровня кластера: CRD, вебхуки и ClusterRole — обычное содержимое чартов. Поэтому в него кладут только то, что ведёт команда платформы, а репозитории ограничивают тем же sourceRepos:
# clusters/stand/projects/platform.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: platform
namespace: argocd
spec:
description: Компоненты платформы
sourceRepos:
- git@git.example.com:platform/k8s-config.git
- https://traefik.github.io/charts
destinations:
- { server: https://kubernetes.default.svc, namespace: "*" }
clusterResourceWhitelist:
- { group: "*", kind: "*" }
Контроллер Argo CD при этом остаётся администратором кластера — проект ограничивает не его, а то, что он согласится сделать для конкретного приложения.
RBAC Argo CD — собственный, поверх RBAC Kubernetes: он определяет, что пользователи могут делать с приложениями через API и интерфейс Argo CD. Правила — в ConfigMap argocd-rbac-cm в формате библиотеки авторизации Casbin, на которой этот RBAC построен (p — разрешение, g — членство группы в роли):
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
data:
policy.default: ""
policy.csv: |
g, platform-admins, role:admin
p, role:sre, applications, get, */*, allow
p, role:sre, applications, sync, */*, allow
p, role:sre, logs, get, */*, allow
g, sre, role:sre
policy.default: "" — без явного разрешения никто ничего не видит; встречающийся в примерах role:readonly по умолчанию показывает всем вошедшим все приложения, включая параметры и манифесты. Группы (platform-admins, sre, demo-developers) приходят из единого входа — из OIDC-токена через Dex. Права людей в самом Kubernetes при этом можно оставить только на чтение (часть 29): изменения идут через Git, а синхронизацию при необходимости запускают через Argo CD.
Секреты в GitOps
Всё в Git — кроме секретов в открытом виде: репозиторий клонируют на ноутбуки, его читает CI, история хранит каждый когда-либо закоммиченный пароль навсегда. Secret в base64 — это не шифрование (часть 12), в Git ему не место. Три распространённых подхода:
- Sealed Secrets — контроллер в кластере с парой ключей. Утилита
kubesealна рабочей станции шифрует Secret открытым ключом контроллера и превращает его в объект SealedSecret, который безопасно коммитить: расшифровать его может только контроллер этого кластера, он и создаёт из него обычный Secret. Минус — привязка к ключу конкретного кластера: потеря ключа контроллера означает перешифровку всех секретов. - SOPS — утилита, которая шифрует в YAML только значения, оставляя ключи читаемыми, ключами age или облачного KMS. Argo CD сам SOPS не понимает: нужен плагин — config management plugin (отдельный контейнер рядом с repo-server, который расшифровывает при рендеринге, например с KSOPS — плагином Kustomize для SOPS). При этом ключ расшифровки должен быть у repo-server, а отрендеренные с секретами манифесты оседают в его кеше и в Redis — это стоит учитывать.
- External Secrets Operator — оператор, который читает секреты из внешнего хранилища (хранилище секретов HashiCorp Vault, облачные менеджеры секретов) и создаёт из них Secret. В Git лежит только объект ExternalSecret — ссылка «возьми ключ
hello/db-passwordиз такого-то хранилища», без самого значения. Это лучший вариант, если внешнее хранилище секретов уже есть: ротация пароля в хранилище доходит до кластера без коммита.
Для стенда без внешнего хранилища проще всего Sealed Secrets; для организации с Vault — External Secrets Operator. Важно одно: ни в одном варианте открытое значение не попадает в Git.
Смотрим руками
Начальная загрузка — тот самый единственный ручной kubectl apply (плюс Secret с deploy key из раздела выше):
$ git clone git@git.example.com:platform/k8s-config.git && cd k8s-config
$ kubectl apply -f clusters/stand/root.yaml
application.argoproj.io/root created
$ argocd app list
NAME CLUSTER NAMESPACE PROJECT STATUS HEALTH SYNCPOLICY CONDITIONS REPO PATH TARGET
argocd/cert-manager https://kubernetes.default.svc cert-manager platform Synced Healthy Auto <none> git@git.example.com:platform/k8s-config.git platform/cert-manager main
argocd/hello-dev https://kubernetes.default.svc demo-dev demo Synced Healthy Auto-Prune <none> git@git.example.com:platform/k8s-config.git apps/hello/overlays/dev main
argocd/hello-prod https://kubernetes.default.svc demo demo Synced Healthy Auto-Prune <none> git@git.example.com:platform/k8s-config.git apps/hello/overlays/prod main
argocd/metallb https://kubernetes.default.svc metallb platform Synced Healthy Auto <none> git@git.example.com:platform/k8s-config.git platform/metallb main
argocd/namespaces https://kubernetes.default.svc namespaces platform Synced Healthy Auto <none> git@git.example.com:platform/k8s-config.git platform/namespaces main
argocd/root https://kubernetes.default.svc argocd default Synced Healthy Auto-Prune <none> git@git.example.com:platform/k8s-config.git clusters/stand main
argocd/traefik https://kubernetes.default.svc traefik platform Synced Healthy Auto <none> git@git.example.com:platform/k8s-config.git platform/traefik main
Такую картину argocd app list показывает не сразу. На стенде от kubectl apply до последнего Synced Healthy прошло пятнадцать минут (в них вошла и перезагрузка мастера, о которой говорилось в разделе об установке), и по дороге было всё, о чём говорилось выше: traefik ждал ключа --enable-helm, сертификат шлюза — запуска cert-manager, а hello-dev и hello-prod повторяли синхронизацию по retry, пока не появились CRD Gateway API. Никто ничего не применял руками, кроме одной правки argocd-cm: система сошлась сама.
Объекты Argo CD — обычные объекты Kubernetes, их видно и через kubectl:
$ kubectl -n argocd get applications,applicationsets,appprojects
NAME SYNC STATUS HEALTH STATUS
application.argoproj.io/cert-manager Synced Healthy
application.argoproj.io/hello-dev Synced Healthy
application.argoproj.io/hello-prod Synced Healthy
application.argoproj.io/metallb Synced Healthy
application.argoproj.io/namespaces Synced Healthy
application.argoproj.io/root Synced Healthy
application.argoproj.io/traefik Synced Healthy
NAME AGE
applicationset.argoproj.io/hello 15m
applicationset.argoproj.io/platform 15m
NAME AGE
appproject.argoproj.io/default 18m
appproject.argoproj.io/demo 15m
appproject.argoproj.io/platform 15m
Тот же список в веб-интерфейсе:

Дерево одного приложения:
$ argocd app get hello-prod
Name: argocd/hello-prod
Project: demo
Server: https://kubernetes.default.svc
Namespace: demo
URL: https://localhost:8080/applications/hello-prod
Source:
- Repo: git@git.example.com:platform/k8s-config.git
Target: main
Path: apps/hello/overlays/prod
SyncWindow: Sync Allowed
Sync Policy: Automated (Prune)
Sync Status: Synced to main (fd6e8da)
Health Status: Healthy
GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
networking.k8s.io NetworkPolicy demo hello Synced networkpolicy.networking.k8s.io/hello serverside-applied
policy PodDisruptionBudget demo hello Synced Healthy PodDisruptionBudget has SufficientPods
Service demo hello Synced Healthy service/hello serverside-applied
apps Deployment demo hello Synced Healthy deployment.apps/hello serverside-applied
autoscaling HorizontalPodAutoscaler demo hello Synced Healthy the HPA controller was able to update the target scale to 2
gateway.networking.k8s.io HTTPRoute demo hello Synced Healthy HTTPRoute is healthy
monitoring.coreos.com ServiceMonitor demo hello Synced servicemonitor.monitoring.coreos.com/hello serverside-applied
batch Job demo hello-smoke Succeeded Synced PostSync Reached expected number of succeeded pods
NetworkPolicy применена первой — волна -1; у неё и у ServiceMonitor нет оценки здоровья. В колонке MESSAGE у объектов с проверкой здоровья — её вывод, у остальных — результат apply. Строка Job осталась от PostSync-хука: сам Job после успеха удалён (HookSucceeded), а в дереве приложения сохранился итог. В веб-интерфейсе то же дерево нагляднее: слева приложение, правее его объекты, а у Deployment — ReplicaSet и поды, которых в Git нет, но которые порождены объектами из Git:

Снимок сделан уже после продвижения версии 1.1, о котором ниже, поэтому у Deployment четыре ReplicaSet: два от ручных выкаток прошлых частей, один от первой синхронизации и один от новой версии. Аннотация отслеживания на объекте:
$ kubectl -n demo get deployment hello -o jsonpath='{.metadata.annotations.argocd\.argoproj\.io/tracking-id}{"\n"}'
hello-prod:apps/Deployment:demo/hello
Дрейф. Поменяем образ в dev руками, в обход Git:
$ kubectl -n demo-dev set image deployment/hello hello=ghcr.io/example/hello:debug
deployment.apps/hello image updated
$ kubectl -n argocd get events --field-selector involvedObject.name=hello-dev \
--sort-by=.lastTimestamp -o custom-columns=TIME:.lastTimestamp,REASON:.reason,MSG:.message | tail -n 6
2026-11-07T12:09:22Z OperationStarted Initiated automated sync to 'fd6e8dad5ec5ce28fa42f2f7052b6c755f12489a'
2026-11-07T12:09:22Z ResourceUpdated Updated sync status: Synced -> OutOfSync
2026-11-07T12:09:22Z ResourceUpdated Updated health status: Healthy -> Progressing (Caused by apps/Deployment:demo-dev/hello)
2026-11-07T12:09:22Z OperationCompleted Partial sync operation to fd6e8dad5ec5ce28fa42f2f7052b6c755f12489a succeeded
2026-11-07T12:09:22Z ResourceUpdated Updated sync status: OutOfSync -> Synced
2026-11-07T12:09:22Z ResourceUpdated Updated health status: Progressing -> Healthy
$ kubectl -n demo-dev get deployment hello -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
ghcr.io/example/hello:1.1
$ kubectl -n demo-dev get rs
NAME DESIRED CURRENT READY AGE
hello-6b67c54cc7 0 0 0 15s
hello-76f76bb446 1 1 1 102s
Контроллер увидел изменение через watch, пометил приложение OutOfSync и в ту же секунду вернул образ из Git — это selfHeal. Синхронизация частичная (Partial sync operation): Argo CD применил только разошедшийся Deployment, а не всё приложение. Deployment успел создать ReplicaSet с образом debug, но под из него не дождался ни одной готовой реплики, и старый под продолжал работать. Без него приложение так и осталось бы OutOfSync, а расхождение показала бы команда argocd app diff hello-dev. Временно выключить самовосстановление у приложения, порождённого ApplicationSet, командой argocd app set не выйдет: контроллер ApplicationSet вернёт настройки из шаблона так же, как Argo CD возвращает объекты из Git.
Изменение через Git. Продвигаем версию 1.1 в prod: в overlays/prod/kustomization.yaml меняются тег и дайджест. До коммита стоит посмотреть, что именно изменится в кластере, — argocd app diff с ключом --local сравнивает локальную копию репозитория с живыми объектами:
$ git diff
diff --git a/apps/hello/overlays/prod/kustomization.yaml b/apps/hello/overlays/prod/kustomization.yaml
index 17f7d80..b392130 100644
--- a/apps/hello/overlays/prod/kustomization.yaml
+++ b/apps/hello/overlays/prod/kustomization.yaml
@@ -8,5 +8,5 @@ resources:
- smoke-test.yaml
images:
- name: ghcr.io/example/hello
- newTag: "1.0"
- digest: sha256:a5066cd40ef37b9b889ddfbe9c1860b3afe231b96b975000e0edca3fdd6f4a4a
+ newTag: "1.1"
+ digest: sha256:2d8774f801594f2b1b115963e54ec54b1b49f95c2dece1d6b0fd1e59ab447c7b
$ argocd app diff hello-prod --local "$PWD" --server-side-generate
===== apps/Deployment demo/hello ======
32c32
< - image: ghcr.io/example/hello:1.0@sha256:a5066cd40ef37b9b889ddfbe9c1860b3afe231b96b975000e0edca3fdd6f4a4a
---
> - image: ghcr.io/example/hello:1.1@sha256:2d8774f801594f2b1b115963e54ec54b1b49f95c2dece1d6b0fd1e59ab447c7b
$ echo $?
1
Слева (<) — то, что сейчас в кластере, справа (>) — то, что будет после синхронизации. Ничего лишнего: меняется только образ. Код выхода 1 значит «есть разница», 0 — «совпадает», так что команду можно поставить в проверку запроса на слияние. Два ключа здесь не случайны. С --server-side-generate CLI отправляет файлы на repo-server, и рендерит их тот же Kustomize с теми же kustomize.buildOptions, что и при синхронизации; без этого ключа CLI пишет, что локальный рендер устарел, и падает, если на машине нет отдельного бинарника kustomize. А --local здесь — корень репозитория, причём абсолютным путём: путь apps/hello/overlays/prod CLI достраивает из настроек приложения, а относительный путь отвергает с ошибкой full path does not contain base path.
Коммит, ревью, слияние в main — и Argo CD доставит. Но argocd app wait сразу после push не ждёт ничего:
$ git commit -am 'Promote hello 1.1 to prod' && git push
$ argocd app wait hello-prod --sync --health --timeout 300
...
Sync Status: Synced to main (fd6e8da)
Health Status: Healthy
Команда вернулась мгновенно: приложение действительно Synced и Healthy — к старому коммиту fd6e8da. Без веб-хука Argo CD узнаёт о push при очередном опросе Git (до трёх минут), и до этого ждать ему нечего. Поэтому либо ждут, пока в status.sync.revision появится новый коммит, либо сразу просят перечитать Git командой argocd app get hello-prod --refresh, а потом ждут. На стенде опрос пришёлся удачно, через 46 секунд после push:
$ kubectl -n argocd get events --field-selector involvedObject.name=hello-prod \
--sort-by=.lastTimestamp -o custom-columns=TIME:.lastTimestamp,REASON:.reason,MSG:.message | tail -n 7
2026-11-07T12:11:28Z OperationStarted Initiated automated sync to 'e114db0f235e327ab5e65cf2ba7340f5ddfa103f'
2026-11-07T12:11:28Z ResourceUpdated Updated sync status: Synced -> OutOfSync
2026-11-07T12:11:31Z ResourceUpdated Updated sync status: OutOfSync -> Synced
2026-11-07T12:11:31Z ResourceUpdated Updated health status: Healthy -> Progressing (Caused by apps/Deployment:demo/hello)
2026-11-07T12:11:33Z ResourceUpdated Updated health status: Progressing -> Healthy
2026-11-07T12:11:40Z ResourceUpdated Updated health status: Healthy -> Degraded (Caused by autoscaling/HorizontalPodAutoscaler:demo/hello)
2026-11-07T12:11:40Z OperationCompleted Sync operation to e114db0f235e327ab5e65cf2ba7340f5ddfa103f succeeded
$ argocd app history hello-prod
SOURCE git@git.example.com:platform/k8s-config.git
ID DATE REVISION
0 2026-11-07 12:08:05 +0000 UTC main (fd6e8da)
1 2026-11-07 12:11:40 +0000 UTC main (e114db0)
Между Progressing -> Healthy и окончанием операции — семь секунд PostSync-хука hello-smoke. Degraded от HPA — те самые секунды без метрик у новых подов, через 30 секунд приложение снова Healthy. Новая версия отвечает через Traefik:
$ curl -s https://hello.apps.example.com/ -D - -o /dev/null | grep -iE '^HTTP|x-app'
HTTP/2 200
x-app-version: 1.1
Откат — тоже через Git: git revert коммита и push. Команда argocd app rollback существует, но при включённой автоматической синхронизации она запрещена: Argo CD немедленно вернул бы состояние из Git, и откат в обход Git был бы тем самым дрейфом.
$ argocd app rollback hello-prod 0
{"level":"fatal","msg":"rpc error: code = FailedPrecondition desc = rollback cannot be initiated when auto-sync is enabled","time":"2026-11-07T12:11:58Z"}
Неудачный коммит и retry. У retry есть обратная сторона. ClusterRole из раздела о проектах попала в main, синхронизация hello-dev начала отказывать, и её откатили git revert. Приложение стало Synced к новому коммиту, но операция синхронизации старого, неудачного коммита продолжала повторяться по retry — до исчерпания limit: 5 это почти восемь минут при паузах 10, 20, 40, 80 и 160 секунд, а пока она идёт, новая синхронизация не начнётся. Такую операцию прерывают руками:
$ argocd app terminate-op hello-dev
Application 'hello-dev' operation terminating
$ kubectl -n argocd get app hello-dev -o jsonpath='{.status.operationState.message}{"\n"}'
Operation terminated (retried 5 times).
Flux
Второй распространённый GitOps-инструмент — Flux, тоже проект CNCF. Идея та же — pull-агент, постоянное согласование, — а устройство другое. Flux — набор независимых контроллеров, каждый со своими CRD: source-controller забирает исходники (GitRepository, HelmRepository, OCIRepository), kustomize-controller применяет каталоги (Kustomization — объект Flux, не путать с файлом Kustomize, хотя внутри он вызывает именно Kustomize), helm-controller ставит чарты настоящими Helm-релизами (HelmRelease, видимые в helm list), notification-controller шлёт уведомления и принимает веб-хуки. Собственного веб-интерфейса у базовой установки нет: состояние смотрят через flux CLI и kubectl, а веб-интерфейс предлагает Flux Operator — отдельный оператор, который устанавливает и обновляет сам Flux. SOPS Flux расшифровывает сам, без плагинов. Argo CD выбран для этой серии за наглядное дерево приложений, ApplicationSet и проекты с собственным RBAC; Flux проще встраивается в кластер как «ещё несколько контроллеров» и ближе к нативным механизмам Kubernetes. Оба — рабочие инструменты, и выбор обычно определяется тем, нужен ли команде интерфейс и многопользовательский доступ к доставке.
Итог серии
Эта часть — последняя основная. Серия началась с цикла согласования: контроллер сравнивает желаемое состояние с фактическим и сводит разницу. Дальше мы собрали на kubeadm кластер, где этот цикл крутится в каждом компоненте: рабочие нагрузки, сеть, хранилище, упаковку манифестов, безопасность, наблюдаемость и обслуживание самого кластера, а потом добавили в него Windows-узлы. GitOps замыкает круг: тот же цикл согласования, только желаемое состояние теперь лежит в Git, а не в головах и терминалах администраторов. Всё, что за 40 частей ставилось руками, становится каталогом в репозитории, который можно прочитать, проверить на ревью и развернуть заново на пустом кластере.
Куда двигаться дальше:
- Отказоустойчивый control plane. Стенд серии живёт на одном мастере, и в частях 34 и 35 это не раз оборачивалось простоем API. Три мастера с etcd на каждом, общий адрес API через kube-vip и Calico по BGP разобраны в серии «Отказоустойчивый Kubernetes на BGP».
- Сервисная сеть (service mesh) — слой прокси рядом с каждым подом или на каждом узле, который даёт взаимную TLS-аутентификацию между сервисами, повторы, разделение трафика между версиями и метрики каждого запроса без изменений в приложении.
- Политики Kyverno — admission-контроллер, в котором правила вида «образы только из
ghcr.io/example/», «у каждого Deployment есть requests и limits» или «добавлять метку команды» описываются объектами Kubernetes и хранятся в том же репозитории конфигурации. - Мультикластер — несколько кластеров (по окружениям, площадкам, командам), которыми управляет один Argo CD через генератор cluster в ApplicationSet, а сами кластеры создаются декларативно, например через Cluster API — набор контроллеров, описывающих кластеры и их машины объектами Kubernetes.
Спасибо, что прошли серию до конца.
Источники
- GitOps: принципы OpenGitOps.
- Argo CD: архитектура, установка, декларативная настройка, приватные репозитории, Kustomize, волны синхронизации и хуки, App of Apps, ApplicationSet, проекты, RBAC, управление секретами.
- GitHub: ключи развёртывания.
- Секреты: Sealed Secrets, SOPS, KSOPS, External Secrets Operator.
- Flux: компоненты.
Дальше — бонусная часть: kind, k3s и minikube — кластеры на одной машине, чем они отличаются от кластера на kubeadm и когда какой из них уместен.
Оставить комментарий могут только зарегистрированные пользователи.
Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.