Деплой в Kubernetes — Versola Docs
VersolaVersola/docs
До 1.00.7.1versola.kzGitHub

Деплой в Kubernetes

Установите Versola в Kubernetes через Helm chart versola: два вида Secret, Job миграций, группы маршрутов ingress, PKI для mTLS, консоль, мониторинг и известные проблемы.

Helm chart versola разворачивает auth, central, edge и админ-консоль в кластере, который у вас уже есть. Базу данных, хранилище секретов и Prometheus он не включает: вы приносите их сами, а chart их использует. Эта страница проводит по установке и перечисляет значения chart. Для одного сервера без Kubernetes используйте Деплой на VPS; эти два пути разделяют только формат конфигурации (ни versola-cli, ни файлов секретов на базе OpenBao здесь нет).

Требования

  • Kubernetes 1.28 или новее. (У самого chart versola такого порога нет; требование 1.28 идёт от chart нагрузочного генератора в том же репозитории.)
  • cert-manager и trust-manager, установленные до chart: pki.enabled по умолчанию равно true и создаёт их ресурсы ClusterIssuer, Certificate и Bundle. Чтобы отказаться, задайте pki.enabled: false.
  • PostgreSQL, который вы предоставляете и который доступен из кластера. Стандартная топология — отдельная база на каждый сервис: у auth, central и edge своя база и своя история миграций, а также свои URL, пользователь и пароль (chart хранит три пароля Postgres). Одна база со схемой на сервис (?currentSchema=auth, central, edge) — простейшая топология, для небольшого кластера её достаточно. Каждый сервис при старте проверяет схему и отказывается стартовать, пока миграции не применены.
  • Ingress-контроллер и имя его IngressClass. ingress.className обязателен (см. Ingress). Вместо Ingress можно использовать Gateway API через ingress.gatewayAPI.
  • Helm. Helm 4 применяет манифесты на стороне сервера, что важно для ошибки дублирующегося окружения в разделе про миграции.
  • Необязательно: Stakater Reloader, чтобы перезапускать поды при изменении Secret или сертификата.
  • scala-cli на вашей рабочей машине для генерации конфигурации через scripts/gen-env.scala. В кластере он не нужен.

Все образы публичны в ghcr.io/versolauth/ (versola-auth, versola-central, versola-edge, versola-tools), так что imagePullSecrets не нужны.

Получите chart

Chart не опубликован ни в Helm-репозитории, ни в OCI-реестре. Он лежит в репозитории продукта в k8s/versola. Переключитесь на тег релиза, который хотите запустить, и ставьте из этого пути:

git clone --branch <version> https://github.com/versolauth/versola.git
cd versola

Тег образа каждого компонента по умолчанию равен appVersion chart, и CI проверяет, что такой тег есть в реестре. На теге 0.7.1 chart имеет версию 0.3.0 и appVersion: "0.6.2", поэтому установка без переопределений разворачивает образы 0.6.2, а не тот релиз, который вы взяли. Задайте теги сами:

services:
  auth:    { image: { tag: "0.7.1" } }
  central: { image: { tag: "0.7.1" } }
  edge:    { image: { tag: "0.7.1" } }
console:
  image:   { tag: "0.7.1" }   # versola-tools, в котором лежит консоль

Если включаете cert-sync, так же задайте pki.certSync.image.tag.

Сгенерируйте конфигурацию

Каждый сервис читает один HOCON-файл, смонтированный в /app/config/env.conf, в котором каждый секрет — плейсхолдер ${VAR}, разрешаемый из переменных окружения. scripts/gen-env.scala генерирует эти файлы и значения за ними как единый согласованный набор; несколько значений должны совпадать у разных сервисов (CENTRAL_SECRET_KEY и CLIENT_SECRETS_SECRET общие у auth и central, а публичная половина ключа edge лежит в central).

scala-cli run scripts/gen-env.scala

На первый вопрос ответьте k8s. Скрипт остаётся интерактивным и спрашивает каждое значение. Читайте значения по умолчанию, прежде чем принимать их; эти три для кластера неверны:

ВопросНе принимайтеОтветьте
Auth internal URLпубличный URLВнутрикластерный адрес, например http://versola-auth:8080.
Postgres URL (три, по одному на сервис)localhostНастоящий JDBC URL базы каждого сервиса. TLS по умолчанию — sslmode=verify-full, а prod без него не стартует; смонтируйте CA-bundle через extraVolumes/extraVolumeMounts и задайте postgres.ssl-root-cert (Конфигурация).
Postgres password (три)пароль-заглушка по умолчаниюПароль, который на самом деле стоит у этой базы.

Каждый вопрос принимает и --flag=value, так что запуск можно автоматизировать. Результат попадает в .local/env/k8s/: auth.conf, central.conf, edge.conf (с плейсхолдерами) и *.generated-secrets.env (значения), а также utils.private-key.jwk — приватный ключ клиента utils. Храните последний в надёжном месте и не загружайте его ни в один сервис; см. Секреты.

Два вида Secret

Chart использует два Secret, и они не взаимозаменяемы:

Ключ valuesЧто хранитКак доставляется
services.<name>.config.existingSecretОдин ключ, env.conf. По одному Secret на сервис.Смонтированный файл.
secrets.existingSecretЗначения, на которые ссылаются плейсхолдеры ${VAR}. Один Secret на все три сервиса.Отдельные записи secretKeyRef; каждый Deployment берёт только нужные ему ключи.

Имена ключей не совпадают с именами переменных. Chart переводит переменную в kebab-case и добавляет к ней префикс с именем сервиса, если она с него ещё не начинается; два значения, общие для auth и central, остаются без префикса. Всего 29 ключей:

СервисКлючи
authauth-access-tokens-secret, auth-refresh-tokens-secret, auth-codes-secret, auth-sessions-secret, auth-passwords-secret, auth-conversation-cookie-secret, auth-session-cookie-secret, auth-user-agent-cookie-secret, auth-par-requests-secret, auth-dpop-nonces-secret, auth-jwt-private-key, auth-postgres-password, auth-admin-bootstrap-password
centralcentral-account-resource-secret, central-resource-secret, central-jwks-json, central-edge-public-jwk, central-utility-client-public-jwk, central-postgres-password
edgeedge-private-key, edge-key-id, edge-token-enc-key, edge-sessions-secret, edge-internal-secret, edge-dpop-nonce-salt, edge-native-blob-key, edge-postgres-password
общие для auth и centralcentral-secret-key, client-secrets-secret

Например, ACCESS_TOKENS_SECRET у auth — это auth-access-tokens-secret, AUTH_CODES_SECRET — auth-codes-secret (а не auth-auth-codes-secret), а EDGE_PUBLIC_JWK у central — central-edge-public-jwk. У каждого сервиса свой пароль Postgres. Отсутствие secrets.existingSecret ломает рендер шаблона, а недостающий ключ проявляется как неразрешённая подстановка при старте, так что обе ошибки заметны сразу. Что защищает каждый секрет — в разделе Секреты.

Создайте Secret’ы с конфигурацией из сгенерированных файлов так, чтобы значения не проходили через терминал:

kubectl create secret generic auth-config -n versola \
  --from-file=env.conf=.local/env/k8s/auth.conf \
  --dry-run=client -o yaml | kubectl apply -f -

Так же создайте central-config и edge-config. Общий Secret собирается так же, с одним --from-literal на ключ, а имена переменных отображаются в имена ключей по правилу выше.

secrets.restartStrategy определяет, как поды замечают изменение общего Secret:

ЗначениеПоведение
none (по умолчанию)Ничего автоматически; после ротации выполните kubectl rollout restart.
checksumChart добавляет аннотацию с контрольной суммой для каждого сервиса, считаемой только по ключам, которые сервис использует. Сервисному аккаунту Helm нужно право get на Secret’ы при рендере; под helm template это не работает.
reloaderДобавляет reloader.stakater.com/auto: "true" для кластеров с Reloader.

Запуск миграций

Chart не мигрирует. Сервисы при старте проверяют схему и отказываются стартовать, если миграций нет, а RUN_MIGRATIONS жёстко задан как "false" в шаблоне Deployment. Попытка задать его через services.<name>.extraEnv рендерит дублирующуюся запись env, которую server-side apply в Helm 4 отклоняет. Job миграций в chart отслеживается в #209.

Запускайте versola-tools migrate как собственный одноразовый Job. Он читает те же auth.conf, central.conf и edge.conf, с которых стартуют сервисы. Ему нужны два собственных Secret’а, потому что конфигурации ему нужны под своими именами, а значения — по именам переменных:

  • versola-migrate-config: ключи auth.conf, central.conf, edge.conf.
  • versola-migrate-env: объединение трёх файлов *.generated-secrets.env. Инструмент разрешает документ целиком, поэтому у каждого плейсхолдера во всех трёх файлах должно быть значение, а не только в блоке Postgres.
apiVersion: batch/v1
kind: Job
metadata:
  name: versola-migrate
  namespace: versola
spec:
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migrate
          image: ghcr.io/versolauth/versola-tools:<version>
          args: ["migrate"]            # add "--dry-run" to report without applying
          envFrom:
            - secretRef:
                name: versola-migrate-env
          volumeMounts:
            - name: config
              mountPath: /opt/versola-tools/config
              readOnly: true
      volumes:
        - name: config
          secret:
            secretName: versola-migrate-config

Он создаёт каждую схему, если её нет, и мигрирует сервисы независимо. --dry-run сообщает, что было бы применено, ничего не меняя. Сначала сделайте бэкап базы: миграции нельзя откатить.

Установка

helm upgrade --install versola k8s/versola --namespace versola --create-namespace \
  --set services.auth.config.existingSecret=auth-config \
  --set services.central.config.existingSecret=central-config \
  --set services.edge.config.existingSecret=edge-config \
  --set secrets.existingSecret=versola-secrets \
  --set ingress.enabled=true \
  --set ingress.className=nginx \
  --set 'ingress.hosts[0].host=id.example.com' \
  --set 'ingress.hosts[0].routes={oidc,login,api}'

Запустите Job миграций до или сразу после, затем дождитесь подов. При первой установке auth и edge поднимаются раньше, чем central начинает отвечать, и ждут его: оба синхронизируют конфигурацию с central при старте, а порядок запуска трёх сервисов ничем не задан. Ожидающий сервис в статусе Running и 0/1 Ready и пишет в лог Still waiting for the source of cache .... Чем ожидание закончится, зависит от пула исходящего HTTP-клиента (HTTP_CLIENT_POOL_KIND, см. Конфигурацию):

  • С dynamic (и HTTP_CLIENT_POOL_MIN=0) сервис сам становится готовым, как только central займёт порт, и restartCount остаётся 0.
  • С пулом fixed по умолчанию этого не происходит. Первый запрос к central открывает все соединения пула; пока central их отклоняет, пул хранит по неудавшемуся соединению на слот (по умолчанию 128), и каждая повторная попытка, раз в 5 секунд, тратит одно. Через две минуты сервис завершается с кодом 1, и restartPolicy: Always его перезапускает; новый процесс строит свежий пул и стартует нормально. Ожидайте один перезапуск при холодной установке.

Ожидание — две минуты и только для central, до которого нельзя достучаться; central, который отвечает и отклоняет запрос (неверный URL, неверный ключ синхронизации), приводит к ошибке старта за секунды. Переменные пула задавайте через services.<name>.extraEnv.

Справочник значений

Значения по умолчанию — из chart этого релиза. Поля размещения (nodeSelector, tolerations, affinity) есть в global и у каждого компонента; значение компонента заменяет глобальное, а не сливается с ним, и на верхнем уровне их нет.

Глобальные и rollout

КлючПо умолчаниюЗначение
global.imageRegistry, global.imageRepositoryghcr.io, versolauthОткуда берутся образы.
global.imagePullPolicyIfNotPresent
global.imagePullSecrets[]
preStopSleepSeconds5Откладывает SIGTERM, чтобы эндпоинты перестали маршрутизировать на завершающийся под.
terminationGracePeriodSeconds30Должен покрывать pre-stop-паузу, 15 с graceful shutdown приложения и запас.
diagnosticsSizeLimit1GiРазмер emptyDir в /diagnostics (дамп кучи, GC-лог). Можно задать и для отдельного сервиса.
metrics.prometheusAnnotationstrueДобавляет подам аннотации prometheus.io/scrape, port и path.
secrets.existingSecret, secrets.restartStrategy"", noneСм. выше.

Сервисы

Для каждого из services.auth, services.central, services.edge:

Ключauthcentraledge
replicaCount111
port (PORT)808080908095
diagnosticsPort (DPORT)808180918096
additionalPort (APORT)8082——
mutualTlsPort (MPORT)8083——
resourcesrequest 250m CPU и 1Gi, limit 1Gi памятитак жетак же

Также у каждого сервиса: image.repository и image.tag, config.existingSecret и config.secretKey (env.conf), extraEnv, extraEnvFrom, а также extraVolumes и extraVolumeMounts (в values chart перечислены только у auth, но шаблон Deployment читает их для каждого сервиса). Лимит 1Gi рассчитан из настроек JVM по умолчанию: фиксированная куча 512 МиБ плюс ограниченная память вне кучи (см. Конфигурацию). Пробы используют порт диагностики: /liveness (начальная задержка 10 с, период 10 с) и /readiness (5 с, 5 с).

Доступность

Для каждого сервиса в services.<name> (распределение по узлам — также в global). Все значения по умолчанию — отправная точка, а не гарантия.

КлючПо умолчаниюЗначение
global.topologySpreadConstraintsмягкое распределение по kubernetes.io/hostname и topology.kubernetes.io/zone (maxSkew: 1, ScheduleAnyway)Распределяет реплики каждого компонента по узлам и зонам. Мягкое, потому что жёсткое ограничение оставляет поды в Pending в кластере с меньшим числом узлов или зон, чем реплик. Ограничение без labelSelector получает метки самого компонента. Собственный непустой topologySpreadConstraints сервиса заменяет этот список; [] в global отключает распределение.
services.<name>.podDisruptionBudgetenabled: true, maxUnavailable: 1Создаётся, только пока у сервиса больше одной реплики (replicaCount или autoscaling.minReplicas). Задавайте вместо него minAvailable (при пустом maxUnavailable), но не оба сразу.
priorityClasses.createfalsetrue создаёт versola-customer-facing (значение 1000000, для auth и edge) и versola-control-plane (100000, для central), чтобы при нехватке ресурсов узла первой вытеснялась административная плоскость. По умолчанию выключено, потому что PriorityClass — cluster-scoped объект. services.<name>.priorityClassName в любом случае называет уже существующий класс.
services.<name>.autoscalingenabled: false; auth от 2 до 6, central от 1 до 2, edge от 2 до 6; targetCPUUtilizationPercentage: 70HorizontalPodAutoscaler. Когда включён, Deployment перестаёт задавать replicas. metrics заменяет метрику CPU по умолчанию списком autoscaling/v2, behavior замедляет уменьшение.

Не масштабируйте эти сервисы по памяти: куча JVM фиксирована, поэтому память от нагрузки не зависит. CPU — слабый сигнал для auth и edge, потому что подписание RSA и хеширование Argon2id идут за семафорами (argon2.max-concurrent), и CPU упирается в то, что они пропускают, пока запросы стоят в очереди. Предпочитайте метрику типа Pods или External, например argon2_hashes_waiting, через адаптер метрик (Наблюдаемость). central — административная плоскость, и его лучше оставить с фиксированным числом реплик.

Консоль

Админ-консоль — это обычный Deployment с nginx (console.server.image, по умолчанию nginx:1.31-alpine). Init-контейнер копирует статическую сборку из образа versola-tools (console.image) в под, поэтому образа веб-сервера, специфичного для консоли, нет.

КлючПо умолчаниюЗначение
console.enabledtrue
console.port2821
console.basePath/Где консоль отвечает на своём хосте; / подходит для отдельного имени хоста.
console.modedirectКак консоль обращается к API. direct вызывает настоящие маршруты edge на origin консоли и — единственный режим, который может обслужить Ingress chart. prefix — поведение VPS, для него нужна собственная конфигурация прокси через console.nginxConfig.
console.nginxConfig.existingConfigMap, .content""Ваша собственная конфигурация nginx.
console.replicaCount, console.resources1, 100m / 128Mi
console.preStopSleepSeconds, console.terminationGracePeriodSeconds5, 20

Значения Ingress

КлючПо умолчаниюЗначение
ingress.enabledfalse
ingress.className""Обязателен при включении; см. ниже.
ingress.annotations{}
ingress.hosts[]Список { host, routes: [...] }.
ingress.tls[]Стандартный TLS-блок Ingress, передаётся как есть.
ingress.gatewayAPI.enabled, .parentRefsfalse, []Создавать HTTPRoute вместо Ingress.

PKI (mutual TLS)

КлючПо умолчаниюЗначение
pki.enabledtrueНужен cert-manager.
pki.issuerRef{}Использовать уже имеющийся у вас издатель вместо самоподписанного корня chart.
pki.trustNamespacecert-managerГде лежит Secret с CA, чтобы trust-manager мог его прочитать.
pki.cacommonName: Versola Client CA, duration: 87600h, renewBefore: 8760hVersola Client CA.
pki.authServerduration: 2160h, renewBefore: 720h, ECDSA 256Листовой сертификат mTLS-listener’а auth; secretName по умолчанию <fullname>-auth-mtls, dnsNames добавляет имена.
pki.clients, pki.clientDefaults[]Сертификаты, выпущенные вне central, по одному Certificate cert-manager на клиента.
pki.trustBundle.enabled, .sourcestrue, []Bundle trust-manager, который auth монтирует как trust anchors; sources добавляет CA, например новый при ротации.
pki.reloadertrueАннотации Reloader у auth и edge.
pki.certSyncвыключенJob cert-sync, который копирует выпущенные снаружи сертификаты в central (intervalSeconds 300).

Ingress

Chart предоставляет группы маршрутов, а не отдельные пути. Хост перечисляет группы, которые обслуживает; всё, что не перечислено, не публикуется, а группы «всё остальное» нет.

ГруппаБэкендПути
oidcauth/authorize, /token, /par, /introspect, /revoke, /userinfo, /logout, /.well-known/, /challenge
loginedge/login/, /complete, /logout/
apiedge/resources/, /permissions/
nativeedge/native/: то, что вызывает мобильное приложение при FAPI 2.0. Укажите её на любом хосте с native-клиентами.
consoleconsoleconsole.basePath
usersauth/users, /users/; обычно вместо этого обращаются по DNS кластера.
serviceauth/service/: инструменты синхронизации конфигурации для не-prod, 404, когда env равен prod.
settingsauth/settings, на дополнительном порту auth. Учётные данные — секрет ресурса, а не пользовательские.

/logout — единственное пересечение: auth владеет им точно, edge — префиксом /logout/. Группы для central нет; его административный API доступен через ресурсный прокси edge (api) или по DNS кластера изнутри кластера.

ingress.className обязателен. Шаблон падает, а не берёт IngressClass кластера по умолчанию. У стандартной установки ingress-nginx класса по умолчанию нет, поэтому Ingress, созданный без класса, не подхватывает ни один контроллер: его ADDRESS остаётся пустым, а запросы получают голый 404. Если отметить класс как default уже потом, существующий Ingress это не исправит; пересоздайте его или задайте spec.ingressClassName.

Типичное разделение — OIDC и вход на публичном хосте, консоль на своём:

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: id.example.com
      routes: [oidc, login]
    - host: console.example.com
      routes: [console, api, login]

Mutual TLS и PKI

При pki.enabled chart настраивает cert-manager для сертификатов, на которых держится mutual-TLS-listener auth на 8083 (порт контейнера и Service). Он создаёт самоподписанный корень и ClusterIssuer Versola Client CA (или использует pki.issuerRef), листовой сертификат listener’а, Bundle trust-manager, который auth монтирует как trust anchors, аннотации Reloader и сервисный аккаунт для central, которому разрешено создавать CertificateRequest cert-manager в namespace релиза и ничего больше. central использует его, чтобы выпускать и продлевать сертификаты клиентов, стоящих за edge: зарегистрируйте клиента с issueEdgeClientCertificate: true и mtlsAuth, и central запросит у издателя, названного в CLIENT_CERT_ISSUER_NAME (его задаёт chart), 14-дневный сертификат, продлеваемый при остатке 4 дня.

Chart не генерирует env.conf; направьте свой на файлы, которые он монтирует:

СервисСмонтирован вИспользовать как
auth/app/mtls/server/tls.crt, tls.keymutual-tls.certificate, mutual-tls.private-key
auth/app/mtls/trust/ca.crtmutual-tls.trusted-certificates (trust bundle или выпускающий CA)
edge/app/mtls/auth-pin/tls.crtnative.trusted-certificates (pin листового сертификата; edge получает сертификат, но никогда не ключ)

gen-env пишет блоки mutual-tls и native с этими путями. auth и edge читают файлы один раз при старте, поэтому после продления нужен перезапуск: используйте pki.reloader с Reloader или kubectl rollout restart после обновления сертификата cert-manager. Продление перезапускает оба сервиса вместе, поэтому для ротации сертификата listener’а auth без простоя опубликуйте следующий листовой сертификат вторым pin’ом на каждом edge, прежде чем auth переключится. Для ротации CA добавьте Secret нового CA в pki.trustBundle.sources рядом с текущим, перевыпустите сертификаты клиентов, затем уберите старый источник. Клиентские сертификаты и способы их получения описаны в разделе клиентские сертификаты edge.

Мониторинг

Chart не устанавливает Prometheus и не создаёт ServiceMonitor или PodMonitor. При metrics.prometheusAnnotations (по умолчанию true) каждый под несёт prometheus.io/scrape, prometheus.io/port (порт диагностики) и prometheus.io/path: /metrics, так что сборщик метрик на аннотациях (vmagent, обычный Prometheus) находит каждую реплику.

Не направляйте цели сбора на имена ClusterIP Service. Один Service стоит перед всеми репликами, поэтому instance цели остаётся неизменным, а под за ним меняется на каждом соединении: rate() занижается, а у серии нет метки pod. Используйте обнаружение подов по аннотациям. Рабочий пример — k8s/monitoring/vmagent-values.yaml в репозитории продукта. Что собирать и по каким сигналам настраивать алерты — в разделе Наблюдаемость.

Диагностика

Каждый под получает emptyDir в /diagnostics, куда JVM пишет дамп кучи при ошибке нехватки памяти и ротируемый GC-лог (diagnosticsSizeLimit). emptyDir переживает перезапуск контейнера, который вызывает OOM, поэтому дамп можно забрать через kubectl cp. См. Наблюдаемость.

Известные проблемы

ПроблемаЭффект
#380Секрет административного API central нельзя получить или ротировать после того, как его сгенерировал bootstrap. Задайте bootstrap.resource-secret до первого запуска. Та же проблема касается пересоздания пары ключей utils после того, как central её сохранил.
#209В chart нет Job миграций; запускайте собственный, как показано выше.

Дальше