
Это вторая часть серии статей, где мы шаг за шагом строим PaaS на базе Kubernetes без написания кода. Наша цель — выжать максимум из современных технологий и экосистемы Kubernetes, чтобы создать PaaS-решение, которое упростит жизнь разработчикам. Мы хотим, чтобы приложения и сервисы разворачивались быстро, удобно и без глубокого погружения в инфраструктуру. Перейдем сразу к делу.
Что уже сделано
В предыдущей части мы сделали подготовительные шаги:
-
Добавили Gateway: подключили Envoy Gateway для управления входящим трафиком. Это позволило легко заводить внешний трафик в кластер через Kubernetes Gateway API, без сложных настроек.
-
Выбрали первый оператор — CloudNativePG: развернули оператор PostgreSQL, который автоматизирует создание и управление кластерами баз данных, обеспечивая высокую доступность и простоту использования.
-
Добавили Cyclops UI: интегрировали веб-интерфейс Cyclops, чтобы пользователи PaaS могли удобно заводить свои приложения. Через формы в UI они задают параметры, не вникая в YAML или CLI.
-
Добавили Template для создания PostgreSQL: создали шаблон в Cyclops UI, который позволяет пользователям одним кликом разворачивать экземпляры PostgreSQL, используя CloudNativePG под капотом.
Эти шаги заложили фундамент для нашей PaaS. Пора идти дальше — к централизованному управлению.
Единый IAM для централизованного управления
На нашей платформе будет доступно множество разнообразных приложений, и ручное управление пользователями и доступом быстро превратится в кошмар, поэтому мы добавим единый IAM. Он решает эту проблему, позволяя централизованно управлять пользователями и их правами из одного места — будь то доступ к интерфейсу, сервисам или данным.
На данный момент существует один хорошо зарекомендовавший себя стандарт — OIDC (OpenID Connect). Он построен поверх OAuth 2.0 и широко используется для аутентификации и авторизации. Единственный его минус — многие бесплатные решения предлагают интеграции с OIDC только в платных Enterprise-версиях. Впрочем, нам это пока не помеха: мы можем выбрать инструмент, который поддерживает OIDC из коробки и вписывается в наш бескодовый подход.
Со стандартом определились, теперь перейдем к выбору конкретного решения. При выборе системы авторизации и аутентификации (IAM) для нашей PaaS-платформы я сразу подумал о Keycloak, но он оказался слишком громоздким для моих задач. Его сложная архитектура и код на Java не вписываются в мой стек, где я активно использую Go, — мне не хотелось нагружать проект этим «багажом».
Вдохновившись статьей «Выбираем IAM в 2023, или Что есть кроме Keycloak», где сравниваются IAM-решения на Go, я рассмотрел несколько вариантов. Я стал выбирать из двух:
-
Casdoor. Этот инструмент меня впечатлил своей простотой. Его можно быстро развернуть и настроить даже без глубоких знаний IAM. Удобный веб-интерфейс, понятная документация и множество готовых интеграций делают его идеальным для моей цели — создать PaaS без лишней разработки.
-
Zitadel. Хотя Zitadel выглядит стильно и современно, настройка оказалась настоящей проблемой. В документации есть примеры as is, но даже после следования им ничего не работало. Я решил, что, если продукт сразу вызывает трудности, лучше пройти мимо.
В итоге выбор пал на Casdoor. Особенно радует, что для него можно использовать PostgreSQL в качестве базы данных. Это идеально вписывается в нашу архитектуру, ведь у нас уже есть CloudNativePG, развернутый в предыдущей части. Благодаря этому оператору мы можем просто создать новый кластер PostgreSQL для Casdoor, не добавляя новых компонентов и не выходя за рамки бескодового подхода. Теперь наша схема выглядит следующим образом:
Добавление Casdoor в PaaS
Пора добавить Casdoor в нашу платформу через чарт paas-system, в котором нам надо будет реализовать следующую схему:
Сам процесс делится на два шага:
Шаг 1: Базовые компоненты
-
Одним запуском деплоим три компонента:
-
Gateway: Разворачиваем Envoy Gateway, который создаёт Deployment и LoadBalancer для Casdoor. Ждём, пока появится внешний IP-адрес.
apiVersion: gateway.networking.k8s.io/v1 kind: Gateway metadata: name: "gateway" spec: gatewayClassName: "eg" listeners: - name: web protocol: HTTP port: "8000" -
HTTPRoute и Service. Настраиваем маршрутизацию — HTTPRoute направляет трафик на Service, а Service привязан к подам Casdoor по селектору
apiVersion: v1 kind: Service metadata: name: "iam-service" spec: ports: - name: http port: 8000 targetPort: 8000 selector: labels: paas-system/app: iam --- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: "casdoor-route" spec: parentRefs: - name: "gateway" # Имя gateway rules: - backendRefs: - group: "" kind: Service name: "iam-service" port: 8000 weight: 1 matches: - path: type: PathPrefix value: / -
Postgres Cluster. Используем CloudNativePG для создания кластера PostgreSQL (master и sync replica) с Secret’ом для пароля. CloudNativePG автоматически читает секрет и создает пользователя.
apiVersion: v1 kind: Secret metadata: name: "iam-db" type: kubernetes.io/basic-auth data: username: {{ "admin" | b64enc }} password: {{ "admin" | b64enc }} --- apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: "iam" spec: instances: 2 bootstrap: initdb: database: "iam" owner: "admin" secret: name: "iam-db" storage: size: "1Gi" storageClass: "csi-ceph-ssd-me1" # Эток SC для vkcloud зоны ME1 -
Для запуска выполняем команду для helmwave и потом ждем, пока у нас появится IP-адрес.
helmwave up -t system --build --kubedog kubectl get gateway/gateway -n paas-system -o=jsonpath='{.status.addresses[0].value}' -w
Шаг 2: Разворачиваем Casdoor:
-
-
После получения IP-адреса LoadBalancer’а обновляем .Values.iam.address в локальном файле values.yaml (он под .gitignore). Добавляем в чарт Secret с конфигурацией Casdoor (OIDC, подключение к базе) и Deployment с селектором paas-system/service: iam. Повторно запускаем деплой — Casdoor запускается и подключается к базе.
apiVersion: v1 kind: Secret metadata: name: iam-config type: Opaque stringData: app.conf: | appname = casdoor httpport = 8000 runmode = dev SessionOn = true copyrequestbody = true driverName = postgres dataSourceName = user=admin password=admin host=iam-rw port=5432 sslmode=disable dbname=iam dbName = ian tableNamePrefix = iam showSql = false redisEndpoint = "" defaultStorageProvider = "" isCloudIntranet = false authState = "casdoor" socks5Proxy = "" verificationCodeTimeout = 10 initScore = 2000 logPostOnly = true origin = "http://<ip>:8000" staticBaseUrl = "http://<ip>:8000" enableGzip = true inactiveTimeoutMinutes = "" --- apiVersion: apps/v1 kind: Deployment metadata: name: iam-casdoor labels: paas-system/app: iam spec: replicas: 1 selector: matchLabels: paas-system/app: iam template: metadata: labels: paas-system/app: iam spec: containers: - name: casdoor-container image: casbin/casdoor:latest imagePullPolicy: Always ports: - containerPort: 8000 volumeMounts: - mountPath: /conf/ name: conf env: - name: RUNNING_IN_DOCKER value: "true" volumes: - name: conf secret: secretName: iam-configШаг 3: Проверяем доступ
-
Заходим по IP-адресу LoadBalancer’а с портом (например, http://<IP>:<port>), попадаем на страницу логина Casdoor. Вводим пароль по умолчанию (обычно admin/123, как указано в документации Casdoor), и после входа оказываемся на главной странице со статистикой и меню управления пользователями и ролями.
Страница логина Casdoor
Главная страница Casdoor
Теперь, когда Casdoor работает, мы можем использовать его как OIDC-провайдера для защиты нашего интерфейса. На данный момент Cyclops UI закрыт через basic auth, но с Casdoor мы перейдем на более удобный и безопасный вариант с помощью oauth2-proxy.
Настройка Casdoor
Для начала настраиваем Casdoor. В нем есть понятие организаций, и по умолчанию уже существует организация Built-in, которая используется самим Casdoor для внутренних нужд. Мы же создадим новую организацию под названием PaaS, куда будем добавлять всех пользователей нашей платформы. Из-за особенности Casdoor — невозможности делить пользователей между организациями — мы не будем создавать несколько организаций, а вместо этого используем группы для управления доступом. Это удобно, так как oauth2-proxy поддерживает проверку групп.
В организации PaaS создаем:
-
группу ops для администраторов PaaS, которые будут иметь полный доступ к платформе;
-
отдельную группу для каждого тенанта, чтобы управлять доступом пользователей к их приложениям и ресурсам.
Кроме того, для каждого тенанта создаем отдельное приложение в Casdoor с уникальными client_id и client_secret. Это позволяет изолировать доступ и настройки аутентификации для каждого тенанта, обеспечивая дополнительную безопасность и гибкость. Все пользователи добавляются в организацию PaaS, а их роли и права определяются через группы и приложения.
Расписывать процесс добавления пользователей, групп и приложений здесь не будем: интерфейс Casdoor интуитивно понятный, все делается через веб-формы и запутаться сложно.
Защита Cyclops UI с помощью oauth2-proxy
Теперь, когда Casdoor настроен, мы заменяем basic auth в Cyclops UI на аутентификацию через OIDC с помощью oauth2-proxy. Все изменения вносим в наш чарт paas-tenant, работая в namespace paas-tenant-1.
Добавляем ConfigMap. Создаем ConfigMap в namespace paas-tenant-1 с конфигурацией для oauth2-proxy. В нем указываем параметры OIDC: client_id и client_secret приложения из Casdoor (для организации PaaS), URL провайдера (IP-адрес Casdoor), настройки для проверки групп (например, ops или группы тенанта). Также в ConfigMap задаем проксирование запросов к Cyclops UI, указав его адрес и порт. Все прописывается в манифесте чарта.
apiVersion: v1 kind: ConfigMap metadata: name: oauth2-proxy-config data: oauth2-proxy.cfg: | provider = "oidc" provider_display_name = "PaaS" # Группы должны быть с префиксом организации. # Первая группа это админы paas, вторая это пользователи tenant allowed_groups = ["paas/ops", "paas/tenant-1"] # Публичный address IAM oidc_issuer_url = "http://<ip>:<iam public port>" # Параметры приложения client_id = "<app client id>" client_secret = "<app client secret>" # Публичный адрес нашего oauth2-proxy redirect_url = "http://<ip>:8000/oauth2/callback" email_domains = "*" cookie_secret = "your-32-char-secret-key1" cookie_secure = false # Это позволяет пока отказаться от HTTPs # Куда будут запроксированы запросы. upstreams = ["http://cyclops-ui:3000"] http_address = "0.0.0.0:3000" scope = "openid profile email"
-
Добавляем Deployment. Деплоим oauth2-proxy через новый Deployment в namespace paas-tenant-1. Deployment подхватывает ConfigMap с настройками, включая проксирование к Cyclops UI.
apiVersion: apps/v1 kind: Deployment metadata: name: oauth2-proxy labels: app: oauth2-proxy spec: replicas: 1 selector: matchLabels: app: oauth2-proxy template: metadata: labels: app: oauth2-proxy spec: containers: - name: oauth2-proxy image: quay.io/oauth2-proxy/oauth2-proxy:latest args: - --config=/etc/oauth2-proxy/oauth2-proxy.cfg ports: - containerPort: 3000 name: http volumeMounts: - name: config mountPath: /etc/oauth2-proxy volumes: - name: config configMap: name: oauth2-proxy-config -
Обновляем Service. В существующем Service (в namespace paas-tenant-1), который направляет трафик на Cyclops UI, меняем селектор, чтобы он указывал на поды oauth2-proxy. Новый селектор, например app: oauth2-proxy, обеспечивает маршрутизацию через oauth2-proxy. Gateway и HTTPRoute остаются без изменений — трафик идет через Envoy Gateway.
apiVersion: v1 kind: Service metadata: name: oauth2-proxy spec: selector: app: oauth2-proxy ports: - name: http port: 3000 targetPort: 3000 protocol: TCP type: ClusterIP
Теперь oauth2-proxy в paas-tenant-1 проверяет аутентификацию через Casdoor и перенаправляет запросы к Cyclops UI, если пользователь авторизован. Пользователь видит страницу логина Casdoor, а после входа попадает в интерфейс Cyclops UI.
Добавление общего ресурса — Grafana
Теперь, когда Cyclops UI защищен через oauth2-proxy и Casdoor, мы добавляем первый общий ресурс для нашей PaaS — Grafana. Это инструмент для визуализации метрик, доступный всем пользователям платформы. Устанавливаем его в namespace paas-system, так как это общий ресурс. Для хранения данных Grafana используем PostgreSQL, благо у нас уже есть CloudNativePG. Авторизацию настроим через Casdoor.
-
Установка Grafana Operator: Через Helmwave деплоим Grafana Operator в namespace paas-operators.
- name: "grafana" chart: name: "oci://ghcr.io/grafana/helm-charts/grafana-operator" # Версию надо явно указывать, в противном случае не ставится version: "v5.17.0" <<: *options-operators tags: [grafana]
-
Создаем PostgreSQL для Grafana. Используем CloudNativePG для создания нового кластера PostgreSQL в paas-system. Через Helmwave добавляем манифест с Secret для пароля и конфигурацией кластера. CloudNativePG сам создает пользователя и базу, как мы делали для Casdoor.
apiVersion: v1 kind: Secret metadata: name: "grafana-secret" type: kubernetes.io/basic-auth data: username: {{ "grafna" | b64enc }} password: {{ "password"| b64enc }} --- apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: "grafana-pg" spec: instances: 1 bootstrap: initdb: database: "grafana" owner: "grafana" secret: name: "grafana-secret" storage: size: "1Gi" storageClass: "csi-ceph-ssd-me1"
-
Разворачиваем Grafana. В чарте через Helmwave добавляем Custom Resource (CR) для Grafana Operator в paas-system. В CR задаем подключение к PostgreSQL (адрес и credentials из CloudNativePG) и параметры OIDC для авторизации через Casdoor: client_id, client_secret и URL провайдера из приложения Casdoor (для организации paas). Также, чтобы появился доступ извне, нужно добавить HTTPRoute (пока будем использовать тот же Gateway, что и для IAM), а оператор сам создаст сервис. Источники данных вроде Prometheus пока не подключаем — добавим их позже.
# Обновим графана secret apiVersion: v1 kind: Secret metadata: name: "grafana-secret" type: kubernetes.io/basic-auth data: ... admin_password: {{ "admin" | b64enc }} client_id: {{ "<client id>" | b64enc }} client_secret: {{ "<client secret>"| b64enc }} --- apiVersion: grafana.integreatly.org/v1beta1 kind: Grafana metadata: name: paas labels: dashboards: "paas" spec: deployment: spec: replicas: 1 template: spec: containers: - name: grafana env: - name: GF_DATABASE_USER valueFrom: secretKeyRef: name: "grafana-secret" key: username - name: GF_DATABASE_PASSWORD valueFrom: secretKeyRef: name: "grafana-secret" key: password - name: GF_SECURITY_ADMIN_PASSWORD valueFrom: secretKeyRef: name: "grafana-secret" key: admin_password - name: GF_AUTH_GENERIC_OAUTH_CLIENT_ID valueFrom: secretKeyRef: name: "grafana-secret" key: client_id - name: GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET valueFrom: secretKeyRef: name: "grafana-secret" key: client_secret config: database: type: postgres host: grafana-pg-rw name: grafana security: admin_user: admin server: root_url: "http://<public gateway ip>:<iam public port>/grafana" serve_from_sub_path: "true" http_port: "3000" auth: disable_login_form: "true" auth.basic: enabled: "true" auth.generic_oauth: enabled: "true" auth_url: "http://<iam public ip>:<iam public port>/login/oauth/authorize" api_url: "http://<iam service name>:<iam internal port>/api/userinfo" token_url: "http://<iam service name>:<iam internal port>/api/login/oauth/access_token" signout_redirect_url: "http://<iam public ip>:<iam public port>/api/logout" scopes: "openid profile email groups offline_access" use_pkce: "true" use_refresh_token: "true" role_attribute_path: contains(groups[*], 'paas/ops') && 'GrafanaAdmin' || 'Viewer' -- apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: "grafana-route" spec: parentRefs: - name: "iam-gateway" rules: - backendRefs: - group: "" kind: Service name: "paas-service" # сервис с таким именем добавляет оператор port: "3000" weight: 1 matches: - path: type: PathPrefix value: /grafana
-
Интеграция с Casdoor. В CR для Grafana включаем oauth2, указав настройки Casdoor. Пользователи перенаправляются на страницу логина Casdoor, а доступ определяется их группами в организации paas (например, ops или группы тенанта).
После деплоя Grafana доступна через Envoy Gateway (по существующему или новому HTTPRoute). Пользователи логинятся через Casdoor и получают доступ к дашбордам для просмотра метрик платформы.
Подключение PostgreSQL как источников данных в Grafana
С Grafana в namespace paas-system и PostgreSQL для Casdoor и самой Grafana, настроенных через CloudNativePG, мы можем использовать эти базы как источники данных для визуализации метрик. Для этого добавим их в Grafana как Custom Resources (CR) GrafanaDatasource.
Добавление GrafanaDatasource для PostgreSQL. Через Helmwave в чарте paas-tenant создаем два CR GrafanaDatasource в namespace paas-system:
-
Для базы Casdoor: указываем адрес PostgreSQL кластера Casdoor (из CloudNativePG), имя базы, пользователя и пароль (из Secret’а, созданного для Casdoor). Тип источника данных — PostgreSQL.
-
Для базы Grafana: аналогично указываем параметры PostgreSQL кластера, используемого самой Grafana (адрес, имя базы, пользователь, пароль из соответствующего Secret’а).
apiVersion: grafana.integreatly.org/v1beta1 kind: GrafanaDatasource metadata: name: grafana-postgres spec: valuesFrom: - targetPath: "secureJsonData.password" valueFrom: secretKeyRef: name: "grafana-secret" key: "password" instanceSelector: matchLabels: dashboards: "paas" datasource: name: grafana-postgres type: postgres url: "grafana-pg-r:5432" user: grafana access: proxy jsonData: database: "grafana" sslmode: 'disable' maxOpenConns: 10 maxIdleConns: 10 maxIdleConnsAuto: true connMaxLifetime: 14400 postgresVersion: 1500 timescaledb: false secureJsonData: password: "${password}" --- apiVersion: grafana.integreatly.org/v1beta1 kind: GrafanaDatasource metadata: name: iam-postgres spec: valuesFrom: - targetPath: "secureJsonData.password" valueFrom: secretKeyRef: name: "iam-db" key: "password" instanceSelector: matchLabels: dashboards: "paas" datasource: name: iam-postgres type: postgres url: "iam-r:5432" user: "admin" access: proxy jsonData: database: "iam" sslmode: 'disable' maxOpenConns: 10 maxIdleConns: 10 maxIdleConnsAuto: true connMaxLifetime: 14400 postgresVersion: 1500 timescaledb: false secureJsonData: password: "${password}"
-
Автоматическое появление в Grafana. После деплоя CR через Helmwave Grafana Operator подхватывает эти GrafanaDatasource и автоматически добавляет источники данных в Grafana. Они появляются в интерфейсе Grafana, готовые для создания дашбордов.
Теперь пользователи, авторизованные через Casdoor, могут заходить в Grafana и использовать эти источники данных для визуализации метрик, связанных с Casdoor (например, статистика логинов) или самой Grafana (например, внутренние метрики).
Как обычно, весь код можно найти в репозитории.
В следующей части наш PaaS начнет обрастать глазами и ушами, а также мы сделаем несколько улучшений в самих чартах.
ссылка на оригинал статьи https://habr.com/ru/articles/890386/
Добавить комментарий