Реализация конфигурируемого SaaS решения via ArgoCD & Terraform

от автора

Вступление

Мне представилось решать интересную задачу. Необходимо реализовать настраиваемый SaaS, где пользователь может выбрать галочками нужные ему модули и щелкнуть кнопку готово. После этого для пользователя должен быть создан отдельный кластер Kubernetes (или отдельный namespace в общем кластере в зависимости от тарифного плана) с выбранными модулями, которые представляют из себя наборы микросервисов.

В этой статье я хочу осветить мой GitOps вариант реализации этой задачи и показать, на что способен ArgoCD и Terraform.

Предисловие

В этой статье я часто выражаюсь понятием “приложение” (application) из терминологии ArgoCD, которое обозначает группу ресурсов k8s. Для упрощения, можно считать, что это — микросервис под управлением ArgoCD.

Пример

Рассмотрим пример:

Имеем Module1 состоящих из двух приложений (app1, app2) и Module2 из одного (app3).

У пользователя №1 выбрано два модуля (№1 и №2 соответственно), всего в его системе будет работать три приложения, а вот пользователь №2 решил, что ему достаточно только модуля №1, поэтому в его распоряжении только два приложения.

Выбор инструмента

Думаю, постановка проблемы стала понятнее, приступаем к инструментам реализации.

В голову сразу пришло два решения: императивное (на PowerShell скриптах) и декларативное. Коллега посоветовал посмотреть в сторону ArgoCD, и я начал копать документацию.

Было быстро запущено первое рабочее приложение, но хотелось создавать приложения пачками, и как назло, мысли программиста нашептывали – давай возьмём API ArgoCD и будем добавлять их циклом. Такой план имеет право на существование, но сегодня мы постараемся реализовать подобный механизм декларативно.

Что нам предлагает ArgoCD? 

В ArgoCD есть такая замечательная вещь, как ApplicationSet. Она позволяет нам автоматически создавать Application с помощью генераторов.

Список генераторов:

  • List:  Фиксированный список значений.

  • Cluster: Позволяет получать информацию о кластерах добавленных в ArgoCD.

  • Git: Можем работать с папками и файлами из git репозитория.

  • Matrix: Позволяет комбинировать значения параметры полученных из нескольких генераторов.

  • Merge: Позволяет объединять параметры полученных из нескольких генераторов.

  • SCM Provider: Предоставляет автоматически находить git репозитории.

  • Pull Request: Предоставляет информацию о Pull Request.

  • Cluster Decision Resource: Позволяет вытягивать данные из ресурсов Kubernetes.

Первым делом я попробовал реализовать эту задачу с помощью Cluster генератора. На каждый кластер можно навесить labels со списком модулей, но тогда не получится поселить нескольких пользователей в одном кластере, поэтому от этого генератора пришлось уйти к другому.

На помощь пришел git генератор. Что может быть проще, чем создавать по файлу конфигурации на каждый модуль клиента и, таким образом, разворачивать приложение?!

Давайте попробуем это реализовать.

Все примеры можно посмотреть тут (каждый пример в отдельной ветке).

Итерация №1

Описываем ApplicationSet.

apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata:   name: app1-appset # Наш ApplicationSet описывает одно приложение   namespace: argocd spec:   generators:   - git: # Файлы конфигурации       repoURL: https://github.com/1kvin/argocd-module-saas.git       revision: implementation1       files:       - path: "cluster-config/module1/*.json" # Берём все файлы .json из папки конфигурации нашего модуля   template:     metadata:       name: 'app1-{{destination.namespace}}' # Названия приложений не должны пересекатся внутри ArgoCD, поэтому делаем их уникальными     spec:       project: default       source:  # Вставляем параметры, откуда берём приложение         repoURL: '{{source.repoURL}}'         targetRevision: '{{source.targetRevision}}'         path: '{{source.path}}'       destination: # Вставляем параметры развертывания приложения         server: '{{destination.server}}'         namespace: '{{destination.namespace}}'       syncPolicy:         automated:           prune: true # Автоматически удаляем           selfHeal: true  # И востанавливаем         syncOptions:         - CreateNamespace=true # Создаём namespace, если его нет

И конфиг файл:

{     "destination":     {         "server" : "https://kubernetes.default.svc",         "namespace" : "user1"     },     "source":     {         "repoURL" : "https://github.com/argoproj/argo-cd.git",         "path" : "applicationset/examples/git-generator-files-discovery/apps/guestbook",         "targetRevision" : "HEAD"     } }

Получаем:

Попробуем добавить второго пользователя.

Для этого необходимо создать ещё один JSON файл.

{     "destination":     {         "server" : "https://kubernetes.default.svc",         "namespace" : "user2"     },     "source":     {         "repoURL" : "https://github.com/argoproj/argo-cd.git",         "path" : "applicationset/examples/git-generator-files-discovery/apps/guestbook",         "targetRevision" : "HEAD"     } } 

Результат:

Логика работы получается такая: ApplicationSet берёт из указанной папки все JSON файлы и на каждый из них создаёт отдельный Application.

 Плюсы:

  • Ура, не надо создавать/удалять Application на каждый чих пользователя, это сделает ArgoCD автоматически.

Минусы:

  • Нужно создавать по ApplicationSet на каждое приложение.

  • Нужно дублировать конфигурацию приложений.

Итерация №2

Попробуем отделить конфигурацию приложения от параметров развёртывания (server+ namespace). Для этого с помощью List генератора определим конфигурацию наших приложений в модуле, а дальше скомбинируем их с параметрами развёртывания с помощью Matrix генератора.

apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata:   name: module1-appset # Наш ApplicationSet описывает один модуль   namespace: argocd spec:   generators:     # Комбинируем файлы конфигурации и список приложений     - matrix:         generators:           # Файлы конфигурации           - git:               repoURL: 'https://github.com/1kvin/argocd-module-saas.git'               revision: implementation2               files:                 - path: cluster-config/module1/*.json           # Список приложений в модуле и их параметров           - list:               elements:               - app-name: app1                 app-repoURL: https://github.com/argoproj/argo-cd.git                 app-path: applicationset/examples/git-generator-files-discovery/apps/guestbook                 app-targetRevision: HEAD               - app-name: app2                 app-repoURL: https://github.com/argoproj/argocd-example-apps/                 app-path: guestbook                 app-targetRevision: HEAD   template:     metadata:       name: '{{app-name}}-{{destination.namespace}}'     spec:       project: default       source: # Берём значение из list generator         repoURL: '{{app-repoURL}}'         targetRevision: '{{app-targetRevision}}'         path: '{{app-path}}'       destination: # Берём значение из git generator         server: '{{destination.server}}'         namespace: '{{destination.namespace}}'       syncPolicy:         automated:           prune: true           selfHeal: true         syncOptions:           - CreateNamespace=true 

Как же похудел наш JSON файл!

{     "destination":     {         "server" : "https://kubernetes.default.svc",         "namespace" : "user1"     } }

Для красивой картинки я добавлю аналогично модуль №2 с одним приложением (app3) и поселю его первому пользователю.

Теперь у первого пользователя есть оба модуля, а у второго только один.

Если я захочу подключить пользователю новый модуль, мне достаточно создать новый файл и запушить его в гит.

Плюсы:

  • Управление модулем происходит через одни файл.

  • Конфигурация приложения отделена от параметров развёртывания.

Минусы:

  • Для каждого модуля нужно создавать свой ApplicationSet.

  • Нужно описывать каждое приложение в ApplicationSet.

Итерация №3

Попробуем избавиться от статического List генератора и заменить его на git генератор.

apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet metadata:   name: module1-appset # Наш ApplicationSet описывает один модуль   namespace: argocd spec:   generators:     # Соеднияем файлы конфигурации и список приложений     - matrix:         generators:           # Файлы конфигурации           - git:               repoURL: 'https://github.com/1kvin/argocd-module-saas.git'               revision: implementation3               files:                 - path: cluster-config/module1/*.json           # Список приложений в модуле и их параметров           - git:               repoURL: 'https://github.com/1kvin/argocd-module-saas.git'               revision: implementation3               files:                 - path: apps/module1/*.json   goTemplate: true   template:     metadata:       name: '{{.appName}}-{{.destination.namespace}}'     spec:       project: default       source:         repoURL: '{{.appRepoURL}}'         targetRevision: '{{.appTargetRevision}}'         path: '{{.appPath}}'       destination:         server: '{{.destination.server}}'         namespace: '{{.destination.namespace}}'       syncPolicy:         automated:           prune: true           selfHeal: true         syncOptions:           - CreateNamespace=true 

Возможно, вы заметили строчку goTemplate: true и то, что перед переменными добавилась точка. Это необходимо из-за того, что два одинаковых генератора вызывают коллизию, и единственный способ от неё избавиться, переключится на использование Go шаблонов.

Теперь конфигурация приложения переехала в git:

{     "appName" : "app1",     "appRepoURL" : "https://github.com/argoproj/argo-cd.git",     "appPath" : "applicationset/examples/git-generator-files-discovery/apps/guestbook",     "appTargetRevision" : "HEAD" }

Красота! Мы избавились от статических компонентов в нашем шаблоне! Или нет? Осталось ещё название модуля ☹

Но не беда, сейчас мы и от него избавимся!

Как?

Добавим ещё один Matrix генератор!

Итерация №4

apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet # Наш ApplicationSet описывает все модули metadata:   name: modules-appset   namespace: argocd spec:   goTemplate: true   generators:     - matrix:         generators:           # Список модулей           - list:               elements:               - moduleName: module1               - moduleName: module2           # Соеднияем файлы конфигурации и список приложений           - matrix:               generators:                 # Файлы конфигурации                 - git:                     repoURL: 'https://github.com/1kvin/argocd-module-saas.git'                     revision: implementation4                     files:                       - path: 'cluster-config/{{.moduleName}}/*.json'                 # Список приложений в модуле и их параметров                 - git:                     repoURL: 'https://github.com/1kvin/argocd-module-saas.git'                     revision: implementation4                     files:                       - path: 'apps/{{.moduleName}}/*.json'    template:     metadata:       name: '{{.appName}}-{{.destination.namespace}}'     spec:       project: default       source:         repoURL: '{{.appRepoURL}}'         targetRevision: '{{.appTargetRevision}}'         path: '{{.appPath}}'       destination:         server: '{{.destination.server}}'         namespace: '{{.destination.namespace}}'       syncPolicy:         automated:           prune: true           selfHeal: true         syncOptions:           - CreateNamespace=true 

Отлично! Оно работает! Данная конфигурация позволяет нам передавать параметры из List генератора в Git, но статические компоненты всё ещё остались.

У git генератора есть два режима работы:

  1. Files – с ним мы работали всё это время и успешно извлекали содержимое файлов.

  2. Directories – позволяет нам вытаскивать путь до каталогов.

Используем новый режим на практике.

Итерация №5

apiVersion: argoproj.io/v1alpha1 kind: ApplicationSet # Наш ApplicationSet описывает все модули metadata:   name: modules-appset   namespace: argocd spec:   goTemplate: true   generators:     - matrix:         generators:           # Список модулей берём из названий папок           - git:               repoURL: 'https://github.com/1kvin/argocd-module-saas.git'               revision: implementation5               directories:               - path: apps/*            # Соеднияем файлы конфигурации и список приложений           - matrix:               generators:                 # Файлы конфигурации                 - git:                     repoURL: 'https://github.com/1kvin/argocd-module-saas.git'                     revision: implementation5                     files:                       - path: 'cluster-config/{{.path.basename}}/*.json'                # Список приложений в модуле и их параметров                 - git:                     repoURL: 'https://github.com/1kvin/argocd-module-saas.git'                     revision: implementation5                     files:                       - path: 'apps/{{.path.basename}}/*.json'    template:     metadata:       name: '{{.appName}}-{{.destination.namespace}}'     spec:       project: default       source:         repoURL: '{{.appRepoURL}}'         targetRevision: '{{.appTargetRevision}}'         path: '{{.appPath}}'       destination:         server: '{{.destination.server}}'         namespace: '{{.destination.namespace}}'       syncPolicy:         automated:           prune: true           selfHeal: true         syncOptions:           - CreateNamespace=true 

Результат:

Теперь в нашем шаблоне нет критических статических полей, всё генерируется исходя из состояния git репозитория.

Terraform

Terraform’ом я пользуюсь второй раз в своей жизни, поэтому я уверен на 100%, что описываю решение не самым изящным образом.

Возложим на Terraform следующие задачи:

  1. Генерация файлов конфигурации для ArgoCD.

  2. Поднятие ресурсов инфраструктуры (в нашем случае базы данных).

  3. Инициализация namespace в k8s для пользователя.

Если для первой задачи Terraform является не самым оптимальным вариантом, то со второй и третьей задачей он справится на ура!

Опишем нашу идеальную конфигурацию одним файлом:

[     {         "user" : "user1",         "server" : "https://kubernetes.default.svc",         "namespace" : "user1",         "modules" : [             "module1",             "module2"         ]     },     {         "user" : "user2",         "server" : "https://kubernetes.default.svc",         "namespace" : "user2",         "modules" : [             "module1"         ]     } ] 

У нас есть массив с параметрами каждого пользователя.

Создадим для пользователей отдельные namespace в кубике.

locals {   users_configuration_json = jsondecode(file("${path.module}/configs/users-configuration.json")) }  resource "kubernetes_namespace" "user_namespaces" {   for_each = { for u in local.users_configuration_json : u.namespace => u.namespace}   metadata {     name = each.value   } }

В целом ничего сложного, считываем данные из JSON и создаём namespace. Поехали дальше, попробуем создать файлы конфигурации для ArgoCD.

Добавим немного данных:

locals {   users_configuration_json = jsondecode(file("${path.module}/configs/users-configuration.json"))    users_configuration_combinations = distinct(flatten([     for cfg in local.users_configuration_json : [ # Проходимся по пользователям       for mdl in cfg.modules : { # Модули пользователя         module    = mdl         user      = cfg.user         server    = cfg.server         namespace = cfg.namespace       }     ]   ])) }

Будем создавать файлы используя integrations/github провайдер:

resource "github_repository_file" "module_setup" {   for_each = { for t in local.users_configuration_combinations : "${t.user} ${t.module}" => t }   repository          = "argocd-module-saas"   branch              = "main"   file                = "cluster-config/${each.value.module}/${each.value.user}.json"   content             = jsonencode({"destination" = {"server"= each.value.server, "namespace" = each.value.namespace}} )   commit_message      = "Managed by Terraform"   commit_author       = "Terraform User"   commit_email        = "terraform@example.com"   overwrite_on_create = true }

Теперь мы свели конфигурацию к одному файлу и упростили развёртывание, но с этим можно были легко справиться и другими инструментами. Теперь попробуем раскрыть мощности Terraform на примере баз данных для наших приложений.

Дополним описание наших приложений массивом databases:

{     "appName" : "app1",     "appRepoURL" : "https://github.com/argoproj/argo-cd.git",     "appPath" : "applicationset/examples/git-generator-files-discovery/apps/guestbook",     "appTargetRevision" : "HEAD",     "databases" : [ "app1-db"] }

Наши данные теперь выглядят так:

locals {   users_configuration_json = jsondecode(file("${path.module}/configs/users-configuration.json"))    users_configuration_combinations = distinct(flatten([     for cfg in local.users_configuration_json : [ # Проходимся по пользователям       for mdl in cfg.modules : { # Модули пользователя         module    = mdl         user      = cfg.user         server    = cfg.server         namespace = cfg.namespace       }     ]   ]))    dbs = distinct(flatten([     for cfg in local.users_configuration_json : [ # Проходимся по пользователям       for mdl in cfg.modules : [ # Модули пользователя         for app in fileset("${path.module}/../apps/${mdl}", "**/*.json") : [ # Поиск всех приложений в папке           for db in (jsondecode(file("${path.module}/../apps/${mdl}/${app}"))).databases : # Проходимся по базам данных приложения           {             namespace = cfg.namespace              module    = mdl             user      = cfg.user             dbname    = db           }         ]       ]     ]   ])) }

Поднимаем базы данных:

module "postgresql" {   for_each = { for t in local.dbs : "${t.user} ${t.module} ${t.dbname} " => t }   source        = "ballj/postgresql/kubernetes"   version       = "~> 1.2"   namespace     = each.value.namespace   object_prefix = "${each.value.user}-${each.value.module}-${each.value.dbname}"   name = each.value.dbname }

Результат:

user1
user1
user2
user2

Общий процесс теперь выглядит так:

  1. Мы добавляем конфигурацию нашего пользователя (название, namespace и список модулей).

  2. Terraform подхватывает это, создаёт файлы конфигурации для ArgoCD и подготавливает инфраструктуру.

  3. ArgoCD начинает синхронизацию изменений и заселяет микросервисы.

Осталось написать панель управления всем этим добром, где мы будем редактировать один-единственный файл конфигурации.

С какими трудностями придётся столкнуться дальше

Почему нельзя было натравить ArgoCD на наш единый файл конфигурации и не использовать Terraform для разбиения на несколько различных файлов?

На данный момент Git generator не умеет работать с массивами, поэтому приходится прибегать к такому костылю, надеюсь в будущем мы увидим поддержку не строковых полей.

Я не хочу автоматически обновляться! Предоставьте мне контроль над обновлениями!

Если пойти на уступки пользователя, то это означает, что придётся держать в системе различные версии конфигурации для каждого модуля и его приложений, поэтому трудности поддержки будут расти с каждой новой версией.

А как мне настраивать конфигурацию микросервисов для различных версий?

Действительно, чаще всего мы привыкли поддерживать только один набор для конфигурации нашего микросервиса на каждое окружение или его тип. Я нашел хорошее решение этой проблемы через Azure App Configuration, где можно прописать Label у переменных и использовать его для фильтрации под необходимую версию. Подробнее тут.

Как узнать, что все модули успешно развернулись или упали с ошибкой?

В этом может помочь ArgoCD API, через который мы можем опрашивать наши приложения. Но лучше пойти по другому пути и заставить ArgoCD уведомлять нас о всех проблемах и успехах через систему уведомлений.

Заключение

Я не DevOps инженер, и у меня мало опыта с Kubernetes, но благодаря хорошей документации ArgoCD и Terraform получилось реализовать подобную систему декларативно. В статье я описал минимальный функционал, который будет легко расширить и прикрутить новые фичи.

Ссылка на репозиторий с примерами (каждая итерация в отдельной ветке).

Хотелось бы узнать Вашу идею реализации или как можно было бы улучшить мою версию.

P.S. Расскажите о Вашем необычном кейсе использования ArgoCD и Terraform.

Спасибо Кириллу Лысаку за прекрасную обложку.


ссылка на оригинал статьи https://habr.com/ru/post/718866/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *