Привет, Хабр! Меня зовут Никита Чубаров. По трудовому договору я инженер-эксперт по разработке и сопровождению сервисов, а по факту 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.

Pipeline настройки namespace'ов
Pipeline настройки 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.yaml

  • image-pull-secret.yaml

  • resource-quota.yaml

  • resource-quota-alerts.yaml

  • logging-flow.yaml

  • logging-output.yaml

  • debug-pod.yaml

  • extra-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-доставке это почти всегда упирается в структуру репозиториев и их количество.

Связь манифестов от входной точки до values файла
Связь манифестов от входной точки до values файла

Нам был нужен один репозиторий, потому что мы создавали единую точку управления инфраструктурой для платформенной команды. При этом внутри репозитория требовалось логическое разделение. Отдельные области для установки на уровне кластера и для установки в каждый namespace.

Схему собрали из уже знакомых компонентов Argo CD. В качестве входной точки использовали подход app-of-apps и ApplicationSets в сочетании с matrix generator: cluster-generator * git-generator.

Входной точкой стала директория applications. Её манифесты применяются через app-of-apps. А после этого ApplicationSets ссылаются на тот же репозиторий, чтобы найти values-файлы для рендеринга манифестов.

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

app-of-apps c дочерними приложениями, среди которых csp-ns-setup
app-of-apps c дочерними приложениями, среди которых csp-ns-setup

Плюсы 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 стало заметно проще. Удовольствие от работы выросло, а счастливые люди создают лучшие продукты. Если вы тоже уже проходили этот путь, делитесь своими успехами и проблемами в комментариях, обсудим.