Общие сведения
По арго есть много хорошей документации, так и различных примеров использования. Но когда понадобилось решить одну задачу, готовых примеров перед глазами я не нашёл.
Задача такая: есть k8s-кластер N, N1, N2 & etc, в каждом из которых есть свой локальный арго. В эти кластеры необходимо деплоить инфровые приложения (ингрес-контроллер, графана и т.п.).
Как правило, приложения поставляются в виде хельм-чартов. Для разделения по окружениям используется отдельный каталог под values.
Можно реализовать деплой по-разному:
- сделать application на каждое окружение
- сделать applicationset на каждое окружение
- сделать applicationset на все окружения
Последний вариант и будет описан далее.
Структура репозитория
Таким образом описанная выше структура репозитория выглядит так:
➜ gitops tree
.
├── charts
│ ├── app1
│ ├── app2
│ └── app3
└── values
├── develop
│ ├── app1.yaml
│ ├── app2.yaml
│ ├── app3.yaml
│ └── config.json
└── production
├── app1.yaml
├── app2.yaml
├── app3.yaml
└── config.json
8 directories, 8 files
Есть каталог с чартами, есть каталог с values под условные два окружения – develop и production. Внутри них расположены app-name.yaml под каждое окружение каждого приложения. Вроде всё стандартно.
Но тут появляется config.json в каждом окружении, который содержит следующее:
[
{
"appname": "app1",
// "version": "6.3.2",
"cluster": "production",
"namespace": "default"
},
{
"appname": "app2",
"cluster": "production",
// "version": "6.3.3",
"namespace": "default"
},
{
"appname": "app3",
"cluster": "production",
// "version": "6.3.3",
"namespace": "test"
}
]
В файле представлен массив всё тех же приложений, в котором можно переопределить какие-то специфические значения, которые не всегда есть возможность указать на уровне helm values.
Например, если указать namespace или cluster в values.yaml, необходимые для использования в applicationset, это может вызвать конфликт с наименованием переменных, которые уже изначально есть в чарте.
То есть мы укажем переменную .values.cluster: develop, которая нужна для арго и его applicationset, а это может оказаться какое-то приложение, которое тоже ожидает эту переменную c другим значением.
Поэтому удобно все переменные для чарта, как и обычно, указывать в values.yaml. А переменные для деплоя и applicationset – в config.json, в котором обязательными полями являются cluster и namespace.
Подготовка кластера
В самом начале было упомянуто, что в каждом кластере есть свой локальный ArgoCD. В манифесте Applicationset есть поле destination, где обычно прописан локальный кластер по умолчанию с именем in-cluster и адресом kubernetes.cluster.local – это имя сервиса из дефолтного namespace.
Основная проблема при деплое в несколько кластеров – как на уровне appset разделить, в какое окружение деплоить, если они все одинаково называются и имеют одинаковые адреса по умолчанию.
Можно настроить внешний доступ, но это скорее в случае когда из одного арго идёт деплой в кластеры, а тут у каждого свой.
И вот есть какое решение: есть опция переименовать дефолтный кластер из in-cluster в нужное окружение, задав ему имя и аннотацию. Это можно сделать либо в настройках самого арго, либо создав секрет:
cat <<EOF | kubectl apply -n argocd -f -
apiVersion: v1
kind: Secret
metadata:
labels:
argocd.argoproj.io/secret-type: cluster
cluster: dev
name: dev
namespace: argocd
type: Opaque
stringData:
name: develop
server: https://kubernetes.default.svc
EOF
По сути после деплоя самого инстанса арго ему надо явно указать через аннотацию и имя кластера, что он находится в develop или production окружении.
Теперь applicationset сможет через генератор кластеров посмотреть этот секрет и его аннотации с именем. Всё это будет доступно в переменных, которые и будут использованы для разделения на окружения приложений. По сути это самый главный момент.
Applicationset
А теперь, собственно, как это будет выглядеть:
cat <<EOF | kubectl apply -n argocd -f -
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: appset
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- matrix:
generators:
- clusters:
selector:
matchExpressions:
- key: cluster
operator: In
values:
- dev
- prod
- git:
repoURL: YOUR_REPO.git
revision: appset
files:
- path: test/values/{{.name}}/config.json
template:
metadata:
name: '{{ .appname }}'
spec:
project: default
sources:
- repoURL: YOUR_REPO.git
targetRevision: appset # change branch
path: test/apps/{{ .appname }}/
helm:
valueFiles:
- "$values/values/{{.name}}/{{.appname}}.yaml"
- repoURL: 'YOUR_REPO.git'
targetRevision: appset # change branch
ref: values
destination:
name: "{{.name}}"
namespace: "{{.namespace}}"
EOF
Как это работает:
- здесь используется matrix генератор – он необходим, когда есть потребность в создании множества комбинаций, например как случай с несколькими окружениями и кластерами
- в matrix генераторе могут использоваться только два дочерних генератора – как раз больше и не нужно, в данном случае это генератор кластеров для чтения созданного секрета и гит-генератор для чтения config.json
- с помощью matchExpressions в генераторе кластеров арго считывает аннотации у созданных секретов, помещая имя кластера и его адрес в переменные .name и .server соот-но. Имя кластера .name равно develop или production.
- И конкретно на основе переменной .name и настроено разделение по окружениям, в т.ч. название каталогов в гит-репозитории – это самый важный момент и основная идея логики работы апсета
- Далее арго, получив значение .name, читает значение ./values/{develop,production}/config.json и также позволяет использовать их использовать в шаблоне манифеста для дальнейшей генерации приложений
- В списке приложения для деплоя, каждый из которых хранится в .appname, поэтому арго пройдется по всем чартам и всем values для них и выполнит рендеринг приложений
- И в заключении в поле destination будет подставлено имя кластера: develop или production. В каждом окружении будет свой кластер с одним и тем же локальным адресом, но разными именами.
- Плюс поле namespace, уникальное для каждого приложения каждого кластера.
Заключение
В описанном способе есть удобства в виде разделения конфигов инфраструктуры от конфигов приложения + есть разные варианты по комбинациям использования генераторов. Там очень гибкий механизм и можно генерировать по разным условиям, найдётся генератор под любую схему деплоя.
Ну, и может быть удобнее, когда есть один апсет на десятки кластеров, вместо кучи однотипных application.
Но тут есть и минусы – схема по началу в целом может показаться излишне сложной, особенно в части отладки. Мне, например, не всегда удобно дебажить по логам applicationset-контроллера, что же пошло не так, и где возникла ошибка.
Да и вообще по сути вместо всего описанного можно было просто использовать один applicationset на каждый кластер. То есть как компромиссное решение. Так меньше вероятность ошибиться и снести\изменить лишнее, или наоборот, затащить лишнее куда-то.