Всем привет! Меня зовут Андрей Шилов, я инженер по DevOps-практикам. В «Экспресс 42» — подразделении «Фланта», которое консультирует компании по DevOps, — мы часто видим одну проблему: инфраструктурный код со временем превращается в хаос, даже небольшое изменение заставляет перелопачивать гору скриптов. Чтобы навести порядок, мы используем модель BSA (Base, Service, Application). В этой статье мы расскажем, как реализовали GitOps по этой модели с помощью Argo CD — результатом наших экспериментов и стал этот материал.
Если вы когда-нибудь страдали от merge-конфликтов между окружениями, копипаста в репозитории и беспорядка в Argo CD — добро пожаловать. Мы посмотрим, как можно перейти от App of Apps к ApplicationSet и попробуем несколько вариантов реализации последнего. Разберём преимущества и недостатки разных схем в поисках решения, которое:
не требует костылей в Argo CD;
работает только через Helm;
живёт в одном Git-репозитории;
легко масштабируется, включая временные окружения.
Ниже — коротко о модели BSA, критериях оценки для эксперимента и самих вариантах реализации.

Что такое модель BSA
Модель BSA (Base, Service, Application) предложил сооснователь и управляющий партнёр компании «Экспресс 42» Александр Титов. Она предлагает делить инфраструктуру на слои, как в Docker-файлах. Важно не смешивать их и не создавать лишних зависимостей между ними. Благодаря этому каждый слой остаётся независимым, а сама система — управляемой.

Нижний слой — базовый, или инфраструктурный (Base). Это то, как настраиваются операционная система, бэкапы и другие низкоуровневые вещи, например то, как развёртывается Kubernetes. В наших примерах роль последнего будет играть Deckhouse Kubernetes Platform.
Второй слой — сервисный (Service). Здесь находится весь SaaS для разработчиков: логирование как сервис, мониторинг как сервис, база данных как сервис, балансировщик как сервис, очередь как сервис, Continuous Delivery как сервис и так далее. Это всё необходимо описывать отдельными модулями в системе управления конфигурацией.
И верхний слой — приложение (Application). Здесь описывается то, как приложение будет развёртываться поверх предыдущих слоев.
Вместо дисклеймера
Прежде чем перейти к сути, давайте сразу очертим рамки, в которых мы будем сравнивать реализации. Это важно, чтобы правильно понимать, о чём пойдёт речь — и о чём не пойдёт.
Мы рассматриваем только два слоя модели BSA — Service и Application. Базовый слой (например, настройку Kubernetes) не затрагиваем, так как Argo CD уже находится внутри Kubernetes и управлять через него базовой инфраструктурой нецелесообразно.
Все приложения и окружения описываются только через Helm-чарты. Мы сознательно отказались от Kustomize и «голых» манифестов ради единообразия и предсказуемости, однако описываемые ниже подходы могут быть применены и для других способов описания конфигураций.
Все окружения живут в одном Git-репозитории. Это сделано для удобства разработчиков и минимизации конфликтов.
В качестве инструмента доставки конфигураций используем только Argo CD. Flux, werf и прочие альтернативы — за пределами нашего эксперимента.
Мы не модифицируем компоненты Argo CD и не делаем патчей CMP. Это нужно, чтобы решение было переносимым, то есть чтобы его можно было бы легко воспроизвести в другой инфраструктуре.
Стратегии деплоя вроде Blue/Green, Canary или Argo Rollouts не рассматриваем, хотя они технически совместимы с нашей схемой.
Начало: подход App of Apps и его ограничения
Основой для эксперимента стала система на базе классической схемы App of Apps в Argo CD. Суть её в том, что создаётся один родительский Application, который управляет всеми остальными — дочерними приложениями.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cluster-authorization-rules
namespace: argocd
spec:
destination:
server: 'https://kubernetes.default.svc'
namespace: e42-service-layer
project: service-layer
source:
repoURL: 'git@gitlab.com:team/express42/internal-infrastructure/layers/service.git'
path: 'deckhouse/services/cluster-authorization-rules/test'
targetRevision: develop
plugin:
name: argocd-vault-plugin
syncPolicy:
automated:
prune: true
selfHeal: true
Для эксперимента мы организовали репозиторий с разбиением по окружениям: в корне — папки production, test, а в каждой из них — дублирующиеся манифесты одних и тех же сервисов. В результате начали всплывать проблемы:
Слияние веток стало болью. Поскольку конфигурации дублировались между окружениями, merge из test в production превращался в лотерею с высоким риском конфликта или багов.
Временные окружения? Забудьте! Поддержка динамических стендов была слишком громоздкой и требовала ручных правок.
DRY? Не слышали. Один и тот же код приходилось копировать в несколько мест и адаптировать. И, как водится, рано или поздно что-то где-то забывали обновить.
Изоляция окружений тоже могла принести проблемы. При коммите напрямую в production всегда есть риск человеческой ошибки: что-то улетит не в ту папку и мы получим некорректную модификацию.
Добавление нового приложения в подходе App of Apps выглядит примерно так:
Создать манифест Application.
Завести директорию под сервис, внутри неё — подпапки под production, test, возможно dev.
Везде прописать нужные значения.
Казалось бы, всего-то несколько шагов, но это рутина с высокой вероятностью ошибиться.
Доработка: ApplicationSet вместо App of Apps
Чтобы избавиться от дублирования и боли с временными окружениями, мы перешли на ApplicationSet — контроллер Argo CD, который сам генерирует нужные Application-ресурсы на основе шаблона и входных данных (генераторов).
Вот какие плюсы получились:
Больше никакой копипасты. Шаблон один, а Application’ы под каждое окружение или сервис генерируются автоматически.
Динамические окружения — на раз-два. Нужно временное окружение? Просто добавьте ветку или директорию — всё подхватится само.
Управление стало централизованным. Один ApplicationSet рулит всем, никаких десятков отдельных Application.
Гибкость генераторов. Хотите формировать приложения по списку кластеров? Или по структуре Git-репозитория? Или по сочетанию параметров? Пожалуйста: генераторы Git, Cluster, Matrix всё это позволяют.
Переход на ApplicationSet сокращает количество ручной работы, снижает риск ошибок и открывает путь к более масштабируемому GitOps-процессу.
Почему мы выбрали Helm для описания приложений
Мы выбрали Helm как единый способ описания приложений — и вот почему:
Один стиль для всех. Helm помогает привести сервисы к общему формату — шаблоны, values.yaml, единая структура.
Гибкость через шаблоны. Конфиги меняются без переписывания манифестов — достаточно передать нужные значения.
Отличный dev-опыт. Helm поддерживает локальное тестирование, helm lint, проверки best practices и всё, что нужно для комфортной работы.
Сообщество и поддержка. Если что-то пошло не так — ответ почти наверняка уже есть на GitHub или в Stack Overflow.
Единственное «но» — старые YAML-манифесты придётся переписать под Helm. Но это разовая плата за порядок и удобство.
Критерии оценки реализации ApplicationSet
Поскольку ApplicationSet можно реализовать по-разному, нам нужны были критерии для оценки вариантов в ходе эксперимента. Их получилось семь.
Первый — изоляция окружений. Каждое окружение должно быть представлено отдельной веткой или директорией, потому что так проще управлять изменениями и контролировать их.
Дрейф конфигураций возникает из-за конфликтов слияния и может приводить к расхождениям конфигураций в разных окружениях. Использование веток Git снижает риск дрейфа, позволяя синхронизировать изменения между окружениями с помощью механизмов слияния.
Третий критерий — поддержка временных или динамических окружений. Хотелось бы иметь возможность автоматически создавать окружения (например, по имени ветки или PR) без дополнительной настройки в Argo CD.
Не менее полезно и минимальное взаимодействие с Argo CD для добавления новых приложений. Хотелось бы, чтобы новые приложения или окружения могли добавляться исключительно через Git, без необходимости вручную создавать Application или ApplicationSet.
Пятый критерий — соответствие принципу DRY. Вся повторяющаяся логика должна быть вынесена в шаблоны, чтобы уменьшить дублирование конфигураций и упростить сопровождение.
Важна и самодостаточность ApplicationSet без внешней шаблонизации. Здорово, если вся генерация манифестов и логика параметризации выполняются средствами Argo CD, а сторонняя шаблонизация не нужна.
И последнее — устойчивость к масштабированию. Некоторые реализации ApplicationSet при 100+ приложениях могут резко «просесть» по производительности. Поэтому полезно оценивать подходы с учётом реального количества приложений.
Критерии обсудили, самое время сравнить возможные варианты реализации.
Варианты реализации ApplicationSet
У нас получились четыре варианта реализации. Рассмотрим их.
1. «В лоб»: по ApplicationSet на каждую ветку
Каждая ветка (например, dev и prod) содержит свой собственный ApplicationSet, который через Git-генератор итерирует по директориям с Helm-чартами и создаёт приложения в Argo CD.

Слева на скриншоте — ApplicationSet, справа — структура репозитория
Плюсы |
Минусы |
|
- Полная изоляция окружений - Самодостаточный ApplicationSet без внешней шаблонизации - Устойчивость к масштабированию |
- Ветки мёржить практически невозможно: конфигурации расходятся - Нет поддержки временных окружений - DRY нарушен: шаблон приходится дублировать в каждой ветке - Добавление нового приложения — всё ещё ручная работа |
2. Несколько ApplicationSet в одной ветке
В одной ветке создаются папки под окружения (dev, prod), в каждой из которых лежит свой ApplicationSet, с жёстко заданными путями к values.yaml.

Плюсы |
Минусы |
|
- Полная изоляция окружений - Самодостаточный ApplicationSet без внешней шаблонизации - Можно мёржить изменения между dev- и prod-окружениями , то есть нет дрейфа конфигураций - Устойчивость к масштабированию |
- Нет поддержки временных окружений - Каждый ApplicationSet всё ещё создаётся вручную - DRY-подход по-прежнему не соблюдён |
3. Рендеринг ApplicationSet во внешнем CI
Здесь используется шаблон ApplicationSet с плейсхолдерами (например, {{ ENV }}), который в CI (например, GitLab) превращается в готовый YAML. Его потом kubectl apply накатывает в кластер.

Плюсы |
Минусы |
|
- Изоляция окружений - DRY соблюдён - Устранение дрейфа конфигураций - Устойчивость к масштабированию - Минимальное взаимодействие с Argo CD для добавления новых приложений - Поддержка динамических окружений |
- Нет самодостаточного ApplicationSet без внешней шаблонизации - Шаблонизируем шаблон — архитектура становится сложнее |
4. Матричный генератор (Matrix generator)
Матричный генератор — это механизм, который перебирает все возможные сочетания значений из двух (или более) генераторов. Для каждого элемента из первого итерируются все элементы второго. В результате получается декартово произведение множеств.
В нашем варианте реализации используются два генератора: один проходит по приложениям (чартам), другой — по окружениям (значениям). Получается декартово произведение: на каждый чарт — по конфигурации для каждого окружения.

Плюсы |
Минусы |
|
- Соответствие принципу DRY - Самодостаточность ApplicationSet без внешней шаблонизации - Устранение дрейфа конфигураций - Гибкость и поддержка динамических окружений - Минимальное взаимодействие с Argo CD для добавления новых приложений |
- Риск для продакшена из-за недостаточной изоляции окружений. Без защиты (например, CODEOWNERS) изменения в ветке могут затронуть критичные окружения - Недостаточная устойчивость к масштабированию из-за нагрузки на ApplicationSet. При каждом синке он генерирует все конфигурации, а потом фильтрует по кластеру — на масштабах это может тормозить |
И ещё пара продвинутых трюков
Cluster generator: выбирает кластеры по лейблам. Можно делать гибкие конфигурации для мультикластерных деплоев.
Сабчарты: создать метачарт, куда включены все приложения.
List generator: вручную задать список окружений и приложений в YAML — удобно, если конфигурация стабильна.
CMP (Config Management Plugin): кастомный плагин, который рендерит всё по своим правилам. Самый гибкий, но и самый сложный способ.
Заключение
Мы прошли путь от простой, но ограниченной схемы App of Apps к более гибким и масштабируемым подходам с использованием ApplicationSet. Каждый вариант — от «в лоб» до матричного генератора и CI-шаблонизации — имеет свои плюсы и ограничения. Выбор зависит от ваших целей: стабильность, гибкость, масштаб, независимость от CI. Ниже — удобная таблица для сравнения.

А какой подход используете вы? Расскажите в комментариях, что сработало, а что нет. Особенно интересно, если вы ушли от ApplicationSet — и к чему именно.
P. S.
Читайте также в нашем блоге:
Комментарии (4)

xitriy87
21.10.2025 16:06А зачем вы используете GoTemplates, если судя по скринам, не используются никакие фишки, ну кроме missingkey? Можно было бы и обычные templates использовать, что бы не усложнять синтаксис.
RieSet
Ветки - кажутся не семантическим решением для этой задачи
Сейчас экспериментирую с Helm поверх Application. Вместо ApplicationSet или CI/CD генерации - вся мощь Helm
zlyoha
А есть возможность сниппеты показать как это выглядит? Интересно для какого кейса оно себя хорошо показывает.
RieSet
Мне кажется, я не очень понял, что именно показать. Могу в личке ответить на вопросы. Этот способ нивелирует некоторые минусы из приведенных в статье, но у меня пока нет длительного опыта использования.
Через ArgoCD менеджерим микросервисы самого приложения, а не инфру для него. И настраиваем сами микросервисы через Helm.
App -> (App Helm dev, stage, prod) -> Argo Application -> Argo Helm (dev_1_cluster, dev_2_cluster, dev_3_cluster)
ArgoCD Application — это CRD. В большинстве случаев ставим один раз и часто изменяем через web интерфейс. Поэтому не проблема обернуть его в чарт.
Конфигурируем через файлик values_еще одно окружение.yaml. А Helm создает манифесты:
apiVersion: argoproj.io/v1alpha1;kind: Application:Определяем:
в каком кластере разврачиваем,
с какими префиксами,
в каких неймспейсах,
какой вариант values файла использовать из самого микросервиса (dev, stage, test...).
набор микросервисов
можно откатить если что-то наредактировали в web интерфейсе
можно независимо версионировать запускаемый набор микросервисов
Можно и вручную развернуть для отладки и дебага, можно и встроить в какой либо процесс депоя через hook. И все площадки приложения оказываются в файлах, которые легко можно сравнить в IDE:
values.yaml,
values-dev.yaml,
values-stage.yaml,
values-....yaml,
values-test.yaml.
Получается, что есть чарты для запуска микросервисов и есть чарт для запуска их всех в каком-то кластере через ArgoCD
Еще думал туда попробовать job добавить для проверки наличия в кластере всех операторов перед запуском в нем микросервисов. И не нравится, что ArgoCD плохо за собой прибирается, может, сборщик мусора удастся впихнуть.