← назад к разделу

Классический деплой в Kubernetes — это kubectl apply или helm upgrade из пайплайна: конвейер имеет доступ в кластер и «пушит» изменения. Проблема — кластер и репозиторий легко расходятся: кто-то поправил объект руками, и что реально крутится в проде, уже не совпадает с тем, что в Git. Argo CD переворачивает подход: не пайплайн пушит в кластер, а кластер сам подтягивает состояние из Git. Это называется GitOps.

Разница между push и pull видна в момент дрейфа: пусть в Git у приложения orders стоит replicas: 2, а кто-то поднял их до шести командой kubectl scale прямо в кластере.

в Git у приложения orders стоит replicas: 2 — желаемое состояние 0 мин 2 4 6 8 10 мин push: пайплайн ходит в кластер, kubectl apply pull: Argo CD внутри кластера, automated + selfHeal на 2-й минуте кто-то делает kubectl scale --replicas=6в кластере 6, в Git 2 — сверить некомурасхождение живёт до следующего деплоя из пайплайна секундыArgo CD увидел правку и вернул 2 из GitArgo CD следит за объектами кластера — правку видит сразу, а не по таймеру push: расхождение живёт до следующего деплояpull + selfHeal: Argo CD вернул состояние из Git за секунды

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). Это нормально, к этому просто надо быть готовым до первого ночного вызова, а не во время него.

Argo CD Git желаемое кластер текущее объекты кластера

Контур 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 — сошлось ли число готовых реплик с желаемым, у пода — проходят ли пробы, у сервиса и задачи — свои признаки.

Отсюда четыре сочетания, и каждое означает своё:

SyncHealthЧто случилось
SyncedHealthyвсё в порядке
SyncedDegradedманифест применён, а приложение не работает: поды падают, пробы не проходят, образа нет
OutOfSyncHealthyработает старая версия: синхронизация не прошла или выключена, а прежние поды живы
SyncedProgressingвыкат идёт, новые поды ещё поднимаются

Второе сочетание и ловит половину инцидентов. «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 с генератором по каталогам, кластерам или ветвям.

Что почитать дальше