Как стать автором
Обновить
367.49
VK Tech
VK Tech — российский разработчик корпоративного ПО

Kubernetes как PaaS: максимум возможностей без разработки. Часть 2

Время на прочтение12 мин
Количество просмотров1K

Это вторая часть серии статей, где мы шаг за шагом строим 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, не добавляя новых компонентов и не выходя за рамки бескодового подхода. Теперь наша схема выглядит следующим образом:

Kubernetes PaaS
Kubernetes PaaS

Добавление Casdoor в PaaS

Пора добавить Casdoor в нашу платформу через чарт paas-system, в котором нам надо будет реализовать следующую схему:

Схема работы casdoor
Схема работы casdoor

Сам процесс делится на два шага:

Шаг 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
    Главная страница 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 интуитивно понятный, все делается через веб-формы и запутаться сложно.

Casdoor UI
Casdoor UI

Защита 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.

Авторизация через oauth2-proxy
Авторизация через oauth2-proxy

Добавление общего ресурса — 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 и получают доступ к дашбордам для просмотра метрик платформы.

Схема работы grafana
Схема работы grafana

Подключение 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, готовые для создания дашбордов.

Postgres grafana
Postgres grafana

Теперь пользователи, авторизованные через Casdoor, могут заходить в Grafana и использовать эти источники данных для визуализации метрик, связанных с Casdoor (например, статистика логинов) или самой Grafana (например, внутренние метрики).

Как обычно, весь код можно найти в репозитории. 

В следующей части наш PaaS начнет обрастать глазами и ушами, а также мы сделаем несколько улучшений в самих чартах.

Теги:
Хабы:
+28
Комментарии2

Публикации

Информация

Сайт
tech.vk.com
Дата регистрации
Численность
1 001–5 000 человек
Местоположение
Россия
Представитель
Евгений Левашов