Всем привет! Меня зовут Андрей Шилов, я инженер по 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.
Читайте также в нашем блоге:
