Классический деплой в Kubernetes — это kubectl apply или helm upgrade из пайплайна: конвейер имеет доступ в кластер и «пушит» изменения. Проблема — кластер и репозиторий легко расходятся: кто-то поправил объект руками, и что реально крутится в проде, уже не совпадает с тем, что в Git. Argo CD переворачивает подход: не пайплайн пушит в кластер, а кластер сам подтягивает состояние из Git. Это называется GitOps.
Разница между push и pull видна в момент дрейфа: пусть в Git у приложения orders стоит replicas: 2, а кто-то поднял их до шести командой kubectl scale прямо в кластере.
Git становится источником правды не по договорённости, а потому что кто-то внутри кластера непрерывно сверяет его с репозиторием. Без такой сверки ручная правка живёт до следующего деплоя; с selfHeal — секунды.
Что это
Argo CD — инструмент непрерывной доставки (CD) для Kubernetes по модели GitOps. Идея в одной фразе: Git — источник правды. В репозитории лежат желаемые манифесты (или Helm-чарты), а Argo CD, живущий внутри кластера, постоянно сравнивает «что в Git» и «что в кластере» и приводит кластер к состоянию из Git.
Что это даёт:
- Декларативность. Хотите что-то изменить в проде — делаете commit в Git, а не
kubectlруками. Вся история изменений — в системе контроля версий, с ревью через pull request. - Автосинхронизация и лечение дрейфа. Argo CD замечает расхождение: если объект в кластере поправили мимо Git, он подсветит это или вернёт как в репозитории (self-heal).
- Наглядность. В UI видно состояние каждого приложения: синхронизировано с Git или нет, здорово ли оно.
Про ручную правку Argo CD узнаёт почти сразу: он живёт внутри кластера и следит за объектами через watch API. Лечение всё же не мгновенное: у self-heal задержка около пяти секунд по умолчанию, а после нескольких сорванных попыток паузы растут, чтобы не биться в стену. А вот новые коммиты в репозитории он забирает опросом — по умолчанию раз в три минуты; чтобы узнавал сразу, настраивают webhook от репозитория.
И одно следствие, о котором лучше договориться с командой заранее. Связка automated + selfHeal меняет не только процесс деплоя, но и то, как чинят прод в аварии. Привычный ход «зайти и поправить kubectl-ом, пока разбираемся» перестаёт работать: Argo CD вернёт всё как в Git через несколько секунд, и дежурный будет смотреть, как его правка исчезает.
Чинить придётся коммитом — либо временно отключать автосинхронизацию на конкретном приложении (в UI это кнопка, в манифесте — убрать automated). Это нормально, к этому просто надо быть готовым до первого ночного вызова, а не во время него.
Контур GitOps замкнут: Argo CD берёт желаемое состояние из Git, текущее читает из кластера и применяет разницу к его объектам, поэтому кластер стоит и на входе сверки, и на её выходе.
Как выглядит
Само приложение Argo CD описывается объектом Application: где лежит исходник (репозиторий + путь) и куда разворачивать (кластер + namespace):
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: orders
namespace: argocd
spec:
project: default # обязательное поле, без него манифест не примут
source:
repoURL: https://git.example.com/infra/orders.git
targetRevision: main
path: deploy/prod # тут лежат манифесты или Helm-чарт
destination:
server: https://kubernetes.default.svc
namespace: orders
syncPolicy:
automated:
prune: true # удалять то, что убрали из Git
selfHeal: true # возвращать состояние из Git при дрейфе
syncOptions:
- CreateNamespace=true # создать namespace orders, если его ещё нет
Две строки тут спасают от двух первых граблей. project — обязательное поле: забыли — и кластер просто не примет манифест; если своих проектов в Argo CD не заводили, пишите default. А CreateNamespace=true нужен потому, что namespace из destination сам по себе не появляется: без этой опции первая же синхронизация упадёт с ошибкой «namespace not found», хотя в манифесте всё написано верно.
Дальше деплой выглядит так: разработчик правит deploy/prod в Git и открывает pull request; после мержа Argo CD видит новую ревизию main и сам применяет её в namespace orders. Никакого kubectl из пайплайна — только commit.
Кто меняет тег образа
Главный практический вопрос, который повисает после слов «деплой — это коммит»: конвейер собрал образ, а кто положил новый тег в репозиторий? Сам по себе он туда не попадёт. Вариантов три, и выбирают один осознанно.
Шаг в конвейере после публикации образа. Самый распространённый способ: сборка заканчивается коммитом в репозиторий манифестов.
# в конвейере, после docker push
git clone https://git.example.com/infra/orders.git manifests
cd manifests
yq -i '.spec.template.spec.containers[0].image = "ghcr.io/acme/orders@'"$DIGEST"'"' deploy/prod/deployment.yaml
git commit -am "orders: $DIGEST" && git push
Плюс — всё видно в истории репозитория, и это обычный коммит с автором-ботом. Минус — конвейеру нужны права на запись в репозиторий манифестов, и коммиты бота смешиваются с людскими (лечится отдельной ветвью или отдельным репозиторием).
Компонент в кластере, который сам следит за реестром. Argo CD Image Updater наблюдает за появлением новых тегов по заданному правилу и обновляет либо репозиторий (коммитом от себя), либо параметры приложения. Плюс — конвейер вообще не знает про манифесты. Минус — ещё один компонент с правами на запись в репозиторий, и логика «какую версию считать новой» переезжает в его настройки.
Не менять тег вовсе. Приложение ссылается на дайджест, который подставляет тот же шаг конвейера; или на плавающий тег среды (orders:staging), который конвейер передвигает в реестре. Второй вариант выглядит проще всего и ломает главное свойство подхода: по репозиторию больше не видно, что развёрнуто, потому что тег тот же, а образ за ним другой. Дайджест — правильный вариант, и именно его даёт шаг сборки, о чём статья про реестры.
Отдельно стоит решить, где живут манифесты: в репозитории сервиса или в отдельном репозитории манифестов. В репозитории сервиса удобно разработчику — код и развёртывание рядом, правка в одном запросе на изменение. Отдельный репозиторий удобнее платформе: единые права, видно состояние всех сервисов сразу, бот пишет только туда.
На практике выбор делают один раз в начале и потом не переигрывают, потому что переезд затрагивает все приложения; для одной команды с несколькими сервисами репозиторий сервиса обычно достаточен, для платформы с десятками сервисов — отдельный.
Sync status против Health status
В интерфейсе у приложения две колонки, и их путают постоянно, хотя они отвечают на разные вопросы.
Sync status (Synced / OutOfSync) — совпадает ли то, что в кластере, с тем, что в репозитории. Это утверждение про манифесты, и только про них.
Health status (Healthy / Progressing / Degraded / Missing) — работает ли то, что развёрнуто. Argo CD смотрит на состояние объектов: у Deployment — сошлось ли число готовых реплик с желаемым, у пода — проходят ли пробы, у сервиса и задачи — свои признаки.
Отсюда четыре сочетания, и каждое означает своё:
| Sync | Health | Что случилось |
|---|---|---|
Synced | Healthy | всё в порядке |
Synced | Degraded | манифест применён, а приложение не работает: поды падают, пробы не проходят, образа нет |
OutOfSync | Healthy | работает старая версия: синхронизация не прошла или выключена, а прежние поды живы |
Synced | Progressing | выкат идёт, новые поды ещё поднимаются |
Второе сочетание и ловит половину инцидентов. «Argo CD зелёный» обычно говорят про синхронизацию — а зелёная синхронизация означает лишь, что файлы применены. Правило для дежурного: сначала смотреть Health, и только потом Sync.
Полезно знать и про Progressing, который не заканчивается: у Deployment есть срок, за который выкат должен завершиться (progressDeadlineSeconds, по умолчанию 600 секунд), и после него состояние становится Degraded. То есть десять минут приложение будет выглядеть «почти готово», хотя новые поды падают — поэтому на Degraded ставят сигнал, а не ждут, пока кто-нибудь заметит.
Порядок и миграции: волны и хуки
В статье про доставку миграция базы делается задачей до выката новых подов. В подходе с репозиторием как источником правды всё применяется одной синхронизацией, поэтому нужен способ задать порядок — и он есть.
Волны синхронизации (sync-wave) — аннотация с числом: объекты применяются по возрастанию, и Argo CD ждёт, пока объекты предыдущей волны станут работоспособными.
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-1" # раньше всех: миграция
Хуки (hook) — то же, что у Helm, только своими аннотациями: PreSync (до применения остального), Sync, PostSync (после), SyncFail (при неудаче).
apiVersion: batch/v1
kind: Job
metadata:
name: orders-migrate
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
Практическая раскладка выглядит так: миграция — PreSync (упала — выката не будет), основные объекты — обычная синхронизация, проверка после выката (например, прогон дымовых тестов) — PostSync. Волны берут, когда порядок нужен внутри одной синхронизации между своими объектами: сначала пространство имён и секреты, потом база, потом приложение.
И то же ограничение, что у Helm: при откате коммита хуки не отменяют выполненную миграцию. Схему меняют совместимо — так, чтобы старая версия работала с новой схемой.
Чем за это платят
Плюсы описаны выше, и честный разговор требует назвать цену.
Ещё один компонент с широкими правами. Argo CD умеет применять в кластере что угодно — значит, доступ к нему равен доступу к кластеру, а компрометация репозитория манифестов равна компрометации прода. Отсюда обязательные вещи: ограниченные проекты (какие репозитории и какие пространства имён разрешены каждому), вход через провайдера входа компании, а не общий пароль администратора, и обязательное ревью изменений в репозитории манифестов.
Аварийная правка руками перестаёт работать. Это уже сказано выше и заслуживает повтора: при включённом самолечении правка через kubectl живёт секунды. Значит, у дежурного должен быть заранее отработанный путь — выключить автосинхронизацию на приложении, сделать правку, потом привести репозиторий в порядок.
Новый класс задач «почему не синхронизируется». Недоступный репозиторий, неверная ветка, ошибка шаблона, зависшая операция синхронизации, расхождение из-за того, что объект правит другой контроллер (типичный случай — число реплик, которое меняет автомасштабирование, и Argo CD возвращает его назад). Последнее лечится исключением поля из сравнения (ignoreDifferences), и об этом узнают не сразу.
Задержка. Опрос репозитория раз в три минуты означает, что коммит доезжает не мгновенно; вебхук это лечит, но его надо настроить.
Когда не брать: один сервис, один человек, выкат раз в неделю — обычного kubectl apply из конвейера достаточно, и он понятнее. Смысл появляется, когда сервисов и сред становится несколько, когда важно видеть в репозитории, что именно развёрнуто, и когда ручные правки в кластере успели стать проблемой.
Глубже: секреты в репозиториирасширенное
Второй вопрос, который встаёт сразу: манифесты в Git, а пароли? Объект Secret хранит значения в base64, и это не шифрование — положить его в репозиторий означает опубликовать пароль.
Три работающих подхода.
Зашифрованный секрет (Sealed Secrets). В кластере стоит контроллер со своей парой ключей. Вы шифруете значение его открытым ключом и кладёте в репозиторий объект SealedSecret — расшифровать его может только этот контроллер в этом кластере.
kubectl create secret generic orders-db --dry-run=client -o yaml \
--from-literal=password='...' | kubeseal -o yaml > deploy/prod/sealed-db.yaml
Просто и не требует внешнего хранилища. Оборотная сторона: значение привязано к кластеру (для нового кластера надо перешифровать), а закрытый ключ контроллера становится тем, что обязательно нужно копировать в резерв.
Файл, зашифрованный ключом (SOPS). Значения шифруются ключом из внешнего хранилища (облачный сервис ключей, age, GPG), файл лежит в репозитории, а расшифровывает его плагин Argo CD при синхронизации. Удобно тем, что в репозитории виден какой ключ поменялся, а не только факт изменения: SOPS шифрует значения, оставляя структуру открытой.
Ссылка на внешнее хранилище (External Secrets Operator). В репозитории лежит объект ExternalSecret, который говорит «возьми пароль по такому-то пути в Vault и создай из него Secret в кластере». Паролей в репозитории нет вообще — есть только указатели.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: orders-db
spec:
secretStoreRef:
name: vault
kind: ClusterSecretStore
target:
name: orders-db
data:
- secretKey: password
remoteRef:
key: prod/orders/db
property: password
Это правильный выбор, если хранилище секретов в компании уже есть: ротация пароля происходит в хранилище и доезжает до кластера сама, а репозиторий вообще не участвует. Если хранилища нет, поднимать его ради этого — отдельный проект, и тогда начинают с Sealed Secrets.
Чего делать не стоит: держать пароли в значениях Helm в открытом виде «пока временно», и складывать их в приватный репозиторий, считая приватность защитой. Приватный репозиторий читают все разработчики компании и все конвейеры.
Глубже: когда приложений становится многорасширенное
Один объект Application на сервис — нормально, пока сервисов три. Дальше появляются два приёма.
App of apps. Одно приложение, единственная задача которого — развернуть другие объекты Application. Получается корень, из которого растёт всё остальное: добавили сервис — добавили один файл в репозиторий корня. Просто и работает на любой версии.
ApplicationSet. Объект, который порождает приложения по шаблону и списку: по каталогам в репозитории, по списку кластеров, по веткам, по запросу к API. Одно описание — десятки приложений.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: services
namespace: argocd
spec:
generators:
- git:
repoURL: https://git.example.com/infra/manifests.git
revision: main
directories:
- path: services/*
template:
metadata:
name: '{{path.basename}}'
spec:
project: default
source:
repoURL: https://git.example.com/infra/manifests.git
targetRevision: main
path: '{{path}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{path.basename}}'
syncPolicy:
automated: {prune: true, selfHeal: true}
Теперь новый каталог в services/ означает новое приложение без единой правки в Argo CD. Тот же приём разворачивает один сервис в несколько кластеров и создаёт временные среды на каждую ветку — по этому же списку они и удаляются, когда ветка закрыта.
Коротко
- Argo CD — GitOps-инструмент непрерывной доставки в Kubernetes: Git — источник правды, а не пайплайн с доступом в кластер. Кластер сам подтягивает желаемое состояние из репозитория, а не получает push извне.
- Объект
Applicationзадаёт: откуда (repo + path) и куда (кластер + namespace) разворачивать. Манифесты держат либо в репозитории сервиса (удобно команде), либо в отдельном репозитории (удобно платформе) — решают один раз в начале. automated+selfHealдержат кластер синхронным с Git и чинят ручной дрейф за секунды; обратная сторона — чинить прод черезkubectlбольше нельзя, правку отменят. Цена: компонент с правами на весь кластер, невозможность аварийной правки руками, задачи «почему не синхронизируется» и конфликт с автомасштабированием, который лечатignoreDifferences.projectобязателен, а namespace изdestinationсам не создастся — нуженCreateNamespace=true.prune: true— отдельный флаг: без него Argo CD не удаляет из кластера то, что убрали из Git, и объект продолжает жить и принимать трафик.- Тег после сборки меняет либо шаг конвейера коммитом в репозиторий манифестов, либо компонент вроде Image Updater, либо ссылка на дайджест; плавающий тег среды ломает главное свойство подхода.
- Пароли в репозиторий не кладут: Sealed Secrets (шифрование под кластер), SOPS (ключ извне) или External Secrets (ссылка на хранилище) — последнее правильно, если хранилище уже есть.
- Sync и Health — разные колонки:
SyncedплюсDegradedозначает «манифест применён, приложение не работает», и смотреть надо сначала Health. - Порядок задают волнами и хуками: миграция —
PreSync, дымовые тесты —PostSync; при откате коммита выполненная миграция не отменяется. - Когда приложений много, берут корневое приложение или
ApplicationSetс генератором по каталогам, кластерам или ветвям.
Что почитать дальше
- Ветки и релизный цикл — где GitOps встаёт в конвейер доставки.
- Helm — чарты, которые Argo CD часто и разворачивает.
- Деплой в Kubernetes — манифесты и rolling update под капотом.