Привет, Хабр! Меня зовут Никита Чубаров. По трудовому договору я инженер-эксперт по разработке и сопровождению сервисов, а по факту DevOps-инженер с фокусом на доставку в платформенных командах, которые поставляют общие решения для десятка дочерних команд. Со временем эта доставка перестала быть прозрачной и предсказуемой, и всё больше напоминала космолёт, в котором пилот перед каждым запуском вручную подключает провода, проверяет давление в контурах и по списку нажимает десятки кнопок. Пока запусков мало, это ещё можно представить, но когда их становится сотни, а кораблей — десятки, такая схема быстро превращается в источник ошибок и выгорания.
Примерно в таком состоянии у нас находился Bootstrap Namespaces. В статье я расскажу, как мы прошли путь от сложной CI-оркестрации к декларативному управлению Bootstrap Namespaces через Argo CD и GitOps, какие проблемы это позволило убрать и какие новые ограничения пришлось принять.
Bootstrap Namespaces
Начнём с того, что под Bootstrap k8s Namespaces мы понимаем инфраструктурную подготовку namespace перед тем, как его получает команда разработки. Это не разовая операция, а воспроизводимый процесс, который должен одинаково работать для всех команд и окружений. В него входит несколько обязательных шагов. Нужно создать namespace, развернуть в нём десяток манифестов и пару приложений.
Были написаны bash-скрипты, которые применяли в namespace портянки сырых манифестов, меняя в них значения в зависимости от закомментированных в скрипте строк. Хранилось это в удалённом git-репозитории и время от времени применялось инженерами платформы интерактивно, по запросу команд разработки.
Попросили → склонил → поправил скрипты → запустил → готово!
Позднее у нас появились чёткие требования к процессу подготовки namespace:
Масштабируемость: число namespaces должно расти без усложнения процесса.
Единый репозиторий для контроля и управления со стороны платформенной команды, любая конфигурация должна быть прозрачной и воспроизводимой.
Контекст тоже важен. На тот момент у нас было восемь Kubernetes-кластеров и более сорока пяти namespaces в каждом. Поэтому любое решение мы сразу оценивали не в рамках одного кластера, а в масштабе всей инфраструктуры. Ручное управление в таких условиях быстро перестало работать, и вся логика неизбежно уезжает в автоматизацию. Так у нас и появился отдельный пайплайн Bootstrap k8s Namespaces.
Космолёты, мои космолёты
Исторически в платформенной команде Bootstrap Namespaces был реализован через Downstream Triggers в CI. Каждый namespace создавался и настраивался отдельным набором джоб, а YAML-конфигурации для них генерировал Go-скрипт, упакованный в специальный Docker-образ. У скрипта была собственная структура конфигурационного файла, описывающего, что именно нужно создать в namespace.

На скриншоте показана лишь малая часть генерируемых джоб. При историческом подходе их общее количество приближалось к тысяче! Настоящий «ковёр джоб». Вносить в него массовые изменения было крайне неудобно. А запустить всё это в автоматическом режиме означало неизбежно создать большую нагрузку на инфраструктуру расшаренных раннеров организации и обеспечить огромную очередь на выполнение не только своих джоб, но и для всех параллельно создающихся задач коллег. Приходилось тратить время и выбирать, что именно ты хочешь запустить.
Ситуацию усугубляла запутанная навигация по пайплайну: изменение масштаба интерфейса, прокрутка по горизонтали, затем по вертикали, выбор нужного пункта в выпадающем списке, снова прокрутка и ручной запуск отдельных джоб. Ошибиться в таком процессе было слишком просто.
В таких условиях массовые изменения занимали десятки минут.
В результате пайплайн больше напоминал панель управления космическим кораблём с миллионом кнопок. Быстро разобраться в нём мог только опытный пилот. Он же разработчик.
Поэтому обучение нового инженера занимало много времени, а знания, полученные в процессе, было сложно переиспользовать где-либо ещё. Для системы, обеспечивающей операционную деятельность банка, где цена ошибки особенно высока, такой уровень сложности выглядел неоправданным.
В таком состоянии я и застал репозиторий настройки namespaces при переходе в команду. Но все эти проблемы были от меня далеки, пока я не взялся за задачу установки коллекторов трейсов. Мы хотели сделать это стандартом поставки, этаким комплектом, идущим вместе с namespace.
Решающий спринт
Чем глубже я погружался в задачу, тем очевиднее становилось, что текущая схема плохо для неё подходит. Техлид уже спрашивал, почему всё занимает столько времени, ведь, казалось бы, «нужно просто добавить в одном месте». На практике это означало распутать клубок Bash-скриптов и Go-скриптов, внести свои изменения в их конфигурации, пройтись по существующему «ковру джоб», вручную запустить нужные пайплайны для всех кластеров и namespaces. Помимо трудоёмкости, такой подход сохранял всю накопившуюся сложность системы и делал её ещё менее управляемой.
Поэтому нужно было либо встроиться в существующую систему и принять её ограничения, либо попытаться упростить процесс, даже если это потребует изменений в самом подходе.
Я начал обсуждать с коллегами, почему Bootstrap устроен именно так, и параллельно изучал более нативные возможности GitLab CI, чтобы уйти от downstream-архитектуры. Я пересмотрел несколько вариантов. Но не все из них вписывались в наши требования и инфраструктуру. В итоге я остановился на parallel:matrix. Это решение идеально подходило.
На одном из спринтов, когда мы обсуждали ход задачи, я рассказал команде про нативный механизм GitLab CI, который позволяет описывать матричные джобы без генерации отдельных YAML-файлов.
Идея была простой. Нужно перемножить список кластеров и namespaces и выполнить однотипные действия в рамках одного пайплайна. Без downstream-триггеров и внешних генераторов.
Моё предложение вызвало полярные реакции: кто-то смотрел косо, а кого-то это очень вдохновило. Но в итоге мы сошлись на том, что такой подход позволит упростить нам жизнь.
После завершения спринта коллекторы были установлены через parallel:matrix. Это стало шагом вперёд. Процесс стал понятнее, но по сути остался тем же «ковром из джоб», пусть и более аккуратно разложенным.
Argo CD и новый Bootstrap
Маленькая победа вдохновила меня на новые изменения. Решение с parallel:matrix сняло часть боли, но не устранило саму причину сложности. Bootstrap по-прежнему оставался привязанным к CI и набору джоб, а не к декларативному описанию желаемого состояния. Поэтому мы начали смотреть шире и анализировать подходы к деплою не только инфраструктуры, но и приложений.
GitOps во многом стал новым стандартом доставки. Можно сделать такой вывод, если оглянуться на западных коллег. Отдельная сцена на Kubecon не первый год посвящена Argo CD, а в блоге CNCF можно почитать о статистике использования GitOps и данные по применяемым инструментам.
На тот момент я уже обладал некоторыми знаниями о функционале Argo CD и о преимуществах GitOps-подхода. Обрамив их в презентации и схемы, я вышел к команде с предложением попробовать применить все это у нас. Команда согласилась, техлид дал зелёный свет, и мы начали переход. Argo CD в сравнении с Flux лучше подходил под наши нужды. Особенно его возможность управлять развёртываниями в нескольких кластерах из единой точки.
В этой статье я не буду разбирать, как «сделать правильно». Для этого есть более подробные материалы, например, доклад: «GitOps на Argo CD. Лучшие практики 2025» (Youtube | Rutube) с UFADEVCONF. Остановлюсь на общем описании схемы, которую мы внедрили для себя, без погружения в частные настройки и best practices.
Инструменты Argo CD
Argo CD работает с Kubernetes декларативно и приводит кластер к состоянию, описанному в Git. Для реализации нового Bootstrap Namespaces мы воспользовались стандартными механизмами получения и применения Kubernetes-манифестов:
Raw-манифесты
Kustomize
Helm
Мы используем все три варианта, но преимущественно опираемся на Helm. Всё зависит от ситуации. Helm однозначно лучше, когда у вас есть потребность в сложном темплейте и вы хотите объединить целую охапку манифестов логически. Kustomize подойдёт, если сложность и объём ниже, скажем, поменять пару строк в двух-трёх манифестах. Raw-манифест — отличный вариант для доставки статичных манифестов, не меняющихся от среды к среде.
Чарт NS-setup
Оставался вопрос содержимого Bootstrap. Часть сервисов, которые мы устанавливали в namespace уже имела собственные Helm-чарты, но базовая конфигурация namespace была разбросана по отдельным манифестам. Поэтому мы решили собрать их в один общий чарт ns-setup, разрабатываемый и версионируемый в отдельном репозитории.
Чарт ns-setup включает:
namespace.yamlimage-pull-secret.yamlresource-quota.yamlresource-quota-alerts.yamllogging-flow.yamllogging-output.yamldebug-pod.yamlextra-manifests.yaml
В нём описано всё, что должно появляться в namespace по умолчанию.
Но выбор механизма рендеринга манифестов решает лишь часть задачи. После создания ещё нужно автоматизировать их доставку в десятки namespaces и несколько кластеров.
ApplicationSets
В Argo CD базовой единицей доставки является Application. Она описывает, откуда забирать манифесты и куда их применять. В простых сценариях этого достаточно, но для Bootstrap Namespaces оказалось мало. Для управления десятками однотипных установок лучше всего подходила фича Argo CD — kind: ApplicationSet. Она выполняет темплейтинг Application по спискам. Список можно задать вручную (list-generator) или сгенерировать автоматически, например, на основе списка подключённых для удалённого управления кластеров (cluster-generator) или структуры каталогов в репозитории (git-generator). Также можно комбинировать генераторы между собой (matrix-generator).
Мы использовали комбинацию генераторов, чтобы перемножить два списка через matrix generator. По сути, это была та же идея, что и в parallel:matrix, но уже на уровне GitOps и без CI.
Для большинства сценариев вам подойдут перечисленные генераторы, но возможностей ещё больше! Тут есть и автодискавери репозиториев SCM, и генерация временных развертываний в качестве реакции на открытые pull requests. Если вы только начали знакомство с Argo CD, обязательно изучите ApplicationSet. Он поможет вам избавиться от лишнего дублирования и ещё больше автоматизирует процесс.
Взглянем на ApplicationSet поближе, это реально работающий у нас в продакшене вариант.
apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata: name: csp-ns-setup namespace: argocd-prod spec: syncPolicy: preserveResourcesOnDeletion: true goTemplate: true goTemplateOptions: ["missingkey=error"] generators: - matrix: generators: - git: repoURL: https://<hidden>.raiffeisen.ru/<hidden>/csp-infra.git revision: HEAD directories: - path: prod/values-and-manifests/namespaces/*/* - clusters: selector: matchLabels: argocd.argoproj.io/secret-type: cluster template: metadata: labels: cluster: "{{ .name }}" name: '{{ .path.basenameNormalized }}-{{ trimSuffix ".<hidden>.raiffeisen.ru" .name | trunc -3 }}-csp-ns-setup' namespace: argocd-prod spec: project: "{{ index .path.segments 3 }}" sources: - chart: csp-ns-setup repoURL: https://<hidden>.raiffeisen.ru/<hidden>/csp-internal-helm targetRevision: 0.7.0 helm: valueFiles: - $values/prod/values-and-manifests/namespaces/stage-csp-ns-setup.yaml - $values/{{ .path.path }}/csp-ns-setup.yaml releaseName: "{{ .path.basenameNormalized }}" parameters: - name: "clusterName" value: "{{ .name }}" - repoURL: "https://<hidden>.raiffeisen.ru/<hidden>/csp-infra.git" targetRevision: HEAD ref: values destination: server: "{{ .server }}" namespace: "{{ .path.basenameNormalized }}" syncPolicy: automated: selfHeal: true prune: false syncOptions: - ServerSideApply=true
Spec состоит из двух крупных блоков: template и generators. В generators хорошо видно, что мы объявляем matrix, а затем указываем git и cluster параллельно. Спецификация блока темплейт повторяет спецификацию kind: Application, в ней мы используем значения, предоставленные генераторами. Например, .name или .path.basenameNormalized. Под капотом go-шный темплейтер, так что можно использовать знакомые по helm функции, например, мне пригодились trimSuffix и trunc, чтобы вычленить индекс кластера из его имени.
Удобным решением оказалась возможность через multi-source передать отдельно лежащие values-файлы. За рамками развертывания namespaces, я использовал multi-source по более прямому назначению. Им можно комбинировать несколько чартов в один комплект поставки. Argo CD под капотом срендерит их все и применит полученные манифесты в рамках одного Application. Можно применить такой подход для деплоя многокомпонентных релизов, хотя я и не фанат такой практики.
App-of-apps
Следующая проблема была организационной. Когда Application становится много, нужен единый вход в систему доставки. Для этого мы использовали подход app-of-apps для организации доставки через Argo CD. Так достаточно создать один Application, который будет забирать из Git-репозитория другие манифесты Application. Получается точка входа, с которой начинается цепочка автоматического применения и доставки.
Связываем всё воедино
Когда базовые механизмы Argo CD стали понятны, самым сложным оказалось организовать их в рабочую схему. В GitOps-доставке это почти всегда упирается в структуру репозиториев и их количество.

Нам был нужен один репозиторий, потому что мы создавали единую точку управления инфраструктурой для платформенной команды. При этом внутри репозитория требовалось логическое разделение. Отдельные области для установки на уровне кластера и для установки в каждый namespace.
Схему собрали из уже знакомых компонентов Argo CD. В качестве входной точки использовали подход app-of-apps и ApplicationSets в сочетании с matrix generator: cluster-generator * git-generator.
Входной точкой стала директория applications. Её манифесты применяются через app-of-apps. А после этого ApplicationSets ссылаются на тот же репозиторий, чтобы найти values-файлы для рендеринга манифестов.

Что мы получили

Плюсы GitOps-подхода хорошо известны и подробно описаны в других материалах, а для нас ключевыми преимуществами стали:
Масштабируемость за счёт отказа от самописных инструментов в пользу промышленных стандартов. Добавлять новые NS через
ApplicationSetпросто и прозрачно.Упрощение внесения массовых изменений: изменить манифест во всех namespaces на тестовом стендe стало легче. Достаточно обновить версию чарта в
ApplicationSet.Унификация CD: инженерам больше не нужно вчитываться в бесконечные bash- или Go-скрипты, разбираться в усложнённом GitLab CI или искать логику, которую закладывали отдельные авторы доработки в пайплайн.
Унификация секрет менеджмента: пересели с самописных скриптов на External Secrets Operator, который ускоряет разработку/доставку.
Версионирование Helm chart: обеспечивает контролируемое обновление на стендах. Стало проще улучшать chart, а breaking changes больше не ломают всё сразу.
Самообслуживание: настолько проще создавать новые namespaces, что обучили коллег работе с новой инфраструктурой репозитория и перешли на самообслуживание. Команды разработки сами составляют MR с минимальным values-файлом под новый namespace, нам остаётся только провести ревью. Остальное возьмет на себя Argo CD.
GitOps как концепция очень подкупает и кажется, что это «за все хорошее, против всего плохого». Дьявол же кроется в деталях реализации.
Приведу несколько сложностей, с которыми вы, скорее всего, столкнётесь на пути внедрения и эксплуатации.
Пример 1. Большая сила — большая ответственность
В приведённом ApplicationSet есть ключ preserveResourcesOnDeletion, но он появился там не сразу. Причесывая наше решение уже во время эксплуатации, я решил, что хочу увеличить прозрачность для команд разработки, переместив принадлежащие им namespaces в их область видимости в Argo CD. Я обновил поле spec.template.spec.project и оно стало динамическим, заполняясь проектом команды-потребителя.
Я посчитал это минорным изменением метадаты. Ну принадлежность и принадлежность. Была одна — станет другая, делов-то. Но реальные последствия были гораздо серьёзнее. Argo CD удалил старый Application и создал новый. Это привело к удалению созданных в его рамках манифестов. В нашем случае… это были namespaces, полные приложений, развёрнутых ещё без Argo CD.
Так лёгким мановением руки мы потеряли сотни приложений в нескольких кластерах. А заметили неладное, только зачистив таким образом две среды. До прода, к счастью, дело не дошло, но наши лица вытянулись, а руки задрожали в поисках самых свежих бекапов. Приложения восстановили и добавили ключ preserveResourcesOnDeletion в ApplicationSet.
Так что, при использовании GitOps внесение массовых изменений становится действительно лёгким, обидно, когда изменение оказывается ошибочным.
Пример 2. Работа с сопротивлением
В процессе внедрения и подготовки к GitOps придётся очень много раз объяснять коллегам-разработчикам, как это теперь работает. Часто они совершенно не чувствуют потребности в переходе. Ведь в зависимости от вашей реализации может исчезнуть и кнопка из пайплайна. Например, с понятным индикатором — «красная» значит плохо, «зелёная» — хорошо. Мы в итоге перешли на Argo CD и для деплоя бизнес-сервисов. И поначалу это не всем понравилось.
В большом проекте это может быть длительный марафон, во время которого легко выдохнуться и потерять желание бежать вовсе. Поэтому, если задумаете это — приготовьтесь, что сопротивление не пройдёт скоро.
Пример 3. Много коммитов в мейн
На самом деле проблема не в самих коммитах, а в том, что хочется построить систему правильно и хорошо. Например, с аудитом изменений (процессом ревью merge request). Но при первом запуске нового сервиса или быстром дебаге это только замедляет. Конечно, можно всегда найти обходное решение, развернуть в dev с рабочей станции. Или настроить механизм автоапрува при MR, если изменения касаются среды dev/test. Мы хотим посмотреть в сторону Pull request generator. Кажется, это будет хорошим выходом из сложившейся ситуации. Потому что дьявол по-прежнему в деталях. Нам тоже не всё удалось сделать удобным сразу.
Итоги
Это был долгий путь от панели управления космическим кораблём с миллионом кнопок к понятной и читаемой структуре доставки. Вносить изменения в существующие конфигурации и добавлять новые namespaces стало заметно проще. Удовольствие от работы выросло, а счастливые люди создают лучшие продукты. Если вы тоже уже проходили этот путь, делитесь своими успехами и проблемами в комментариях, обсудим.
