From efc506e5def7935e50f910a9f6a158ea80d818a7 Mon Sep 17 00:00:00 2001 From: Dmitrii Sashchenko Date: Wed, 2 Sep 2026 16:37:42 +0600 Subject: [PATCH 1/3] =?UTF-8?q?DEVOPS-117:=20=D1=87=D0=B0=D1=80=D1=82=20on?= =?UTF-8?q?-premise=203.0=20=E2=80=94=20Nanabush=20Player,=20=D1=81=D0=B5?= =?UTF-8?q?=D0=BA=D1=80=D0=B5=D1=82=D1=8B=20=D0=B2=20Secret,=20=D0=B2?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D1=8F=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Актуализация чарта по продовому .helm из HRBOX. Nanabush Player перенесён из прода и упрощён под one-release-топологию on-premise: без карт env/ci_dc, но с сохранёнными инвариантами (audience = https://, basePath отдельно от aud, allowlist обратного канала только на внутрикластерный web, метрики на непубликуемом порту). Выделенного поддомена в on-premise нет, поэтому плеер монтируется на основной домен под /nanabush-player. Три переключателя для аварийного отката сохранены. Из прода также перенесены: пул конвертации видео, PodDisruptionBudget для web, OpenTelemetry-сайдкар, assetlinks, набор location в nginx (/healthz, /health-check, потоковый /api/v1/integration/commit, /ai/, sw.js, шрифты). Версии образов подтянуты к продовым. Приведение чарта в порядок: все образы в блоке image, imagePullSecrets в настройках, девять одинаковых define ресурсов заменены одним хелпером, удалено ~100 строк мёртвого кода в _helpers.tpl, пароли и ключи уехали в Secret и читаются через secretKeyRef, добавлены 00-validate.yaml и NOTES.txt. Исправлено в 2.x: - SERVICES_INTERNAL_SECRET имел захардкоженное значение по умолчанию, то есть все установки, где его не заполнили, работали на одном секрете межсервисной аутентификации; - cluster.domain игнорировался: маршруты кластера NATS были захардкожены на cluster.local; - CLUSTER_ADVERTISE без сегмента svc — ноды NATS объявляли соседям недостижимый адрес; - nats.replicas и nats.cluster.replicas задавались независимо, при расхождении часть нод не входила в кластер; - пробы web проверяли только TCP-порт 9000, поэтому под с неработающим PHP считался готовым; - внутренние URL были захардкожены строками hrbox-* и молча ломались при nameOverride. Имена объектов и селекторы Deployment не менялись — обновление 2.x -> 3.0 не пересоздаёт объекты. Переехавшие ключи values.yaml чарт распознаёт и останавливает установку с подсказкой; порядок перехода — в CHANGELOG.md. Проверено: helm lint чистый; отрендерены 4 конфигурации (дефолт, всё включено, плеер и инфра выключены, nameOverride); структурная проверка рендера без замечаний; 20 негативных сценариев срабатывают с ожидаемыми сообщениями; реалистичный values.yaml из 2.x последовательно ловится всеми шестью guard'ами. Co-Authored-By: Claude Opus 5 --- .helm/Chart.yaml | 22 +- .helm/templates/00-app-secret.yaml | 38 + .../templates/00-nanabush-player-secret.yaml | 33 + .helm/templates/00-validate.yaml | 151 +++ .helm/templates/01-nats-configmap.yaml | 25 +- .helm/templates/01-nginx-assetlinks.yaml | 46 + .helm/templates/01-nginx-configmap.yaml | 225 +++-- .helm/templates/01-otel-configmap.yaml | 38 + .helm/templates/01-wait-configmap.yaml | 12 +- .helm/templates/02-dragonfly.yaml | 66 +- .helm/templates/02-nats-box.yaml | 131 +-- .helm/templates/02-web-pdb.yaml | 21 + .helm/templates/02-web.yaml | 121 ++- .helm/templates/03-chatbox.yaml | 61 ++ .helm/templates/03-websocket.yaml | 51 - .helm/templates/04-worker.yaml | 31 +- .helm/templates/05-conductor.yaml | 76 +- .helm/templates/09-file-processor-video.yaml | 58 ++ .helm/templates/09-file-processor.yaml | 29 +- .helm/templates/09-geonames.yaml | 60 ++ .helm/templates/09-geonames.yml | 50 - .helm/templates/09-goworker.yaml | 39 +- .helm/templates/09-kedoca.yaml | 58 +- .helm/templates/09-nanabush-player.yaml | 232 +++++ .helm/templates/10-app-ingress.yaml | 52 +- .helm/templates/98-migrate-job.yaml | 37 +- .helm/templates/99-update-cluser-job.yaml | 24 - .helm/templates/99-update-cluster-job.yaml | 36 + .helm/templates/NOTES.txt | 72 ++ .helm/templates/_envs.tpl | 421 +++++---- .helm/templates/_helpers.tpl | 301 ++++-- .helm/templates/_resources.tpl | 146 +-- .helm/values.yaml | 890 ++++++++++++------ CHANGELOG.md | 263 ++++++ README.md | 352 ++++++- 35 files changed, 3150 insertions(+), 1118 deletions(-) create mode 100644 .helm/templates/00-app-secret.yaml create mode 100644 .helm/templates/00-nanabush-player-secret.yaml create mode 100644 .helm/templates/00-validate.yaml create mode 100644 .helm/templates/01-nginx-assetlinks.yaml create mode 100644 .helm/templates/01-otel-configmap.yaml create mode 100644 .helm/templates/02-web-pdb.yaml create mode 100644 .helm/templates/03-chatbox.yaml delete mode 100644 .helm/templates/03-websocket.yaml create mode 100644 .helm/templates/09-file-processor-video.yaml create mode 100644 .helm/templates/09-geonames.yaml delete mode 100644 .helm/templates/09-geonames.yml create mode 100644 .helm/templates/09-nanabush-player.yaml delete mode 100644 .helm/templates/99-update-cluser-job.yaml create mode 100644 .helm/templates/99-update-cluster-job.yaml create mode 100644 .helm/templates/NOTES.txt diff --git a/.helm/Chart.yaml b/.helm/Chart.yaml index a4f081b..f0d591d 100644 --- a/.helm/Chart.yaml +++ b/.helm/Chart.yaml @@ -1,19 +1,25 @@ apiVersion: v2 name: hrbox -description: Simplified HRBox application Helm chart +description: HRBox — HR-платформа для on-premise установки type: application -# Версия самого чарта. Увеличивается при каждом изменении чарта -version: 2.0.0 +# Версия чарта. Увеличивается при каждом изменении чарта. +version: 3.0.0 -# Версия приложения, которое деплоится. Не обязательно увеличивать при изменении чарта -appVersion: "3.0.0" +# Версия приложения. Совпадает с линейкой образов HRBox. +appVersion: "3.1.0" -# Ключевые слова для поиска keywords: - hrbox - hr - - application + - lms + - kedo + - on-premise -# Домашняя страница проекта home: https://hrbox.io + +annotations: + # Компоненты, которые разворачивает чарт + hrbox.io/components: >- + web, worker, conductor, chatbox, goworker, kedoca, geonames, + file-processor, nanabush-player, dragonfly, nats diff --git a/.helm/templates/00-app-secret.yaml b/.helm/templates/00-app-secret.yaml new file mode 100644 index 0000000..a67dce4 --- /dev/null +++ b/.helm/templates/00-app-secret.yaml @@ -0,0 +1,38 @@ +{{- if not .Values.secrets.existingSecret }} +{{/* +Единый Secret с чувствительными настройками HRBox. + +В чарте 2.x пароли и ключи подставлялись прямо в env-блоки Deployment, +поэтому любой, кто мог сделать `kubectl get deploy -o yaml`, видел пароль +базы и ключ шифрования. Здесь значения лежат в одном Secret, а контейнеры +читают их через secretKeyRef (см. hrbox.secretEnv в _helpers.tpl). + +Если секретами управляет внешний инструмент, задайте secrets.existingSecret — +тогда чарт этот объект не создаёт и только ссылается на указанный. Набор +ключей должен совпадать с перечисленным ниже. +*/}} +--- +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: {{ include "hrbox.secretName" . | quote }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "app") | nindent 4 }} +stringData: + cookie-validation-key: {{ .Values.secrets.cookieValidationKey | quote }} + encryption-key: {{ .Values.secrets.encryptionKey | quote }} + services-internal-secret: {{ .Values.secrets.servicesInternalSecret | quote }} + postgres-password: {{ .Values.app.postgres.password | quote }} + s3-key: {{ .Values.app.s3.key | quote }} + s3-secret: {{ .Values.app.s3.secret | quote }} + smtp-password: {{ .Values.app.smtp.password | quote }} + hub-token: {{ .Values.app.hub.token | quote }} + chatbox-wrapping-key: {{ .Values.app.chatbox.wrappingKey | quote }} + livekit-api-secret: {{ .Values.app.livekit.apiSecret | quote }} + yandex-client-password: {{ .Values.app.yandex.clientPassword | quote }} + yandex-ai-api-key: {{ .Values.app.ai.yandex.apiKey | quote }} + google-client-secret: {{ .Values.app.integrations.google.clientSecret | quote }} + slack-client-secret: {{ .Values.app.slack.clientSecret | quote }} + slack-verification-token: {{ .Values.app.slack.verificationToken | quote }} +{{- end }} diff --git a/.helm/templates/00-nanabush-player-secret.yaml b/.helm/templates/00-nanabush-player-secret.yaml new file mode 100644 index 0000000..95ca43f --- /dev/null +++ b/.helm/templates/00-nanabush-player-secret.yaml @@ -0,0 +1,33 @@ +{{- $cfg := .Values.app.nanabushPlayer.deployment }} +{{- if and $cfg.enabled $cfg.existingSecret.create }} +{{/* +Ключи Nanabush Player. + +ticketHs256 читают обе стороны: HRBox подписывает им launch-тикет +(NANABUSH_PLAYER_HS256_SECRET в поде web), плеер этим же ключом тикет +проверяет (PLAYER_HS256_SECRET). Поэтому ротация — одно значение на обе +стороны, а не два независимых. + +sessionHs256 принадлежит только плееру: им подписывается сессия учащегося +внутри плеера, HRBox её не читает. + +При existingSecret.create: false этот объект не создаётся — оба потребителя +всё так же ссылаются на existingSecret.name, но Secret должен быть заведён +вне чарта. При смене владельца уже существующего Secret берите новое имя, +чтобы Helm не удалил внешний объект во время перехода. +*/}} +--- +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: {{ include "nanabushPlayer.secretName" . | quote }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "nanabush-player") | nindent 4 }} +stringData: + {{ required "Нужно имя ключа ticketHs256 в секрете Nanabush Player" $cfg.existingSecret.keys.ticketHs256 }}: {{ .Values.app.nanabushPlayer.keys.ticketHs256 | quote }} + {{ required "Нужно имя ключа sessionHs256 в секрете Nanabush Player" $cfg.existingSecret.keys.sessionHs256 }}: {{ .Values.app.nanabushPlayer.keys.sessionHs256 | quote }} + {{- with $cfg.existingSecret.keys.ticketKeys }} + {{ . }}: {{ $.Values.app.nanabushPlayer.keys.ticketKeys | quote }} + {{- end }} +{{- end }} diff --git a/.helm/templates/00-validate.yaml b/.helm/templates/00-validate.yaml new file mode 100644 index 0000000..f96904b --- /dev/null +++ b/.helm/templates/00-validate.yaml @@ -0,0 +1,151 @@ +{{/* +============================================================================ + Проверка values.yaml +============================================================================ + +Этот файл не создаёт ни одного объекта. Он выполняется первым и падает с +понятным сообщением, если конфигурация неполная или противоречивая, — до +того, как в кластере появятся поды, которые всё равно не заработают. + +Проверяется три вещи: + 1) ключи, переехавшие в версии 3.0 (подсказка вместо тихого игнора); + 2) обязательные значения, без которых приложение не поднимется; + 3) инварианты Nanabush Player. +*/}} + +{{/* --- 1. Ключи из чарта 2.x ---------------------------------------------- */}} +{{- if .Values.app.image }} + {{- fail "app.image переехал в image.hrbox (см. CHANGELOG, миграция 2.x -> 3.0)" }} +{{- end }} +{{- if .Values.app.chatbox.image }} + {{- fail "app.chatbox.image переехал в image.chatbox" }} +{{- end }} +{{- if .Values.app.goworker.image }} + {{- fail "app.goworker.image переехал в image.goworker" }} +{{- end }} +{{- if .Values.app.kedoca.image }} + {{- fail "app.kedoca.image переехал в image.kedoca" }} +{{- end }} +{{- if .Values.app.file_processor }} + {{- fail "app.file_processor переименован в app.fileProcessor, а образ переехал в image.fileProcessor" }} +{{- end }} +{{- if .Values.app.custom_policy }} + {{- fail "app.custom_policy переименован в app.customPolicy" }} +{{- end }} +{{- if .Values.app.replicas.websocket }} + {{- fail "app.replicas.websocket переименован в app.replicas.chatbox ещё в версии 2.0" }} +{{- end }} +{{- if .Values.resources.websocket }} + {{- fail "resources.websocket переименован в resources.chatbox ещё в версии 2.0" }} +{{- end }} +{{- if .Values.nats.image }} + {{- fail "nats.image переехал в image.nats" }} +{{- end }} + +{{/* --- 2. Обязательные значения ------------------------------------------- */}} +{{- if not .Values.ingress.host }} + {{- fail "Заполните ingress.host — домен, по которому открывается HRBox" }} +{{- end }} +{{- if not .Values.app.defaultTenantHostname }} + {{- fail "Заполните app.defaultTenantHostname (обычно совпадает с ingress.host)" }} +{{- end }} +{{- if not .Values.app.postgres.password }} + {{- fail "Заполните app.postgres.password" }} +{{- end }} +{{- if not .Values.app.s3.key }} + {{- fail "Заполните app.s3.key — без хранилища файлов HRBox не работает" }} +{{- end }} +{{- if not .Values.app.s3.secret }} + {{- fail "Заполните app.s3.secret" }} +{{- end }} +{{- if not .Values.app.hub.token }} + {{- fail "Заполните app.hub.token — токен выдают сотрудники HRBox" }} +{{- end }} +{{- if not .Values.secrets.existingSecret }} + {{- if not .Values.secrets.cookieValidationKey }} + {{- fail "Заполните secrets.cookieValidationKey (openssl rand -hex 16)" }} + {{- end }} + {{- if not .Values.secrets.encryptionKey }} + {{- fail "Заполните secrets.encryptionKey (openssl rand -base64 48). Потеря ключа делает зашифрованные данные нечитаемыми" }} + {{- end }} + {{- if not .Values.secrets.servicesInternalSecret }} + {{- fail "Заполните secrets.servicesInternalSecret (openssl rand -base64 48)" }} + {{- end }} + {{- if not .Values.app.chatbox.wrappingKey }} + {{- fail "Заполните app.chatbox.wrappingKey (openssl rand -hex 16)" }} + {{- end }} +{{- end }} +{{- if .Values.ingress.tls.enabled }} + {{- if not .Values.ingress.tls.secretName }} + {{- fail "При ingress.tls.enabled нужен ingress.tls.secretName с TLS-сертификатом" }} + {{- end }} +{{- end }} + +{{/* --- 3. Внешние зависимости ---------------------------------------------- */}} +{{- if and (not .Values.dragonfly.enabled) (not .Values.app.redis.host) }} + {{- fail "dragonfly.enabled: false требует app.redis.host — иначе приложение обратится к сервису, которого нет в релизе" }} +{{- end }} +{{- if and (not .Values.nats.enabled) (not .Values.app.nats.url) }} + {{- fail "nats.enabled: false требует app.nats.url — иначе приложение обратится к брокеру, которого нет в релизе" }} +{{- end }} + +{{- if and .Values.nats.enabled .Values.nats.cluster.enabled }} + {{- if ne (.Values.nats.replicas | int) (.Values.nats.cluster.replicas | int) }} + {{- fail (printf "nats.replicas (%d) и nats.cluster.replicas (%d) должны совпадать: иначе часть нод не войдёт в кластер, а реплики JetStream-стримов будут посчитаны неверно" (.Values.nats.replicas | int) (.Values.nats.cluster.replicas | int)) }} + {{- end }} +{{- end }} + +{{/* --- 4. Nanabush Player -------------------------------------------------- */}} +{{- $player := .Values.app.nanabushPlayer }} +{{- if and $player.enabled (not $player.deployment.enabled) }} + {{- fail "app.nanabushPlayer.enabled требует deployment.enabled: HRBox нельзя переключить на плеер, которого нет в кластере" }} +{{- end }} +{{- if and $player.enabled (not $player.ingress.enabled) }} + {{- fail "app.nanabushPlayer.enabled требует ingress.enabled: иначе HRBox выдаст ссылку на недоступный адрес" }} +{{- end }} +{{- if and $player.ingress.enabled (not $player.deployment.enabled) }} + {{- fail "app.nanabushPlayer.ingress.enabled требует deployment.enabled: маршрут вёл бы в несуществующий Service" }} +{{- end }} +{{- if and $player.ingress.enabled (not .Values.ingress.enabled) }} + {{- fail "Путь плеера публикуется на основном Ingress, поэтому нужен ingress.enabled: true" }} +{{- end }} +{{- if $player.deployment.enabled }} + {{- $basePath := $player.basePath | default "/nanabush-player" }} + {{- if eq (trimSuffix "/" $basePath) "" }} + {{- fail "app.nanabushPlayer.basePath не может быть корнем: корень домена занят самим HRBox" }} + {{- end }} + {{- if not (hasPrefix "/" $basePath) }} + {{- fail "app.nanabushPlayer.basePath должен начинаться со слэша, например /nanabush-player" }} + {{- end }} + {{- if not (include "hrbox.imageTag" .Values.image.nanabushPlayer) }} + {{- fail "image.nanabushPlayer должен содержать явный тег версии, например nanabush-player:1.0.11 — из него выводится версия плеера в launch-тикете" }} + {{- end }} + {{- if not $player.deployment.binaryCapabilities.basePath }} + {{- fail "On-premise монтирует плеер под путём, поэтому нужен образ с поддержкой base path. Проверьте сборку и верните deployment.binaryCapabilities.basePath: true" }} + {{- end }} + {{- if not $player.deployment.existingSecret.name }} + {{- fail "Заполните app.nanabushPlayer.deployment.existingSecret.name" }} + {{- end }} + {{- if $player.deployment.existingSecret.create }} + {{- if not $player.keys.ticketHs256 }} + {{- fail "Заполните app.nanabushPlayer.keys.ticketHs256 (openssl rand -base64 48)" }} + {{- end }} + {{- if not $player.keys.sessionHs256 }} + {{- fail "Заполните app.nanabushPlayer.keys.sessionHs256 (openssl rand -base64 48)" }} + {{- end }} + {{- if eq $player.keys.ticketHs256 $player.keys.sessionHs256 }} + {{- fail "app.nanabushPlayer.keys.ticketHs256 и sessionHs256 должны отличаться: это ключи разных контуров" }} + {{- end }} + {{- if and $player.deployment.existingSecret.keys.ticketKeys (not $player.keys.ticketKeys) }} + {{- fail "Указано имя ключа ticketKeys в секрете, но само кольцо app.nanabushPlayer.keys.ticketKeys пустое" }} + {{- end }} + {{- end }} + {{- if and $player.deployment.redis.urlFromExistingSecret $player.deployment.existingSecret.create }} + {{- fail "redis.urlFromExistingSecret: true требует existingSecret.create: false — Secret, создаваемый чартом, не содержит URL Redis" }} + {{- end }} + {{- if $player.deployment.metrics.enabled }} + {{- if eq ($player.deployment.metrics.port | int) ($player.service.port | int) }} + {{- fail "Порт метрик плеера должен отличаться от публичного порта сервиса, иначе метрики окажутся доступны содержимому курсов" }} + {{- end }} + {{- end }} +{{- end }} diff --git a/.helm/templates/01-nats-configmap.yaml b/.helm/templates/01-nats-configmap.yaml index 38e58d7..0714b2b 100644 --- a/.helm/templates/01-nats-configmap.yaml +++ b/.helm/templates/01-nats-configmap.yaml @@ -1,11 +1,23 @@ +{{- if .Values.nats.enabled }} +{{/* +Конфигурация NATS JetStream. + +Маршруты кластера строятся из служебных DNS-имён StatefulSet, поэтому домен +кластера берётся из cluster.domain — в чарте 2.x он был захардкожен здесь и +одновременно настраивался в values, из-за чего нестандартный домен ломал +сборку кластера. +*/}} +{{- $name := include "hrbox.component" (dict "ctx" . "name" "nats") -}} --- apiVersion: v1 kind: ConfigMap metadata: - name: {{ .Chart.Name }}-nats-config + name: {{ printf "%s-config" $name }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "nats") | nindent 4 }} data: nats.conf: | - # PID file shared with configuration reloader. + # PID-файл, общий с перезагрузчиком конфигурации. pid_file: "/var/run/nats/nats.pid" ############### @@ -42,17 +54,18 @@ data: ################################### cluster { port: 6222 - name: {{ .Chart.Name }}-nats-cluster + name: {{ printf "%s-cluster" $name }} routes = [ {{- range $i := until (int .Values.nats.cluster.replicas) }} - nats://{{ $.Chart.Name }}-nats-{{ $i }}.{{ $.Chart.Name }}-nats.{{ $.Release.Namespace }}.svc.cluster.local:6222, + nats://{{ $name }}-{{ $i }}.{{ $name }}.{{ $.Release.Namespace }}.svc.{{ $.Values.cluster.domain }}:6222, {{- end }} ] - + cluster_advertise: $CLUSTER_ADVERTISE connect_retries: 120 } {{- end }} - lame_duck_duration: 120s + lame_duck_duration: {{ .Values.nats.terminationGracePeriodSeconds }}s +{{- end }} diff --git a/.helm/templates/01-nginx-assetlinks.yaml b/.helm/templates/01-nginx-assetlinks.yaml new file mode 100644 index 0000000..efb30fd --- /dev/null +++ b/.helm/templates/01-nginx-assetlinks.yaml @@ -0,0 +1,46 @@ +{{/* +Привязка мобильных приложений к домену инсталляции. + +Android читает /.well-known/assetlinks.json, iOS — /.well-known/apple-app-site-association. +Без них ссылки из писем и уведомлений открываются в браузере, а не в приложении. +По умолчанию здесь идентификаторы штатных приложений HRBox; если у вас +собственная сборка, переопределите app.mobileApps в values.yaml. +*/}} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: assetlinks + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "web") | nindent 4 }} +data: + assetlinks.json: | + [ + {{- range $i, $app := .Values.app.mobileApps.android }} + {{- if $i }},{{ end }} + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": {{ $app.packageName | quote }}, + "sha256_cert_fingerprints": {{ toJson $app.sha256CertFingerprints }} + } + } + {{- end }} + ] + + apple-app-site-association: | + { + "applinks": { + "details": [ + { + "appIDs": {{ toJson .Values.app.mobileApps.apple.appIds }}, + "components": [ + { + "/": "/*" + } + ] + } + ] + } + } diff --git a/.helm/templates/01-nginx-configmap.yaml b/.helm/templates/01-nginx-configmap.yaml index ecf780d..8e183e9 100644 --- a/.helm/templates/01-nginx-configmap.yaml +++ b/.helm/templates/01-nginx-configmap.yaml @@ -1,85 +1,91 @@ +{{/* +Конфигурация nginx в поде web. Отличия от чарта 2.x — набор location, +перенесённый из продового окружения: health-check для проб, потоковая +загрузка оргструктуры из 1С, длинные таймауты для AI-запросов, отдача +assetlinks мобильных приложений и корректные заголовки для service worker. +*/}} --- apiVersion: v1 kind: ConfigMap metadata: name: web-configuration + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "web") | nindent 4 }} data: nginx.conf: | user www-data; worker_processes 6; pid /run/nginx.pid; - + events { - worker_connections 10000; - use epoll; + worker_connections 10000; + use epoll; multi_accept on; } - + http { - + set_real_ip_from 10.0.0.0/8; - - ## - # Basic Settings - ## - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 0; - types_hash_max_size 2048; - server_tokens off; - + + ## + # Basic Settings + ## + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 0; + types_hash_max_size 2048; + server_tokens off; + limit_conn_zone $binary_remote_addr zone=addr:10m; - server_names_hash_bucket_size 64; - server_name_in_redirect off; - + server_names_hash_bucket_size 64; + server_name_in_redirect off; + fastcgi_buffer_size 256k; fastcgi_buffers 4 256k; - + client_body_buffer_size 1024k; client_max_body_size 20m; - + client_header_buffer_size 1024k; large_client_header_buffers 16 1024k; - - - default_type application/octet-stream; - include /etc/nginx/mime.types; - - ## - # SSL Settings - ## - - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE - ssl_prefer_server_ciphers on; + + default_type application/octet-stream; + include /etc/nginx/mime.types; + + ## + # SSL Settings + ## + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; - - ## - # Logging Settings - ## - - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - rewrite_log on; - - ## - # Gzip Settings - ## - - gzip on; - gzip_disable "msie6"; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_buffers 16 8k; - gzip_http_version 1.1; - gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; - - ## - # Virtual Host Configs - ## + + ## + # Logging Settings + ## + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + ## + # Gzip Settings + ## + + gzip on; + gzip_disable "msie6"; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_buffers 16 8k; + gzip_http_version 1.1; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + ## + # Virtual Host Configs + ## server { listen 80 default; server_name _ ; @@ -96,7 +102,80 @@ data: location ~ /\.(git) { deny all; } - + + # Liveness: отвечает сам nginx, не дожидаясь PHP-FPM. + location = /healthz { + access_log off; + return 200 "ok\n"; + } + + # Readiness: короткая проверка, что PHP-FPM отвечает. + location = /health-check { + fastcgi_pass 127.0.0.1:9000; + fastcgi_connect_timeout 1s; + fastcgi_read_timeout 3s; + fastcgi_send_timeout 1s; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + fastcgi_param SCRIPT_NAME /index.php; + fastcgi_param PATH_INFO /health-check; + } + + # Загрузка оргструктуры из 1С. PHP читает тело запроса потоком и + # проксирует в GoWorker без полной буферизации, поэтому nginx + # тоже не должен буферизовать тело. + location = /api/v1/integration/commit { + client_max_body_size 500m; + fastcgi_request_buffering off; + + fastcgi_pass localhost:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + fastcgi_param SCRIPT_NAME /index.php; + include fastcgi_params; + fastcgi_read_timeout 300s; + fastcgi_send_timeout 300s; + fastcgi_connect_timeout 60s; + } + + # Привязка мобильных приложений к домену + location /.well-known/assetlinks.json { + default_type "application/json"; + alias /var/www/assetlinks/assetlinks.json; + } + + location /.well-known/apple-app-site-association { + default_type "application/json"; + alias /var/www/assetlinks/apple-app-site-association; + } + + location = /frontend/sw.js { + add_header Service-Worker-Allowed /; + add_header Cache-Control "no-cache, must-revalidate, public"; + expires off; + try_files $uri $uri/ /index.php?$args; + access_log off; + } + + # AI-запросы отвечают дольше обычных, им нужен свой таймаут. + location ^~ /ai/ { + + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + add_header Permissions-Policy "browsing-topics=()" always; + + fastcgi_pass localhost:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + fastcgi_param SCRIPT_NAME /index.php; + include fastcgi_params; + fastcgi_read_timeout 300s; + fastcgi_connect_timeout 60s; + fastcgi_send_timeout 60s; + } + location / { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; @@ -105,38 +184,44 @@ data: add_header X-Content-Type-Options "nosniff"; add_header Permissions-Policy "browsing-topics=()" always; - # Redirect everything that isn't a real file to index.php + # Всё, что не является файлом, уходит в index.php try_files $uri $uri/ /index.php?$args; - # uncomment to avoid processing of calls to non-existing static files by Yii - #location ~* \.(js|css|png|jpg|gif|ico|pdf|mov|fla|zip|rar)$ { location ~* \.(js|css|png|jpg|gif|ico)$ { - + expires 30d; add_header Vary Accept-Encoding; add_header Pragma "public"; add_header Cache-Control "public, must-revalidate, proxy-revalidate"; access_log off; tcp_nodelay off; - + try_files $uri $uri/ /index.php?$args; - + open_file_cache max=3000 inactive=120s; open_file_cache_valid 45s; open_file_cache_min_uses 2; open_file_cache_errors off; } - - #error_page 404 /404.html; + + location ~* \.(woff|woff2|ttf|otf|eot)$ { + + expires 30d; + add_header Vary Accept-Encoding; + add_header Cache-Control "public, must-revalidate, proxy-revalidate"; + access_log off; + + try_files $uri $uri/ /index.php?$args; + } } - + location ~ \.php$ { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; add_header X-XSS-Protection "1; mode=block"; add_header X-Frame-Options "SAMEORIGIN"; add_header X-Content-Type-Options "nosniff"; - + fastcgi_split_path_info ^(.+?\.php)(/.*)$; if (!-f $document_root$fastcgi_script_name) { return 404; @@ -151,4 +236,4 @@ data: fastcgi_send_timeout 60s; } } - } \ No newline at end of file + } diff --git a/.helm/templates/01-otel-configmap.yaml b/.helm/templates/01-otel-configmap.yaml new file mode 100644 index 0000000..675e725 --- /dev/null +++ b/.helm/templates/01-otel-configmap.yaml @@ -0,0 +1,38 @@ +{{- if .Values.app.tracing.enabled }} +{{/* +Конфигурация sidecar-коллектора OpenTelemetry в поде web. +Создаётся только при app.tracing.enabled. +*/}} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: otel-config + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "web") | nindent 4 }} +data: + config.yml: | + receivers: + otlp: + protocols: + grpc: + endpoint: "0.0.0.0:4317" + http: + endpoint: "0.0.0.0:4318" + + processors: + batch: + + exporters: + otlp: + endpoint: {{ required "При app.tracing.enabled нужен app.tracing.tempoUrl — адрес приёмника трейсов" .Values.app.tracing.tempoUrl | quote }} + tls: + insecure: true + + service: + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [otlp] +{{- end }} diff --git a/.helm/templates/01-wait-configmap.yaml b/.helm/templates/01-wait-configmap.yaml index 98c1d03..e04d13d 100644 --- a/.helm/templates/01-wait-configmap.yaml +++ b/.helm/templates/01-wait-configmap.yaml @@ -1,12 +1,18 @@ +{{/* +Скрипт init-контейнера джобы миграций: ждёт, пока PostgreSQL начнёт принимать +подключения. Без этого миграции падают, если база стартует медленнее подов. +*/}} --- apiVersion: v1 kind: ConfigMap metadata: name: job-scripts + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "jobs") | nindent 4 }} data: wait.sh: | - until pg_isready -h $POSTGRES_VHOST -p $POSTGRES_PORT -U $POSTGRES_USER + until pg_isready -h "$POSTGRES_VHOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" do echo "Waiting for postgres" - sleep 2; - done \ No newline at end of file + sleep 2 + done diff --git a/.helm/templates/02-dragonfly.yaml b/.helm/templates/02-dragonfly.yaml index 920f4a6..930055b 100644 --- a/.helm/templates/02-dragonfly.yaml +++ b/.helm/templates/02-dragonfly.yaml @@ -1,46 +1,68 @@ {{- if .Values.dragonfly.enabled }} +{{- $name := include "hrbox.component" (dict "ctx" . "name" "dragonfly") -}} +{{/* +Redis-совместимый кеш: сессии, кеш приложения и очереди Nanabush Player. + +Одна реплика и стратегия Recreate — намеренно: данные хранятся в памяти, +второй экземпляр не даёт ни отказоустойчивости, ни консистентности. +Селектор оставлен как в чарте 2.x. +*/}} --- apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Chart.Name }}-dragonfly + name: {{ $name }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "dragonfly") | nindent 4 }} spec: revisionHistoryLimit: 3 strategy: type: Recreate selector: matchLabels: - component: {{ .Chart.Name }}-dragonfly + component: {{ $name }} template: metadata: labels: - component: {{ .Chart.Name }}-dragonfly + component: {{ $name }} + app: dragonfly spec: - imagePullSecrets: - - name: regsecret + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} containers: - - name: dragonfly - image: cr.yandex/crpnn6fi85p3dauha6nc/dragonflydb:1.15.1 - {{- if .Values.dragonfly.maxmemory }} - args: - - --maxmemory={{ .Values.dragonfly.maxmemory }} - {{- end }} - ports: - - name: dragonfly - containerPort: 6379 - livenessProbe: - tcpSocket: - port: 6379 - initialDelaySeconds: 30 + - name: dragonfly + image: {{ .Values.image.dragonfly | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.dragonfly.maxmemory }} + args: + - --maxmemory={{ . }} + {{- end }} + ports: + - name: dragonfly + containerPort: 6379 + {{- include "hrbox.resources" (dict "ctx" . "component" "dragonfly") | indent 10 }} + readinessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: 6379 + initialDelaySeconds: 30 + periodSeconds: 10 + --- apiVersion: v1 kind: Service metadata: - name: {{ .Chart.Name }}-dragonfly + name: {{ $name }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "dragonfly") | nindent 4 }} spec: clusterIP: None selector: - component: {{ .Chart.Name }}-dragonfly + component: {{ $name }} ports: - - port: 6379 -{{- end }} \ No newline at end of file + - name: dragonfly + port: 6379 +{{- end }} diff --git a/.helm/templates/02-nats-box.yaml b/.helm/templates/02-nats-box.yaml index 4ff7e03..cea0903 100644 --- a/.helm/templates/02-nats-box.yaml +++ b/.helm/templates/02-nats-box.yaml @@ -1,18 +1,23 @@ +{{- if .Values.nats.enabled }} +{{- $name := include "hrbox.component" (dict "ctx" . "name" "nats") -}} +{{/* +Брокер сообщений. Селектор StatefulSet намеренно оставлен таким же, как в +чарте 2.x: он неизменяем, и его правка потребовала бы пересоздания объекта. +*/}} --- apiVersion: apps/v1 kind: StatefulSet metadata: - name: {{ .Chart.Name }}-nats + name: {{ $name }} labels: - app.kubernetes.io/name: nats - app.kubernetes.io/instance: {{ .Release.Name }} + {{- include "hrbox.labels" (dict "ctx" . "name" "nats") | nindent 4 }} spec: selector: matchLabels: app.kubernetes.io/name: nats app.kubernetes.io/instance: {{ .Release.Name }} replicas: {{ .Values.nats.replicas }} - serviceName: {{ .Chart.Name }}-nats + serviceName: {{ $name }} template: metadata: labels: @@ -22,50 +27,55 @@ spec: volumes: - name: config-volume configMap: - name: {{ .Chart.Name }}-nats-config + name: {{ printf "%s-config" $name }} - name: pid emptyDir: {} + # Общее пространство процессов нужно перезагрузчику конфигурации: + # он посылает сигнал процессу nats-server из соседнего контейнера. shareProcessNamespace: true terminationGracePeriodSeconds: {{ .Values.nats.terminationGracePeriodSeconds }} - imagePullSecrets: - - name: regsecret + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} containers: - name: nats - image: {{ .Values.nats.image }} - imagePullPolicy: IfNotPresent + image: {{ .Values.image.nats | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - - containerPort: 4222 - name: client - - containerPort: 6222 - name: cluster - - containerPort: 8222 - name: monitor - - containerPort: 7777 - name: metrics + - containerPort: 4222 + name: client + - containerPort: 6222 + name: cluster + - containerPort: 8222 + name: monitor + - containerPort: 7777 + name: metrics command: - - "nats-server" - - "--config" - - "/etc/nats-config/nats.conf" + - "nats-server" + - "--config" + - "/etc/nats-config/nats.conf" env: - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: SERVER_NAME - value: $(POD_NAME) - - name: POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: CLUSTER_ADVERTISE - value: $(POD_NAME).{{ .Chart.Name }}-nats.$(POD_NAMESPACE).{{ .Values.cluster.domain | default "cluster.local" }} + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: SERVER_NAME + value: $(POD_NAME) + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + # Адрес, который нода объявляет остальным участникам кластера. + # Сегмент svc обязателен: без него имя не резолвится и ноды + # не находят друг друга. + - name: CLUSTER_ADVERTISE + value: $(POD_NAME).{{ $name }}.$(POD_NAMESPACE).svc.{{ .Values.cluster.domain }} + {{- include "hrbox.resources" (dict "ctx" . "component" "nats") | indent 10 }} volumeMounts: - name: config-volume mountPath: /etc/nats-config - name: pid mountPath: /var/run/nats {{- if .Values.nats.jetstream.fileStorage.enabled }} - - name: {{ .Chart.Name }}-nats-js-pvc + - name: {{ printf "%s-js-pvc" $name }} mountPath: {{ .Values.nats.jetstream.fileStorage.storageDirectory }} {{- end }} livenessProbe: @@ -81,23 +91,25 @@ spec: initialDelaySeconds: 10 timeoutSeconds: 5 lifecycle: + # Перевод ноды в lame duck: она перестаёт принимать новые + # подключения и даёт клиентам время переподключиться к соседям. preStop: exec: command: - - "/bin/sh" - - "-c" - - "nats-server -sl=ldm=/var/run/nats/nats.pid && /bin/sleep 120" - + - "/bin/sh" + - "-c" + - "nats-server -sl=ldm=/var/run/nats/nats.pid && /bin/sleep {{ .Values.nats.terminationGracePeriodSeconds }}" + {{- if .Values.nats.reloader.enabled }} - name: reloader - image: natsio/nats-server-config-reloader:0.6.2 - imagePullPolicy: IfNotPresent + image: {{ .Values.image.natsConfigReloader | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} command: - - "nats-server-config-reloader" - - "-pid" - - "/var/run/nats/nats.pid" - - "-config" - - "/etc/nats-config/nats.conf" + - "nats-server-config-reloader" + - "-pid" + - "/var/run/nats/nats.pid" + - "-config" + - "/etc/nats-config/nats.conf" volumeMounts: - name: config-volume mountPath: /etc/nats-config @@ -108,15 +120,15 @@ spec: {{- if .Values.nats.jetstream.fileStorage.enabled }} volumeClaimTemplates: - metadata: - name: {{ .Chart.Name }}-nats-js-pvc + name: {{ printf "%s-js-pvc" $name }} spec: accessModes: - ReadWriteOnce resources: requests: storage: {{ .Values.nats.jetstream.fileStorage.size }} - {{- if .Values.nats.jetstream.fileStorage.storageClassName }} - storageClassName: {{ .Values.nats.jetstream.fileStorage.storageClassName }} + {{- with .Values.nats.jetstream.fileStorage.storageClassName }} + storageClassName: {{ . | quote }} {{- end }} {{- end }} @@ -124,21 +136,22 @@ spec: apiVersion: v1 kind: Service metadata: - name: {{ .Chart.Name }}-nats + name: {{ $name }} labels: - app.kubernetes.io/name: nats - app.kubernetes.io/instance: {{ .Release.Name }} + {{- include "hrbox.labels" (dict "ctx" . "name" "nats") | nindent 4 }} spec: + # Headless: клиентам нужны адреса конкретных нод, а не общий VIP. + clusterIP: None selector: app.kubernetes.io/name: nats app.kubernetes.io/instance: {{ .Release.Name }} - clusterIP: None ports: - - name: client - port: 4222 - - name: cluster - port: 6222 - - name: monitor - port: 8222 - - name: metrics - port: 7777 \ No newline at end of file + - name: client + port: 4222 + - name: cluster + port: 6222 + - name: monitor + port: 8222 + - name: metrics + port: 7777 +{{- end }} diff --git a/.helm/templates/02-web-pdb.yaml b/.helm/templates/02-web-pdb.yaml new file mode 100644 index 0000000..9653c3c --- /dev/null +++ b/.helm/templates/02-web-pdb.yaml @@ -0,0 +1,21 @@ +{{- $replicas := .Values.app.replicas.web | int }} +{{/* +Бюджет прерываний для web. + +Имеет смысл только начиная с двух реплик: при одной minAvailable округляется +до 1 и намертво блокирует drain ноды, ничего при этом не защищая. +*/}} +{{- if gt $replicas 1 }} +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "hrbox.component" (dict "ctx" . "name" "web") }}-pdb + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "web") | nindent 4 }} +spec: + minAvailable: 75% + selector: + matchLabels: + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "web") | nindent 6 }} +{{- end }} diff --git a/.helm/templates/02-web.yaml b/.helm/templates/02-web.yaml index dceb2a2..f1d97e7 100644 --- a/.helm/templates/02-web.yaml +++ b/.helm/templates/02-web.yaml @@ -1,48 +1,69 @@ +{{- $name := include "hrbox.component" (dict "ctx" . "name" "web") -}} +{{/* +Веб-под: PHP-FPM, nginx перед ним и экспортёр метрик FPM. +При app.tracing.enabled рядом поднимается коллектор OpenTelemetry. + +Аннотации checksum перезапускают поды, когда меняется конфигурация nginx или +секрет приложения: без них правка ConfigMap остаётся незамеченной до +следующего рестарта. +*/}} --- apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Chart.Name }}-web + name: {{ $name }} labels: - app: web + {{- include "hrbox.labels" (dict "ctx" . "name" "web") | nindent 4 }} spec: minReadySeconds: 5 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 - maxUnavailable: 1 + maxUnavailable: 0 replicas: {{ .Values.app.replicas.web }} selector: matchLabels: - service: {{ .Chart.Name }}-web + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "web") | nindent 6 }} template: metadata: labels: - service: {{ .Chart.Name }}-web + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "web") | nindent 8 }} + app: web annotations: prometheus.io/scrape: 'true' prometheus.io/port: '9253' + checksum/nginx-config: {{ include (print $.Template.BasePath "/01-nginx-configmap.yaml") . | sha256sum }} + checksum/app-secret: {{ include (print $.Template.BasePath "/00-app-secret.yaml") . | sha256sum }} spec: - imagePullSecrets: - - name: regsecret + terminationGracePeriodSeconds: 30 + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} containers: - name: fpm - image: {{ .Values.app.image }} + image: {{ .Values.image.hrbox | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} command: ["/usr/local/sbin/php-fpm", "-c", "/usr/local/etc/php-fpm"] ports: - name: fpm containerPort: 9000 env: {{- include "envs_all" . | indent 12 }} - {{- include "app_resources" . | indent 10 }} + {{- include "envs_nanabush_player_signing" . | indent 12 }} + - name: VIDEO_CONVERTER_ENABLED + value: {{ .Values.app.videoConverter.enabled | quote }} + {{- if .Values.app.tracing.enabled }} + {{- include "envs_tracing" . | indent 12 }} + {{- end }} + {{- include "hrbox.resources" (dict "ctx" . "component" "app") | indent 10 }} lifecycle: + # Пауза даёт ingress-контроллеру убрать под из балансировки, + # прежде чем FPM начнёт корректное завершение. preStop: exec: command: - sh - '-c' - - sleep 5 && kill -SIGQUIT 1 + - sleep 15 && kill -SIGQUIT 1 readinessProbe: tcpSocket: port: 9000 @@ -53,48 +74,106 @@ spec: port: 9000 initialDelaySeconds: 5 periodSeconds: 10 + - name: nginx - image: {{ .Values.app.image }} + image: {{ .Values.image.hrbox | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} command: ["/usr/sbin/nginx", "-g", "daemon off;"] ports: - name: http containerPort: 80 env: {{- include "envs_all" . | indent 12 }} - {{- include "nginx_resources" . | indent 10}} + {{- include "hrbox.resources" (dict "ctx" . "component" "nginx") | indent 10 }} lifecycle: preStop: exec: command: - sh - '-c' - - sleep 5 && /usr/sbin/nginx -s quit + - sleep 15 && /usr/sbin/nginx -s quit + # /healthz отвечает сам nginx, /health-check проверяет, + # что за ним жив PHP-FPM. + livenessProbe: + httpGet: + path: /healthz + port: 80 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 10 + failureThreshold: 5 + readinessProbe: + httpGet: + path: /health-check + port: 80 + initialDelaySeconds: 15 + periodSeconds: 5 + timeoutSeconds: 10 + failureThreshold: 3 volumeMounts: - name: configs mountPath: /etc/nginx/nginx.conf subPath: nginx.conf - - name: phpfpm-exporter - image: cr.yandex/crpnn6fi85p3dauha6nc/php-fpm_exporter:latest + - name: assetlinks + mountPath: /var/www/assetlinks/assetlinks.json + subPath: assetlinks.json + - name: assetlinks + mountPath: /var/www/assetlinks/apple-app-site-association + subPath: apple-app-site-association + + {{- if .Values.app.tracing.enabled }} + - name: otel-collector + image: {{ .Values.image.otelCollector | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: ["--config=/etc/otel-config/otel-collector-config.yaml"] ports: - - containerPort: 9253 + - containerPort: 4317 + name: otlp-grpc + - containerPort: 4318 + name: otlp-http + volumeMounts: + - name: otel-config + mountPath: /etc/otel-config/otel-collector-config.yaml + subPath: config.yml + {{- end }} + + - name: phpfpm-exporter + image: {{ .Values.image.phpFpmExporter | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: 9253 + name: metrics env: - - name: PHP_FPM_SCRAPE_URI - value: "tcp://127.0.0.1:9000/hrbox-fpm-status" + - name: PHP_FPM_SCRAPE_URI + value: "tcp://127.0.0.1:9000/hrbox-fpm-status" + volumes: - name: configs configMap: name: web-configuration defaultMode: 420 + - name: assetlinks + configMap: + name: assetlinks + defaultMode: 420 + {{- if .Values.app.tracing.enabled }} + - name: otel-config + configMap: + name: otel-config + defaultMode: 420 + {{- end }} --- apiVersion: v1 kind: Service metadata: - name: {{ .Chart.Name }}-web + name: {{ $name }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "web") | nindent 4 }} spec: selector: - service: {{ .Chart.Name }}-web + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "web") | nindent 4 }} ports: - name: http port: 80 - protocol: TCP \ No newline at end of file + protocol: TCP diff --git a/.helm/templates/03-chatbox.yaml b/.helm/templates/03-chatbox.yaml new file mode 100644 index 0000000..7ba3768 --- /dev/null +++ b/.helm/templates/03-chatbox.yaml @@ -0,0 +1,61 @@ +{{- $name := include "hrbox.component" (dict "ctx" . "name" "chatbox") -}} +{{/* +Chatbox — вебсокет-сервер чатов и real-time уведомлений. +До версии 2.0 назывался websocket; имена объектов не менялись с тех пор. +*/}} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $name }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "chatbox") | nindent 4 }} +spec: + minReadySeconds: 5 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + replicas: {{ .Values.app.replicas.chatbox }} + selector: + matchLabels: + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "chatbox") | nindent 6 }} + template: + metadata: + labels: + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "chatbox") | nindent 8 }} + app: chatbox + annotations: + prometheus.io/scrape: 'true' + prometheus.io/port: '9797' + prometheus.io/path: "/info" + checksum/app-secret: {{ include (print $.Template.BasePath "/00-app-secret.yaml") . | sha256sum }} + spec: + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} + containers: + - name: chatbox + image: {{ .Values.image.chatbox | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 9797 + env: + {{- include "envs_all" . | indent 12 }} + {{- include "hrbox.resources" (dict "ctx" . "component" "chatbox") | indent 10 }} + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $name }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "chatbox") | nindent 4 }} +spec: + selector: + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "chatbox") | nindent 4 }} + ports: + - name: http + port: 80 + targetPort: 9797 + protocol: TCP diff --git a/.helm/templates/03-websocket.yaml b/.helm/templates/03-websocket.yaml deleted file mode 100644 index b820086..0000000 --- a/.helm/templates/03-websocket.yaml +++ /dev/null @@ -1,51 +0,0 @@ ---- -# Chatbox (WebSocket server) - обеспечивает real-time функционал -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Chart.Name }}-chatbox - labels: - app: chatbox -spec: - minReadySeconds: 5 - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - replicas: {{ .Values.app.replicas.chatbox }} - selector: - matchLabels: - service: {{ .Chart.Name }}-chatbox - template: - metadata: - labels: - service: {{ .Chart.Name }}-chatbox - annotations: - prometheus.io/scrape: 'true' - prometheus.io/port: '9797' - prometheus.io/path: "/info" - spec: - imagePullSecrets: - - name: regsecret - containers: - - name: chatbox - image: {{ .Values.app.chatbox.image }} - imagePullPolicy: Always - env: - {{- include "envs_all" . | indent 12 }} - {{- include "chatbox_resources" . | indent 10 }} - ---- -apiVersion: v1 -kind: Service -metadata: - name: {{ .Chart.Name }}-chatbox -spec: - selector: - service: {{ .Chart.Name }}-chatbox - ports: - - name: http - port: 80 - targetPort: 9797 - protocol: TCP diff --git a/.helm/templates/04-worker.yaml b/.helm/templates/04-worker.yaml index 259078d..a3f28fb 100644 --- a/.helm/templates/04-worker.yaml +++ b/.helm/templates/04-worker.yaml @@ -1,10 +1,19 @@ +{{- $name := include "hrbox.component" (dict "ctx" . "name" "worker") -}} +{{/* +Обработчик фоновых задач: конвертация файлов, рассылка почты и уведомлений, +генерация отчётов, обслуживание Nanabush Player. + +Стратегия Recreate: воркеры разбирают задачи из общей очереди, и пересечение +старых и новых подов во время выката привело бы к двойной обработке. +Длинный terminationGracePeriodSeconds даёт закончить уже взятую задачу. +*/}} --- apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Chart.Name }}-worker + name: {{ $name }} labels: - app: worker + {{- include "hrbox.labels" (dict "ctx" . "name" "worker") | nindent 4 }} spec: minReadySeconds: 5 strategy: @@ -12,22 +21,26 @@ spec: replicas: {{ .Values.app.replicas.worker }} selector: matchLabels: - service: {{ .Chart.Name }}-worker + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "worker") | nindent 6 }} template: metadata: labels: - service: {{ .Chart.Name }}-worker + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "worker") | nindent 8 }} + app: worker annotations: prometheus.io/scrape: 'true' prometheus.io/port: '9253' + checksum/app-secret: {{ include (print $.Template.BasePath "/00-app-secret.yaml") . | sha256sum }} spec: - imagePullSecrets: - - name: regsecret + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} containers: - name: worker - image: {{ .Values.app.image }} + image: {{ .Values.image.hrbox | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} command: ["hrbox-worker"] env: {{- include "envs_all" . | indent 12 }} - {{- include "worker_resources" . | indent 10}} - terminationGracePeriodSeconds: 600 \ No newline at end of file + - name: VIDEO_CONVERTER_ENABLED + value: {{ .Values.app.videoConverter.enabled | quote }} + {{- include "hrbox.resources" (dict "ctx" . "component" "worker") | indent 10 }} + terminationGracePeriodSeconds: 600 diff --git a/.helm/templates/05-conductor.yaml b/.helm/templates/05-conductor.yaml index a481e70..d49cb99 100644 --- a/.helm/templates/05-conductor.yaml +++ b/.helm/templates/05-conductor.yaml @@ -1,40 +1,78 @@ +{{- $name := include "hrbox.component" (dict "ctx" . "name" "conductor") -}} +{{/* +Планировщик задач по расписанию. + +Строго одна реплика: два планировщика запустили бы каждую задачу дважды. +По той же причине стратегия Recreate, а не RollingUpdate. +*/}} --- apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Chart.Name }}-conductor + name: {{ $name }} labels: - app: conductor + {{- include "hrbox.labels" (dict "ctx" . "name" "conductor") | nindent 4 }} spec: replicas: 1 strategy: type: Recreate selector: matchLabels: - service: {{ .Chart.Name }}-conductor + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "conductor") | nindent 6 }} template: metadata: labels: - service: {{ .Chart.Name }}-conductor + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "conductor") | nindent 8 }} + app: conductor + annotations: + prometheus.io/scrape: 'true' + prometheus.io/port: '8080' + prometheus.io/path: '/metrics' + checksum/app-secret: {{ include (print $.Template.BasePath "/00-app-secret.yaml") . | sha256sum }} spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app - operator: In - values: - - conductor - topologyKey: kubernetes.io/hostname - imagePullSecrets: - - name: regsecret + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - conductor + topologyKey: kubernetes.io/hostname + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} containers: - name: conductor - {{- include "conductor_resources" . | indent 10 }} - image: cr.yandex/crphnph9c2s776j32sjd/hrbox-conductor:2.2.0 - imagePullPolicy: IfNotPresent + image: {{ .Values.image.conductor | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 8080 env: - {{- include "envs_all" . | indent 12 }} \ No newline at end of file + {{- include "envs_all" . | indent 12 }} + {{- include "hrbox.resources" (dict "ctx" . "component" "conductor") | indent 10 }} + livenessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 8080 + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + lifecycle: + preStop: + exec: + command: + - /bin/sh + - -c + - "sleep 5" diff --git a/.helm/templates/09-file-processor-video.yaml b/.helm/templates/09-file-processor-video.yaml new file mode 100644 index 0000000..3288704 --- /dev/null +++ b/.helm/templates/09-file-processor-video.yaml @@ -0,0 +1,58 @@ +{{- if .Values.app.videoConverter.enabled }} +{{- $name := include "hrbox.component" (dict "ctx" . "name" "file-processor-video") -}} +{{/* +Конвертация видео. + +Отдельный деплоймент, потому что профиль ресурсов у него другой на порядок: +несколько ядер, гигабайты памяти и десятки гигабайт временного диска на под. +Смешивать это с обычной обработкой файлов нельзя — либо ресурсы простаивают, +либо конвертация видео вытесняет всё остальное. Через nodeSelector и +tolerations пул выносится на выделенные ноды. +*/}} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $name }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "file-processor-video") | nindent 4 }} +spec: + minReadySeconds: 5 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + replicas: {{ .Values.app.replicas.fileProcessorVideo }} + selector: + matchLabels: + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "file-processor-video") | nindent 6 }} + template: + metadata: + labels: + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "file-processor-video") | nindent 8 }} + app: file-processor + annotations: + checksum/app-secret: {{ include (print $.Template.BasePath "/00-app-secret.yaml") . | sha256sum }} + spec: + {{- with .Values.app.videoConverter.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.app.videoConverter.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} + containers: + - name: file-processor + image: {{ .Values.image.fileProcessor | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + {{- include "envs_all" . | indent 12 }} + - name: VIDEO_CONVERTER_ENABLED + value: "true" + - name: IMAGE_RESIZE_MAX_PARALLEL + value: {{ .Values.app.fileProcessor.imageResizeMaxParallel | quote }} + {{- include "hrbox.resources" (dict "ctx" . "component" "fileProcessorVideo") | indent 10 }} +{{- end }} diff --git a/.helm/templates/09-file-processor.yaml b/.helm/templates/09-file-processor.yaml index 200df85..478a6ec 100644 --- a/.helm/templates/09-file-processor.yaml +++ b/.helm/templates/09-file-processor.yaml @@ -1,11 +1,17 @@ +{{- $name := include "hrbox.component" (dict "ctx" . "name" "file-processor") -}} +{{/* +Конвертация документов и обработка изображений. + +Конвертация видео здесь всегда выключена: она требует принципиально другого +профиля ресурсов и живёт в отдельном деплойменте 09-file-processor-video. +*/}} --- -{{ $name := printf "%s-file-processor" (.Chart.Name) }} apiVersion: apps/v1 kind: Deployment metadata: name: {{ $name }} labels: - app: file-processor + {{- include "hrbox.labels" (dict "ctx" . "name" "file-processor") | nindent 4 }} spec: minReadySeconds: 5 strategy: @@ -13,22 +19,25 @@ spec: rollingUpdate: maxSurge: 1 maxUnavailable: 1 - replicas: {{ .Values.app.file_processor.replicas }} + replicas: {{ .Values.app.replicas.fileProcessor }} selector: matchLabels: - service: {{ $name }} + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "file-processor") | nindent 6 }} template: metadata: labels: - service: {{ $name }} + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "file-processor") | nindent 8 }} + app: file-processor + annotations: + checksum/app-secret: {{ include (print $.Template.BasePath "/00-app-secret.yaml") . | sha256sum }} spec: - imagePullSecrets: - - name: regsecret + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} containers: - name: file-processor - image: {{ .Values.app.file_processor.image }} - imagePullPolicy: Always + image: {{ .Values.image.fileProcessor | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} env: - {{- include "envs_all" . | indent 12 }} + {{- include "envs_all" . | indent 12 }} - name: VIDEO_CONVERTER_ENABLED value: "false" + {{- include "hrbox.resources" (dict "ctx" . "component" "fileProcessor") | indent 10 }} diff --git a/.helm/templates/09-geonames.yaml b/.helm/templates/09-geonames.yaml new file mode 100644 index 0000000..3341e7b --- /dev/null +++ b/.helm/templates/09-geonames.yaml @@ -0,0 +1,60 @@ +{{- $name := include "hrbox.component" (dict "ctx" . "name" "geonames") -}} +{{/* +Справочник городов, стран и часовых поясов. +*/}} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $name }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "geonames") | nindent 4 }} +spec: + minReadySeconds: 5 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + replicas: {{ .Values.app.replicas.geonames }} + selector: + matchLabels: + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "geonames") | nindent 6 }} + template: + metadata: + labels: + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "geonames") | nindent 8 }} + app: geonames + annotations: + prometheus.io/scrape: 'true' + prometheus.io/port: '9797' + prometheus.io/path: "/info" + checksum/app-secret: {{ include (print $.Template.BasePath "/00-app-secret.yaml") . | sha256sum }} + spec: + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} + containers: + - name: geonames + image: {{ .Values.image.geonames | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 8181 + env: + {{- include "envs_all" . | indent 12 }} + {{- include "hrbox.resources" (dict "ctx" . "component" "geonames") | indent 10 }} + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $name }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "geonames") | nindent 4 }} +spec: + selector: + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "geonames") | nindent 4 }} + ports: + - name: http + port: 8181 + targetPort: 8181 + protocol: TCP diff --git a/.helm/templates/09-geonames.yml b/.helm/templates/09-geonames.yml deleted file mode 100644 index 06ee5e0..0000000 --- a/.helm/templates/09-geonames.yml +++ /dev/null @@ -1,50 +0,0 @@ ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ .Chart.Name }}-geonames - labels: - app: geonames -spec: - minReadySeconds: 5 - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 - maxUnavailable: 1 - replicas: {{ .Values.app.replicas.geonames }} - selector: - matchLabels: - service: {{ .Chart.Name }}-geonames - template: - metadata: - labels: - service: {{ .Chart.Name }}-geonames - annotations: - prometheus.io/scrape: 'true' - prometheus.io/port: '9797' - prometheus.io/path: "/info" - spec: - imagePullSecrets: - - name: regsecret - containers: - - name: geonames - {{- include "geonames_resources" . | indent 10}} - image: cr.yandex/crphnph9c2s776j32sjd/hrbox-geonames:3.1 - imagePullPolicy: Always - env: - {{- include "envs_all" . | indent 12 }} - ---- -apiVersion: v1 -kind: Service -metadata: - name: {{ .Chart.Name }}-geonames -spec: - selector: - service: {{ .Chart.Name }}-geonames - ports: - - name: http - port: 8181 - targetPort: 8181 - protocol: TCP \ No newline at end of file diff --git a/.helm/templates/09-goworker.yaml b/.helm/templates/09-goworker.yaml index 87c7196..e94c424 100644 --- a/.helm/templates/09-goworker.yaml +++ b/.helm/templates/09-goworker.yaml @@ -1,10 +1,18 @@ +{{- $name := include "hrbox.component" (dict "ctx" . "name" "goworker") -}} +{{/* +Интеграции с мессенджерами, push-уведомления, синхронизация оргструктуры +с 1С и восстановление проекций Nanabush Player. + +Метрики Prometheus отдаются на том же порту 8686 по пути /metrics, поэтому +аннотации сбора висят на Service, а не на поде. +*/}} --- apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Chart.Name }}-goworker + name: {{ $name }} labels: - app: goworker + {{- include "hrbox.labels" (dict "ctx" . "name" "goworker") | nindent 4 }} spec: minReadySeconds: 5 strategy: @@ -12,39 +20,46 @@ spec: rollingUpdate: maxSurge: 1 maxUnavailable: 1 - replicas: {{ .Values.app.goworker.replicas }} + replicas: {{ .Values.app.replicas.goworker }} selector: matchLabels: - service: {{ .Chart.Name }}-goworker + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "goworker") | nindent 6 }} template: metadata: labels: - service: {{ .Chart.Name }}-goworker + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "goworker") | nindent 8 }} + app: goworker + annotations: + checksum/app-secret: {{ include (print $.Template.BasePath "/00-app-secret.yaml") . | sha256sum }} spec: - imagePullSecrets: - - name: regsecret + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} containers: - name: goworker + image: {{ .Values.image.goworker | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} stdin: true tty: true - image: {{ .Values.app.goworker.image }} - imagePullPolicy: Always + ports: + - name: http + containerPort: 8686 env: {{- include "envs_all" . | indent 12 }} - {{- include "goworker_resources" . | indent 10 }} + {{- include "hrbox.resources" (dict "ctx" . "component" "goworker") | indent 10 }} --- apiVersion: v1 kind: Service metadata: - name: {{ .Chart.Name }}-goworker + name: {{ $name }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "goworker") | nindent 4 }} annotations: prometheus.io/scrape: "true" prometheus.io/port: "8686" prometheus.io/path: "/metrics" spec: selector: - service: {{ .Chart.Name }}-goworker + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "goworker") | nindent 4 }} ports: - name: http port: 8686 diff --git a/.helm/templates/09-kedoca.yaml b/.helm/templates/09-kedoca.yaml index d4ee3d0..b1c609e 100644 --- a/.helm/templates/09-kedoca.yaml +++ b/.helm/templates/09-kedoca.yaml @@ -1,10 +1,19 @@ +{{- $name := include "hrbox.component" (dict "ctx" . "name" "kedoca") -}} +{{/* +Сервис электронной подписи: КЭДО, МЧД, метки времени. +Умеет ГОСТ (через КриптоПро) и обычный OpenSSL — выбор в app.kedoca.engine. + +app.kedoca.secretName подключает Secret с сертификатами удостоверяющего +центра. Без него сервис создаёт собственный корневой сертификат при первом +запуске — это годится для теста, но не для юридически значимого документооборота. +*/}} --- apiVersion: apps/v1 kind: Deployment metadata: - name: {{ .Chart.Name }}-kedoca + name: {{ $name }} labels: - app: kedoca + {{- include "hrbox.labels" (dict "ctx" . "name" "kedoca") | nindent 4 }} spec: minReadySeconds: 5 strategy: @@ -12,27 +21,42 @@ spec: rollingUpdate: maxSurge: 1 maxUnavailable: 1 - replicas: {{ .Values.app.kedoca.replicas }} + replicas: {{ .Values.app.replicas.kedoca }} selector: matchLabels: - service: {{ .Chart.Name }}-kedoca + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "kedoca") | nindent 6 }} template: metadata: labels: - service: {{ .Chart.Name }}-kedoca + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "kedoca") | nindent 8 }} + app: kedoca + annotations: + checksum/app-secret: {{ include (print $.Template.BasePath "/00-app-secret.yaml") . | sha256sum }} spec: - imagePullSecrets: - - name: regsecret + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - kedoca + topologyKey: kubernetes.io/hostname + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} containers: - name: kedoca - image: {{ .Values.app.kedoca.image }} - imagePullPolicy: Always + image: {{ .Values.image.kedoca | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 8558 env: {{- include "envs_all" . | indent 12 }} {{- include "envs_kedoca" . | indent 12 }} - ports: - - containerPort: 8558 - {{- include "kedoca_resources" . | indent 10 }} + {{- include "hrbox.resources" (dict "ctx" . "component" "kedoca") | indent 10 }} {{- if .Values.app.kedoca.secretName }} volumeMounts: - name: ca-cert @@ -42,22 +66,22 @@ spec: volumes: - name: ca-cert secret: - secretName: {{ .Values.app.kedoca.secretName }} + secretName: {{ .Values.app.kedoca.secretName | quote }} {{- end }} --- apiVersion: v1 kind: Service metadata: - name: {{ .Chart.Name }}-kedoca + name: {{ $name }} labels: - app: kedoca + {{- include "hrbox.labels" (dict "ctx" . "name" "kedoca") | nindent 4 }} spec: + type: ClusterIP selector: - service: {{ .Chart.Name }}-kedoca + {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "kedoca") | nindent 4 }} ports: - name: http port: 8558 targetPort: 8558 protocol: TCP - type: ClusterIP diff --git a/.helm/templates/09-nanabush-player.yaml b/.helm/templates/09-nanabush-player.yaml new file mode 100644 index 0000000..fd7bedd --- /dev/null +++ b/.helm/templates/09-nanabush-player.yaml @@ -0,0 +1,232 @@ +{{- $cfg := .Values.app.nanabushPlayer.deployment }} +{{- if $cfg.enabled }} +{{- $name := include "nanabushPlayer.serviceName" . -}} +{{- $secretName := include "nanabushPlayer.secretName" . -}} +{{/* +Nanabush Player — проигрыватель учебных материалов: SCORM 1.2 и 2004, cmi5, +xAPI, нативные курсы, тесты и эссе. + +Как это работает: + * HRBox выпускает launch-тикет, подписанный общим ключом ticketHs256, + и отправляет браузер на https://<домен>{{ include "nanabushPlayer.basePath" . }}/start; + * плеер проверяет подпись, audience и issuer, заводит свою сессию; + * за пакетом курса и для отправки отчётов плеер ходит обратно в HRBox по + внутрикластерному адресу — только он и разрешён в allowlist; + * учебные события копятся в Redis Streams и досылаются форвардером, + поэтому короткая недоступность HRBox не теряет прогресс. + +Ограничения on-premise описаны в values.yaml рядом с app.nanabushPlayer. +Полный контракт — docs/nanabush-player-deployment.md в репозитории HRBox. +*/}} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $name }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "nanabush-player") | nindent 4 }} +spec: + replicas: {{ $cfg.replicas }} + revisionHistoryLimit: 3 + minReadySeconds: 5 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + service: {{ $name }} + template: + metadata: + labels: + service: {{ $name }} + app: nanabush-player + {{- if $cfg.metrics.enabled }} + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: {{ $cfg.metrics.port | quote }} + prometheus.io/path: "/metrics" + {{- end }} + spec: + # Плееру не нужен доступ к API Kubernetes, а исполняет он содержимое, + # загруженное пользователями, — поэтому токен не монтируется, процесс + # не root, корневая файловая система только на чтение. + automountServiceAccountToken: false + terminationGracePeriodSeconds: {{ $cfg.terminationGracePeriodSeconds }} + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + seccompProfile: + type: RuntimeDefault + containers: + - name: player + image: {{ .Values.image.nanabushPlayer | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + ports: + - name: http + containerPort: {{ .Values.app.nanabushPlayer.service.port }} + protocol: TCP + {{- if $cfg.metrics.enabled }} + - name: metrics + containerPort: {{ $cfg.metrics.port }} + protocol: TCP + {{- end }} + env: + - name: PLAYER_HTTP_ADDR + value: {{ printf ":%d" (.Values.app.nanabushPlayer.service.port | int) | quote }} + - name: PLAYER_METRICS_ADDR + {{- if $cfg.metrics.enabled }} + value: {{ printf ":%d" ($cfg.metrics.port | int) | quote }} + {{- else }} + value: "off" + {{- end }} + # Путь монтирования не входит в audience: audience — это origin + # браузера, иначе сломались бы проверки на стороне клиента. + - name: PLAYER_BASE_PATH + value: {{ include "nanabushPlayer.basePath" . | quote }} + - name: PLAYER_AUDIENCE + value: {{ include "nanabushPlayer.publicOrigin" . | quote }} + - name: PLAYER_ALLOWED_ISSUERS + value: {{ include "nanabushPlayer.issuer" . | quote }} + - name: PLAYER_HS256_SECRET + valueFrom: + secretKeyRef: + name: {{ $secretName | quote }} + key: {{ required "Нужно имя ключа ticketHs256 в секрете Nanabush Player" $cfg.existingSecret.keys.ticketHs256 | quote }} + {{- with $cfg.existingSecret.keys.ticketKeys }} + # Кольцо ключей: подпись проверяется ключом, выбранным по kid из + # тикета, поэтому во время ротации работают обе версии HRBox. + - name: PLAYER_TICKET_KEYS + valueFrom: + secretKeyRef: + name: {{ $secretName | quote }} + key: {{ . | quote }} + {{- with $cfg.ticket.keylessAcceptedUntil }} + # Только на первую раскатку: принимать тикеты без kid от версии + # HRBox, которая ещё не знает про кольцо ключей. + - name: PLAYER_TICKET_KEYLESS_UNTIL + value: {{ . | quote }} + {{- end }} + {{- end }} + - name: PLAYER_SESSION_SECRET + valueFrom: + secretKeyRef: + name: {{ $secretName | quote }} + key: {{ required "Нужно имя ключа sessionHs256 в секрете Nanabush Player" $cfg.existingSecret.keys.sessionHs256 | quote }} + - name: PLAYER_SESSION_ISSUER + value: {{ include "nanabushPlayer.publicOrigin" . | quote }} + - name: PLAYER_SESSION_TTL + value: {{ $cfg.ticket.sessionTtl | quote }} + - name: PLAYER_MAX_TICKET_LIFETIME + value: {{ $cfg.ticket.maxLifetime | quote }} + - name: REDIS_URL + {{- if $cfg.redis.urlFromExistingSecret }} + valueFrom: + secretKeyRef: + name: {{ $secretName | quote }} + key: {{ required "Нужно имя ключа redisUrl, когда redis.urlFromExistingSecret: true" $cfg.existingSecret.keys.redisUrl | quote }} + {{- else }} + value: {{ include "nanabushPlayer.redisUrl" . | quote }} + {{- end }} + - name: PLAYER_REDIS_KEY_PREFIX + value: {{ include "nanabushPlayer.redisKeyPrefix" . | quote }} + # Обратный канал: единственный разрешённый адрес — внутрикластерный + # HRBox. Так пакет курса не сможет заставить плеер сходить наружу. + - name: PLAYER_PACKAGE_FETCH_SCHEMES + value: {{ $cfg.backchannel.schemes | quote }} + - name: PLAYER_PACKAGE_FETCH_ALLOWLIST + value: {{ include "nanabushPlayer.backchannelHost" . | quote }} + - name: PLAYER_PACKAGE_FETCH_ALLOW_PRIVATE_IPS + value: {{ $cfg.backchannel.allowPrivateIps | quote }} + - name: PLAYER_PACKAGE_FETCH_MAX_BYTES + value: {{ $cfg.backchannel.packageFetchMaxBytes | quote }} + - name: PLAYER_PACKAGE_CONTENT_MAX_BYTES + value: {{ $cfg.backchannel.packageContentMaxBytes | quote }} + - name: PLAYER_PACKAGE_FETCH_TIMEOUT + value: {{ $cfg.backchannel.packageFetchTimeout | quote }} + - name: PLAYER_PACKAGE_STREAM_TIMEOUT + value: {{ $cfg.backchannel.packageStreamTimeout | quote }} + - name: PLAYER_REPORT_ENDPOINT_SCHEMES + value: {{ $cfg.backchannel.schemes | quote }} + - name: PLAYER_REPORT_ENDPOINT_ALLOWLIST + value: {{ include "nanabushPlayer.backchannelHost" . | quote }} + - name: PLAYER_REPORT_ENDPOINT_ALLOW_PRIVATE_IPS + value: {{ $cfg.backchannel.allowPrivateIps | quote }} + - name: PLAYER_REPORT_ENDPOINT_TIMEOUT + value: {{ $cfg.backchannel.reportTimeout | quote }} + - name: PLAYER_FORWARDER_QUEUE_SIZE + value: {{ $cfg.forwarder.queueSize | quote }} + - name: PLAYER_FORWARDER_WORKERS + value: {{ $cfg.forwarder.workers | quote }} + - name: PLAYER_FORWARDER_MAX_ATTEMPTS + value: {{ $cfg.forwarder.maxAttempts | quote }} + - name: PLAYER_FORWARDER_INITIAL_BACKOFF + value: {{ $cfg.forwarder.initialBackoff | quote }} + - name: PLAYER_FORWARDER_MAX_BACKOFF + value: {{ $cfg.forwarder.maxBackoff | quote }} + - name: PLAYER_FORWARDER_PER_ATTEMPT_TIMEOUT + value: {{ $cfg.forwarder.perAttemptTimeout | quote }} + - name: PLAYER_FORWARDER_DEAD_LETTER_SIZE + value: {{ $cfg.forwarder.deadLetterSize | quote }} + - name: PLAYER_FORWARDER_CLAIM_IDLE + value: {{ $cfg.forwarder.claimIdle | quote }} + - name: PLAYER_FORWARDER_POLL_INTERVAL + value: {{ $cfg.forwarder.pollInterval | quote }} + - name: PLAYER_FORWARDER_RATE_LIMIT_EVENTS_PER_MINUTE + value: {{ $cfg.forwarder.rateLimitEventsPerMinute | quote }} + - name: PLAYER_FORWARDER_RATE_LIMIT_BATCHES_PER_MINUTE + value: {{ $cfg.forwarder.rateLimitBatchesPerMinute | quote }} + - name: PLAYER_FORWARDER_RATE_LIMIT_PAYLOAD_BYTES_PER_MINUTE + value: {{ $cfg.forwarder.rateLimitPayloadBytesPerMinute | quote }} + - name: LOG_LEVEL + value: {{ $cfg.log.level | quote }} + - name: LOG_FORMAT + value: {{ $cfg.log.format | quote }} + - name: PLAYER_DEV_HMR + value: "false" + readinessProbe: + httpGet: + path: {{ $cfg.probes.readiness.path }} + port: http + initialDelaySeconds: {{ $cfg.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ $cfg.probes.readiness.periodSeconds }} + timeoutSeconds: {{ $cfg.probes.readiness.timeoutSeconds }} + failureThreshold: {{ $cfg.probes.readiness.failureThreshold }} + livenessProbe: + httpGet: + path: {{ $cfg.probes.liveness.path }} + port: http + initialDelaySeconds: {{ $cfg.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ $cfg.probes.liveness.periodSeconds }} + timeoutSeconds: {{ $cfg.probes.liveness.timeoutSeconds }} + failureThreshold: {{ $cfg.probes.liveness.failureThreshold }} + {{- include "hrbox.resources" (dict "ctx" . "component" "nanabushPlayer") | indent 10 }} + +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $name }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "nanabush-player") | nindent 4 }} +spec: + type: ClusterIP + selector: + service: {{ $name }} + # Порт метрик наружу намеренно не публикуется: содержимое курсов приходит + # с того же origin и не должно читать эксплуатационные метрики. + ports: + - name: http + port: {{ .Values.app.nanabushPlayer.service.port }} + targetPort: http + protocol: TCP +{{- end }} diff --git a/.helm/templates/10-app-ingress.yaml b/.helm/templates/10-app-ingress.yaml index 79a5b89..9f359a0 100644 --- a/.helm/templates/10-app-ingress.yaml +++ b/.helm/templates/10-app-ingress.yaml @@ -1,35 +1,70 @@ +{{- if .Values.ingress.enabled }} +{{- $player := .Values.app.nanabushPlayer -}} +{{/* +Единая точка входа. + +Все три маршрута живут на одном домене, потому что в on-premise выделенных +поддоменов под сервисы нет: + + {{ include "nanabushPlayer.basePath" . }}/ -> плеер учебных материалов + /chatbox/ -> вебсокеты чата + / -> само приложение + +Длинные таймауты нужны вебсокетам и плееру: и то и другое держит соединение +открытым, пока пользователь работает. +*/}} --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: {{ .Chart.Name }}-ingress + name: {{ include "hrbox.component" (dict "ctx" . "name" "ingress") }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "web") | nindent 4 }} annotations: - kubernetes.io/ingress.class: {{ .Values.ingress.className }} - nginx.org/websocket-services: {{ .Chart.Name }}-chatbox - nginx.ingress.kubernetes.io/proxy-body-size: "10m" + # Аннотация класса устарела в пользу spec.ingressClassName ниже, но + # оставлена: контроллеры старше ingress-nginx 1.0 понимают только её. + # Когда заданы обе, современный контроллер использует ingressClassName. + kubernetes.io/ingress.class: {{ .Values.ingress.className | quote }} + # Аннотация NGINX Inc.; ingress-nginx её игнорирует, вебсокеты у него + # работают за счёт proxy-read-timeout выше. + nginx.org/websocket-services: {{ include "hrbox.component" (dict "ctx" . "name" "chatbox") | quote }} + nginx.ingress.kubernetes.io/proxy-body-size: {{ .Values.ingress.maxBodySize | quote }} nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" nginx.ingress.kubernetes.io/configuration-snippet: | proxy_set_header X-Forwarded-For "$remote_addr, $server_addr"; proxy_set_header X-Forwarded-Port $pass_port; proxy_set_header X-Forwarded-Proto $pass_access_scheme; + {{- with .Values.ingress.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} spec: + ingressClassName: {{ .Values.ingress.className | quote }} rules: - - host: {{ .Values.ingress.host | quote }} # something.example.com + - host: {{ .Values.ingress.host | quote }} http: paths: + {{- if and $player.ingress.enabled $player.deployment.enabled }} + - path: {{ include "nanabushPlayer.basePath" . | quote }} + pathType: Prefix + backend: + service: + name: {{ include "nanabushPlayer.serviceName" . }} + port: + number: {{ $player.service.port }} + {{- end }} - path: /chatbox/ pathType: Prefix backend: service: - name: {{ .Chart.Name }}-chatbox + name: {{ include "hrbox.component" (dict "ctx" . "name" "chatbox") }} port: number: 80 - path: / pathType: Prefix backend: service: - name: {{ .Chart.Name }}-web + name: {{ include "hrbox.component" (dict "ctx" . "name" "web") }} port: number: 80 {{- if .Values.ingress.tls.enabled }} @@ -37,4 +72,5 @@ spec: - hosts: - {{ .Values.ingress.host | quote }} secretName: {{ .Values.ingress.tls.secretName | quote }} - {{- end }} \ No newline at end of file + {{- end }} +{{- end }} diff --git a/.helm/templates/98-migrate-job.yaml b/.helm/templates/98-migrate-job.yaml index d5e8d20..c822357 100644 --- a/.helm/templates/98-migrate-job.yaml +++ b/.helm/templates/98-migrate-job.yaml @@ -1,21 +1,38 @@ +{{/* +Миграции базы данных. + +Хук post-install/post-upgrade с весом 10: выполняется после того, как Helm +создал ConfigMap и Secret, но до джобы обновления кластера (вес 20). +Имя джобы содержит номер ревизии релиза, поэтому в истории видно, какой +выкат её запускал, а повторный upgrade не спотыкается об уже существующий +объект. +*/}} --- apiVersion: batch/v1 kind: Job metadata: - name: {{ .Chart.Name }}-migrate + name: {{ include "hrbox.component" (dict "ctx" . "name" "migrate") }}-{{ .Release.Revision }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "jobs") | nindent 4 }} annotations: "helm.sh/hook": post-install,post-upgrade - "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation + "helm.sh/hook-delete-policy": before-hook-creation "helm.sh/hook-weight": "10" spec: - ttlSecondsAfterFinished: 100 + ttlSecondsAfterFinished: 3600 + backoffLimit: 6 template: + metadata: + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "jobs") | nindent 8 }} spec: - imagePullSecrets: - - name: regsecret + restartPolicy: OnFailure + terminationGracePeriodSeconds: 5 + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} initContainers: - name: wait-postgres - image: postgres:14-alpine + image: {{ .Values.image.postgresClient | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} command: - bash - /wait.sh @@ -27,16 +44,14 @@ spec: {{- include "envs_all" . | indent 12 }} containers: - name: run-migrations - image: {{ .Values.app.image }} + image: {{ .Values.image.hrbox | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} command: ["php", "yii", "migrate", "--interactive=0"] env: {{- include "envs_all" . | indent 12 }} - {{- include "jobs_resources" . | indent 10 }} + {{- include "hrbox.resources" (dict "ctx" . "component" "jobs") | indent 10 }} volumes: - name: configs configMap: name: job-scripts defaultMode: 420 - restartPolicy: OnFailure - terminationGracePeriodSeconds: 5 - backoffLimit: 6 diff --git a/.helm/templates/99-update-cluser-job.yaml b/.helm/templates/99-update-cluser-job.yaml deleted file mode 100644 index 06b8215..0000000 --- a/.helm/templates/99-update-cluser-job.yaml +++ /dev/null @@ -1,24 +0,0 @@ ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: {{ .Chart.Name }}-update-cluster - annotations: - "helm.sh/hook": post-install,post-upgrade - "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation - "helm.sh/hook-weight": "20" -spec: - template: - spec: - imagePullSecrets: - - name: regsecret - containers: - - name: run-update - image: {{ .Values.app.image }} - command: [ "php", "yii", "util/update-cluster" ] - env: - {{- include "envs_all" . | indent 12 }} - {{- include "jobs_resources" . | indent 10 }} - restartPolicy: OnFailure - terminationGracePeriodSeconds: 5 - backoffLimit: 6 \ No newline at end of file diff --git a/.helm/templates/99-update-cluster-job.yaml b/.helm/templates/99-update-cluster-job.yaml new file mode 100644 index 0000000..9aaac8f --- /dev/null +++ b/.helm/templates/99-update-cluster-job.yaml @@ -0,0 +1,36 @@ +{{/* +Обновление данных кластера после миграций: пересчёт справочников, +перерегистрация фоновых задач, обновление настроек арендаторов. + +Вес 20 — строго после джобы миграций (вес 10). +*/}} +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "hrbox.component" (dict "ctx" . "name" "update-cluster") }}-{{ .Release.Revision }} + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "jobs") | nindent 4 }} + annotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-delete-policy": before-hook-creation + "helm.sh/hook-weight": "20" +spec: + ttlSecondsAfterFinished: 3600 + backoffLimit: 6 + template: + metadata: + labels: + {{- include "hrbox.labels" (dict "ctx" . "name" "jobs") | nindent 8 }} + spec: + restartPolicy: OnFailure + terminationGracePeriodSeconds: 5 + {{- include "hrbox.imagePullSecrets" . | nindent 6 }} + containers: + - name: run-update + image: {{ .Values.image.hrbox | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["php", "yii", "util/update-cluster"] + env: + {{- include "envs_all" . | indent 12 }} + {{- include "hrbox.resources" (dict "ctx" . "component" "jobs") | indent 10 }} diff --git a/.helm/templates/NOTES.txt b/.helm/templates/NOTES.txt new file mode 100644 index 0000000..08cdc59 --- /dev/null +++ b/.helm/templates/NOTES.txt @@ -0,0 +1,72 @@ + +HRBox {{ .Chart.AppVersion }} — релиз «{{ .Release.Name }}» в namespace «{{ .Release.Namespace }}». + +Адрес: http{{ if .Values.ingress.tls.enabled }}s{{ end }}://{{ .Values.ingress.host }} + +Что происходит прямо сейчас +--------------------------- +1. Поднимаются поды приложения и сервисов. +2. Джоба {{ include "hrbox.component" (dict "ctx" . "name" "migrate") }}-{{ .Release.Revision }} накатывает миграции базы. +3. Джоба {{ include "hrbox.component" (dict "ctx" . "name" "update-cluster") }}-{{ .Release.Revision }} обновляет данные кластера. + +Дождитесь завершения миграций: + + kubectl -n {{ .Release.Namespace }} wait --for=condition=complete --timeout=30m \ + job/{{ include "hrbox.component" (dict "ctx" . "name" "migrate") }}-{{ .Release.Revision }} + +Проверка состояния +------------------ + kubectl -n {{ .Release.Namespace }} get pods + kubectl -n {{ .Release.Namespace }} logs -l app=web -c fpm --tail=100 + +{{ if .Release.IsInstall -}} +Первый запуск: создайте администратора +-------------------------------------- + kubectl -n {{ .Release.Namespace }} exec -it \ + deploy/{{ include "hrbox.component" (dict "ctx" . "name" "worker") }} -- \ + php yii user/create-admin admin@example.com --tenant-id=1 + +{{ end -}} +{{- if .Values.app.nanabushPlayer.deployment.enabled }} +Nanabush Player +--------------- +Смонтирован на {{ include "nanabushPlayer.publicOrigin" . }}{{ include "nanabushPlayer.basePath" . }} +Версия: {{ include "nanabushPlayer.version" . }} + +Проверить, что плеер отвечает сам, а не HRBox через общий маршрут +(настоящий плеер возвращает JSON, HTML означает, что запрос ушёл в HRBox): + + kubectl -n {{ .Release.Namespace }} exec deploy/{{ include "hrbox.component" (dict "ctx" . "name" "worker") }} -- \ + curl -s http://{{ include "nanabushPlayer.serviceName" . }}:{{ .Values.app.nanabushPlayer.service.port }}{{ .Values.app.nanabushPlayer.deployment.probes.readiness.path }} + +Служебные пути (/healthz, /readyz, метрики) живут в корне и не зависят от +basePath; под basePath публикуются /start, статика и API плеера. + +{{- if not .Values.app.nanabushPlayer.enabled }} + +ВНИМАНИЕ: плеер развёрнут, но выключен для пользователей +(app.nanabushPlayer.enabled: false). Учебные материалы открываются старым +проигрывателем. +{{- end }} +{{- else }} +Nanabush Player выключен (app.nanabushPlayer.deployment.enabled: false). +Учебные материалы открываются старым проигрывателем. +{{- end }} + +{{ if not .Values.dragonfly.enabled -}} +Dragonfly выключен — проверьте, что app.redis указывает на внешний Redis: +{{ include "hrbox.redisHost" . }}:{{ .Values.app.redis.port }} +{{ end -}} +{{- if not .Values.nats.enabled }} +NATS выключен — проверьте, что app.nats.url указывает на внешний брокер: +{{ include "hrbox.natsUrl" . }} +{{ end -}} +{{- if eq .Values.app.debug "1" }} + +ВНИМАНИЕ: включён режим отладки (app.debug: "1"). Для production верните "0". +{{- end }} +{{- if not .Values.ingress.tls.enabled }} + +ВНИМАНИЕ: TLS выключен (ingress.tls.enabled: false). Приложение отдаёт +cookie сессий по открытому HTTP. +{{- end }} diff --git a/.helm/templates/_envs.tpl b/.helm/templates/_envs.tpl index 4cf8e93..f28f4d6 100644 --- a/.helm/templates/_envs.tpl +++ b/.helm/templates/_envs.tpl @@ -1,64 +1,66 @@ +{{/* +============================================================================ + Переменные окружения HRBox +============================================================================ + +envs_all включается во все контейнеры, которым нужен конфиг приложения: +web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames +и джобы миграций. Один общий блок проще расходящихся наборов, и именно так +устроено продовое окружение. + +Чувствительные значения приходят через secretKeyRef из секрета приложения +(00-app-secret.yaml), а не открытым текстом в спеке пода. +*/}} {{- define "envs_all" }} -# Основные настройки приложения +# --- Приложение ----------------------------------------------------------- - name: INSTANCE_NAME value: {{ .Values.app.instanceName | quote }} - name: DEFAULT_TENANT_HOSTNAME value: {{ .Values.app.defaultTenantHostname | quote }} - name: APP_ENV value: "prod" +- name: APP_PORT + value: "80" - name: YII_DEBUG value: {{ .Values.app.debug | quote }} - name: YII_TRACE_LEVEL value: "0" -- name: APP_COOKIE_VALIDATION_KEY - value: {{ .Values.secrets.cookieValidationKey | quote }} -- name: APP_ENCRYPTION_KEY - value: {{ .Values.secrets.encryptionKey | quote }} -- name: APP_PORT - value: "80" - name: AUTH_HOST value: {{ .Values.app.authHost | quote }} +- name: SINGLE_TENANT + value: {{ .Values.app.singleTenant | quote }} +- name: FORCE_HTTPS + value: {{ .Values.app.forceHttps | quote }} +- name: ENABLE_FRONTEND + value: {{ .Values.app.front2 | quote }} - name: LAUNCHPAD_TOPICS value: {{ .Values.app.launchpad.topics | quote }} - name: SMS_SENDER_NAME - value: "HRBOX" -- name: ENABLE_FRONTEND - value: {{ .Values.app.front2 | quote }} -{{- if .Values.app.custom_policy }} + value: {{ .Values.app.sms.senderName | quote }} +- name: IFRAME_FULL_ACCESS + value: {{ .Values.app.security.iframeFullAccess | quote }} +- name: LDAPTLS_REQCERT + value: "never" +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "APP_COOKIE_VALIDATION_KEY" "key" "cookie-validation-key") | nindent 0 }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "APP_ENCRYPTION_KEY" "key" "encryption-key") | nindent 0 }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "SERVICES_INTERNAL_SECRET" "key" "services-internal-secret") | nindent 0 }} + +# --- Своя политика конфиденциальности ------------------------------------- - name: ALLOW_CUSTOM_POLICY_URLS - value: {{ .Values.app.custom_policy.enabled | default false | quote }} -{{- if .Values.app.custom_policy.enabled }} + value: {{ .Values.app.customPolicy.enabled | quote }} +{{- if .Values.app.customPolicy.enabled }} - name: CUSTOM_PRIVACY_POLICY_URL - value: {{ .Values.app.custom_policy.privacy_url | default "https://hrbox.io/policy" | quote }} + value: {{ .Values.app.customPolicy.privacyUrl | default "https://hrbox.io/policy" | quote }} - name: CUSTOM_TERMS_URL - value: {{ .Values.app.custom_policy.terms_url | default "https://hrbox.io/terms" | quote }} -{{- end }} + value: {{ .Values.app.customPolicy.termsUrl | default "https://hrbox.io/terms" | quote }} {{- else }} -- name: ALLOW_CUSTOM_POLICY_URLS - value: "false" +- name: CUSTOM_PRIVACY_POLICY_URL + value: "" +- name: CUSTOM_TERMS_URL + value: "" {{- end }} -# NATS -- name: BROKER - value: "nats" -- name: NATS_URL - value: {{ .Values.app.nats.url | quote }} - -# Sentry (опционально) -{{- if .Values.app.sentry.dsn }} -- name: SENTRY_DSN - value: {{ .Values.app.sentry.dsn | quote }} -- name: SENTRY_DSN_LAUNCHPAD - value: {{ .Values.app.sentry.dsn | quote }} -- name: SENTRY_DSN_CONDUCTOR - value: {{ .Values.app.sentry.dsn | quote }} -- name: SENTRY_ENVIRONMENT - value: {{ .Values.app.sentry.environment | quote }} -- name: SENTRY_RELEASE - value: {{ .Values.app.image | quote }} -{{- end }} - -# PostgreSQL +# --- PostgreSQL ----------------------------------------------------------- - name: POSTGRES_VHOST value: {{ .Values.app.postgres.host | quote }} - name: POSTGRES_PORT @@ -67,74 +69,37 @@ value: {{ .Values.app.postgres.database | quote }} - name: POSTGRES_USER value: {{ .Values.app.postgres.user | quote }} -- name: POSTGRES_PASSWORD - value: {{ .Values.app.postgres.password | quote }} - name: POSTGRES_CITUS value: {{ .Values.app.postgres.citus | quote }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "POSTGRES_PASSWORD" "key" "postgres-password") | nindent 0 }} -# Redis +# --- Redis / Dragonfly ---------------------------------------------------- - name: REDIS_HOST - value: {{ .Values.app.redis.host | quote }} + value: {{ include "hrbox.redisHost" . | quote }} - name: REDIS_PORT value: {{ .Values.app.redis.port | quote }} - name: REDIS_CONNECTION - value: {{ printf "tcp://%s:%s" .Values.app.redis.host .Values.app.redis.port | quote }} + value: {{ printf "tcp://%s:%s" (include "hrbox.redisHost" .) (.Values.app.redis.port | toString) | quote }} -# Yandex API (опционально) -{{- if .Values.app.yandex.clientId }} -- name: YANDEX_CLIENT_ID - value: {{ .Values.app.yandex.clientId | quote }} -- name: YANDEX_CLIENT_PASSWORD - value: {{ .Values.app.yandex.clientPassword | quote }} +# --- NATS ----------------------------------------------------------------- +- name: BROKER + value: "nats" +- name: NATS_URL + value: {{ include "hrbox.natsUrl" . | quote }} +# Число реплик JetStream-стримов должно совпадать с размером кластера NATS, +# иначе стрим создаётся в одном экземпляре и теряется вместе со своей нодой. +- name: NATS_STREAM_REPLICAS +{{- if and .Values.nats.enabled .Values.nats.cluster.enabled }} + value: {{ .Values.nats.cluster.replicas | quote }} +{{- else }} + value: "1" {{- end }} -# Slack (опционально) -{{- if .Values.app.slack.clientId }} -- name: SLACK_APP_CLIENT_ID - value: {{ .Values.app.slack.clientId | quote }} -- name: SLACK_APP_CLIENT_SECRET - value: {{ .Values.app.slack.clientSecret | quote }} -- name: SLACK_APP_VERIFICATION_TOKEN - value: {{ .Values.app.slack.verificationToken | quote }} -{{- end }} - -# Email -- name: SENDER_ENV - value: "PROD" -- name: SENDER_DEV_EMAIL - value: "" -- name: SENDGRID_API_KEY - value: "" - -# SMTP -- name: SMTP_USERNAME - value: {{ .Values.app.smtp.user | quote }} -- name: SMTP_PASSWORD - value: {{ .Values.app.smtp.password | quote }} -- name: SMTP_HOST - value: {{ .Values.app.smtp.host | quote }} -- name: SMTP_PORT - value: {{ .Values.app.smtp.port | quote }} -- name: SMTP_FROM - value: {{ .Values.app.smtp.from | quote }} -- name: SMTP_FROM_HOST - value: {{ .Values.app.smtp.fromHost | quote }} -- name: SMTP_SECURITY_TYPE - value: {{ .Values.app.smtp.securityType | quote }} -- name: SMTP_SSL_ALLOW_SELF_SIGNED - value: {{ .Values.app.smtp.sslAllowSelfSigned | quote }} -- name: SMTP_SSL_VERIFY_PEER - value: {{ .Values.app.smtp.sslVerifyPeer | quote }} - -# S3 +# --- S3 ------------------------------------------------------------------- - name: S3_ENDPOINT value: {{ .Values.app.s3.endpoint | quote }} - name: S3_BUCKET value: {{ .Values.app.s3.bucket | quote }} -- name: S3_KEY - value: {{ .Values.app.s3.key | quote }} -- name: S3_SECRET - value: {{ .Values.app.s3.secret | quote }} - name: S3_PREFIX value: {{ .Values.app.s3.prefix | quote }} - name: S3_REGION @@ -145,40 +110,120 @@ value: {{ .Values.app.s3.pathStyle | quote }} - name: S3_MULTIPART value: {{ .Values.app.s3.multipart | quote }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "S3_KEY" "key" "s3-key") | nindent 0 }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "S3_SECRET" "key" "s3-secret") | nindent 0 }} -# Hub -- name: HUB_URL - value: {{ .Values.app.hub.url | quote }} -- name: HUB_TOKEN - value: {{ .Values.app.hub.token | quote }} +# --- Почта ---------------------------------------------------------------- +- name: SENDER_ENV + value: "PROD" +- name: SENDER_DEV_EMAIL + value: "" +- name: SMTP_HOST + value: {{ .Values.app.smtp.host | quote }} +- name: SMTP_PORT + value: {{ .Values.app.smtp.port | quote }} +- name: SMTP_USERNAME + value: {{ .Values.app.smtp.user | quote }} +- name: SMTP_FROM + value: {{ .Values.app.smtp.from | quote }} +- name: SMTP_FROM_HOST + value: {{ .Values.app.smtp.fromHost | quote }} +- name: SMTP_SECURITY_TYPE + value: {{ .Values.app.smtp.securityType | quote }} +- name: SMTP_SSL_ALLOW_SELF_SIGNED + value: {{ .Values.app.smtp.sslAllowSelfSigned | quote }} +- name: SMTP_SSL_VERIFY_PEER + value: {{ .Values.app.smtp.sslVerifyPeer | quote }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "SMTP_PASSWORD" "key" "smtp-password") | nindent 0 }} -# Geonames +# --- Внутренние сервисы --------------------------------------------------- - name: GEONAMES_URL - value: {{ .Values.app.geonames.url | quote }} - -# Статические переменные + value: {{ include "hrbox.geonamesUrl" . | quote }} +- name: GOWORKER_URL + value: {{ include "hrbox.goworkerUrl" . | quote }} +- name: GOWORKER_PUSH_MIN_INTERVAL + value: {{ .Values.app.goworker.pushMinInterval | quote }} +- name: GOWORKER_SENTRY_DSN + value: {{ .Values.app.sentry.goworkerDsn | quote }} +- name: KEDOCA_URL + value: {{ include "hrbox.kedocaUrl" . | quote }} +- name: TSP_URL + value: {{ .Values.app.kedoca.tspUrl | quote }} +- name: APP_INTERNAL_URL + value: {{ .Values.app.videoConverter.internalUrl | default (printf "http://%s" (include "hrbox.component" (dict "ctx" . "name" "web"))) | quote }} - name: CONDUCTOR_TOPIC_CONDUCTOR value: "conductor" - name: CONDUCTOR_TOPIC_LAUNCHER value: "launcher" -- name: GOOGLE_CLIENT_ID - value: "692956564858-ccapt35khk7tmadl17b3879ht2cropp6.apps.googleusercontent.com" -- name: GOOGLE_CLIENT_SECRET - value: "nW4pl4YsVWAR371gMRStW9qg" -- name: SINGLE_TENANT - value: "0" -- name: FORCE_HTTPS - value: "true" -- name: GLAGOL_PARTNER_ID - value: "ab230ee1-6946-43b9-8kfa-447beb8348b0" -- name: GLAGOL_API_URL - value: "https://api.glagol.online/glagol-api" -- name: LDAPTLS_REQCERT - value: "never" -- name: IFRAME_FULL_ACCESS - value: {{ .Values.app.security.iframeFullAccess | default "false" | quote }} +- name: PYTHONUNBUFFERED + value: "1" +- name: DOCCONV_MAX_PARALLEL_TASKS + value: {{ .Values.app.fileProcessor.docconvMaxParallelTasks | quote }} -# Метаданные пода +# --- HRBox Hub ------------------------------------------------------------ +- name: HUB_URL + value: {{ .Values.app.hub.url | quote }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "HUB_TOKEN" "key" "hub-token") | nindent 0 }} + +# --- Nanabush Player ------------------------------------------------------ +{{- include "envs_nanabush_player" . }} + +# --- Чат ------------------------------------------------------------------ +- name: CHATBOX_SENTRY_DSN + value: {{ .Values.app.sentry.chatboxDsn | quote }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "CHATBOX_WRAPPING_KEY" "key" "chatbox-wrapping-key") | nindent 0 }} + +# --- Видеозвонки (LiveKit) ------------------------------------------------ +- name: LIVEKIT_URL + value: {{ .Values.app.livekit.url | quote }} +- name: LIVEKIT_API_KEY + value: {{ .Values.app.livekit.apiKey | quote }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "LIVEKIT_API_SECRET" "key" "livekit-api-secret") | nindent 0 }} + +# --- AI ------------------------------------------------------------------- +- name: AI_DOCS_INDEX_ENABLED + value: {{ .Values.app.ai.docsIndexEnabled | quote }} +- name: AI_SEMANTIC_INDEX_ENABLED + value: {{ .Values.app.ai.semanticIndexEnabled | quote }} +- name: AI_SEMANTIC_INDEX_USE_BATCH + value: {{ .Values.app.ai.semanticIndexUseBatch | quote }} +- name: YANDEX_CLOUD_AI_FOLDER_ID + value: {{ .Values.app.ai.yandex.folderId | quote }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "YANDEX_CLOUD_AI_API_KEY" "key" "yandex-ai-api-key") | nindent 0 }} + +# --- Внешние интеграции --------------------------------------------------- +- name: TELEGRAM_API_URL + value: {{ .Values.app.integrations.telegram.apiUrl | quote }} +- name: AUTH_GOOGLE_ENABLED + value: {{ .Values.app.integrations.google.enabled | quote }} +- name: GOOGLE_CLIENT_ID + value: {{ .Values.app.integrations.google.clientId | quote }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "GOOGLE_CLIENT_SECRET" "key" "google-client-secret") | nindent 0 }} +- name: YANDEX_CLIENT_ID + value: {{ .Values.app.yandex.clientId | quote }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "YANDEX_CLIENT_PASSWORD" "key" "yandex-client-password") | nindent 0 }} +- name: SLACK_APP_CLIENT_ID + value: {{ .Values.app.slack.clientId | quote }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "SLACK_APP_CLIENT_SECRET" "key" "slack-client-secret") | nindent 0 }} +{{- include "hrbox.secretEnv" (dict "ctx" . "env" "SLACK_APP_VERIFICATION_TOKEN" "key" "slack-verification-token") | nindent 0 }} +- name: GLAGOL_PARTNER_ID + value: {{ .Values.app.integrations.glagol.partnerId | quote }} +- name: GLAGOL_API_URL + value: {{ .Values.app.integrations.glagol.apiUrl | quote }} + +# --- Sentry --------------------------------------------------------------- +- name: SENTRY_DSN + value: {{ .Values.app.sentry.dsn | quote }} +- name: SENTRY_DSN_LAUNCHPAD + value: {{ .Values.app.sentry.launchpadDsn | default .Values.app.sentry.dsn | quote }} +- name: SENTRY_DSN_CONDUCTOR + value: {{ .Values.app.sentry.conductorDsn | default .Values.app.sentry.dsn | quote }} +- name: SENTRY_ENVIRONMENT + value: {{ .Values.app.sentry.environment | quote }} +- name: SENTRY_RELEASE + value: {{ .Values.image.hrbox | quote }} + +# --- Метаданные пода ------------------------------------------------------ - name: POD_NAME valueFrom: fieldRef: @@ -195,57 +240,83 @@ valueFrom: fieldRef: fieldPath: spec.nodeName - -# AI (опционально) -{{- if .Values.app.ai.yandex.folderId }} -- name: YANDEX_CLOUD_AI_FOLDER_ID - value: {{ .Values.app.ai.yandex.folderId | quote }} -- name: YANDEX_CLOUD_AI_API_KEY - value: {{ .Values.app.ai.yandex.apiKey | quote }} {{- end }} -# Integration Services -- name: GOWORKER_URL - value: {{ .Values.app.goworker.url | quote }} -{{- if .Values.app.goworker.sentryDsn }} -- name: GOWORKER_SENTRY_DSN - value: {{ .Values.app.goworker.sentryDsn | quote }} +{{/* +Настройки Nanabush Player на стороне HRBox. + +NANABUSH_PLAYER_AUDIENCE и NANABUSH_PLAYER_PUBLIC_ORIGIN всегда равны: +audience тикета — это origin браузера без пути, а путь монтирования едет +отдельной переменной NANABUSH_PLAYER_BASE_PATH. +*/}} +{{- define "envs_nanabush_player" -}} +{{- $player := .Values.app.nanabushPlayer }} +- name: NANABUSH_PLAYER_ENABLED + value: {{ $player.enabled | quote }} +{{- if $player.deployment.enabled }} +- name: NANABUSH_PLAYER_PUBLIC_ORIGIN + value: {{ include "nanabushPlayer.publicOrigin" . | quote }} +- name: NANABUSH_PLAYER_AUDIENCE + value: {{ include "nanabushPlayer.publicOrigin" . | quote }} +- name: NANABUSH_PLAYER_BASE_PATH + value: {{ include "nanabushPlayer.basePath" . | quote }} +- name: NANABUSH_PLAYER_INTERNAL_ORIGIN + value: {{ include "nanabushPlayer.internalOrigin" . | quote }} +- name: NANABUSH_PLAYER_ISSUER + value: {{ include "nanabushPlayer.issuer" . | quote }} +- name: NANABUSH_PLAYER_CONTRACT_VERSION + value: {{ $player.contractVersion | quote }} +- name: NANABUSH_PLAYER_PLAYER_VERSION + value: {{ include "nanabushPlayer.version" . | quote }} +- name: NANABUSH_PLAYER_TICKET_TTL_SECONDS + value: {{ $player.deployment.ticket.ttlSeconds | quote }} +- name: NANABUSH_PLAYER_SESSION_TTL_SECONDS + value: {{ $player.deployment.ticket.credentialTtlSeconds | quote }} +- name: NANABUSH_PLAYER_PACKAGE_CONTENT_MAX_BYTES + value: {{ $player.deployment.backchannel.packageContentMaxBytes | quote }} +- name: NANABUSH_PLAYER_SCORM_PACKAGE_MAX_BYTES + value: {{ $player.deployment.backchannel.scormPackageMaxBytes | quote }} +{{- with $player.deployment.backchannel.scormPackageMaxFiles }} +- name: NANABUSH_PLAYER_SCORM_PACKAGE_MAX_FILES + value: {{ . | quote }} {{- end }} -- name: GOWORKER_PUSH_MIN_INTERVAL - value: {{ .Values.app.goworker.pushMinInterval | quote }} - -- name: KEDOCA_URL - value: {{ .Values.app.kedoca.url | quote }} -- name: TSP_URL - value: "http://qs.cryptopro.ru/tsp/tsp.srf" - -# Internal Services -- name: SERVICES_INTERNAL_SECRET - value: {{ .Values.secrets.servicesInternalSecret | default "QrrH84K2dG5JfoYxi4ZNPyg5p2LEj6rN69z5tIQYkfY6SJZ8GDXOcRjTNnefbOx5" | quote }} - -# Document Converter -- name: PYTHONUNBUFFERED - value: "1" -- name: DOCCONV_MAX_PARALLEL_TASKS - value: "1" - -# AI Features -- name: AI_DOCS_INDEX_ENABLED - value: "true" -- name: AI_SEMANTIC_INDEX_ENABLED - value: "true" -- name: AI_SEMANTIC_INDEX_USE_BATCH - value: "true" - -# Chatbox -{{- if .Values.app.chatbox.sentryDsn }} -- name: CHATBOX_SENTRY_DSN - value: {{ .Values.app.chatbox.sentryDsn | quote }} -{{- end }} -- name: CHATBOX_WRAPPING_KEY - value: {{ .Values.app.chatbox.wrappingKey | quote }} +- name: NANABUSH_PLAYER_LEGACY_FALLBACK + value: {{ $player.rollout.legacyFallback | quote }} +- name: NANABUSH_PLAYER_ALLOWED_CONTENT_TYPES + value: {{ join "," $player.rollout.allowedContentTypes | quote }} +- name: NANABUSH_PLAYER_ALLOWED_TENANTS + value: {{ join "," $player.rollout.allowedTenants | quote }} +- name: NANABUSH_PLAYER_ROLLOUT_PERCENT + value: {{ $player.rollout.percent | quote }} {{- end }} +{{- end -}} +{{/* +Ключ подписи launch-тикета. Живёт только в PHP-контейнерах, которые выпускают +тикеты: nginx и вспомогательным контейнерам он не нужен. +*/}} +{{- define "envs_nanabush_player_signing" -}} +{{- $cfg := .Values.app.nanabushPlayer.deployment }} +{{- if $cfg.enabled }} +- name: NANABUSH_PLAYER_HS256_SECRET + valueFrom: + secretKeyRef: + name: {{ include "nanabushPlayer.secretName" . | quote }} + key: {{ required "Нужно имя ключа ticketHs256 в секрете Nanabush Player" $cfg.existingSecret.keys.ticketHs256 | quote }} +{{- with $cfg.existingSecret.keys.ticketKeys }} +# Кольцо ключей launch-тикета: HRBox подписывает первым ключом, Player +# принимает все. Тот же ключ секрета читает и сам Player, поэтому ротация — +# это одно значение на обе стороны. +- name: NANABUSH_PLAYER_TICKET_KEYS + valueFrom: + secretKeyRef: + name: {{ include "nanabushPlayer.secretName" $ | quote }} + key: {{ . | quote }} +{{- end }} +{{- end }} +{{- end -}} + +{{/* Криптографические настройки сервиса электронной подписи. */}} {{- define "envs_kedoca" }} - name: ROOT_CA_PASS value: {{ .Values.app.kedoca.rootCaPass | quote }} @@ -255,4 +326,22 @@ value: {{ .Values.app.kedoca.algorithm | quote }} - name: KEDO_PKEYOPT value: {{ .Values.app.kedoca.pkeyopt | quote }} -{{- end }} \ No newline at end of file +{{- end }} + +{{/* OpenTelemetry — только когда app.tracing.enabled. */}} +{{- define "envs_tracing" }} +- name: OTEL_PHP_AUTOLOAD_ENABLED + value: "true" +- name: OTEL_EXPORTER_OTLP_ENDPOINT + value: {{ .Values.app.tracing.exporterUrl | quote }} +- name: OTEL_SERVICE_NAME + value: "hrbox-app" +- name: OTEL_TRACES_EXPORTER + value: "otlp" +- name: OTEL_EXPORTER_OTLP_PROTOCOL + value: "http/protobuf" +- name: OTEL_PROPAGATORS + value: "baggage,tracecontext" +- name: OTEL_LOG_LEVEL + value: "info" +{{- end }} diff --git a/.helm/templates/_helpers.tpl b/.helm/templates/_helpers.tpl index 092713b..642a440 100644 --- a/.helm/templates/_helpers.tpl +++ b/.helm/templates/_helpers.tpl @@ -1,110 +1,231 @@ {{/* -Expand the name of the chart. +============================================================================ + Общие хелперы чарта HRBox +============================================================================ + +Правило именования: все объекты называются "<имя чарта>-<компонент>" +(hrbox-web, hrbox-worker, ...). Имя чарта можно переопределить через +nameOverride, но по умолчанию оно равно "hrbox" — так же, как в чарте 2.x, +поэтому обновление 2.x -> 3.x не пересоздаёт объекты. */}} -{{- define "nats.name" -}} -{{- default .Chart.Name .Values.nats.nameOverride | trunc 63 | trimSuffix "-" -}} + +{{/* Базовое имя релиза: "hrbox" или nameOverride. */}} +{{- define "hrbox.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} {{- end -}} +{{/* Имя чарта с версией — для лейбла helm.sh/chart. */}} +{{- define "hrbox.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} -{{- define "nats.fullname" -}} -{{- if .Values.nats.fullnameOverride -}} -{{- .Values.nats.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{/* +Полное имя объекта компонента. + +Usage: {{ include "hrbox.component" (dict "ctx" . "name" "web") }} -> hrbox-web +*/}} +{{- define "hrbox.component" -}} +{{- printf "%s-%s" (include "hrbox.name" .ctx) .name | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Селектор пода. ВАЖНО: spec.selector в Deployment неизменяем, поэтому набор +лейблов здесь совпадает с чартом 2.x (один лейбл "service") и меняться не должен. + +Usage: {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "web") | nindent 6 }} +*/}} +{{- define "hrbox.selectorLabels" -}} +service: {{ include "hrbox.component" (dict "ctx" .ctx "name" .name) }} +{{- end -}} + +{{/* +Лейблы объекта. Лейбл "app" сохранён из чарта 2.x — на него опираются +привычные команды вида "kubectl get pods -l app=worker". + +Usage: {{- include "hrbox.labels" (dict "ctx" . "name" "web") | nindent 4 }} +*/}} +{{- define "hrbox.labels" -}} +app: {{ .name }} +app.kubernetes.io/name: {{ include "hrbox.name" .ctx }} +app.kubernetes.io/component: {{ .name }} +app.kubernetes.io/instance: {{ .ctx.Release.Name }} +app.kubernetes.io/managed-by: {{ .ctx.Release.Service }} +app.kubernetes.io/version: {{ .ctx.Chart.AppVersion | quote }} +helm.sh/chart: {{ include "hrbox.chart" .ctx }} +{{- end -}} + +{{/* +Секреты для доступа к приватному Docker-реестру. + +Usage: {{- include "hrbox.imagePullSecrets" . | nindent 6 }} +*/}} +{{- define "hrbox.imagePullSecrets" -}} +{{- if .Values.imagePullSecrets }} +imagePullSecrets: +{{- range .Values.imagePullSecrets }} + - name: {{ .name | quote }} +{{- end }} +{{- end }} +{{- end -}} + +{{/* +Тег из ссылки на образ: "registry/repo:1.2.3" -> "1.2.3". +Двоеточие ищется только в последнем сегменте пути, чтобы не спутать тег +с портом реестра ("registry:5000/repo"). +*/}} +{{- define "hrbox.imageTag" -}} +{{- $lastSegment := splitList "/" . | last -}} +{{- if contains ":" $lastSegment -}} +{{- splitList ":" $lastSegment | last -}} +{{- end -}} +{{- end -}} + +{{/* +============================================================================ + Секрет приложения +============================================================================ + +Чувствительные значения (пароли БД и SMTP, ключи шифрования, токены) не +попадают в env-блоки Deployment открытым текстом: чарт складывает их в один +Secret, а контейнеры читают его через secretKeyRef. Полный список ключей — +в README, раздел "Секреты". + +secrets.existingSecret позволяет подставить Secret, созданный вне чарта +(например, из внешнего vault-оператора) — тогда чарт свой Secret не создаёт. +*/}} +{{/* +Адреса внутренних сервисов. + +Сервисы релиза адресуются по именам, которые создаёт этот же чарт, поэтому +пустое значение в values резолвится в имя из hrbox.component: адреса не +разъезжаются при nameOverride и их не нужно править вручную. Непустое +значение используется как есть — для сервиса за пределами релиза. +*/}} +{{- define "hrbox.geonamesUrl" -}} +{{- .Values.app.geonames.url | default (printf "http://%s:8181" (include "hrbox.component" (dict "ctx" . "name" "geonames"))) -}} +{{- end -}} + +{{- define "hrbox.goworkerUrl" -}} +{{- .Values.app.goworker.url | default (printf "http://%s:8686" (include "hrbox.component" (dict "ctx" . "name" "goworker"))) -}} +{{- end -}} + +{{- define "hrbox.kedocaUrl" -}} +{{- .Values.app.kedoca.url | default (printf "http://%s:8558" (include "hrbox.component" (dict "ctx" . "name" "kedoca"))) -}} +{{- end -}} + +{{- define "hrbox.redisHost" -}} +{{- .Values.app.redis.host | default (include "hrbox.component" (dict "ctx" . "name" "dragonfly")) -}} +{{- end -}} + +{{- define "hrbox.natsUrl" -}} +{{- .Values.app.nats.url | default (printf "nats://%s:4222" (include "hrbox.component" (dict "ctx" . "name" "nats"))) -}} +{{- end -}} + +{{- define "hrbox.secretName" -}} +{{- if .Values.secrets.existingSecret -}} +{{- .Values.secrets.existingSecret -}} {{- else -}} -{{- $name := default .Chart.Name .Values.nats.nameOverride -}} -{{- if contains $name .Release.Name -}} -{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- include "hrbox.component" (dict "ctx" . "name" "secrets") -}} +{{- end -}} +{{- end -}} + +{{/* +Одна переменная окружения из секрета приложения. + +Usage: {{ include "hrbox.secretEnv" (dict "ctx" . "env" "POSTGRES_PASSWORD" "key" "postgres-password") }} +*/}} +{{- define "hrbox.secretEnv" -}} +- name: {{ .env }} + valueFrom: + secretKeyRef: + name: {{ include "hrbox.secretName" .ctx | quote }} + key: {{ .key | quote }} +{{- end -}} + +{{/* +============================================================================ + Nanabush Player +============================================================================ + +On-premise-топология Player'а отличается от облачной: выделенного поддомена +нет, поэтому Player живёт на основном домене HRBox под путём basePath +(по умолчанию /nanabush-player). Отсюда следуют три инварианта, которые +проверяет 00-validate.yaml: + + * audience тикета = https:// (origin без пути) + * basePath = отдельная переменная, в aud не входит + * issuer тикета = https:///lms/player + +Подробности контракта — docs/nanabush-player-deployment.md в репозитории HRBox. +*/}} + +{{- define "nanabushPlayer.host" -}} +{{- required "Для Nanabush Player нужен непустой ingress.host" .Values.ingress.host -}} +{{- end -}} + +{{- define "nanabushPlayer.publicOrigin" -}} +{{- printf "https://%s" (include "nanabushPlayer.host" .) -}} +{{- end -}} + +{{/* +Путь монтирования Player'а на основном домене. Пустое значение и "/" запрещены: +это отдало бы Player'у корень, занятый самим HRBox. +*/}} +{{- define "nanabushPlayer.basePath" -}} +{{- $path := .Values.app.nanabushPlayer.basePath | default "/nanabush-player" -}} +{{- $path | trimSuffix "/" -}} +{{- end -}} + +{{- define "nanabushPlayer.issuer" -}} +{{- if .Values.app.nanabushPlayer.issuer -}} +{{- .Values.app.nanabushPlayer.issuer -}} {{- else -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} +{{- printf "https://%s/lms/player" (include "nanabushPlayer.host" .) -}} {{- end -}} {{- end -}} -{{- define "nats.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} +{{- define "nanabushPlayer.serviceName" -}} +{{- .Values.app.nanabushPlayer.service.name | default (include "hrbox.component" (dict "ctx" . "name" "nanabush-player")) -}} +{{- end -}} {{/* -Common labels +Хост обратного канала Player -> HRBox. Только внутрикластерное имя: по нему +Player забирает пакеты курсов и отправляет отчёты, и оно же попадает в +allowlist самого Player'а. */}} -{{- define "nats.labels" -}} -app.kubernetes.io/name: nats -app.kubernetes.io/instance: {{ .Release.Name }} -helm.sh/chart: {{ include "nats.chart" . }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "nats.selectorLabels" -}} -app.kubernetes.io/name: nats -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - - - -{{/* -Return the proper NATS image name -*/}} -{{- define "nats.clusterAdvertise" -}} -{{- printf "$(POD_NAME).%s.$(POD_NAMESPACE).svc.%s" (include "nats.fullname" . ) $.Values.nats.k8sClusterDomain }} -{{- end }} - -{{/* -Return the NATS cluster routes. -*/}} -{{- define "nats.clusterRoutes" -}} -{{- $name := (include "nats.fullname" . ) -}} -{{- range $i, $e := until (.Values.nats.cluster.replicas | int) -}} -{{- printf "nats://%s-%d.%s.%s.svc.%s:6222," $name $i $name $.Release.Namespace $.Values.nats.k8sClusterDomain -}} +{{- define "nanabushPlayer.backchannelHost" -}} +{{- .Values.app.nanabushPlayer.deployment.backchannel.host | default (include "hrbox.component" (dict "ctx" . "name" "web")) -}} {{- end -}} -{{- end }} -{{- define "nats.extraRoutes" -}} -{{- range $i, $url := .Values.nats.cluster.extraRoutes -}} -{{- printf "%s," $url -}} +{{- define "nanabushPlayer.internalOrigin" -}} +{{- printf "http://%s" (include "nanabushPlayer.backchannelHost" .) -}} {{- end -}} -{{- end }} - -{{- define "nats.tlsConfig" -}} -tls { -{{- if .cert }} - cert_file: {{ .secretPath }}/{{ .secret.name }}/{{ .cert }} -{{- end }} -{{- if .key }} - key_file: {{ .secretPath }}/{{ .secret.name }}/{{ .key }} -{{- end }} -{{- if .ca }} - ca_file: {{ .secretPath }}/{{ .secret.name }}/{{ .ca }} -{{- end }} -{{- if .insecure }} - insecure: {{ .insecure }} -{{- end }} -{{- if .verify }} - verify: {{ .verify }} -{{- end }} -{{- if .verifyAndMap }} - verify_and_map: {{ .verifyAndMap }} -{{- end }} -{{- if .curvePreferences }} - curve_preferences: {{ .curvePreferences }} -{{- end }} -{{- if .timeout }} - timeout: {{ .timeout }} -{{- end }} -} -{{- end }} - {{/* -Renders a value that contains template. -Usage: -{{ include "tplvalues.render" ( dict "value" .Values.nats.path.to.the.Value "context" $) }} +Версия Player'а берётся из тега образа, а не из отдельной настройки: тикет +не должен объявлять версию, отличную от той, что реально запущена в кластере. */}} -{{- define "tplvalues.render" -}} - {{- if typeIs "string" .value }} - {{- tpl .value .context }} - {{- else }} - {{- tpl (.value | toYaml) .context }} - {{- end }} +{{- define "nanabushPlayer.version" -}} +{{- $tag := include "hrbox.imageTag" .Values.image.nanabushPlayer -}} +{{- if not $tag -}} +{{- fail "image.nanabushPlayer должен содержать явный тег версии, например nanabush-player:1.0.11" -}} +{{- end -}} +{{- $tag -}} +{{- end -}} + +{{- define "nanabushPlayer.secretName" -}} +{{- required "app.nanabushPlayer.deployment.existingSecret.name обязателен, когда Player включён" .Values.app.nanabushPlayer.deployment.existingSecret.name -}} +{{- end -}} + +{{/* URL Redis, который чарт собирает из штатных настроек app.redis. */}} +{{- define "nanabushPlayer.redisUrl" -}} +{{- printf "redis://%s:%s/%d" (include "hrbox.redisHost" .) (.Values.app.redis.port | toString) (.Values.app.nanabushPlayer.deployment.redis.database | int) -}} +{{- end -}} + +{{/* +Префикс ключей Redis. Namespace в префиксе разводит несколько релизов HRBox, +живущих на одном Dragonfly. +*/}} +{{- define "nanabushPlayer.redisKeyPrefix" -}} +{{- printf "hrbox:%s:%s" .Release.Namespace .Values.app.nanabushPlayer.deployment.redis.keyPrefixSuffix -}} {{- end -}} diff --git a/.helm/templates/_resources.tpl b/.helm/templates/_resources.tpl index 2072e26..243d176 100644 --- a/.helm/templates/_resources.tpl +++ b/.helm/templates/_resources.tpl @@ -1,119 +1,43 @@ -{{- define "app_resources" }} -resources: - requests: - memory: {{ .Values.resources.app.requests.memory | quote }} - cpu: {{ .Values.resources.app.requests.cpu | quote }} - ephemeral-storage: {{ .Values.resources.app.requests.ephemeralStorage | quote }} - limits: - memory: {{ .Values.resources.app.limits.memory | quote }} - cpu: {{ .Values.resources.app.limits.cpu | quote }} - ephemeral-storage: {{ .Values.resources.app.limits.ephemeralStorage | quote }} -{{- end }} +{{/* +============================================================================ + resources: для контейнера +============================================================================ -{{- define "nginx_resources" }} -resources: - requests: - memory: {{ .Values.resources.nginx.requests.memory | quote }} - cpu: {{ .Values.resources.nginx.requests.cpu | quote }} - ephemeral-storage: {{ .Values.resources.nginx.requests.ephemeralStorage | quote }} - limits: - memory: {{ .Values.resources.nginx.limits.memory | quote }} - cpu: {{ .Values.resources.nginx.limits.cpu | quote }} - ephemeral-storage: {{ .Values.resources.nginx.limits.ephemeralStorage | quote }} -{{- end }} +В чарте 2.x на каждый компонент был отдельный define из восьми одинаковых +строк. Здесь один хелпер, который читает resources.<компонент> из values.yaml. +Незаданные поля просто не рендерятся, поэтому компоненту можно указать, +например, только requests. -{{- define "worker_resources" }} +Usage: + {{- include "hrbox.resources" (dict "ctx" . "component" "app") | indent 10 }} +*/}} +{{- define "hrbox.resources" -}} +{{- $r := index .ctx.Values.resources .component -}} +{{- if $r }} resources: +{{- with $r.requests }} requests: - memory: {{ .Values.resources.worker.requests.memory | quote }} - cpu: {{ .Values.resources.worker.requests.cpu | quote }} - ephemeral-storage: {{ .Values.resources.worker.requests.ephemeralStorage | quote }} - limits: - memory: {{ .Values.resources.worker.limits.memory | quote }} - cpu: {{ .Values.resources.worker.limits.cpu | quote }} - ephemeral-storage: {{ .Values.resources.worker.limits.ephemeralStorage | quote }} + {{- with .cpu }} + cpu: {{ . | quote }} + {{- end }} + {{- with .memory }} + memory: {{ . | quote }} + {{- end }} + {{- with .ephemeralStorage }} + ephemeral-storage: {{ . | quote }} + {{- end }} {{- end }} - -{{- define "websocket_resources" }} -resources: - requests: - memory: {{ .Values.resources.websocket.requests.memory | quote }} - cpu: {{ .Values.resources.websocket.requests.cpu | quote }} - ephemeral-storage: {{ .Values.resources.websocket.requests.ephemeralStorage | quote }} +{{- with $r.limits }} limits: - memory: {{ .Values.resources.websocket.limits.memory | quote }} - cpu: {{ .Values.resources.websocket.limits.cpu | quote }} - ephemeral-storage: {{ .Values.resources.websocket.limits.ephemeralStorage | quote }} + {{- with .cpu }} + cpu: {{ . | quote }} + {{- end }} + {{- with .memory }} + memory: {{ . | quote }} + {{- end }} + {{- with .ephemeralStorage }} + ephemeral-storage: {{ . | quote }} + {{- end }} {{- end }} - -{{- define "geonames_resources" }} -resources: - requests: - memory: {{ .Values.resources.geonames.requests.memory | quote }} - cpu: {{ .Values.resources.geonames.requests.cpu | quote }} - ephemeral-storage: {{ .Values.resources.geonames.requests.ephemeralStorage | quote }} - limits: - memory: {{ .Values.resources.geonames.limits.memory | quote }} - cpu: {{ .Values.resources.geonames.limits.cpu | quote }} - ephemeral-storage: {{ .Values.resources.geonames.limits.ephemeralStorage | quote }} {{- end }} - -{{- define "conductor_resources" }} -resources: - requests: - memory: {{ .Values.resources.conductor.requests.memory | quote }} - cpu: {{ .Values.resources.conductor.requests.cpu | quote }} - ephemeral-storage: {{ .Values.resources.conductor.requests.ephemeralStorage | quote }} - limits: - memory: {{ .Values.resources.conductor.limits.memory | quote }} - cpu: {{ .Values.resources.conductor.limits.cpu | quote }} - ephemeral-storage: {{ .Values.resources.conductor.limits.ephemeralStorage | quote }} -{{- end }} - -{{- define "jobs_resources" }} -resources: - requests: - memory: {{ .Values.resources.jobs.requests.memory | quote }} - cpu: {{ .Values.resources.jobs.requests.cpu | quote }} - ephemeral-storage: {{ .Values.resources.jobs.requests.ephemeralStorage | quote }} - limits: - memory: {{ .Values.resources.jobs.limits.memory | quote }} - cpu: {{ .Values.resources.jobs.limits.cpu | quote }} - ephemeral-storage: {{ .Values.resources.jobs.limits.ephemeralStorage | quote }} -{{- end }} - -{{- define "goworker_resources" }} -resources: - requests: - memory: {{ .Values.resources.goworker.requests.memory | quote }} - cpu: {{ .Values.resources.goworker.requests.cpu | quote }} - ephemeral-storage: {{ .Values.resources.goworker.requests.ephemeralStorage | quote }} - limits: - memory: {{ .Values.resources.goworker.limits.memory | quote }} - cpu: {{ .Values.resources.goworker.limits.cpu | quote }} - ephemeral-storage: {{ .Values.resources.goworker.limits.ephemeralStorage | quote }} -{{- end }} - -{{- define "kedoca_resources" }} -resources: - requests: - memory: {{ .Values.resources.kedoca.requests.memory | quote }} - cpu: {{ .Values.resources.kedoca.requests.cpu | quote }} - ephemeral-storage: {{ .Values.resources.kedoca.requests.ephemeralStorage | quote }} - limits: - memory: {{ .Values.resources.kedoca.limits.memory | quote }} - cpu: {{ .Values.resources.kedoca.limits.cpu | quote }} - ephemeral-storage: {{ .Values.resources.kedoca.limits.ephemeralStorage | quote }} -{{- end }} - -{{- define "chatbox_resources" }} -resources: - requests: - memory: {{ .Values.resources.chatbox.requests.memory | quote }} - cpu: {{ .Values.resources.chatbox.requests.cpu | quote }} - ephemeral-storage: {{ .Values.resources.chatbox.requests.ephemeralStorage | quote }} - limits: - memory: {{ .Values.resources.chatbox.limits.memory | quote }} - cpu: {{ .Values.resources.chatbox.limits.cpu | quote }} - ephemeral-storage: {{ .Values.resources.chatbox.limits.ephemeralStorage | quote }} -{{- end }} \ No newline at end of file +{{- end -}} diff --git a/.helm/values.yaml b/.helm/values.yaml index b84d7fa..3892c80 100644 --- a/.helm/values.yaml +++ b/.helm/values.yaml @@ -1,464 +1,738 @@ --- -# Основные настройки приложения HRBox +# ============================================================================ +# HRBox — конфигурация on-premise установки +# ============================================================================ +# +# Минимум, который нужно заполнить перед первым деплоем: +# +# ingress.host домен инсталляции +# app.defaultTenantHostname тот же домен +# app.postgres.* подключение к PostgreSQL +# app.s3.* S3-совместимое хранилище +# app.smtp.* почта +# app.hub.token токен HRBox Hub (выдаёт HRBox) +# secrets.* три сгенерированных ключа +# app.chatbox.wrappingKey ключ шифрования чатов +# app.nanabushPlayer.keys.* два ключа плеера курсов +# +# Чарт проверяет их на этапе рендера и падает с понятной ошибкой, если +# что-то не заполнено, — до того, как в кластере появятся поды. +# +# Команды генерации ключей перечислены рядом с каждым полем и целиком — +# в README, раздел «Секреты». +# ============================================================================ + +# Переопределение базового имени объектов. По умолчанию — имя чарта (hrbox), +# то есть hrbox-web, hrbox-worker и так далее. Менять на работающей установке +# нельзя: selector у Deployment неизменяем. +nameOverride: "" + +# ============================================================================ +# Образы +# ============================================================================ +# Все образы собраны в одном месте: обновление версии сервиса — это правка +# одной строки здесь, а не поиск по шаблонам. +image: + # Основное приложение (PHP-FPM, nginx, worker, conductor-задачи, миграции). + # Доступ к реестру выдают сотрудники HRBox. + hrbox: "cr.yandex/crphnph9c2s776j32sjd/hrbox:latest" + + # Чат и вебсокеты + chatbox: "cr.yandex/crphnph9c2s776j32sjd/hrbox-chatbox:5.18" + # Интеграции с мессенджерами, пуши, синхронизация оргструктуры + goworker: "cr.yandex/crphnph9c2s776j32sjd/hrbox-goworker:3.21.2" + # Электронная подпись (КЭДО, МЧД) + kedoca: "cr.yandex/crphnph9c2s776j32sjd/hrbox-kedoca:1.0.6" + # Справочник городов, стран и часовых поясов + geonames: "cr.yandex/crphnph9c2s776j32sjd/hrbox-geonames:3.3" + # Конвертация документов и обработка изображений + fileProcessor: "cr.yandex/crphnph9c2s776j32sjd/hrbox-file-processor:1.4.6" + # Планировщик задач по расписанию + conductor: "cr.yandex/crphnph9c2s776j32sjd/hrbox-conductor:2.2.0" + # Плеер учебных материалов (SCORM, cmi5, xAPI, нативные курсы). + # Тег обязателен и не может быть latest: из него выводится версия плеера, + # которую HRBox кладёт в launch-тикет. + nanabushPlayer: "cr.yandex/crphnph9c2s776j32sjd/nanabush-player:1.0.11" + + # Инфраструктура + dragonfly: "cr.yandex/crpnn6fi85p3dauha6nc/dragonflydb:1.15.1" + nats: "nats:2.10" + natsConfigReloader: "natsio/nats-server-config-reloader:0.6.2" + phpFpmExporter: "cr.yandex/crpnn6fi85p3dauha6nc/php-fpm_exporter:latest" + # Используется только init-контейнером джобы миграций (ждёт готовности БД) + postgresClient: "postgres:14-alpine" + # Используется только при app.tracing.enabled + otelCollector: "otel/opentelemetry-collector:0.109.0" + + pullPolicy: IfNotPresent + +# Секреты доступа к приватному Docker-реестру. +# Создаются отдельно, см. README, шаг 1. +imagePullSecrets: + - name: regsecret + +# ============================================================================ +# Приложение +# ============================================================================ app: - # Docker образ приложения HRBox - # Получите доступ к реестру у сотрудников HRBox - image: "cr.yandex/crphnph9c2s776j32sjd/hrbox:latest" - - # Настройки домена - # defaultTenantHostname - основной домен вашей HRBox инсталляции - # Например: hrbox.company.com - # Должен совпадать с ingress.host + # Основной домен инсталляции. Должен совпадать с ingress.host. defaultTenantHostname: "hrbox.example.com" - - # Уникальное имя инстанса для идентификации + + # Имя инстанса — попадает в логи и метрики, помогает различать установки. instanceName: "onprem" - # Включение/отключение frontend2. - # "true" - включено - # "false" или пустое значение - отключено + # Режим одного арендатора. "1" — вся установка обслуживает одну компанию. + singleTenant: "0" + + # Frontend 2: "true" — включён (по умолчанию), "false" — старый интерфейс. front2: "true" - - # Домен для авторизации - # Оставьте пустым + + # Отдельный домен авторизации. Для on-premise почти всегда пусто. authHost: "" - - # Режим отладки - # "0" - production режим (рекомендуется) - # "1" - debug режим (только для разработки) + + # "0" — production, "1" — отладка. В production всегда "0". debug: "0" - - # Количество реплик для каждого сервиса - # Рекомендации по масштабированию: - # - web: 2-10 реплик в зависимости от количества пользователей - # - worker: 2-5 реплик для обработки фоновых задач - # - chatbox: 2-6 реплик для real-time функционала (WebSocket) - # - geonames: 1-2 реплики (сервис не требователен к ресурсам) + + # Принудительный https во всех сгенерированных ссылках. + forceHttps: "true" + + # Количество реплик. + # web 2-10, по числу пользователей + # worker 2-5, фоновые задачи + # chatbox 2-6, real-time + # остальные обычно хватает 1-2 replicas: web: 2 worker: 2 chatbox: 2 geonames: 1 + goworker: 2 + kedoca: 1 + fileProcessor: 1 + fileProcessorVideo: 1 - # Показ собственной политики конфиденциальности и terms of use - # privacy_url и terms_url передаются в приложение только если enabled установлена в true - # если enabled = true, но privacy_url и terms_url закомментированы, - # то будут использоваться ссылки https://hrbox.io/policy и https://hrbox.io/terms - custom_policy: + # Своя политика конфиденциальности и правила использования. + # При enabled: false приложение показывает ссылки HRBox. + customPolicy: enabled: false - # privacy_url: "https://example.com/privacy" - # terms_url: "https://example.com/terms" - - # PostgreSQL база данных - # Требования: PostgreSQL 14+ - # Рекомендуется настроить репликацию для отказоустойчивости + # privacyUrl: "https://example.com/privacy" + # termsUrl: "https://example.com/terms" + + # --- PostgreSQL ----------------------------------------------------------- + # Требуется PostgreSQL 14+. Для отказоустойчивости настройте репликацию. postgres: host: "postgresql" port: "5432" database: "hrbox" user: "hrbox" - # Пароль для подключения к БД + # Пароль попадает в Secret чарта, а не в env-блок Deployment. password: "" - # Использование Citus для шардирования - # "true" - если используете Citus расширение - # "false" - для обычного PostgreSQL + # "true", если база работает на расширении Citus (шардирование). citus: "false" - - # Redis для кеша и сессий - # Потеря данных не критична - хранятся только сессии и кеш - # При перезапуске Redis все пользователи будут разлогинены + + # --- Redis / Dragonfly ---------------------------------------------------- + # Хранит сессии, кеш и очереди плеера курсов. Потеря данных не критична, + # но разлогинивает пользователей и сбрасывает незавершённые события плеера. + # + # Пусто — используется Dragonfly из этого же релиза. Заполняйте, только + # если Redis внешний (тогда обычно dragonfly.enabled: false). redis: - host: "hrbox-dragonfly" + host: "" port: "6379" - - # NATS JetStream - брокер сообщений - # Для отказоустойчивости используйте кластер из 3+ нод - # Формат для кластера: nats://node1:4222,nats://node2:4222,nats://node3:4222 + + # --- NATS JetStream ------------------------------------------------------- + # Пусто — NATS из этого же релиза. + # Внешний кластер: nats://node1:4222,nats://node2:4222,nats://node3:4222 nats: - url: "nats://hrbox-nats:4222" - - # S3-совместимое хранилище для файлов - # Поддерживаются: AWS S3, MinIO, Yandex Object Storage, VK Cloud Storage + url: "" + + # --- S3 ------------------------------------------------------------------- + # Подходят AWS S3, MinIO, Yandex Object Storage, VK Cloud Storage. s3: - # URL endpoint вашего S3 - # Примеры: - # - MinIO: http://minio:9000 - # - Yandex: https://storage.yandexcloud.net - # - VK Cloud: https://hb.vkcloud-storage.ru + # MinIO: http://minio:9000 + # Yandex: https://storage.yandexcloud.net + # VK: https://hb.vkcloud-storage.ru endpoint: "https://storage.example.com" - - # Название bucket для хранения файлов bucket: "hrbox" - - # Access Key для доступа к S3 key: "" - - # Secret Key для доступа к S3 secret: "" - - # Регион S3 (обычно us-east-1 для MinIO) region: "us-east-1" - - # Версия API (оставьте "latest") version: "latest" - - # Использовать path-style URLs - # "true" для MinIO и self-hosted S3 - # "false" для AWS S3 + # "true" для MinIO и self-hosted S3, "false" для AWS S3 pathStyle: "true" - - # Включить multipart загрузку для больших файлов multipart: "true" - - # Префикс для всех объектов (опционально) prefix: "" - - # SMTP настройки для отправки email + + # --- Почта ---------------------------------------------------------------- smtp: - # SMTP сервер host: "smtp.example.com" - - # Порт SMTP - # 25 - без шифрования - # 587 - STARTTLS - # 465 - SSL/TLS + # 25 — без шифрования, 587 — STARTTLS, 465 — SSL/TLS port: "587" - - # Логин для SMTP авторизации user: "noreply@example.com" - - # Пароль для SMTP password: "" - - # Email отправителя from: "noreply@example.com" - - # Домен отправителя для HELO/EHLO fromHost: "example.com" - - # Тип шифрования: "", "tls", "ssl" + # "", "tls" или "ssl" securityType: "tls" - - # Разрешить самоподписанные сертификаты sslAllowSelfSigned: "true" - - # Проверять SSL сертификат сервера sslVerifyPeer: "false" - - # Sentry для мониторинга ошибок + + # --- Мониторинг ошибок ---------------------------------------------------- sentry: - # DSN для отправки ошибок dsn: "" - - # Окружение для группировки ошибок environment: "production" - - # Настройки сервиса геолокации + # Отдельные DSN для подсистем. Пусто — используется общий dsn. + launchpadDsn: "" + conductorDsn: "" + # У этих двух общего fallback нет: пусто означает «не отправлять». + goworkerDsn: "" + chatboxDsn: "" + + # --- Внутренние сервисы --------------------------------------------------- + # Пустой url означает сервис из этого же релиза: адрес собирается из имени, + # которое создаёт чарт. Заполняйте, только если вынесли сервис наружу. geonames: - # URL внутреннего сервиса geonames - # Не меняйте, если используете стандартный деплой - url: "http://hrbox-geonames:8181" + url: "" - # Goworker goworker: - # URL внутреннего сервиса goworker - url: "http://hrbox-goworker:8686" - # Docker образ goworker - image: "cr.yandex/crphnph9c2s776j32sjd/hrbox-goworker:2.7" - # Количество реплик - replicas: 2 - # Минимальный интервал между push + url: "" + # Минимальный интервал между push-уведомлениями одному пользователю pushMinInterval: "5m" - # Sentry DSN для мониторинга ошибок - sentryDsn: "" - # Kedoca - сервис электронной подписи (ГОСТ, OpenSSL) kedoca: - # URL внутреннего сервиса kedoca - url: "http://hrbox-kedoca:8558" - # Docker образ kedoca - image: "cr.yandex/crphnph9c2s776j32sjd/hrbox-kedoca:0.8" - # Количество реплик - replicas: 1 - # Пароль для корневого сертификата + url: "" + # Пароль корневого сертификата, который сервис создаёт при первом запуске rootCaPass: "hrbox" - # Криптографический движок: "gost" или "openssl" + # "gost" (ГОСТ, КриптоПро) или "openssl" engine: "gost" - # Алгоритм: "gost2012_256" для GОСТ или "rsa" для OpenSSL + # "gost2012_256" для ГОСТ, "rsa" для OpenSSL algorithm: "gost2012_256" - # Параметры ключа: "paramset:A" для ГОСТ или "rsa_keygen_bits:2048" для RSA + # "paramset:A" для ГОСТ, "rsa_keygen_bits:2048" для RSA pkeyopt: "paramset:A" - # Имя Kubernetes Secret с сертификатами - # Если указано, будет смонтирован в /ca-cert + # Служба меток времени + tspUrl: "http://qs.cryptopro.ru/tsp/tsp.srf" + # Kubernetes Secret с сертификатами УЦ. Если задан — монтируется в /ca-cert. secretName: "" - # File Processor - обработка файлов (конвертация, ресайз изображений) - file_processor: - # Docker образ file processor - image: "cr.yandex/crphnph9c2s776j32sjd/hrbox-file-processor:1.0" - # Количество реплик - replicas: 1 + fileProcessor: + # Сколько документов конвертировать одновременно в одном поде + docconvMaxParallelTasks: "1" + # Сколько изображений ресайзить одновременно (пул конвертации видео) + imageResizeMaxParallel: "20" - # Chatbox - WebSocket сервер для real-time функционала + # --- Конвертация видео ---------------------------------------------------- + # Отдельный пул подов: конвертация видео требует много CPU и диска и + # выносится на выделенные ноды, чтобы не мешать остальным задачам. + videoConverter: + enabled: false + # Внутренний адрес HRBox, по которому конвертер забирает и отдаёт файлы. + # Пусто — берётся http://<релиз>-web. + internalUrl: "" + # Куда планировать поды конвертера. Пусто — без ограничений. + nodeSelector: {} + # role: converter-dedicated + tolerations: [] + # - key: dedicated + # operator: Equal + # value: video-converter + # effect: NoSchedule + + # --- Чат ------------------------------------------------------------------ chatbox: - # Docker образ chatbox - image: "cr.yandex/crphnph9c2s776j32sjd/hrbox-chatbox:3.0" - # Wrapping key - часть ключа для ассиметричного шифрования чатов - # Сгенерируйте произвольную строку из 32 символов - # openssl rand -hex 16 + # Часть ключа асимметричного шифрования чатов, 32 символа. + # Генерация: openssl rand -hex 16 wrappingKey: "" - # Sentry DSN для мониторинга ошибок - sentryDsn: "" - - # Настройки воркеров Launchpad - # Формат: "название-топика:количество-воркеров" + + # --- Видеозвонки ---------------------------------------------------------- + livekit: + url: "" + apiKey: "" + apiSecret: "" + + # --- Nanabush Player ------------------------------------------------------ + # Плеер учебных материалов: SCORM 1.2/2004, cmi5, xAPI, нативные курсы, + # тесты и эссе. Полное описание контракта — docs/nanabush-player-deployment.md + # в репозитории HRBox. + # + # В облаке плеер живёт на отдельном поддомене. В on-premise выделенного + # поддомена нет, поэтому плеер монтируется на основной домен под путём + # basePath. Практическое следствие: cookie HRBox видны содержимому курсов + # (один origin). Если для сторонних SCORM-пакетов нужна изоляция — заводите + # отдельный поддомен и обращайтесь в HRBox за облачной схемой. + nanabushPlayer: + # Отдавать ли учебные материалы через плеер. false — старый проигрыватель. + enabled: true + + # Путь монтирования на основном домене. Пустой и "/" запрещены: корень + # занят самим HRBox. + basePath: "/nanabush-player" + + # Issuer launch-тикета. Пусто — https:///lms/player. + issuer: "" + + contractVersion: "v0-current" + + # Ключи плеера. Оба обязательны и должны отличаться друг от друга. + # Генерация каждого: openssl rand -base64 48 + keys: + ticketHs256: "" + sessionHs256: "" + # Необязательное кольцо ключей для ротации без простоя: + # "kid1:секрет1,kid2:секрет2", текущий ключ подписи — первым. + # Секрет не должен содержать запятую (используйте base64url). + ticketKeys: "" + + # Доля запусков, уходящих в новый плеер. 100 — все. + rollout: + percent: 100 + # Разрешить откат на старый проигрыватель, если плеер недоступен + legacyFallback: true + allowedContentTypes: + - document + - presentation + - video + - audio + - scorm + - cmi5 + - xapi + - course + - native + - native_quiz + - native_essay + # "*" — все арендаторы, включая будущих + allowedTenants: + - "*" + + service: + # Пусто — <релиз>-nanabush-player + name: "" + port: 8094 + + # Публиковать ли путь плеера на основном Ingress. + # Выключать имеет смысл только при аварийном откате. + ingress: + enabled: true + + deployment: + # Создавать ли Deployment и Service плеера. + enabled: true + replicas: 1 + terminationGracePeriodSeconds: 120 + + # Образ плеера должен уметь работать под путём (а не только в корне + # домена). Если ставите старый образ без этой поддержки — переключите + # в false, и чарт остановит рендер с объяснением вместо неработающего + # плеера в кластере. + binaryCapabilities: + basePath: true + + # Secret с ключами плеера. create: false — Secret создаётся вне чарта + # (например, внешним vault-оператором), чарт только ссылается на него. + existingSecret: + create: true + name: "hrbox-nanabush-player" + keys: + ticketHs256: player-hs256-secret + sessionHs256: player-session-hs256-secret + redisUrl: redis-url + ticketKeys: "" + + ticket: + # Время жизни launch-тикета, секунды + ttlSeconds: 300 + # Время жизни учётных данных обратного канала: сессия плеера + 2 минуты + # на расхождение часов + credentialTtlSeconds: 28920 + maxLifetime: "5m" + sessionTtl: "8h" + # Только на время первой раскатки кольца ключей: до какого момента + # (RFC3339) принимать тикеты без kid. Пусто — не принимать. + keylessAcceptedUntil: "" + + redis: + # false — URL собирается из app.redis. + # true — полный redis:// или rediss:// URL читается из внешнего + # Secret (нужно для ACL, пароля или TLS) и требует + # existingSecret.create: false. + urlFromExistingSecret: false + database: 0 + # Итоговый префикс: hrbox:: — разводит несколько + # релизов HRBox на одном Dragonfly. + keyPrefixSuffix: "nanabush-player" + + backchannel: + # Внутрикластерный адрес HRBox. Пусто — <релиз>-web. + host: "" + schemes: "http" + allowPrivateIps: true + packageFetchMaxBytes: "5242880" + # Транспорт должен вмещать самый большой поддерживаемый медиафайл + packageContentMaxBytes: "2147483648" + scormPackageMaxBytes: "1073741824" + # Пусто — ограничение по числу файлов в SCORM-пакете берётся из + # умолчаний приложения (4000) + scormPackageMaxFiles: "" + packageFetchTimeout: "15s" + # Таймаут покрывает всю передачу, включая отдачу байтов браузеру + # учащегося, поэтому упирается в скорость его канала. Двух минут + # не хватало на крупное видео. + packageStreamTimeout: "20m" + reportTimeout: "30s" + + # Очередь отправки учебных событий в HRBox. + forwarder: + queueSize: "1000" + workers: "4" + maxAttempts: "5" + initialBackoff: "1s" + maxBackoff: "30s" + perAttemptTimeout: "30s" + deadLetterSize: "256" + claimIdle: "5m" + pollInterval: "1s" + rateLimitEventsPerMinute: "600" + rateLimitBatchesPerMinute: "120" + rateLimitPayloadBytesPerMinute: "8388608" + + log: + level: info + format: json + + # Метрики отдаются на отдельном порту, который не публикуется наружу: + # содержимое курсов не должно читать эксплуатационные метрики. + metrics: + enabled: true + port: 9090 + + probes: + readiness: + path: /readyz + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 2 + failureThreshold: 6 + liveness: + path: /healthz + initialDelaySeconds: 20 + periodSeconds: 20 + timeoutSeconds: 2 + failureThreshold: 3 + + # --- Фоновые задачи ------------------------------------------------------- + # Формат: "топик:количество-воркеров" launchpad: topics: "launcher:1,file-processor:1,image-processor:1,send-mail:1,send-im:1,send-bell:1,send-push:1,backup:1,cleaning:1" - - # Интеграция с Yandex (опционально) + + sms: + senderName: "HRBOX" + + # --- Внешние интеграции --------------------------------------------------- + integrations: + telegram: + # Прямой API Telegram; для установок без доступа к нему укажите прокси + apiUrl: "https://api.telegram.org/bot" + google: + enabled: "false" + # Client ID публичный (уходит в браузер), secret — в Secret чарта + clientId: "692956564858-ccapt35khk7tmadl17b3879ht2cropp6.apps.googleusercontent.com" + clientSecret: "" + glagol: + partnerId: "ab230ee1-6946-43b9-8kfa-447beb8348b0" + apiUrl: "https://api.glagol.online/glagol-api" + + # Привязка мобильных приложений к домену: без неё ссылки из писем и пушей + # открываются в браузере вместо приложения. По умолчанию — штатные + # приложения HRBox; меняйте, только если у вас собственная сборка. + mobileApps: + android: + - packageName: "io.hrbox.hrboxapp" + sha256CertFingerprints: + - "DE:75:B2:CA:CB:50:96:02:8D:E1:07:4F:CE:E9:92:1C:4C:ED:87:B0:64:12:1A:69:27:7B:D2:B5:1A:23:31:70" + apple: + appIds: + - "DX67FD7GQJ.io.hrbox.app" + yandex: - # Client ID для Yandex OAuth clientId: "" - # Client Secret для Yandex OAuth clientPassword: "" - - # Интеграция со Slack (опционально) + slack: - # Slack App Client ID clientId: "" - # Slack App Client Secret clientSecret: "" - # Slack Verification Token verificationToken: "" - - # HRBox Hub + + # --- HRBox Hub ------------------------------------------------------------ + # Лицензирование и общие сервисы. Токен выдают сотрудники HRBox. hub: - # URL Hub сервиса url: "https://hub.hrbox.io" - # Токен для авторизации в Hub, получите от сотрудника HrBox token: "" - - # AI интеграции (опционально) + + # --- AI ------------------------------------------------------------------- ai: - # Yandex Cloud AI + docsIndexEnabled: "true" + semanticIndexEnabled: "true" + semanticIndexUseBatch: "true" yandex: - # ID каталога в Yandex Cloud folderId: "" - # API ключ для Yandex Cloud apiKey: "" - - # Настройки безопасности + + # --- Трассировка ---------------------------------------------------------- + # Включает OpenTelemetry в PHP и sidecar-коллектор в поде web. + tracing: + enabled: false + exporterUrl: "http://localhost:4318" + # Куда коллектор пересылает трейсы (Tempo, Jaeger) + tempoUrl: "" + + # --- Безопасность --------------------------------------------------------- security: - # Разрешить полный доступ к iframe - # "true" - разрешает iframe полный доступ к родительскому окну - # "false" - ограничивает доступ iframe (рекомендуется для production) + # "true" разрешает встроенному iframe полный доступ к родительскому окну. + # Для production рекомендуется "false". iframeFullAccess: "false" -# Секретные ключи приложения +# ============================================================================ +# Секреты приложения +# ============================================================================ +# Эти значения вместе с паролями из app.* складываются в один Kubernetes +# Secret, а контейнеры читают его через secretKeyRef. secrets: - # Ключ валидации cookie (32 символа) - # Используется для защиты от XSS атак + # Имя Secret, созданного вне чарта. Если задано — чарт свой Secret не + # создаёт и берёт все ключи из указанного. Список ключей — в README. + existingSecret: "" + + # Защита cookie от подделки, 32 символа. # Генерация: openssl rand -hex 16 cookieValidationKey: "" - # Ключ шифрования данных (64 символа) - # ВАЖНО: Не теряйте этот ключ! Без него невозможно расшифровать данные - # Используется для шифрования паролей интеграций (S3, ADFS и др.) + # Шифрование данных: пароли интеграций (S3, ADFS и другие). + # ВАЖНО: без этого ключа данные не расшифровать. Не теряйте его. # Генерация: openssl rand -base64 48 encryptionKey: "" - # Секрет для внутренней коммуникации микросервисов (64 символа) - # Используется для защиты внутренних API вызовов между сервисами + # Аутентификация вызовов между микросервисами релиза. # Генерация: openssl rand -base64 48 servicesInternalSecret: "" -# Настройки Ingress +# ============================================================================ +# Ingress +# ============================================================================ ingress: - # Включить Ingress enabled: true - - # Класс Ingress контроллера - # Обычно: "nginx" или "traefik" + + # Обычно "nginx" или "traefik" className: "nginx" - - # Хост для доступа к приложению - # Используйте wildcard для мультитенантности: *.hrbox.company.com - # Или конкретный домен: hrbox.company.com + + # Домен инсталляции. Wildcard (*.hrbox.company.com) — для мультиарендности. host: "hrbox.example.com" - - # Настройки TLS/HTTPS + + # Дополнительные аннотации ingress-контроллера + annotations: {} + + # Максимальный размер загружаемого файла на общих путях. + # Загрузка оргструктуры из 1С идёт потоком и имеет собственный лимит. + maxBodySize: "10m" + tls: - # Включить HTTPS - # ВАЖНО: В production всегда используйте HTTPS enabled: true - - # Имя Kubernetes Secret с SSL сертификатом - # Создайте через: kubectl create secret tls hrbox-tls --cert=cert.pem --key=key.pem - # Или используйте cert-manager для автоматического получения сертификатов + # kubectl create secret tls hrbox-tls --cert=cert.pem --key=key.pem + # либо выпуск через cert-manager secretName: "hrbox-tls" +# ============================================================================ +# Кластер +# ============================================================================ cluster: - domain: "cluster.local" # Укажите домен текущего k8s-кластера, если используется нестандартный + # Домен служебных имён Kubernetes. Меняйте, только если он нестандартный. + domain: "cluster.local" -# Настройки ресурсов для контейнеров -# Указаны рекомендуемые значения для production +# ============================================================================ +# Ресурсы +# ============================================================================ +# Значения близки к продовым. Незаданные поля не рендерятся, поэтому +# компоненту можно оставить только requests. resources: - # Web-сервер (PHP-FPM + Nginx) - # Обрабатывает HTTP запросы, API, веб-интерфейс + # PHP-FPM: HTTP-запросы, API, веб-интерфейс app: requests: - cpu: "0.5" # Минимум 0.5 CPU - memory: "512Mi" # Минимум 512MB RAM + cpu: "1" + memory: "2Gi" ephemeralStorage: "64Mi" - limits: - cpu: "2" # Максимум 2 CPU - memory: "2Gi" # Максимум 2GB RAM - ephemeralStorage: "256Mi" - - # Nginx (в составе web pod) - # Проксирует запросы к PHP-FPM - nginx: - requests: - cpu: "0.1" - memory: "64Mi" - ephemeralStorage: "64Mi" - limits: - cpu: "0.5" - memory: "256Mi" - ephemeralStorage: "64Mi" - - # Worker - обработчик фоновых задач - # Выполняет: конвертацию файлов, отправку email, генерацию отчетов - worker: - requests: - cpu: "0.5" - memory: "512Mi" - ephemeralStorage: "1Gi" # Больше места для обработки файлов limits: cpu: "2" memory: "2Gi" ephemeralStorage: "2Gi" - # Geonames - сервис геолокации - # Определяет города, страны, часовые пояса - geonames: + # nginx в поде web: проксирует запросы в PHP-FPM + nginx: requests: cpu: "0.1" - memory: "196Mi" + memory: "256Mi" ephemeralStorage: "64Mi" limits: cpu: "0.5" memory: "256Mi" - ephemeralStorage: "64Mi" - - # Conductor - планировщик задач - # ВАЖНО: Всегда должен быть в единственном экземпляре! - # Запускает задачи по расписанию (cron) - conductor: + ephemeralStorage: "512Mi" + + # Фоновые задачи: конвертация файлов, почта, отчёты + worker: requests: - cpu: "0.1" - memory: "64Mi" - ephemeralStorage: "64Mi" - limits: - cpu: "0.5" - memory: "256Mi" - ephemeralStorage: "64Mi" - - # Jobs - одноразовые задачи (миграции, инициализация) - jobs: - requests: - cpu: "0.5" - memory: "512Mi" - ephemeralStorage: "64Mi" + cpu: "0.6" + memory: "640Mi" + ephemeralStorage: "4Gi" limits: cpu: "2" - memory: "2Gi" - ephemeralStorage: "256Mi" + memory: "4Gi" + ephemeralStorage: "8Gi" - # Goworker - интеграции с мессенджерами - goworker: + # Планировщик задач по расписанию + conductor: requests: - cpu: "0.1" - memory: "196Mi" + cpu: "0.25" + memory: "128Mi" ephemeralStorage: "64Mi" limits: - cpu: "0.3" - memory: "256Mi" + cpu: "0.7" + memory: "384Mi" ephemeralStorage: "64Mi" - # Kedoca - электронная подпись - kedoca: - requests: - cpu: "0.1" - memory: "196Mi" - ephemeralStorage: "64Mi" - limits: - cpu: "0.5" - memory: "256Mi" - ephemeralStorage: "64Mi" - - # Chatbox - WebSocket сервер chatbox: requests: cpu: "0.3" memory: "96Mi" ephemeralStorage: "128Mi" limits: - cpu: "0.3" - memory: "192Mi" + cpu: "1" + memory: "256Mi" ephemeralStorage: "256Mi" -# Настройки NATS JetStream + geonames: + requests: + cpu: "0.1" + memory: "196Mi" + ephemeralStorage: "64Mi" + limits: + cpu: "0.5" + memory: "256Mi" + ephemeralStorage: "64Mi" + + goworker: + requests: + cpu: "0.1" + memory: "196Mi" + ephemeralStorage: "64Mi" + limits: + cpu: "0.5" + memory: "256Mi" + ephemeralStorage: "64Mi" + + kedoca: + requests: + cpu: "0.1" + memory: "196Mi" + ephemeralStorage: "64Mi" + limits: + cpu: "1" + memory: "256Mi" + ephemeralStorage: "64Mi" + + fileProcessor: + requests: + cpu: "0.5" + memory: "512Mi" + ephemeralStorage: "2Gi" + limits: + cpu: "1" + memory: "1Gi" + ephemeralStorage: "2Gi" + + # Конвертация видео: тяжёлый профиль, отдельные ноды + fileProcessorVideo: + requests: + cpu: "4" + memory: "4Gi" + ephemeralStorage: "40Gi" + limits: + cpu: "8" + memory: "8Gi" + ephemeralStorage: "44Gi" + + nanabushPlayer: + requests: + cpu: "0.1" + memory: "192Mi" + limits: + memory: "512Mi" + + dragonfly: + requests: + cpu: "0.1" + memory: "256Mi" + limits: + cpu: "1" + memory: "1Gi" + + nats: + requests: + cpu: "0.1" + memory: "256Mi" + limits: + cpu: "1" + memory: "2Gi" + + # Разовые задачи: миграции, обновление кластера + jobs: + requests: + cpu: "0.5" + memory: "1Gi" + ephemeralStorage: "64Mi" + limits: + cpu: "2" + memory: "4Gi" + ephemeralStorage: "256Mi" + +# ============================================================================ +# NATS JetStream +# ============================================================================ nats: - # Включить NATS (обязательно для работы HRBox) + # Обязателен для работы HRBox. Выключайте только если ставите NATS отдельно + # и указали его адрес в app.nats.url. enabled: true - - # Docker образ NATS - image: "nats:2.10" - - # Количество реплик NATS - # Для отказоустойчивости используйте 3 или 5 + + # Для отказоустойчивости — 3 или 5. Должно совпадать с cluster.replicas. replicas: 3 - - # JetStream - персистентные очереди сообщений + jetstream: enabled: true - - # Хранилище в памяти (для небольших инсталляций) + + # Хранение в памяти — для небольших установок memStorage: enabled: true size: "2Gi" - - # Хранилище на диске (для production) + + # Хранение на диске — для production fileStorage: enabled: false size: "4Gi" storageDirectory: "/data" - # StorageClass для PVC - # Используйте SSD для лучшей производительности + # Для JetStream нужен быстрый диск storageClassName: "fast-ssd" - - # Настройки кластера NATS + cluster: - # Включить кластеризацию для отказоустойчивости enabled: true - # Должно совпадать с replicas + # Должно совпадать с nats.replicas replicas: 3 - - # Prometheus exporter (для мониторинга) - exporter: - enabled: false - - # Config reloader - автоматическая перезагрузка конфигурации + + # Автоматическая перезагрузка конфигурации без рестарта сервера reloader: enabled: true - - # Время ожидания graceful shutdown в секундах + terminationGracePeriodSeconds: 120 +# ============================================================================ +# Dragonfly (Redis-совместимый кеш) +# ============================================================================ dragonfly: - # Включает или отключает развёртывание Dragonfly (Redis). - # Установите в true, чтобы активировать, или в false, чтобы отключить. + # false — если Redis разворачивается отдельно; тогда укажите его в app.redis. enabled: true - # По умолчанию параметр не используется. По необходимости, раскомментируйте и установите требуемое значение maxmemory - # maxmemory: "256M" \ No newline at end of file + # Ограничение памяти. Раскомментируйте при необходимости. + # maxmemory: "256M" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a44f44..7eed1b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,268 @@ All notable changes to the HRBox Helm chart will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.0.0] - 2026-09-02 + +Синхронизация с продовым окружением HRBox и приведение чарта в порядок: +добавлен плеер учебных материалов, секреты перестали лежать открытым текстом +в спеках подов, вместо девяти одинаковых блоков ресурсов — один хелпер, +а неполная конфигурация теперь останавливает установку с внятным текстом. + +### Added + +#### Nanabush Player + +Проигрыватель учебных материалов: SCORM 1.2 и 2004, cmi5, xAPI, нативные +курсы, тесты и эссе. + +- `09-nanabush-player.yaml` — Deployment и Service, образ `nanabush-player:1.0.11` +- `00-nanabush-player-secret.yaml` — ключи launch-тикета и сессии +- маршрут `/nanabush-player` на основном Ingress +- настройки: `app.nanabushPlayer` в values.yaml, ресурсы — `resources.nanabushPlayer` +- переменные `NANABUSH_PLAYER_*` в HRBox и `PLAYER_*` в самом плеере + +On-premise-топология отличается от облачной: выделенного поддомена нет, +плеер монтируется на основной домен под путём. Отсюда два ограничения, +описанные в README: нет изоляции cookie между HRBox и содержимым курсов, +и нужны синхронизированные часы (тикет живёт 5 минут). + +Три независимых переключателя (`enabled`, `ingress.enabled`, +`deployment.enabled`) сохранены для аварийного отката; чарт не даёт оставить +маршрут без пода или раскатку без маршрута. + +#### Проверка конфигурации + +- `00-validate.yaml` — объектов не создаёт, но останавливает рендер, если + не заполнены обязательные значения, использованы ключи из версии 2.x или + нарушены инварианты плеера. Сообщения на русском, с указанием, что править. + +#### Секрет приложения + +- `00-app-secret.yaml` — единый Secret с паролями и ключами. Контейнеры + читают его через `secretKeyRef`, поэтому `kubectl get deploy -o yaml` + больше не показывает пароль базы и ключ шифрования. +- `secrets.existingSecret` — подстановка Secret, созданного вне чарта. + +#### Новые компоненты и возможности + +- `09-file-processor-video.yaml` — отдельный пул конвертации видео с + собственным профилем ресурсов, `nodeSelector` и `tolerations` + (`app.videoConverter.enabled`, по умолчанию выключен) +- `02-web-pdb.yaml` — PodDisruptionBudget для `web`, создаётся начиная с + двух реплик +- `01-otel-configmap.yaml` и sidecar-коллектор в поде `web` + (`app.tracing.enabled`) +- `01-nginx-assetlinks.yaml` — привязка мобильных приложений к домену + (`app.mobileApps`) +- `templates/NOTES.txt` — что делать после установки, с готовыми командами + и предупреждениями о небезопасных настройках + +#### Новые переменные окружения + +- `NATS_STREAM_REPLICAS` — число реплик JetStream-стримов по размеру кластера +- `TELEGRAM_API_URL`, `AUTH_GOOGLE_ENABLED` +- `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET` — видеозвонки +- `APP_INTERNAL_URL` — внутренний адрес HRBox для конвертера +- `VIDEO_CONVERTER_ENABLED` — отдельно для каждого пула +- `SENDER_ENV`, `IMAGE_RESIZE_MAX_PARALLEL`, `OTEL_*` + +### Changed + +#### Структура values.yaml + +- все образы собраны в блок `image` — обновление версии сервиса стало + правкой одной строки +- `imagePullSecrets` вынесен в настройки, `regsecret` больше не захардкожен +- ключи приведены к camelCase: `file_processor` → `fileProcessor`, + `custom_policy` → `customPolicy` +- `resources` дополнен компонентами `fileProcessor`, `fileProcessorVideo`, + `nanabushPlayer`, `dragonfly`, `nats` +- адреса внутренних сервисов (`app.geonames.url`, `app.goworker.url`, + `app.kedoca.url`, `app.redis.host`, `app.nats.url`) по умолчанию пустые и + выводятся из имён, которые создаёт сам чарт. Раньше это были захардкоженные + строки `hrbox-*`, которые молча ломались при `nameOverride`. Явно заданное + значение по-прежнему используется как есть, поэтому старые values работают + без правок. Выключение `dragonfly` или `nats` без указания внешнего адреса + теперь останавливает установку + +#### Шаблоны + +- `_resources.tpl`: девять почти одинаковых define заменены одним + `hrbox.resources`; незаданные поля не рендерятся +- `_helpers.tpl`: убрано около сотни строк мёртвого кода (хелперы `nats.*`, + которыми ничего не пользовалось и которые ссылались на несуществующий + `nats.k8sClusterDomain`); добавлены `hrbox.component`, `hrbox.labels`, + `hrbox.selectorLabels`, `hrbox.secretEnv` и хелперы плеера +- у всех объектов появились стандартные лейблы `app.kubernetes.io/*`; + лейбл `app` сохранён, привычные `kubectl get pods -l app=worker` работают +- селекторы Deployment не менялись — обновление 2.x → 3.0 не пересоздаёт + объекты +- аннотации `checksum/*` перезапускают поды при изменении конфигурации nginx + или секрета приложения +- образы, порты и таймауты вынесены из шаблонов в values.yaml + +#### nginx + +Набор `location` синхронизирован с продовым: + +- `/healthz` и `/health-check` — под них настроены пробы пода `web` + (раньше проверялся только TCP-порт) +- `/api/v1/integration/commit` — потоковая загрузка оргструктуры из 1С без + буферизации тела, лимит 500 МБ +- `/ai/` — увеличенные таймауты +- `/.well-known/assetlinks.json` и `/.well-known/apple-app-site-association` +- `/frontend/sw.js` — корректные заголовки service worker +- кеширование шрифтов +- убран TLSv1 и TLSv1.1 + +#### Версии образов + +| Компонент | Было | Стало | +|---|---|---| +| chatbox | 3.0 | 5.18 | +| goworker | 2.7 | 3.21.2 | +| kedoca | 0.8 | 1.0.6 | +| geonames | 3.1 | 3.3 | +| file-processor | 1.0 | 1.4.6 | +| nanabush-player | — | 1.0.11 | + +#### Джобы + +- имя содержит номер ревизии релиза: в `kubectl get jobs` видно, какой выкат + запускал миграции, а повторный `upgrade` не спотыкается об уже существующий + объект +- `ttlSecondsAfterFinished` — час у обеих джоб (было 100 секунд у миграций и + ничего у обновления кластера): логи упавшей джобы успевают дожить до того, + как их посмотрят + +#### Переименования файлов + +- `03-websocket.yaml` → `03-chatbox.yaml` (содержимое было chatbox с версии 2.0) +- `09-geonames.yml` → `09-geonames.yaml` +- `99-update-cluser-job.yaml` → `99-update-cluster-job.yaml` + +### Fixed + +- **Общий секрет в коде чарта.** `SERVICES_INTERNAL_SECRET` имел захардкоженное + значение по умолчанию, то есть все установки, где его не заполнили, работали + на одном и том же секрете межсервисной аутентификации. Теперь значение + обязательно. +- **Домен кластера игнорировался.** Маршруты кластера NATS были захардкожены + на `cluster.local`, хотя `cluster.domain` присутствовал в values и + использовался в соседней переменной. На кластере с нестандартным доменом + ноды не находили друг друга. +- **`CLUSTER_ADVERTISE` без сегмента `svc`.** Имя не резолвилось, и ноды NATS + объявляли соседям недостижимый адрес. +- **Рассинхрон реплик NATS.** `nats.replicas` и `nats.cluster.replicas` + задавались независимо; при расхождении часть нод не входила в кластер. + Теперь чарт требует их совпадения. +- **Пробы `web` не проверяли приложение.** Проверялся TCP-порт 9000, поэтому + под с неработающим PHP считался готовым. Теперь readiness ходит в + `/health-check` через FPM. +- Пароли и ключи больше не видны в спеках Deployment. +- `imagePullPolicy` у части сервисов был `Always` на закреплённых тегах — + лишние обращения к реестру при каждом рестарте пода. + +### Removed + +- `secrets` больше не имеет значений по умолчанию — пустые ключи + останавливают установку вместо тихого запуска на предсказуемых секретах +- убраны неиспользуемые хелперы `nats.name`, `nats.fullname`, `nats.chart`, + `nats.labels`, `nats.selectorLabels`, `nats.clusterAdvertise`, + `nats.clusterRoutes`, `nats.extraRoutes`, `nats.tlsConfig`, `tplvalues.render` +- `SENDGRID_API_KEY` — не используется в on-premise + +### Migration Guide + +#### Переход с 2.x на 3.0 + +Все переехавшие ключи чарт распознаёт и сообщает, куда именно они переехали, +поэтому надёжнее всего запустить `helm template` и править по сообщениям. + +**Шаг 1. Перенесите образы в блок `image`** + +```yaml +# было +app: + image: "cr.yandex/.../hrbox:latest" + chatbox: + image: "cr.yandex/.../hrbox-chatbox:3.0" + goworker: + image: "cr.yandex/.../hrbox-goworker:2.7" + kedoca: + image: "cr.yandex/.../hrbox-kedoca:0.8" + file_processor: + image: "cr.yandex/.../hrbox-file-processor:1.0" + replicas: 1 + +# стало +image: + hrbox: "cr.yandex/.../hrbox:latest" + chatbox: "cr.yandex/.../hrbox-chatbox:5.18" + goworker: "cr.yandex/.../hrbox-goworker:3.21.2" + kedoca: "cr.yandex/.../hrbox-kedoca:1.0.6" + fileProcessor: "cr.yandex/.../hrbox-file-processor:1.4.6" +app: + replicas: + fileProcessor: 1 +``` + +**Шаг 2. Переименуйте ключи** + +| Было | Стало | +|---|---| +| `app.file_processor` | `app.fileProcessor` | +| `app.custom_policy` | `app.customPolicy` | +| `app.custom_policy.privacy_url` | `app.customPolicy.privacyUrl` | +| `app.custom_policy.terms_url` | `app.customPolicy.termsUrl` | +| `app.goworker.replicas` | `app.replicas.goworker` | +| `app.kedoca.replicas` | `app.replicas.kedoca` | +| `app.goworker.sentryDsn` | `app.sentry.goworkerDsn` | +| `app.chatbox.sentryDsn` | `app.sentry.chatboxDsn` | +| `nats.image` | `image.nats` | + +**Шаг 3. Заполните ключи плеера** + +```bash +openssl rand -base64 48 # app.nanabushPlayer.keys.ticketHs256 +openssl rand -base64 48 # app.nanabushPlayer.keys.sessionHs256 +``` + +Ключи должны отличаться друг от друга. + +**Шаг 4. Проверьте `secrets.servicesInternalSecret`** + +Если поле было пустым, установка работала на общем значении по умолчанию. +Сгенерируйте своё: `openssl rand -base64 48`. После обновления перезапустятся +все сервисы, обменивающиеся внутренними вызовами, — это ожидаемо. + +**Шаг 5. Проверьте рендер и обновитесь** + +```bash +helm template hrbox .helm -f values.yaml > /dev/null +helm upgrade hrbox .helm -n hrbox -f values.yaml +``` + +**Шаг 6. Включите плеер** + +Порядок первого включения и проверки — в README, раздел «Nanabush Player». + +#### Breaking Changes + +1. **Ключи `values.yaml` переехали.** Установка останавливается с подсказкой, + молча ничего не игнорируется. +2. **`secrets.*` стали обязательными.** Установка без них не пройдёт. +3. **Появился Nanabush Player.** Это дополнительный под, Service, Secret и + маршрут; учтите ресурсы кластера. Плеер требует синхронизированного времени + на нодах. +4. **Пробы `web` стали строже.** Под, где PHP-FPM не отвечает по HTTP, теперь + не проходит readiness. Если приложение отдаёт `/health-check` иначе, + проверьте это до обновления. +5. **`app.replicas` расширился** полями `goworker`, `kedoca`, `fileProcessor`, + `fileProcessorVideo`; соответствующие `replicas` внутри сервисов удалены. + +--- + ## [2.0.0] - 2026-01-30 Масштабная актуализация чарта на основе продового окружения hrbox. Синхронизированы версии компонентов, добавлены новые микросервисы, обновлены переменные окружения. @@ -227,5 +489,6 @@ kubectl get pods -n hrbox -l app=websocket # Должно быть пусто --- +[3.0.0]: #300---2026-09-02 [2.0.0]: #200---2026-01-30 [1.2.0]: #120---previous-version diff --git a/README.md b/README.md index fdaf1f0..8de1465 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,48 @@ # HRBox Helm Chart -Helm-чарт для развертывания приложения HRBox в Kubernetes. +Helm-чарт для развёртывания HRBox в собственном Kubernetes-кластере. + +Чарт лежит в каталоге [`.helm`](.helm), настройки — в [`.helm/values.yaml`](.helm/values.yaml). +Каждый параметр там прокомментирован; ниже — только то, что нужно сделать руками. --- -## Быстрый старт +## Что разворачивается -### 1. Создайте секрет для доступа к Docker-реестру +| Компонент | Что делает | Порт | +|---|---|---| +| `web` | PHP-FPM + nginx: веб-интерфейс и API | 80 | +| `worker` | фоновые задачи: файлы, почта, отчёты | — | +| `conductor` | планировщик задач по расписанию (строго 1 реплика) | 8080 | +| `chatbox` | вебсокеты чата и real-time уведомлений | 80 → 9797 | +| `nanabush-player` | проигрыватель курсов: SCORM, cmi5, xAPI, нативные материалы | 8094 | +| `goworker` | мессенджеры, push, синхронизация оргструктуры с 1С | 8686 | +| `kedoca` | электронная подпись: КЭДО, МЧД | 8558 | +| `geonames` | справочник городов, стран, часовых поясов | 8181 | +| `file-processor` | конвертация документов и изображений | — | +| `file-processor-video` | конвертация видео, выключена по умолчанию | — | +| `dragonfly` | Redis-совместимый кеш и сессии | 6379 | +| `nats` | брокер сообщений JetStream | 4222 | -Для скачивания приватного Docker-образа создайте Kubernetes Secret с помощью предоставленного JSON-ключа: +Все три публичных маршрута живут на одном домене: `/` — приложение, +`/chatbox/` — вебсокеты, `/nanabush-player` — плеер курсов. + +## Что нужно от вас + +- Kubernetes 1.21+ и ingress-контроллер (nginx или traefik); +- PostgreSQL 14+ (с расширением Citus, если планируется шардирование); +- S3-совместимое хранилище: AWS S3, MinIO, Yandex Object Storage, VK Cloud; +- SMTP-сервер; +- доступ к Docker-реестру HRBox и токен HRBox Hub — выдают сотрудники HRBox. + +PostgreSQL и S3 чарт не разворачивает: это состояние, которое переживает +установку и требует собственного резервного копирования. + +--- + +## Установка + +### 1. Доступ к реестру образов ```bash kubectl create secret docker-registry regsecret \ @@ -18,57 +52,289 @@ kubectl create secret docker-registry regsecret \ --docker-email=not@used.com ``` -### 2. Создайте TLS-секрет с сертификатом для домена +### 2. TLS-сертификат домена ```bash kubectl create secret tls hrbox-tls --cert=cert.pem --key=key.pem ``` -### 3. Сгенерируйте секретные ключи приложения +Или выпустите сертификат через cert-manager и укажите его Secret +в `ingress.tls.secretName`. -Для обеспечения безопасности необходимо сгенерировать и указать в `values.yaml` следующие ключи: - -- **cookieValidationKey** — 32 байта в hex (например: `openssl rand -hex 16`) -- **encryptionKey** — 64 байта в base64 (например: `openssl rand -base64 48`) -- **servicesInternalSecret** — 64 байта в base64 (например: `openssl rand -base64 48`) -- **chatbox.wrappingKey** — 32 символа в hex (например: `openssl rand -hex 16`) - -Эти ключи используются для защиты cookie, шифрования данных, межсервисной коммуникации и шифрования чатов. - -### 4. Заполните обязательные параметры в `values.yaml` - -Обязательно укажите в конфигурации: - -- Параметры подключения к базе данных PostgreSQL (`app.postgres.host`, `port`, `database`, `user`, `password`). -- Настройки S3-совместимого хранилища (`app.s3.endpoint`, `bucket`, `key`, `secret`). -- SMTP-настройки для отправки почты (`app.smtp.host`, `port`, `user`, `password`, `from`, `fromHost`). -- Домен для приложения (`app.defaultTenantHostname`) и Ingress (`ingress.host`). -- Имя TLS-секрета для HTTPS (`ingress.tls.secretName`), созданного на шаге 2. -- Ключ для hrbox hub (предоставляется сотрудниками hrbox) - -Прочие настройки и пример `values.yml` с подробным описанием ключей вы можете найти [тут](.helm/values.yaml) - -### 5. Установите или обновите Helm-релиз +### 3. Ключи ```bash -helm upgrade --install hrbox . -f values.yaml +# secrets.cookieValidationKey и app.chatbox.wrappingKey +openssl rand -hex 16 + +# secrets.encryptionKey, secrets.servicesInternalSecret, +# app.nanabushPlayer.keys.ticketHs256, app.nanabushPlayer.keys.sessionHs256 +openssl rand -base64 48 ``` -### 6. Дождитесь завершения миграций +`secrets.encryptionKey` — единственный ключ, потеря которого необратима: +им зашифрованы пароли интеграций в базе. Сохраните его отдельно от кластера. -Job `hrbox-migrate-xxxxx` должна завершиться +Ключи плеера `ticketHs256` и `sessionHs256` должны отличаться друг от друга — +это ключи разных контуров, и чарт откажется ставить одинаковые. -### 7. При первом развертывании - создайте суперпользователя +### 4. Заполните `values.yaml` + +Обязательный минимум: + +```yaml +ingress: + host: "hrbox.company.com" + tls: + secretName: "hrbox-tls" + +app: + defaultTenantHostname: "hrbox.company.com" + postgres: + host: "postgresql" + database: "hrbox" + user: "hrbox" + password: "..." + s3: + endpoint: "https://storage.company.com" + bucket: "hrbox" + key: "..." + secret: "..." + smtp: + host: "smtp.company.com" + user: "noreply@company.com" + password: "..." + from: "noreply@company.com" + fromHost: "company.com" + hub: + token: "..." + chatbox: + wrappingKey: "..." + nanabushPlayer: + keys: + ticketHs256: "..." + sessionHs256: "..." + +secrets: + cookieValidationKey: "..." + encryptionKey: "..." + servicesInternalSecret: "..." +``` + +Всё остальное имеет рабочие значения по умолчанию. + +Проверить конфигурацию, ничего не устанавливая: ```bash -kubectl get pods - -NAME READY STATUS RESTARTS AGE -... -hrbox-worker-7898b77b85-5ppwm 1/1 Running 0 53s -hrbox-worker-7898b77b85-9fnkc 1/1 Running 0 53s - -kubectl exec -it hrbox-worker-7898b77b85-5ppwm bash - -php yii user/create-admin youremail@hrbox.io --tenant-id=1 +helm template hrbox .helm -f values.yaml > /dev/null ``` + +Если чего-то не хватает, чарт скажет об этом текстом, а не оставит +неработающие поды в кластере. + +### 5. Установка + +```bash +helm upgrade --install hrbox .helm -n hrbox --create-namespace -f values.yaml +``` + +### 6. Дождитесь миграций + +```bash +kubectl -n hrbox wait --for=condition=complete --timeout=30m job -l app=jobs +``` + +### 7. Первый администратор + +```bash +kubectl -n hrbox exec -it deploy/hrbox-worker -- \ + php yii user/create-admin admin@company.com --tenant-id=1 +``` + +--- + +## Секреты + +Чувствительные значения из `values.yaml` чарт складывает в один Secret +`hrbox-secrets` и монтирует контейнерам через `secretKeyRef` — в спеке +Deployment паролей нет. + +Если секретами управляет внешний инструмент, создайте Secret сами и укажите +его имя в `secrets.existingSecret`. Ключи должны называться так: + +| Ключ | Откуда берётся при `existingSecret: ""` | +|---|---| +| `cookie-validation-key` | `secrets.cookieValidationKey` | +| `encryption-key` | `secrets.encryptionKey` | +| `services-internal-secret` | `secrets.servicesInternalSecret` | +| `postgres-password` | `app.postgres.password` | +| `s3-key` | `app.s3.key` | +| `s3-secret` | `app.s3.secret` | +| `smtp-password` | `app.smtp.password` | +| `hub-token` | `app.hub.token` | +| `chatbox-wrapping-key` | `app.chatbox.wrappingKey` | +| `livekit-api-secret` | `app.livekit.apiSecret` | +| `yandex-client-password` | `app.yandex.clientPassword` | +| `yandex-ai-api-key` | `app.ai.yandex.apiKey` | +| `google-client-secret` | `app.integrations.google.clientSecret` | +| `slack-client-secret` | `app.slack.clientSecret` | +| `slack-verification-token` | `app.slack.verificationToken` | + +Ключи плеера лежат в отдельном Secret `hrbox-nanabush-player` — им управляет +`app.nanabushPlayer.deployment.existingSecret`. + +--- + +## Nanabush Player + +Проигрыватель учебных материалов: SCORM 1.2 и 2004, cmi5, xAPI, нативные +курсы, тесты и эссе. + +В облаке HRBox плеер живёт на отдельном поддомене. В on-premise выделенного +поддомена нет, поэтому плеер монтируется на основной домен под путём +`/nanabush-player`. Что из этого следует: + +- **Изоляции cookie нет.** Плеер и HRBox делят один origin, поэтому cookie + сессии HRBox отправляются браузером и на запросы к содержимому курсов, + включая сторонние SCORM-пакеты. Если нужна настоящая изоляция — + запрашивайте у HRBox схему с отдельным поддоменом. +- **Часы должны быть синхронизированы.** Launch-тикет живёт 5 минут, сессия + проверяется с допуском 2 минуты. Настройте NTP на нодах и сервере БД; + если это невозможно, выключите плеер (`app.nanabushPlayer.enabled: false`). + +Порядок первого включения: + +1. Убедитесь, что миграции применились: плееру нужны таблицы проекций. +2. Дождитесь, пока **все** поды `web` перейдут на новую ревизию. Смешанный + выкат при `rollout.percent > 0` может создать вторую активную попытку по + одному назначению. +3. Проверьте, что плеер отвечает сам, а не HRBox через общий маршрут: + + ```bash + kubectl -n hrbox exec deploy/hrbox-worker -- \ + curl -s http://hrbox-nanabush-player:8094/readyz + ``` + + Настоящий плеер вернёт JSON. HTML означает, что запрос ушёл в HRBox. + +4. Проверьте публичный путь: `https://<домен>/nanabush-player/start`, + статику и API плеера под этим путём. + +Служебные пути (`/healthz`, `/readyz`, метрики) живут в корне и не зависят +от `basePath`. Порт метрик наружу не публикуется намеренно. + +Аварийное отключение — три независимых переключателя: + +```yaml +app: + nanabushPlayer: + enabled: false # пользователи возвращаются на старый проигрыватель + ingress: + enabled: false # маршрут снимается + deployment: + enabled: false # под останавливается +``` + +Выключать нужно сверху вниз: чарт не даст оставить включённым маршрут +без пода или раскатку без маршрута. + +Подробности контракта — `docs/nanabush-player-deployment.md` в репозитории HRBox. + +--- + +## Эксплуатация + +### Обновление + +```bash +helm upgrade hrbox .helm -n hrbox -f values.yaml +``` + +Миграции и обновление кластера запускаются автоматически как хуки +`post-upgrade` и выполняются строго по порядку. Имя джобы содержит номер +ревизии релиза, поэтому в `kubectl get jobs` видно, какой выкат её запускал. + +### Масштабирование + +```yaml +app: + replicas: + web: 4 # по числу одновременных пользователей + worker: 3 # по объёму фоновых задач + chatbox: 4 # по числу открытых вкладок +``` + +`conductor` всегда в одном экземпляре: второй планировщик запустил бы +каждую задачу дважды. + +Для `web` при двух и более репликах создаётся PodDisruptionBudget +(`minAvailable: 75%`), чтобы drain ноды не уронил приложение целиком. + +### Конвертация видео + +Выключена по умолчанию: профиль ресурсов у неё на порядок тяжелее обычной +обработки файлов. Включение: + +```yaml +app: + videoConverter: + enabled: true + nodeSelector: + role: converter-dedicated + tolerations: + - key: dedicated + operator: Equal + value: video-converter + effect: NoSchedule +``` + +### Трассировка + +```yaml +app: + tracing: + enabled: true + tempoUrl: "http://tempo.monitoring:4317" +``` + +Рядом с `web` поднимется коллектор OpenTelemetry, PHP начнёт слать трейсы. + +### Внешние Redis и NATS + +```yaml +dragonfly: + enabled: false +nats: + enabled: false +app: + redis: + host: "redis.company.local" + nats: + url: "nats://n1:4222,nats://n2:4222,nats://n3:4222" +``` + +### Диагностика + +```bash +kubectl -n hrbox get pods +kubectl -n hrbox logs -l app=web -c fpm --tail=200 +kubectl -n hrbox logs -l app=worker --tail=200 +kubectl -n hrbox logs -l app=nanabush-player --tail=200 +kubectl -n hrbox get jobs +``` + +| Симптом | Куда смотреть | +|---|---| +| Поды `web` не готовы | пробы `/healthz` и `/health-check`, логи контейнера `fpm` | +| Джоба миграций висит | init-контейнер `wait-postgres`: доступность и права в БД | +| Курс не открывается | логи `nanabush-player`, совпадение домена в `ingress.host` и `app.defaultTenantHostname` | +| Не приходят уведомления | логи `goworker`, очереди NATS, `app.launchpad.topics` | +| Пользователей разлогинивает | перезапуски `dragonfly`: сессии живут в памяти | + +--- + +## Миграция с версии 2.x + +Часть ключей `values.yaml` переехала. Чарт проверяет старые имена и +останавливает установку с подсказкой, поэтому незамеченным ничего не +останется. Полный список изменений и порядок перехода — в [CHANGELOG.md](CHANGELOG.md). From 67786a01605f0c461274293ea10e560437f25c00 Mon Sep 17 00:00:00 2001 From: Dmitrii Sashchenko Date: Sat, 5 Sep 2026 19:07:31 +0600 Subject: [PATCH 2/3] chart 3.0 --- .helm/Chart.yaml | 4 - .helm/templates/00-app-secret.yaml | 12 - .../templates/00-nanabush-player-secret.yaml | 16 - .helm/templates/00-validate.yaml | 19 - .helm/templates/01-nats-configmap.yaml | 9 - .helm/templates/01-nginx-assetlinks.yaml | 8 - .helm/templates/01-nginx-configmap.yaml | 14 - .helm/templates/01-otel-configmap.yaml | 4 - .helm/templates/01-wait-configmap.yaml | 4 - .helm/templates/02-dragonfly.yaml | 7 - .helm/templates/02-nats-box.yaml | 12 - .helm/templates/02-web-pdb.yaml | 6 - .helm/templates/02-web.yaml | 12 - .helm/templates/03-chatbox.yaml | 4 - .helm/templates/04-worker.yaml | 8 - .helm/templates/05-conductor.yaml | 6 - .helm/templates/09-file-processor-video.yaml | 9 - .helm/templates/09-file-processor.yaml | 6 - .helm/templates/09-geonames.yaml | 3 - .helm/templates/09-goworker.yaml | 7 - .helm/templates/09-kedoca.yaml | 8 - .helm/templates/09-nanabush-player.yaml | 29 -- .helm/templates/10-app-ingress.yaml | 18 - .helm/templates/98-migrate-job.yaml | 9 - .helm/templates/99-update-cluster-job.yaml | 6 - .helm/templates/_envs.tpl | 63 +-- .helm/templates/_helpers.tpl | 101 ----- .helm/templates/_resources.tpl | 13 - .helm/values.yaml | 369 ++++++++---------- CHANGELOG.md | 183 +++++---- README.md | 152 ++++---- 31 files changed, 348 insertions(+), 773 deletions(-) diff --git a/.helm/Chart.yaml b/.helm/Chart.yaml index f0d591d..c76d31c 100644 --- a/.helm/Chart.yaml +++ b/.helm/Chart.yaml @@ -3,10 +3,7 @@ name: hrbox description: HRBox — HR-платформа для on-premise установки type: application -# Версия чарта. Увеличивается при каждом изменении чарта. version: 3.0.0 - -# Версия приложения. Совпадает с линейкой образов HRBox. appVersion: "3.1.0" keywords: @@ -19,7 +16,6 @@ keywords: home: https://hrbox.io annotations: - # Компоненты, которые разворачивает чарт hrbox.io/components: >- web, worker, conductor, chatbox, goworker, kedoca, geonames, file-processor, nanabush-player, dragonfly, nats diff --git a/.helm/templates/00-app-secret.yaml b/.helm/templates/00-app-secret.yaml index a67dce4..db1865a 100644 --- a/.helm/templates/00-app-secret.yaml +++ b/.helm/templates/00-app-secret.yaml @@ -1,16 +1,4 @@ {{- if not .Values.secrets.existingSecret }} -{{/* -Единый Secret с чувствительными настройками HRBox. - -В чарте 2.x пароли и ключи подставлялись прямо в env-блоки Deployment, -поэтому любой, кто мог сделать `kubectl get deploy -o yaml`, видел пароль -базы и ключ шифрования. Здесь значения лежат в одном Secret, а контейнеры -читают их через secretKeyRef (см. hrbox.secretEnv в _helpers.tpl). - -Если секретами управляет внешний инструмент, задайте secrets.existingSecret — -тогда чарт этот объект не создаёт и только ссылается на указанный. Набор -ключей должен совпадать с перечисленным ниже. -*/}} --- apiVersion: v1 kind: Secret diff --git a/.helm/templates/00-nanabush-player-secret.yaml b/.helm/templates/00-nanabush-player-secret.yaml index 95ca43f..8ac26d1 100644 --- a/.helm/templates/00-nanabush-player-secret.yaml +++ b/.helm/templates/00-nanabush-player-secret.yaml @@ -1,21 +1,5 @@ {{- $cfg := .Values.app.nanabushPlayer.deployment }} {{- if and $cfg.enabled $cfg.existingSecret.create }} -{{/* -Ключи Nanabush Player. - -ticketHs256 читают обе стороны: HRBox подписывает им launch-тикет -(NANABUSH_PLAYER_HS256_SECRET в поде web), плеер этим же ключом тикет -проверяет (PLAYER_HS256_SECRET). Поэтому ротация — одно значение на обе -стороны, а не два независимых. - -sessionHs256 принадлежит только плееру: им подписывается сессия учащегося -внутри плеера, HRBox её не читает. - -При existingSecret.create: false этот объект не создаётся — оба потребителя -всё так же ссылаются на existingSecret.name, но Secret должен быть заведён -вне чарта. При смене владельца уже существующего Secret берите новое имя, -чтобы Helm не удалил внешний объект во время перехода. -*/}} --- apiVersion: v1 kind: Secret diff --git a/.helm/templates/00-validate.yaml b/.helm/templates/00-validate.yaml index f96904b..de11381 100644 --- a/.helm/templates/00-validate.yaml +++ b/.helm/templates/00-validate.yaml @@ -1,19 +1,3 @@ -{{/* -============================================================================ - Проверка values.yaml -============================================================================ - -Этот файл не создаёт ни одного объекта. Он выполняется первым и падает с -понятным сообщением, если конфигурация неполная или противоречивая, — до -того, как в кластере появятся поды, которые всё равно не заработают. - -Проверяется три вещи: - 1) ключи, переехавшие в версии 3.0 (подсказка вместо тихого игнора); - 2) обязательные значения, без которых приложение не поднимется; - 3) инварианты Nanabush Player. -*/}} - -{{/* --- 1. Ключи из чарта 2.x ---------------------------------------------- */}} {{- if .Values.app.image }} {{- fail "app.image переехал в image.hrbox (см. CHANGELOG, миграция 2.x -> 3.0)" }} {{- end }} @@ -42,7 +26,6 @@ {{- fail "nats.image переехал в image.nats" }} {{- end }} -{{/* --- 2. Обязательные значения ------------------------------------------- */}} {{- if not .Values.ingress.host }} {{- fail "Заполните ingress.host — домен, по которому открывается HRBox" }} {{- end }} @@ -81,7 +64,6 @@ {{- end }} {{- end }} -{{/* --- 3. Внешние зависимости ---------------------------------------------- */}} {{- if and (not .Values.dragonfly.enabled) (not .Values.app.redis.host) }} {{- fail "dragonfly.enabled: false требует app.redis.host — иначе приложение обратится к сервису, которого нет в релизе" }} {{- end }} @@ -95,7 +77,6 @@ {{- end }} {{- end }} -{{/* --- 4. Nanabush Player -------------------------------------------------- */}} {{- $player := .Values.app.nanabushPlayer }} {{- if and $player.enabled (not $player.deployment.enabled) }} {{- fail "app.nanabushPlayer.enabled требует deployment.enabled: HRBox нельзя переключить на плеер, которого нет в кластере" }} diff --git a/.helm/templates/01-nats-configmap.yaml b/.helm/templates/01-nats-configmap.yaml index 0714b2b..98841d4 100644 --- a/.helm/templates/01-nats-configmap.yaml +++ b/.helm/templates/01-nats-configmap.yaml @@ -1,12 +1,4 @@ {{- if .Values.nats.enabled }} -{{/* -Конфигурация NATS JetStream. - -Маршруты кластера строятся из служебных DNS-имён StatefulSet, поэтому домен -кластера берётся из cluster.domain — в чарте 2.x он был захардкожен здесь и -одновременно настраивался в values, из-за чего нестандартный домен ломал -сборку кластера. -*/}} {{- $name := include "hrbox.component" (dict "ctx" . "name" "nats") -}} --- apiVersion: v1 @@ -17,7 +9,6 @@ metadata: {{- include "hrbox.labels" (dict "ctx" . "name" "nats") | nindent 4 }} data: nats.conf: | - # PID-файл, общий с перезагрузчиком конфигурации. pid_file: "/var/run/nats/nats.pid" ############### diff --git a/.helm/templates/01-nginx-assetlinks.yaml b/.helm/templates/01-nginx-assetlinks.yaml index efb30fd..901bec7 100644 --- a/.helm/templates/01-nginx-assetlinks.yaml +++ b/.helm/templates/01-nginx-assetlinks.yaml @@ -1,11 +1,3 @@ -{{/* -Привязка мобильных приложений к домену инсталляции. - -Android читает /.well-known/assetlinks.json, iOS — /.well-known/apple-app-site-association. -Без них ссылки из писем и уведомлений открываются в браузере, а не в приложении. -По умолчанию здесь идентификаторы штатных приложений HRBox; если у вас -собственная сборка, переопределите app.mobileApps в values.yaml. -*/}} --- apiVersion: v1 kind: ConfigMap diff --git a/.helm/templates/01-nginx-configmap.yaml b/.helm/templates/01-nginx-configmap.yaml index 8e183e9..e69e5c1 100644 --- a/.helm/templates/01-nginx-configmap.yaml +++ b/.helm/templates/01-nginx-configmap.yaml @@ -1,9 +1,3 @@ -{{/* -Конфигурация nginx в поде web. Отличия от чарта 2.x — набор location, -перенесённый из продового окружения: health-check для проб, потоковая -загрузка оргструктуры из 1С, длинные таймауты для AI-запросов, отдача -assetlinks мобильных приложений и корректные заголовки для service worker. -*/}} --- apiVersion: v1 kind: ConfigMap @@ -103,13 +97,11 @@ data: deny all; } - # Liveness: отвечает сам nginx, не дожидаясь PHP-FPM. location = /healthz { access_log off; return 200 "ok\n"; } - # Readiness: короткая проверка, что PHP-FPM отвечает. location = /health-check { fastcgi_pass 127.0.0.1:9000; fastcgi_connect_timeout 1s; @@ -121,9 +113,6 @@ data: fastcgi_param PATH_INFO /health-check; } - # Загрузка оргструктуры из 1С. PHP читает тело запроса потоком и - # проксирует в GoWorker без полной буферизации, поэтому nginx - # тоже не должен буферизовать тело. location = /api/v1/integration/commit { client_max_body_size 500m; fastcgi_request_buffering off; @@ -138,7 +127,6 @@ data: fastcgi_connect_timeout 60s; } - # Привязка мобильных приложений к домену location /.well-known/assetlinks.json { default_type "application/json"; alias /var/www/assetlinks/assetlinks.json; @@ -157,7 +145,6 @@ data: access_log off; } - # AI-запросы отвечают дольше обычных, им нужен свой таймаут. location ^~ /ai/ { add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; @@ -184,7 +171,6 @@ data: add_header X-Content-Type-Options "nosniff"; add_header Permissions-Policy "browsing-topics=()" always; - # Всё, что не является файлом, уходит в index.php try_files $uri $uri/ /index.php?$args; location ~* \.(js|css|png|jpg|gif|ico)$ { diff --git a/.helm/templates/01-otel-configmap.yaml b/.helm/templates/01-otel-configmap.yaml index 675e725..7fbd607 100644 --- a/.helm/templates/01-otel-configmap.yaml +++ b/.helm/templates/01-otel-configmap.yaml @@ -1,8 +1,4 @@ {{- if .Values.app.tracing.enabled }} -{{/* -Конфигурация sidecar-коллектора OpenTelemetry в поде web. -Создаётся только при app.tracing.enabled. -*/}} --- apiVersion: v1 kind: ConfigMap diff --git a/.helm/templates/01-wait-configmap.yaml b/.helm/templates/01-wait-configmap.yaml index e04d13d..d1eff32 100644 --- a/.helm/templates/01-wait-configmap.yaml +++ b/.helm/templates/01-wait-configmap.yaml @@ -1,7 +1,3 @@ -{{/* -Скрипт init-контейнера джобы миграций: ждёт, пока PostgreSQL начнёт принимать -подключения. Без этого миграции падают, если база стартует медленнее подов. -*/}} --- apiVersion: v1 kind: ConfigMap diff --git a/.helm/templates/02-dragonfly.yaml b/.helm/templates/02-dragonfly.yaml index 930055b..cb2b2d9 100644 --- a/.helm/templates/02-dragonfly.yaml +++ b/.helm/templates/02-dragonfly.yaml @@ -1,12 +1,5 @@ {{- if .Values.dragonfly.enabled }} {{- $name := include "hrbox.component" (dict "ctx" . "name" "dragonfly") -}} -{{/* -Redis-совместимый кеш: сессии, кеш приложения и очереди Nanabush Player. - -Одна реплика и стратегия Recreate — намеренно: данные хранятся в памяти, -второй экземпляр не даёт ни отказоустойчивости, ни консистентности. -Селектор оставлен как в чарте 2.x. -*/}} --- apiVersion: apps/v1 kind: Deployment diff --git a/.helm/templates/02-nats-box.yaml b/.helm/templates/02-nats-box.yaml index cea0903..a610d96 100644 --- a/.helm/templates/02-nats-box.yaml +++ b/.helm/templates/02-nats-box.yaml @@ -1,9 +1,5 @@ {{- if .Values.nats.enabled }} {{- $name := include "hrbox.component" (dict "ctx" . "name" "nats") -}} -{{/* -Брокер сообщений. Селектор StatefulSet намеренно оставлен таким же, как в -чарте 2.x: он неизменяем, и его правка потребовала бы пересоздания объекта. -*/}} --- apiVersion: apps/v1 kind: StatefulSet @@ -30,8 +26,6 @@ spec: name: {{ printf "%s-config" $name }} - name: pid emptyDir: {} - # Общее пространство процессов нужно перезагрузчику конфигурации: - # он посылает сигнал процессу nats-server из соседнего контейнера. shareProcessNamespace: true terminationGracePeriodSeconds: {{ .Values.nats.terminationGracePeriodSeconds }} {{- include "hrbox.imagePullSecrets" . | nindent 6 }} @@ -63,9 +57,6 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - # Адрес, который нода объявляет остальным участникам кластера. - # Сегмент svc обязателен: без него имя не резолвится и ноды - # не находят друг друга. - name: CLUSTER_ADVERTISE value: $(POD_NAME).{{ $name }}.$(POD_NAMESPACE).svc.{{ .Values.cluster.domain }} {{- include "hrbox.resources" (dict "ctx" . "component" "nats") | indent 10 }} @@ -91,8 +82,6 @@ spec: initialDelaySeconds: 10 timeoutSeconds: 5 lifecycle: - # Перевод ноды в lame duck: она перестаёт принимать новые - # подключения и даёт клиентам время переподключиться к соседям. preStop: exec: command: @@ -140,7 +129,6 @@ metadata: labels: {{- include "hrbox.labels" (dict "ctx" . "name" "nats") | nindent 4 }} spec: - # Headless: клиентам нужны адреса конкретных нод, а не общий VIP. clusterIP: None selector: app.kubernetes.io/name: nats diff --git a/.helm/templates/02-web-pdb.yaml b/.helm/templates/02-web-pdb.yaml index 9653c3c..2849908 100644 --- a/.helm/templates/02-web-pdb.yaml +++ b/.helm/templates/02-web-pdb.yaml @@ -1,10 +1,4 @@ {{- $replicas := .Values.app.replicas.web | int }} -{{/* -Бюджет прерываний для web. - -Имеет смысл только начиная с двух реплик: при одной minAvailable округляется -до 1 и намертво блокирует drain ноды, ничего при этом не защищая. -*/}} {{- if gt $replicas 1 }} --- apiVersion: policy/v1 diff --git a/.helm/templates/02-web.yaml b/.helm/templates/02-web.yaml index f1d97e7..7b6c0de 100644 --- a/.helm/templates/02-web.yaml +++ b/.helm/templates/02-web.yaml @@ -1,12 +1,4 @@ {{- $name := include "hrbox.component" (dict "ctx" . "name" "web") -}} -{{/* -Веб-под: PHP-FPM, nginx перед ним и экспортёр метрик FPM. -При app.tracing.enabled рядом поднимается коллектор OpenTelemetry. - -Аннотации checksum перезапускают поды, когда меняется конфигурация nginx или -секрет приложения: без них правка ConfigMap остаётся незамеченной до -следующего рестарта. -*/}} --- apiVersion: apps/v1 kind: Deployment @@ -56,8 +48,6 @@ spec: {{- end }} {{- include "hrbox.resources" (dict "ctx" . "component" "app") | indent 10 }} lifecycle: - # Пауза даёт ingress-контроллеру убрать под из балансировки, - # прежде чем FPM начнёт корректное завершение. preStop: exec: command: @@ -92,8 +82,6 @@ spec: - sh - '-c' - sleep 15 && /usr/sbin/nginx -s quit - # /healthz отвечает сам nginx, /health-check проверяет, - # что за ним жив PHP-FPM. livenessProbe: httpGet: path: /healthz diff --git a/.helm/templates/03-chatbox.yaml b/.helm/templates/03-chatbox.yaml index 7ba3768..fdfc41c 100644 --- a/.helm/templates/03-chatbox.yaml +++ b/.helm/templates/03-chatbox.yaml @@ -1,8 +1,4 @@ {{- $name := include "hrbox.component" (dict "ctx" . "name" "chatbox") -}} -{{/* -Chatbox — вебсокет-сервер чатов и real-time уведомлений. -До версии 2.0 назывался websocket; имена объектов не менялись с тех пор. -*/}} --- apiVersion: apps/v1 kind: Deployment diff --git a/.helm/templates/04-worker.yaml b/.helm/templates/04-worker.yaml index a3f28fb..8901c90 100644 --- a/.helm/templates/04-worker.yaml +++ b/.helm/templates/04-worker.yaml @@ -1,12 +1,4 @@ {{- $name := include "hrbox.component" (dict "ctx" . "name" "worker") -}} -{{/* -Обработчик фоновых задач: конвертация файлов, рассылка почты и уведомлений, -генерация отчётов, обслуживание Nanabush Player. - -Стратегия Recreate: воркеры разбирают задачи из общей очереди, и пересечение -старых и новых подов во время выката привело бы к двойной обработке. -Длинный terminationGracePeriodSeconds даёт закончить уже взятую задачу. -*/}} --- apiVersion: apps/v1 kind: Deployment diff --git a/.helm/templates/05-conductor.yaml b/.helm/templates/05-conductor.yaml index d49cb99..bf93a28 100644 --- a/.helm/templates/05-conductor.yaml +++ b/.helm/templates/05-conductor.yaml @@ -1,10 +1,4 @@ {{- $name := include "hrbox.component" (dict "ctx" . "name" "conductor") -}} -{{/* -Планировщик задач по расписанию. - -Строго одна реплика: два планировщика запустили бы каждую задачу дважды. -По той же причине стратегия Recreate, а не RollingUpdate. -*/}} --- apiVersion: apps/v1 kind: Deployment diff --git a/.helm/templates/09-file-processor-video.yaml b/.helm/templates/09-file-processor-video.yaml index 3288704..7b0b7bb 100644 --- a/.helm/templates/09-file-processor-video.yaml +++ b/.helm/templates/09-file-processor-video.yaml @@ -1,14 +1,5 @@ {{- if .Values.app.videoConverter.enabled }} {{- $name := include "hrbox.component" (dict "ctx" . "name" "file-processor-video") -}} -{{/* -Конвертация видео. - -Отдельный деплоймент, потому что профиль ресурсов у него другой на порядок: -несколько ядер, гигабайты памяти и десятки гигабайт временного диска на под. -Смешивать это с обычной обработкой файлов нельзя — либо ресурсы простаивают, -либо конвертация видео вытесняет всё остальное. Через nodeSelector и -tolerations пул выносится на выделенные ноды. -*/}} --- apiVersion: apps/v1 kind: Deployment diff --git a/.helm/templates/09-file-processor.yaml b/.helm/templates/09-file-processor.yaml index 478a6ec..772a273 100644 --- a/.helm/templates/09-file-processor.yaml +++ b/.helm/templates/09-file-processor.yaml @@ -1,10 +1,4 @@ {{- $name := include "hrbox.component" (dict "ctx" . "name" "file-processor") -}} -{{/* -Конвертация документов и обработка изображений. - -Конвертация видео здесь всегда выключена: она требует принципиально другого -профиля ресурсов и живёт в отдельном деплойменте 09-file-processor-video. -*/}} --- apiVersion: apps/v1 kind: Deployment diff --git a/.helm/templates/09-geonames.yaml b/.helm/templates/09-geonames.yaml index 3341e7b..306d6a1 100644 --- a/.helm/templates/09-geonames.yaml +++ b/.helm/templates/09-geonames.yaml @@ -1,7 +1,4 @@ {{- $name := include "hrbox.component" (dict "ctx" . "name" "geonames") -}} -{{/* -Справочник городов, стран и часовых поясов. -*/}} --- apiVersion: apps/v1 kind: Deployment diff --git a/.helm/templates/09-goworker.yaml b/.helm/templates/09-goworker.yaml index e94c424..8186f4a 100644 --- a/.helm/templates/09-goworker.yaml +++ b/.helm/templates/09-goworker.yaml @@ -1,11 +1,4 @@ {{- $name := include "hrbox.component" (dict "ctx" . "name" "goworker") -}} -{{/* -Интеграции с мессенджерами, push-уведомления, синхронизация оргструктуры -с 1С и восстановление проекций Nanabush Player. - -Метрики Prometheus отдаются на том же порту 8686 по пути /metrics, поэтому -аннотации сбора висят на Service, а не на поде. -*/}} --- apiVersion: apps/v1 kind: Deployment diff --git a/.helm/templates/09-kedoca.yaml b/.helm/templates/09-kedoca.yaml index b1c609e..46ea5c9 100644 --- a/.helm/templates/09-kedoca.yaml +++ b/.helm/templates/09-kedoca.yaml @@ -1,12 +1,4 @@ {{- $name := include "hrbox.component" (dict "ctx" . "name" "kedoca") -}} -{{/* -Сервис электронной подписи: КЭДО, МЧД, метки времени. -Умеет ГОСТ (через КриптоПро) и обычный OpenSSL — выбор в app.kedoca.engine. - -app.kedoca.secretName подключает Secret с сертификатами удостоверяющего -центра. Без него сервис создаёт собственный корневой сертификат при первом -запуске — это годится для теста, но не для юридически значимого документооборота. -*/}} --- apiVersion: apps/v1 kind: Deployment diff --git a/.helm/templates/09-nanabush-player.yaml b/.helm/templates/09-nanabush-player.yaml index fd7bedd..6de1890 100644 --- a/.helm/templates/09-nanabush-player.yaml +++ b/.helm/templates/09-nanabush-player.yaml @@ -2,22 +2,6 @@ {{- if $cfg.enabled }} {{- $name := include "nanabushPlayer.serviceName" . -}} {{- $secretName := include "nanabushPlayer.secretName" . -}} -{{/* -Nanabush Player — проигрыватель учебных материалов: SCORM 1.2 и 2004, cmi5, -xAPI, нативные курсы, тесты и эссе. - -Как это работает: - * HRBox выпускает launch-тикет, подписанный общим ключом ticketHs256, - и отправляет браузер на https://<домен>{{ include "nanabushPlayer.basePath" . }}/start; - * плеер проверяет подпись, audience и issuer, заводит свою сессию; - * за пакетом курса и для отправки отчётов плеер ходит обратно в HRBox по - внутрикластерному адресу — только он и разрешён в allowlist; - * учебные события копятся в Redis Streams и досылаются форвардером, - поэтому короткая недоступность HRBox не теряет прогресс. - -Ограничения on-premise описаны в values.yaml рядом с app.nanabushPlayer. -Полный контракт — docs/nanabush-player-deployment.md в репозитории HRBox. -*/}} --- apiVersion: apps/v1 kind: Deployment @@ -49,9 +33,6 @@ spec: prometheus.io/path: "/metrics" {{- end }} spec: - # Плееру не нужен доступ к API Kubernetes, а исполняет он содержимое, - # загруженное пользователями, — поэтому токен не монтируется, процесс - # не root, корневая файловая система только на чтение. automountServiceAccountToken: false terminationGracePeriodSeconds: {{ $cfg.terminationGracePeriodSeconds }} {{- include "hrbox.imagePullSecrets" . | nindent 6 }} @@ -89,8 +70,6 @@ spec: {{- else }} value: "off" {{- end }} - # Путь монтирования не входит в audience: audience — это origin - # браузера, иначе сломались бы проверки на стороне клиента. - name: PLAYER_BASE_PATH value: {{ include "nanabushPlayer.basePath" . | quote }} - name: PLAYER_AUDIENCE @@ -103,16 +82,12 @@ spec: name: {{ $secretName | quote }} key: {{ required "Нужно имя ключа ticketHs256 в секрете Nanabush Player" $cfg.existingSecret.keys.ticketHs256 | quote }} {{- with $cfg.existingSecret.keys.ticketKeys }} - # Кольцо ключей: подпись проверяется ключом, выбранным по kid из - # тикета, поэтому во время ротации работают обе версии HRBox. - name: PLAYER_TICKET_KEYS valueFrom: secretKeyRef: name: {{ $secretName | quote }} key: {{ . | quote }} {{- with $cfg.ticket.keylessAcceptedUntil }} - # Только на первую раскатку: принимать тикеты без kid от версии - # HRBox, которая ещё не знает про кольцо ключей. - name: PLAYER_TICKET_KEYLESS_UNTIL value: {{ . | quote }} {{- end }} @@ -139,8 +114,6 @@ spec: {{- end }} - name: PLAYER_REDIS_KEY_PREFIX value: {{ include "nanabushPlayer.redisKeyPrefix" . | quote }} - # Обратный канал: единственный разрешённый адрес — внутрикластерный - # HRBox. Так пакет курса не сможет заставить плеер сходить наружу. - name: PLAYER_PACKAGE_FETCH_SCHEMES value: {{ $cfg.backchannel.schemes | quote }} - name: PLAYER_PACKAGE_FETCH_ALLOWLIST @@ -222,8 +195,6 @@ spec: type: ClusterIP selector: service: {{ $name }} - # Порт метрик наружу намеренно не публикуется: содержимое курсов приходит - # с того же origin и не должно читать эксплуатационные метрики. ports: - name: http port: {{ .Values.app.nanabushPlayer.service.port }} diff --git a/.helm/templates/10-app-ingress.yaml b/.helm/templates/10-app-ingress.yaml index 9f359a0..7252fa2 100644 --- a/.helm/templates/10-app-ingress.yaml +++ b/.helm/templates/10-app-ingress.yaml @@ -1,18 +1,5 @@ {{- if .Values.ingress.enabled }} {{- $player := .Values.app.nanabushPlayer -}} -{{/* -Единая точка входа. - -Все три маршрута живут на одном домене, потому что в on-premise выделенных -поддоменов под сервисы нет: - - {{ include "nanabushPlayer.basePath" . }}/ -> плеер учебных материалов - /chatbox/ -> вебсокеты чата - / -> само приложение - -Длинные таймауты нужны вебсокетам и плееру: и то и другое держит соединение -открытым, пока пользователь работает. -*/}} --- apiVersion: networking.k8s.io/v1 kind: Ingress @@ -21,12 +8,7 @@ metadata: labels: {{- include "hrbox.labels" (dict "ctx" . "name" "web") | nindent 4 }} annotations: - # Аннотация класса устарела в пользу spec.ingressClassName ниже, но - # оставлена: контроллеры старше ingress-nginx 1.0 понимают только её. - # Когда заданы обе, современный контроллер использует ingressClassName. kubernetes.io/ingress.class: {{ .Values.ingress.className | quote }} - # Аннотация NGINX Inc.; ingress-nginx её игнорирует, вебсокеты у него - # работают за счёт proxy-read-timeout выше. nginx.org/websocket-services: {{ include "hrbox.component" (dict "ctx" . "name" "chatbox") | quote }} nginx.ingress.kubernetes.io/proxy-body-size: {{ .Values.ingress.maxBodySize | quote }} nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" diff --git a/.helm/templates/98-migrate-job.yaml b/.helm/templates/98-migrate-job.yaml index c822357..9d3eb6f 100644 --- a/.helm/templates/98-migrate-job.yaml +++ b/.helm/templates/98-migrate-job.yaml @@ -1,12 +1,3 @@ -{{/* -Миграции базы данных. - -Хук post-install/post-upgrade с весом 10: выполняется после того, как Helm -создал ConfigMap и Secret, но до джобы обновления кластера (вес 20). -Имя джобы содержит номер ревизии релиза, поэтому в истории видно, какой -выкат её запускал, а повторный upgrade не спотыкается об уже существующий -объект. -*/}} --- apiVersion: batch/v1 kind: Job diff --git a/.helm/templates/99-update-cluster-job.yaml b/.helm/templates/99-update-cluster-job.yaml index 9aaac8f..360d09d 100644 --- a/.helm/templates/99-update-cluster-job.yaml +++ b/.helm/templates/99-update-cluster-job.yaml @@ -1,9 +1,3 @@ -{{/* -Обновление данных кластера после миграций: пересчёт справочников, -перерегистрация фоновых задач, обновление настроек арендаторов. - -Вес 20 — строго после джобы миграций (вес 10). -*/}} --- apiVersion: batch/v1 kind: Job diff --git a/.helm/templates/_envs.tpl b/.helm/templates/_envs.tpl index f28f4d6..e694f65 100644 --- a/.helm/templates/_envs.tpl +++ b/.helm/templates/_envs.tpl @@ -1,18 +1,5 @@ -{{/* -============================================================================ - Переменные окружения HRBox -============================================================================ - -envs_all включается во все контейнеры, которым нужен конфиг приложения: -web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames -и джобы миграций. Один общий блок проще расходящихся наборов, и именно так -устроено продовое окружение. - -Чувствительные значения приходят через secretKeyRef из секрета приложения -(00-app-secret.yaml), а не открытым текстом в спеке пода. -*/}} {{- define "envs_all" }} -# --- Приложение ----------------------------------------------------------- +# Приложение - name: INSTANCE_NAME value: {{ .Values.app.instanceName | quote }} - name: DEFAULT_TENANT_HOSTNAME @@ -45,7 +32,7 @@ web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames {{- include "hrbox.secretEnv" (dict "ctx" . "env" "APP_ENCRYPTION_KEY" "key" "encryption-key") | nindent 0 }} {{- include "hrbox.secretEnv" (dict "ctx" . "env" "SERVICES_INTERNAL_SECRET" "key" "services-internal-secret") | nindent 0 }} -# --- Своя политика конфиденциальности ------------------------------------- +# Своя политика конфиденциальности - name: ALLOW_CUSTOM_POLICY_URLS value: {{ .Values.app.customPolicy.enabled | quote }} {{- if .Values.app.customPolicy.enabled }} @@ -60,7 +47,7 @@ web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames value: "" {{- end }} -# --- PostgreSQL ----------------------------------------------------------- +# PostgreSQL - name: POSTGRES_VHOST value: {{ .Values.app.postgres.host | quote }} - name: POSTGRES_PORT @@ -73,7 +60,7 @@ web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames value: {{ .Values.app.postgres.citus | quote }} {{- include "hrbox.secretEnv" (dict "ctx" . "env" "POSTGRES_PASSWORD" "key" "postgres-password") | nindent 0 }} -# --- Redis / Dragonfly ---------------------------------------------------- +# Redis / Dragonfly - name: REDIS_HOST value: {{ include "hrbox.redisHost" . | quote }} - name: REDIS_PORT @@ -81,13 +68,11 @@ web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames - name: REDIS_CONNECTION value: {{ printf "tcp://%s:%s" (include "hrbox.redisHost" .) (.Values.app.redis.port | toString) | quote }} -# --- NATS ----------------------------------------------------------------- +# NATS - name: BROKER value: "nats" - name: NATS_URL value: {{ include "hrbox.natsUrl" . | quote }} -# Число реплик JetStream-стримов должно совпадать с размером кластера NATS, -# иначе стрим создаётся в одном экземпляре и теряется вместе со своей нодой. - name: NATS_STREAM_REPLICAS {{- if and .Values.nats.enabled .Values.nats.cluster.enabled }} value: {{ .Values.nats.cluster.replicas | quote }} @@ -95,7 +80,7 @@ web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames value: "1" {{- end }} -# --- S3 ------------------------------------------------------------------- +# S3 - name: S3_ENDPOINT value: {{ .Values.app.s3.endpoint | quote }} - name: S3_BUCKET @@ -113,7 +98,7 @@ web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames {{- include "hrbox.secretEnv" (dict "ctx" . "env" "S3_KEY" "key" "s3-key") | nindent 0 }} {{- include "hrbox.secretEnv" (dict "ctx" . "env" "S3_SECRET" "key" "s3-secret") | nindent 0 }} -# --- Почта ---------------------------------------------------------------- +# Почта - name: SENDER_ENV value: "PROD" - name: SENDER_DEV_EMAIL @@ -136,7 +121,7 @@ web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames value: {{ .Values.app.smtp.sslVerifyPeer | quote }} {{- include "hrbox.secretEnv" (dict "ctx" . "env" "SMTP_PASSWORD" "key" "smtp-password") | nindent 0 }} -# --- Внутренние сервисы --------------------------------------------------- +# Внутренние сервисы - name: GEONAMES_URL value: {{ include "hrbox.geonamesUrl" . | quote }} - name: GOWORKER_URL @@ -160,27 +145,27 @@ web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames - name: DOCCONV_MAX_PARALLEL_TASKS value: {{ .Values.app.fileProcessor.docconvMaxParallelTasks | quote }} -# --- HRBox Hub ------------------------------------------------------------ +# HRBox Hub - name: HUB_URL value: {{ .Values.app.hub.url | quote }} {{- include "hrbox.secretEnv" (dict "ctx" . "env" "HUB_TOKEN" "key" "hub-token") | nindent 0 }} -# --- Nanabush Player ------------------------------------------------------ +# Nanabush Player {{- include "envs_nanabush_player" . }} -# --- Чат ------------------------------------------------------------------ +# Чат - name: CHATBOX_SENTRY_DSN value: {{ .Values.app.sentry.chatboxDsn | quote }} {{- include "hrbox.secretEnv" (dict "ctx" . "env" "CHATBOX_WRAPPING_KEY" "key" "chatbox-wrapping-key") | nindent 0 }} -# --- Видеозвонки (LiveKit) ------------------------------------------------ +# Видеозвонки (LiveKit) - name: LIVEKIT_URL value: {{ .Values.app.livekit.url | quote }} - name: LIVEKIT_API_KEY value: {{ .Values.app.livekit.apiKey | quote }} {{- include "hrbox.secretEnv" (dict "ctx" . "env" "LIVEKIT_API_SECRET" "key" "livekit-api-secret") | nindent 0 }} -# --- AI ------------------------------------------------------------------- +# AI - name: AI_DOCS_INDEX_ENABLED value: {{ .Values.app.ai.docsIndexEnabled | quote }} - name: AI_SEMANTIC_INDEX_ENABLED @@ -191,7 +176,7 @@ web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames value: {{ .Values.app.ai.yandex.folderId | quote }} {{- include "hrbox.secretEnv" (dict "ctx" . "env" "YANDEX_CLOUD_AI_API_KEY" "key" "yandex-ai-api-key") | nindent 0 }} -# --- Внешние интеграции --------------------------------------------------- +# Внешние интеграции - name: TELEGRAM_API_URL value: {{ .Values.app.integrations.telegram.apiUrl | quote }} - name: AUTH_GOOGLE_ENABLED @@ -211,7 +196,7 @@ web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames - name: GLAGOL_API_URL value: {{ .Values.app.integrations.glagol.apiUrl | quote }} -# --- Sentry --------------------------------------------------------------- +# Sentry - name: SENTRY_DSN value: {{ .Values.app.sentry.dsn | quote }} - name: SENTRY_DSN_LAUNCHPAD @@ -223,7 +208,7 @@ web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames - name: SENTRY_RELEASE value: {{ .Values.image.hrbox | quote }} -# --- Метаданные пода ------------------------------------------------------ +# Метаданные пода - name: POD_NAME valueFrom: fieldRef: @@ -242,13 +227,6 @@ web, worker, conductor, chatbox, goworker, kedoca, file-processor, geonames fieldPath: spec.nodeName {{- end }} -{{/* -Настройки Nanabush Player на стороне HRBox. - -NANABUSH_PLAYER_AUDIENCE и NANABUSH_PLAYER_PUBLIC_ORIGIN всегда равны: -audience тикета — это origin браузера без пути, а путь монтирования едет -отдельной переменной NANABUSH_PLAYER_BASE_PATH. -*/}} {{- define "envs_nanabush_player" -}} {{- $player := .Values.app.nanabushPlayer }} - name: NANABUSH_PLAYER_ENABLED @@ -291,10 +269,6 @@ audience тикета — это origin браузера без пути, а п {{- end }} {{- end -}} -{{/* -Ключ подписи launch-тикета. Живёт только в PHP-контейнерах, которые выпускают -тикеты: nginx и вспомогательным контейнерам он не нужен. -*/}} {{- define "envs_nanabush_player_signing" -}} {{- $cfg := .Values.app.nanabushPlayer.deployment }} {{- if $cfg.enabled }} @@ -304,9 +278,6 @@ audience тикета — это origin браузера без пути, а п name: {{ include "nanabushPlayer.secretName" . | quote }} key: {{ required "Нужно имя ключа ticketHs256 в секрете Nanabush Player" $cfg.existingSecret.keys.ticketHs256 | quote }} {{- with $cfg.existingSecret.keys.ticketKeys }} -# Кольцо ключей launch-тикета: HRBox подписывает первым ключом, Player -# принимает все. Тот же ключ секрета читает и сам Player, поэтому ротация — -# это одно значение на обе стороны. - name: NANABUSH_PLAYER_TICKET_KEYS valueFrom: secretKeyRef: @@ -316,7 +287,6 @@ audience тикета — это origin браузера без пути, а п {{- end }} {{- end -}} -{{/* Криптографические настройки сервиса электронной подписи. */}} {{- define "envs_kedoca" }} - name: ROOT_CA_PASS value: {{ .Values.app.kedoca.rootCaPass | quote }} @@ -328,7 +298,6 @@ audience тикета — это origin браузера без пути, а п value: {{ .Values.app.kedoca.pkeyopt | quote }} {{- end }} -{{/* OpenTelemetry — только когда app.tracing.enabled. */}} {{- define "envs_tracing" }} - name: OTEL_PHP_AUTOLOAD_ENABLED value: "true" diff --git a/.helm/templates/_helpers.tpl b/.helm/templates/_helpers.tpl index 642a440..2d1d224 100644 --- a/.helm/templates/_helpers.tpl +++ b/.helm/templates/_helpers.tpl @@ -1,49 +1,19 @@ -{{/* -============================================================================ - Общие хелперы чарта HRBox -============================================================================ - -Правило именования: все объекты называются "<имя чарта>-<компонент>" -(hrbox-web, hrbox-worker, ...). Имя чарта можно переопределить через -nameOverride, но по умолчанию оно равно "hrbox" — так же, как в чарте 2.x, -поэтому обновление 2.x -> 3.x не пересоздаёт объекты. -*/}} - -{{/* Базовое имя релиза: "hrbox" или nameOverride. */}} {{- define "hrbox.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} {{- end -}} -{{/* Имя чарта с версией — для лейбла helm.sh/chart. */}} {{- define "hrbox.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} {{- end -}} -{{/* -Полное имя объекта компонента. - -Usage: {{ include "hrbox.component" (dict "ctx" . "name" "web") }} -> hrbox-web -*/}} {{- define "hrbox.component" -}} {{- printf "%s-%s" (include "hrbox.name" .ctx) .name | trunc 63 | trimSuffix "-" -}} {{- end -}} -{{/* -Селектор пода. ВАЖНО: spec.selector в Deployment неизменяем, поэтому набор -лейблов здесь совпадает с чартом 2.x (один лейбл "service") и меняться не должен. - -Usage: {{- include "hrbox.selectorLabels" (dict "ctx" . "name" "web") | nindent 6 }} -*/}} {{- define "hrbox.selectorLabels" -}} service: {{ include "hrbox.component" (dict "ctx" .ctx "name" .name) }} {{- end -}} -{{/* -Лейблы объекта. Лейбл "app" сохранён из чарта 2.x — на него опираются -привычные команды вида "kubectl get pods -l app=worker". - -Usage: {{- include "hrbox.labels" (dict "ctx" . "name" "web") | nindent 4 }} -*/}} {{- define "hrbox.labels" -}} app: {{ .name }} app.kubernetes.io/name: {{ include "hrbox.name" .ctx }} @@ -54,11 +24,6 @@ app.kubernetes.io/version: {{ .ctx.Chart.AppVersion | quote }} helm.sh/chart: {{ include "hrbox.chart" .ctx }} {{- end -}} -{{/* -Секреты для доступа к приватному Docker-реестру. - -Usage: {{- include "hrbox.imagePullSecrets" . | nindent 6 }} -*/}} {{- define "hrbox.imagePullSecrets" -}} {{- if .Values.imagePullSecrets }} imagePullSecrets: @@ -68,11 +33,6 @@ imagePullSecrets: {{- end }} {{- end -}} -{{/* -Тег из ссылки на образ: "registry/repo:1.2.3" -> "1.2.3". -Двоеточие ищется только в последнем сегменте пути, чтобы не спутать тег -с портом реестра ("registry:5000/repo"). -*/}} {{- define "hrbox.imageTag" -}} {{- $lastSegment := splitList "/" . | last -}} {{- if contains ":" $lastSegment -}} @@ -80,27 +40,6 @@ imagePullSecrets: {{- end -}} {{- end -}} -{{/* -============================================================================ - Секрет приложения -============================================================================ - -Чувствительные значения (пароли БД и SMTP, ключи шифрования, токены) не -попадают в env-блоки Deployment открытым текстом: чарт складывает их в один -Secret, а контейнеры читают его через secretKeyRef. Полный список ключей — -в README, раздел "Секреты". - -secrets.existingSecret позволяет подставить Secret, созданный вне чарта -(например, из внешнего vault-оператора) — тогда чарт свой Secret не создаёт. -*/}} -{{/* -Адреса внутренних сервисов. - -Сервисы релиза адресуются по именам, которые создаёт этот же чарт, поэтому -пустое значение в values резолвится в имя из hrbox.component: адреса не -разъезжаются при nameOverride и их не нужно править вручную. Непустое -значение используется как есть — для сервиса за пределами релиза. -*/}} {{- define "hrbox.geonamesUrl" -}} {{- .Values.app.geonames.url | default (printf "http://%s:8181" (include "hrbox.component" (dict "ctx" . "name" "geonames"))) -}} {{- end -}} @@ -129,11 +68,6 @@ secrets.existingSecret позволяет подставить Secret, созд {{- end -}} {{- end -}} -{{/* -Одна переменная окружения из секрета приложения. - -Usage: {{ include "hrbox.secretEnv" (dict "ctx" . "env" "POSTGRES_PASSWORD" "key" "postgres-password") }} -*/}} {{- define "hrbox.secretEnv" -}} - name: {{ .env }} valueFrom: @@ -142,23 +76,6 @@ Usage: {{ include "hrbox.secretEnv" (dict "ctx" . "env" "POSTGRES_PASSWORD" "key key: {{ .key | quote }} {{- end -}} -{{/* -============================================================================ - Nanabush Player -============================================================================ - -On-premise-топология Player'а отличается от облачной: выделенного поддомена -нет, поэтому Player живёт на основном домене HRBox под путём basePath -(по умолчанию /nanabush-player). Отсюда следуют три инварианта, которые -проверяет 00-validate.yaml: - - * audience тикета = https:// (origin без пути) - * basePath = отдельная переменная, в aud не входит - * issuer тикета = https:///lms/player - -Подробности контракта — docs/nanabush-player-deployment.md в репозитории HRBox. -*/}} - {{- define "nanabushPlayer.host" -}} {{- required "Для Nanabush Player нужен непустой ingress.host" .Values.ingress.host -}} {{- end -}} @@ -167,10 +84,6 @@ On-premise-топология Player'а отличается от облачно {{- printf "https://%s" (include "nanabushPlayer.host" .) -}} {{- end -}} -{{/* -Путь монтирования Player'а на основном домене. Пустое значение и "/" запрещены: -это отдало бы Player'у корень, занятый самим HRBox. -*/}} {{- define "nanabushPlayer.basePath" -}} {{- $path := .Values.app.nanabushPlayer.basePath | default "/nanabush-player" -}} {{- $path | trimSuffix "/" -}} @@ -188,11 +101,6 @@ On-premise-топология Player'а отличается от облачно {{- .Values.app.nanabushPlayer.service.name | default (include "hrbox.component" (dict "ctx" . "name" "nanabush-player")) -}} {{- end -}} -{{/* -Хост обратного канала Player -> HRBox. Только внутрикластерное имя: по нему -Player забирает пакеты курсов и отправляет отчёты, и оно же попадает в -allowlist самого Player'а. -*/}} {{- define "nanabushPlayer.backchannelHost" -}} {{- .Values.app.nanabushPlayer.deployment.backchannel.host | default (include "hrbox.component" (dict "ctx" . "name" "web")) -}} {{- end -}} @@ -201,10 +109,6 @@ allowlist самого Player'а. {{- printf "http://%s" (include "nanabushPlayer.backchannelHost" .) -}} {{- end -}} -{{/* -Версия Player'а берётся из тега образа, а не из отдельной настройки: тикет -не должен объявлять версию, отличную от той, что реально запущена в кластере. -*/}} {{- define "nanabushPlayer.version" -}} {{- $tag := include "hrbox.imageTag" .Values.image.nanabushPlayer -}} {{- if not $tag -}} @@ -217,15 +121,10 @@ allowlist самого Player'а. {{- required "app.nanabushPlayer.deployment.existingSecret.name обязателен, когда Player включён" .Values.app.nanabushPlayer.deployment.existingSecret.name -}} {{- end -}} -{{/* URL Redis, который чарт собирает из штатных настроек app.redis. */}} {{- define "nanabushPlayer.redisUrl" -}} {{- printf "redis://%s:%s/%d" (include "hrbox.redisHost" .) (.Values.app.redis.port | toString) (.Values.app.nanabushPlayer.deployment.redis.database | int) -}} {{- end -}} -{{/* -Префикс ключей Redis. Namespace в префиксе разводит несколько релизов HRBox, -живущих на одном Dragonfly. -*/}} {{- define "nanabushPlayer.redisKeyPrefix" -}} {{- printf "hrbox:%s:%s" .Release.Namespace .Values.app.nanabushPlayer.deployment.redis.keyPrefixSuffix -}} {{- end -}} diff --git a/.helm/templates/_resources.tpl b/.helm/templates/_resources.tpl index 243d176..9d423b3 100644 --- a/.helm/templates/_resources.tpl +++ b/.helm/templates/_resources.tpl @@ -1,16 +1,3 @@ -{{/* -============================================================================ - resources: для контейнера -============================================================================ - -В чарте 2.x на каждый компонент был отдельный define из восьми одинаковых -строк. Здесь один хелпер, который читает resources.<компонент> из values.yaml. -Незаданные поля просто не рендерятся, поэтому компоненту можно указать, -например, только requests. - -Usage: - {{- include "hrbox.resources" (dict "ctx" . "component" "app") | indent 10 }} -*/}} {{- define "hrbox.resources" -}} {{- $r := index .ctx.Values.resources .component -}} {{- if $r }} diff --git a/.helm/values.yaml b/.helm/values.yaml index 3892c80..ffe8cc5 100644 --- a/.helm/values.yaml +++ b/.helm/values.yaml @@ -1,57 +1,44 @@ --- -# ============================================================================ -# HRBox — конфигурация on-premise установки -# ============================================================================ +# Конфигурация HRBox для установки в своём кластере. # -# Минимум, который нужно заполнить перед первым деплоем: +# Заполните перед первой установкой: +# ingress.host домен, по которому будет открываться HRBox +# app.defaultTenantHostname тот же домен +# app.postgres.* подключение к PostgreSQL +# app.s3.* хранилище файлов +# app.smtp.* почта +# app.hub.token токен HRBox Hub, выдают сотрудники HRBox +# secrets.* три ключа, генерируются openssl +# app.chatbox.wrappingKey ключ шифрования чатов +# app.nanabushPlayer.keys.* два ключа плеера курсов # -# ingress.host домен инсталляции -# app.defaultTenantHostname тот же домен -# app.postgres.* подключение к PostgreSQL -# app.s3.* S3-совместимое хранилище -# app.smtp.* почта -# app.hub.token токен HRBox Hub (выдаёт HRBox) -# secrets.* три сгенерированных ключа -# app.chatbox.wrappingKey ключ шифрования чатов -# app.nanabushPlayer.keys.* два ключа плеера курсов -# -# Чарт проверяет их на этапе рендера и падает с понятной ошибкой, если -# что-то не заполнено, — до того, как в кластере появятся поды. -# -# Команды генерации ключей перечислены рядом с каждым полем и целиком — -# в README, раздел «Секреты». -# ============================================================================ +# Если что-то из этого не заполнено, helm остановится и напишет, чего не +# хватает. Команды генерации ключей — в README, раздел «Секреты». -# Переопределение базового имени объектов. По умолчанию — имя чарта (hrbox), -# то есть hrbox-web, hrbox-worker и так далее. Менять на работающей установке -# нельзя: selector у Deployment неизменяем. +# Базовое имя объектов. Пусто — имя чарта: hrbox-web, hrbox-worker и т.д. +# На уже работающей установке менять нельзя. nameOverride: "" -# ============================================================================ -# Образы -# ============================================================================ -# Все образы собраны в одном месте: обновление версии сервиса — это правка -# одной строки здесь, а не поиск по шаблонам. +# --- Образы --------------------------------------------------------------- image: - # Основное приложение (PHP-FPM, nginx, worker, conductor-задачи, миграции). + # Приложение: PHP-FPM, nginx, worker, миграции. # Доступ к реестру выдают сотрудники HRBox. hrbox: "cr.yandex/crphnph9c2s776j32sjd/hrbox:latest" # Чат и вебсокеты chatbox: "cr.yandex/crphnph9c2s776j32sjd/hrbox-chatbox:5.18" - # Интеграции с мессенджерами, пуши, синхронизация оргструктуры + # Мессенджеры, пуши, синхронизация оргструктуры goworker: "cr.yandex/crphnph9c2s776j32sjd/hrbox-goworker:3.21.2" - # Электронная подпись (КЭДО, МЧД) + # Электронная подпись: КЭДО, МЧД kedoca: "cr.yandex/crphnph9c2s776j32sjd/hrbox-kedoca:1.0.6" - # Справочник городов, стран и часовых поясов + # Справочник городов, стран, часовых поясов geonames: "cr.yandex/crphnph9c2s776j32sjd/hrbox-geonames:3.3" - # Конвертация документов и обработка изображений + # Конвертация документов и изображений fileProcessor: "cr.yandex/crphnph9c2s776j32sjd/hrbox-file-processor:1.4.6" # Планировщик задач по расписанию conductor: "cr.yandex/crphnph9c2s776j32sjd/hrbox-conductor:2.2.0" - # Плеер учебных материалов (SCORM, cmi5, xAPI, нативные курсы). - # Тег обязателен и не может быть latest: из него выводится версия плеера, - # которую HRBox кладёт в launch-тикет. + # Плеер курсов. Нужен конкретный тег, latest не подходит: + # из тега берётся версия плеера. nanabushPlayer: "cr.yandex/crphnph9c2s776j32sjd/nanabush-player:1.0.11" # Инфраструктура @@ -59,48 +46,43 @@ image: nats: "nats:2.10" natsConfigReloader: "natsio/nats-server-config-reloader:0.6.2" phpFpmExporter: "cr.yandex/crpnn6fi85p3dauha6nc/php-fpm_exporter:latest" - # Используется только init-контейнером джобы миграций (ждёт готовности БД) + # Только для джобы миграций: ждёт готовности базы postgresClient: "postgres:14-alpine" - # Используется только при app.tracing.enabled + # Только при app.tracing.enabled otelCollector: "otel/opentelemetry-collector:0.109.0" pullPolicy: IfNotPresent -# Секреты доступа к приватному Docker-реестру. -# Создаются отдельно, см. README, шаг 1. +# Secret с доступом к реестру образов. Создаётся отдельно, см. README, шаг 1. imagePullSecrets: - name: regsecret -# ============================================================================ -# Приложение -# ============================================================================ +# --- Приложение ----------------------------------------------------------- app: - # Основной домен инсталляции. Должен совпадать с ingress.host. + # Домен установки. Должен совпадать с ingress.host. defaultTenantHostname: "hrbox.example.com" - # Имя инстанса — попадает в логи и метрики, помогает различать установки. + # Имя установки, видно в логах и метриках instanceName: "onprem" - # Режим одного арендатора. "1" — вся установка обслуживает одну компанию. + # "1" — установка обслуживает одну компанию singleTenant: "0" - # Frontend 2: "true" — включён (по умолчанию), "false" — старый интерфейс. + # "true" — новый интерфейс, "false" — старый front2: "true" - # Отдельный домен авторизации. Для on-premise почти всегда пусто. + # Отдельный домен авторизации. Для on-premise обычно пусто. authHost: "" - # "0" — production, "1" — отладка. В production всегда "0". + # "0" — production, "1" — отладка debug: "0" - # Принудительный https во всех сгенерированных ссылках. + # Подставлять https во всех ссылках, которые генерирует приложение forceHttps: "true" - # Количество реплик. - # web 2-10, по числу пользователей - # worker 2-5, фоновые задачи - # chatbox 2-6, real-time - # остальные обычно хватает 1-2 + # Число подов каждого сервиса. + # web — по числу пользователей, worker — по объёму фоновых задач, + # chatbox — по числу открытых вкладок, остальным обычно хватает 1-2. replicas: web: 2 worker: 2 @@ -111,43 +93,41 @@ app: fileProcessor: 1 fileProcessorVideo: 1 - # Своя политика конфиденциальности и правила использования. - # При enabled: false приложение показывает ссылки HRBox. + # Свои ссылки на политику конфиденциальности и правила использования. + # false — показываются ссылки HRBox. customPolicy: enabled: false # privacyUrl: "https://example.com/privacy" # termsUrl: "https://example.com/terms" - # --- PostgreSQL ----------------------------------------------------------- - # Требуется PostgreSQL 14+. Для отказоустойчивости настройте репликацию. + # --- PostgreSQL --------------------------------------------------------- + # Нужен PostgreSQL 14 или новее. Чарт базу не разворачивает. postgres: host: "postgresql" port: "5432" database: "hrbox" user: "hrbox" - # Пароль попадает в Secret чарта, а не в env-блок Deployment. + # Попадает в Secret, в спеке подов пароля нет password: "" - # "true", если база работает на расширении Citus (шардирование). + # "true", если база на Citus (шардирование) citus: "false" - # --- Redis / Dragonfly ---------------------------------------------------- - # Хранит сессии, кеш и очереди плеера курсов. Потеря данных не критична, - # но разлогинивает пользователей и сбрасывает незавершённые события плеера. - # - # Пусто — используется Dragonfly из этого же релиза. Заполняйте, только - # если Redis внешний (тогда обычно dragonfly.enabled: false). + # --- Redis / Dragonfly -------------------------------------------------- + # Сессии, кеш и очереди плеера курсов. + # Пусто — используется Dragonfly из этого же релиза. + # Адрес указывают, если Redis внешний (тогда обычно dragonfly.enabled: false). redis: host: "" port: "6379" - # --- NATS JetStream ------------------------------------------------------- - # Пусто — NATS из этого же релиза. + # --- NATS --------------------------------------------------------------- + # Пусто — используется NATS из этого же релиза. # Внешний кластер: nats://node1:4222,nats://node2:4222,nats://node3:4222 nats: url: "" - # --- S3 ------------------------------------------------------------------- - # Подходят AWS S3, MinIO, Yandex Object Storage, VK Cloud Storage. + # --- S3 ----------------------------------------------------------------- + # Подойдёт AWS S3, MinIO, Yandex Object Storage, VK Cloud Storage. s3: # MinIO: http://minio:9000 # Yandex: https://storage.yandexcloud.net @@ -158,12 +138,12 @@ app: secret: "" region: "us-east-1" version: "latest" - # "true" для MinIO и self-hosted S3, "false" для AWS S3 + # "true" для MinIO и своего S3, "false" для AWS S3 pathStyle: "true" multipart: "true" prefix: "" - # --- Почта ---------------------------------------------------------------- + # --- Почта -------------------------------------------------------------- smtp: host: "smtp.example.com" # 25 — без шифрования, 587 — STARTTLS, 465 — SSL/TLS @@ -177,33 +157,33 @@ app: sslAllowSelfSigned: "true" sslVerifyPeer: "false" - # --- Мониторинг ошибок ---------------------------------------------------- + # --- Sentry ------------------------------------------------------------- sentry: dsn: "" environment: "production" - # Отдельные DSN для подсистем. Пусто — используется общий dsn. + # Пусто — используется общий dsn launchpadDsn: "" conductorDsn: "" - # У этих двух общего fallback нет: пусто означает «не отправлять». + # Здесь пусто означает «не отправлять» goworkerDsn: "" chatboxDsn: "" - # --- Внутренние сервисы --------------------------------------------------- - # Пустой url означает сервис из этого же релиза: адрес собирается из имени, - # которое создаёт чарт. Заполняйте, только если вынесли сервис наружу. + # --- Внутренние сервисы ------------------------------------------------- + # Пустой url — сервис берётся из этого же релиза. + # Заполняют, если сервис вынесен наружу. geonames: url: "" goworker: url: "" - # Минимальный интервал между push-уведомлениями одному пользователю + # Не чаще одного push одному пользователю за этот интервал pushMinInterval: "5m" kedoca: url: "" # Пароль корневого сертификата, который сервис создаёт при первом запуске rootCaPass: "hrbox" - # "gost" (ГОСТ, КриптоПро) или "openssl" + # "gost" (КриптоПро) или "openssl" engine: "gost" # "gost2012_256" для ГОСТ, "rsa" для OpenSSL algorithm: "gost2012_256" @@ -211,81 +191,79 @@ app: pkeyopt: "paramset:A" # Служба меток времени tspUrl: "http://qs.cryptopro.ru/tsp/tsp.srf" - # Kubernetes Secret с сертификатами УЦ. Если задан — монтируется в /ca-cert. + # Secret с сертификатами УЦ. Если задан, монтируется в /ca-cert. secretName: "" fileProcessor: # Сколько документов конвертировать одновременно в одном поде docconvMaxParallelTasks: "1" - # Сколько изображений ресайзить одновременно (пул конвертации видео) + # Сколько изображений обрабатывать одновременно в поде конвертации видео imageResizeMaxParallel: "20" - # --- Конвертация видео ---------------------------------------------------- - # Отдельный пул подов: конвертация видео требует много CPU и диска и - # выносится на выделенные ноды, чтобы не мешать остальным задачам. + # --- Конвертация видео -------------------------------------------------- + # Отдельные поды: видео требует много CPU и диска, поэтому его выносят + # на выделенные ноды. videoConverter: enabled: false - # Внутренний адрес HRBox, по которому конвертер забирает и отдаёт файлы. - # Пусто — берётся http://<релиз>-web. + # Внутренний адрес HRBox, откуда конвертер берёт и куда отдаёт файлы. + # Пусто — http://<релиз>-web. internalUrl: "" - # Куда планировать поды конвертера. Пусто — без ограничений. + # На каких нодах запускать. Пусто — на любых. + # Пример: role: converter-dedicated nodeSelector: {} - # role: converter-dedicated - tolerations: [] + # Пример: # - key: dedicated # operator: Equal # value: video-converter # effect: NoSchedule + tolerations: [] - # --- Чат ------------------------------------------------------------------ + # --- Чат ---------------------------------------------------------------- chatbox: - # Часть ключа асимметричного шифрования чатов, 32 символа. + # Часть ключа шифрования чатов, 32 символа. # Генерация: openssl rand -hex 16 wrappingKey: "" - # --- Видеозвонки ---------------------------------------------------------- + # --- Видеозвонки -------------------------------------------------------- livekit: url: "" apiKey: "" apiSecret: "" - # --- Nanabush Player ------------------------------------------------------ - # Плеер учебных материалов: SCORM 1.2/2004, cmi5, xAPI, нативные курсы, - # тесты и эссе. Полное описание контракта — docs/nanabush-player-deployment.md - # в репозитории HRBox. + # --- Nanabush Player ---------------------------------------------------- + # Плеер учебных материалов: SCORM 1.2 и 2004, cmi5, xAPI, нативные курсы, + # тесты и эссе. # - # В облаке плеер живёт на отдельном поддомене. В on-premise выделенного - # поддомена нет, поэтому плеер монтируется на основной домен под путём - # basePath. Практическое следствие: cookie HRBox видны содержимому курсов - # (один origin). Если для сторонних SCORM-пакетов нужна изоляция — заводите - # отдельный поддомен и обращайтесь в HRBox за облачной схемой. + # В облаке плеер стоит на отдельном поддомене, в on-premise поддомена нет, + # поэтому он живёт на основном домене по пути basePath. Домен общий, значит + # cookie HRBox доступны содержимому курсов. Если нужна изоляция сторонних + # SCORM-пакетов, обратитесь в HRBox за схемой с отдельным поддоменом. nanabushPlayer: - # Отдавать ли учебные материалы через плеер. false — старый проигрыватель. + # false — материалы открываются старым проигрывателем enabled: true - # Путь монтирования на основном домене. Пустой и "/" запрещены: корень - # занят самим HRBox. + # Путь на основном домене. Корень "/" занят самим HRBox. basePath: "/nanabush-player" - # Issuer launch-тикета. Пусто — https:///lms/player. + # Issuer launch-тикета. Пусто — https:///lms/player issuer: "" contractVersion: "v0-current" - # Ключи плеера. Оба обязательны и должны отличаться друг от друга. + # Оба ключа обязательны и должны быть разными. # Генерация каждого: openssl rand -base64 48 keys: ticketHs256: "" sessionHs256: "" - # Необязательное кольцо ключей для ротации без простоя: - # "kid1:секрет1,kid2:секрет2", текущий ключ подписи — первым. - # Секрет не должен содержать запятую (используйте base64url). + # Набор ключей для ротации без простоя, необязательный: + # "kid1:секрет1,kid2:секрет2". Первым идёт ключ, которым подписывают + # сейчас. Запятая внутри секрета недопустима, используйте base64url. ticketKeys: "" - # Доля запусков, уходящих в новый плеер. 100 — все. rollout: + # Какая доля запусков идёт в новый плеер. 100 — все. percent: 100 - # Разрешить откат на старый проигрыватель, если плеер недоступен + # Открывать материал старым проигрывателем, если плеер не ответил legacyFallback: true allowedContentTypes: - document @@ -299,7 +277,7 @@ app: - native - native_quiz - native_essay - # "*" — все арендаторы, включая будущих + # "*" — все компании, включая новые allowedTenants: - "*" @@ -308,26 +286,25 @@ app: name: "" port: 8094 - # Публиковать ли путь плеера на основном Ingress. - # Выключать имеет смысл только при аварийном откате. + # Публиковать ли путь плеера на общем Ingress. + # Выключают только при откате. ingress: enabled: true deployment: - # Создавать ли Deployment и Service плеера. + # Создавать ли Deployment и Service плеера enabled: true replicas: 1 terminationGracePeriodSeconds: 120 - # Образ плеера должен уметь работать под путём (а не только в корне - # домена). Если ставите старый образ без этой поддержки — переключите - # в false, и чарт остановит рендер с объяснением вместо неработающего - # плеера в кластере. + # Умеет ли образ плеера работать не в корне домена, а по пути. + # Со старым образом без такой поддержки поставьте false — чарт + # остановит установку вместо того, чтобы развернуть нерабочий плеер. binaryCapabilities: basePath: true - # Secret с ключами плеера. create: false — Secret создаётся вне чарта - # (например, внешним vault-оператором), чарт только ссылается на него. + # Secret с ключами плеера. + # create: false — Secret заводят вне чарта, чарт только ссылается. existingSecret: create: true name: "hrbox-nanabush-player" @@ -338,48 +315,46 @@ app: ticketKeys: "" ticket: - # Время жизни launch-тикета, секунды + # Сколько живёт launch-тикет, секунды ttlSeconds: 300 - # Время жизни учётных данных обратного канала: сессия плеера + 2 минуты - # на расхождение часов + # Сколько живут учётные данные обратного канала: сессия плеера плюс + # 2 минуты на расхождение часов credentialTtlSeconds: 28920 maxLifetime: "5m" sessionTtl: "8h" - # Только на время первой раскатки кольца ключей: до какого момента + # Нужно только при первом переходе на набор ключей: до какого момента # (RFC3339) принимать тикеты без kid. Пусто — не принимать. keylessAcceptedUntil: "" redis: - # false — URL собирается из app.redis. - # true — полный redis:// или rediss:// URL читается из внешнего - # Secret (нужно для ACL, пароля или TLS) и требует - # existingSecret.create: false. + # false — адрес собирается из app.redis. + # true — полный redis:// или rediss:// URL берётся из внешнего Secret + # (нужно для пароля, ACL или TLS), требует existingSecret.create: false urlFromExistingSecret: false database: 0 - # Итоговый префикс: hrbox:: — разводит несколько - # релизов HRBox на одном Dragonfly. + # Полный префикс: hrbox::<суффикс>. + # Разделяет несколько установок HRBox на одном Redis. keyPrefixSuffix: "nanabush-player" + # Обращения плеера обратно в HRBox: за пакетом курса и с отчётами backchannel: - # Внутрикластерный адрес HRBox. Пусто — <релиз>-web. + # Внутренний адрес HRBox. Пусто — <релиз>-web. host: "" schemes: "http" allowPrivateIps: true packageFetchMaxBytes: "5242880" - # Транспорт должен вмещать самый большой поддерживаемый медиафайл + # Должно вмещать самый большой медиафайл курса packageContentMaxBytes: "2147483648" scormPackageMaxBytes: "1073741824" - # Пусто — ограничение по числу файлов в SCORM-пакете берётся из - # умолчаний приложения (4000) + # Пусто — берётся значение приложения (4000 файлов) scormPackageMaxFiles: "" packageFetchTimeout: "15s" - # Таймаут покрывает всю передачу, включая отдачу байтов браузеру - # учащегося, поэтому упирается в скорость его канала. Двух минут - # не хватало на крупное видео. + # Считается до конца передачи файла в браузер учащегося, поэтому + # зависит от скорости его интернета packageStreamTimeout: "20m" reportTimeout: "30s" - # Очередь отправки учебных событий в HRBox. + # Очередь отправки учебных событий в HRBox forwarder: queueSize: "1000" workers: "4" @@ -398,8 +373,7 @@ app: level: info format: json - # Метрики отдаются на отдельном порту, который не публикуется наружу: - # содержимое курсов не должно читать эксплуатационные метрики. + # Метрики отдаются на отдельном порту и наружу не публикуются metrics: enabled: true port: 9090 @@ -418,22 +392,22 @@ app: timeoutSeconds: 2 failureThreshold: 3 - # --- Фоновые задачи ------------------------------------------------------- - # Формат: "топик:количество-воркеров" + # --- Фоновые задачи ----------------------------------------------------- + # Формат: "очередь:сколько воркеров" launchpad: topics: "launcher:1,file-processor:1,image-processor:1,send-mail:1,send-im:1,send-bell:1,send-push:1,backup:1,cleaning:1" sms: senderName: "HRBOX" - # --- Внешние интеграции --------------------------------------------------- + # --- Внешние интеграции ------------------------------------------------- integrations: telegram: - # Прямой API Telegram; для установок без доступа к нему укажите прокси + # Если прямого доступа к Telegram нет, укажите адрес прокси apiUrl: "https://api.telegram.org/bot" google: enabled: "false" - # Client ID публичный (уходит в браузер), secret — в Secret чарта + # clientId уходит в браузер, clientSecret попадает в Secret clientId: "692956564858-ccapt35khk7tmadl17b3879ht2cropp6.apps.googleusercontent.com" clientSecret: "" glagol: @@ -441,8 +415,8 @@ app: apiUrl: "https://api.glagol.online/glagol-api" # Привязка мобильных приложений к домену: без неё ссылки из писем и пушей - # открываются в браузере вместо приложения. По умолчанию — штатные - # приложения HRBox; меняйте, только если у вас собственная сборка. + # открываются в браузере, а не в приложении. По умолчанию указаны + # приложения HRBox, меняйте только для своей сборки. mobileApps: android: - packageName: "io.hrbox.hrboxapp" @@ -461,13 +435,13 @@ app: clientSecret: "" verificationToken: "" - # --- HRBox Hub ------------------------------------------------------------ + # --- HRBox Hub ---------------------------------------------------------- # Лицензирование и общие сервисы. Токен выдают сотрудники HRBox. hub: url: "https://hub.hrbox.io" token: "" - # --- AI ------------------------------------------------------------------- + # --- AI ----------------------------------------------------------------- ai: docsIndexEnabled: "true" semanticIndexEnabled: "true" @@ -476,82 +450,74 @@ app: folderId: "" apiKey: "" - # --- Трассировка ---------------------------------------------------------- - # Включает OpenTelemetry в PHP и sidecar-коллектор в поде web. + # --- Трассировка -------------------------------------------------------- + # Включает OpenTelemetry в PHP и коллектор рядом с подом web tracing: enabled: false exporterUrl: "http://localhost:4318" - # Куда коллектор пересылает трейсы (Tempo, Jaeger) + # Куда коллектор отправляет трейсы: Tempo, Jaeger tempoUrl: "" - # --- Безопасность --------------------------------------------------------- + # --- Безопасность ------------------------------------------------------- security: - # "true" разрешает встроенному iframe полный доступ к родительскому окну. - # Для production рекомендуется "false". + # "true" даёт встроенному iframe полный доступ к родительскому окну. + # Для production лучше "false". iframeFullAccess: "false" -# ============================================================================ -# Секреты приложения -# ============================================================================ +# --- Секреты -------------------------------------------------------------- # Эти значения вместе с паролями из app.* складываются в один Kubernetes -# Secret, а контейнеры читают его через secretKeyRef. +# Secret, поды читают их оттуда. secrets: - # Имя Secret, созданного вне чарта. Если задано — чарт свой Secret не - # создаёт и берёт все ключи из указанного. Список ключей — в README. + # Имя Secret, созданного вне чарта. Если задано, чарт свой Secret не + # создаёт и берёт ключи из указанного. Список ключей — в README. existingSecret: "" # Защита cookie от подделки, 32 символа. # Генерация: openssl rand -hex 16 cookieValidationKey: "" - # Шифрование данных: пароли интеграций (S3, ADFS и другие). - # ВАЖНО: без этого ключа данные не расшифровать. Не теряйте его. + # Шифрование паролей интеграций в базе. + # Без этого ключа данные не расшифровать, храните копию отдельно. # Генерация: openssl rand -base64 48 encryptionKey: "" - # Аутентификация вызовов между микросервисами релиза. + # Проверка вызовов между сервисами внутри релиза. # Генерация: openssl rand -base64 48 servicesInternalSecret: "" -# ============================================================================ -# Ingress -# ============================================================================ +# --- Ingress -------------------------------------------------------------- ingress: enabled: true # Обычно "nginx" или "traefik" className: "nginx" - # Домен инсталляции. Wildcard (*.hrbox.company.com) — для мультиарендности. + # Домен установки. Для мультиарендности можно wildcard: *.hrbox.company.com host: "hrbox.example.com" # Дополнительные аннотации ingress-контроллера annotations: {} - # Максимальный размер загружаемого файла на общих путях. - # Загрузка оргструктуры из 1С идёт потоком и имеет собственный лимит. + # Максимальный размер загружаемого файла. + # У загрузки оргструктуры из 1С отдельный лимит, он выше. maxBodySize: "10m" tls: enabled: true # kubectl create secret tls hrbox-tls --cert=cert.pem --key=key.pem - # либо выпуск через cert-manager + # или выпустить сертификат через cert-manager secretName: "hrbox-tls" -# ============================================================================ -# Кластер -# ============================================================================ +# --- Кластер -------------------------------------------------------------- cluster: - # Домен служебных имён Kubernetes. Меняйте, только если он нестандартный. + # Домен служебных имён Kubernetes. Менять только если он нестандартный. domain: "cluster.local" -# ============================================================================ -# Ресурсы -# ============================================================================ -# Значения близки к продовым. Незаданные поля не рендерятся, поэтому -# компоненту можно оставить только requests. +# --- Ресурсы -------------------------------------------------------------- +# Значения близки к тем, что используются в облаке HRBox. +# Можно оставить только requests: незаполненные поля в манифест не попадают. resources: - # PHP-FPM: HTTP-запросы, API, веб-интерфейс + # PHP-FPM: веб-интерфейс и API app: requests: cpu: "1" @@ -562,7 +528,7 @@ resources: memory: "2Gi" ephemeralStorage: "2Gi" - # nginx в поде web: проксирует запросы в PHP-FPM + # nginx в поде web nginx: requests: cpu: "0.1" @@ -573,7 +539,7 @@ resources: memory: "256Mi" ephemeralStorage: "512Mi" - # Фоновые задачи: конвертация файлов, почта, отчёты + # Фоновые задачи: файлы, почта, отчёты worker: requests: cpu: "0.6" @@ -645,7 +611,7 @@ resources: memory: "1Gi" ephemeralStorage: "2Gi" - # Конвертация видео: тяжёлый профиль, отдельные ноды + # Конвертация видео: тяжёлые поды на отдельных нодах fileProcessorVideo: requests: cpu: "4" @@ -679,7 +645,7 @@ resources: cpu: "1" memory: "2Gi" - # Разовые задачи: миграции, обновление кластера + # Разовые задачи: миграции и обновление кластера jobs: requests: cpu: "0.5" @@ -690,31 +656,29 @@ resources: memory: "4Gi" ephemeralStorage: "256Mi" -# ============================================================================ -# NATS JetStream -# ============================================================================ +# --- NATS ----------------------------------------------------------------- nats: - # Обязателен для работы HRBox. Выключайте только если ставите NATS отдельно - # и указали его адрес в app.nats.url. + # HRBox без брокера не работает. Выключают, только если NATS ставят + # отдельно и указали его адрес в app.nats.url. enabled: true - # Для отказоустойчивости — 3 или 5. Должно совпадать с cluster.replicas. + # Для отказоустойчивости 3 или 5. Должно совпадать с cluster.replicas. replicas: 3 jetstream: enabled: true - # Хранение в памяти — для небольших установок + # Хранение в памяти: подходит небольшим установкам memStorage: enabled: true size: "2Gi" - # Хранение на диске — для production + # Хранение на диске: для production fileStorage: enabled: false size: "4Gi" storageDirectory: "/data" - # Для JetStream нужен быстрый диск + # JetStream нужен быстрый диск storageClassName: "fast-ssd" cluster: @@ -722,17 +686,16 @@ nats: # Должно совпадать с nats.replicas replicas: 3 - # Автоматическая перезагрузка конфигурации без рестарта сервера + # Подхватывает изменения конфигурации без перезапуска NATS reloader: enabled: true terminationGracePeriodSeconds: 120 -# ============================================================================ -# Dragonfly (Redis-совместимый кеш) -# ============================================================================ +# --- Dragonfly ------------------------------------------------------------ +# Redis-совместимый кеш. dragonfly: - # false — если Redis разворачивается отдельно; тогда укажите его в app.redis. + # false — если Redis ставят отдельно; тогда заполните app.redis.host enabled: true - # Ограничение памяти. Раскомментируйте при необходимости. + # Ограничение памяти # maxmemory: "256M" diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eed1b3..5755dba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,63 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [3.0.0] - 2026-09-02 -Синхронизация с продовым окружением HRBox и приведение чарта в порядок: -добавлен плеер учебных материалов, секреты перестали лежать открытым текстом -в спеках подов, вместо девяти одинаковых блоков ресурсов — один хелпер, -а неполная конфигурация теперь останавливает установку с внятным текстом. +Чарт подтянут к тому, как HRBox работает в облаке: добавлен плеер учебных +материалов, пароли и ключи уехали в Secret, повторяющиеся куски шаблонов +заменены одним хелпером, а неполная конфигурация теперь останавливает +установку с понятным сообщением. ### Added #### Nanabush Player -Проигрыватель учебных материалов: SCORM 1.2 и 2004, cmi5, xAPI, нативные -курсы, тесты и эссе. +Плеер учебных материалов: SCORM 1.2 и 2004, cmi5, xAPI, нативные курсы, +тесты и эссе. - `09-nanabush-player.yaml` — Deployment и Service, образ `nanabush-player:1.0.11` - `00-nanabush-player-secret.yaml` — ключи launch-тикета и сессии -- маршрут `/nanabush-player` на основном Ingress -- настройки: `app.nanabushPlayer` в values.yaml, ресурсы — `resources.nanabushPlayer` +- маршрут `/nanabush-player` на общем Ingress +- настройки `app.nanabushPlayer`, ресурсы `resources.nanabushPlayer` - переменные `NANABUSH_PLAYER_*` в HRBox и `PLAYER_*` в самом плеере -On-premise-топология отличается от облачной: выделенного поддомена нет, -плеер монтируется на основной домен под путём. Отсюда два ограничения, -описанные в README: нет изоляции cookie между HRBox и содержимым курсов, -и нужны синхронизированные часы (тикет живёт 5 минут). +В on-premise выделенного поддомена нет, поэтому плеер стоит на основном +домене по пути. Отсюда два ограничения, они описаны в README: cookie HRBox +доступны содержимому курсов, и нужны синхронизированные часы — тикет живёт +5 минут. -Три независимых переключателя (`enabled`, `ingress.enabled`, -`deployment.enabled`) сохранены для аварийного отката; чарт не даёт оставить -маршрут без пода или раскатку без маршрута. +Для быстрого отката оставлены три переключателя: `enabled`, +`ingress.enabled`, `deployment.enabled`. Маршрут без пода или включённый +плеер без маршрута чарт не пропустит. #### Проверка конфигурации -- `00-validate.yaml` — объектов не создаёт, но останавливает рендер, если - не заполнены обязательные значения, использованы ключи из версии 2.x или - нарушены инварианты плеера. Сообщения на русском, с указанием, что править. +- `00-validate.yaml` — объектов не создаёт, но останавливает установку, если + не заполнены обязательные значения, остались ключи из версии 2.x или + нарушены требования плеера. Сообщения на русском, с указанием, что править. -#### Секрет приложения +#### Secret приложения -- `00-app-secret.yaml` — единый Secret с паролями и ключами. Контейнеры +- `00-app-secret.yaml` — один Secret со всеми паролями и ключами. Поды читают его через `secretKeyRef`, поэтому `kubectl get deploy -o yaml` больше не показывает пароль базы и ключ шифрования. -- `secrets.existingSecret` — подстановка Secret, созданного вне чарта. +- `secrets.existingSecret` — можно подставить Secret, созданный вне чарта. -#### Новые компоненты и возможности +#### Новые компоненты -- `09-file-processor-video.yaml` — отдельный пул конвертации видео с - собственным профилем ресурсов, `nodeSelector` и `tolerations` - (`app.videoConverter.enabled`, по умолчанию выключен) -- `02-web-pdb.yaml` — PodDisruptionBudget для `web`, создаётся начиная с - двух реплик -- `01-otel-configmap.yaml` и sidecar-коллектор в поде `web` +- `09-file-processor-video.yaml` — отдельные поды для конвертации видео, + со своими ресурсами, `nodeSelector` и `tolerations` + (`app.videoConverter.enabled`, по умолчанию выключено) +- `02-web-pdb.yaml` — PodDisruptionBudget для `web`, создаётся от двух реплик +- `01-otel-configmap.yaml` и коллектор OpenTelemetry рядом с `web` (`app.tracing.enabled`) - `01-nginx-assetlinks.yaml` — привязка мобильных приложений к домену (`app.mobileApps`) - `templates/NOTES.txt` — что делать после установки, с готовыми командами - и предупреждениями о небезопасных настройках #### Новые переменные окружения -- `NATS_STREAM_REPLICAS` — число реплик JetStream-стримов по размеру кластера +- `NATS_STREAM_REPLICAS` — число реплик стримов JetStream по размеру кластера - `TELEGRAM_API_URL`, `AUTH_GOOGLE_ENABLED` - `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET` — видеозвонки - `APP_INTERNAL_URL` — внутренний адрес HRBox для конвертера @@ -72,52 +70,53 @@ On-premise-топология отличается от облачной: выд ### Changed -#### Структура values.yaml +#### values.yaml -- все образы собраны в блок `image` — обновление версии сервиса стало - правкой одной строки -- `imagePullSecrets` вынесен в настройки, `regsecret` больше не захардкожен +- все образы собраны в блок `image`: сменить версию сервиса — это правка + одной строки +- `imagePullSecrets` вынесен в настройки, `regsecret` больше не зашит + в шаблоны - ключи приведены к camelCase: `file_processor` → `fileProcessor`, `custom_policy` → `customPolicy` -- `resources` дополнен компонентами `fileProcessor`, `fileProcessorVideo`, +- в `resources` добавлены `fileProcessor`, `fileProcessorVideo`, `nanabushPlayer`, `dragonfly`, `nats` - адреса внутренних сервисов (`app.geonames.url`, `app.goworker.url`, - `app.kedoca.url`, `app.redis.host`, `app.nats.url`) по умолчанию пустые и - выводятся из имён, которые создаёт сам чарт. Раньше это были захардкоженные - строки `hrbox-*`, которые молча ломались при `nameOverride`. Явно заданное - значение по-прежнему используется как есть, поэтому старые values работают - без правок. Выключение `dragonfly` или `nats` без указания внешнего адреса - теперь останавливает установку + `app.kedoca.url`, `app.redis.host`, `app.nats.url`) по умолчанию пустые + и собираются из имён, которые создаёт сам чарт. Раньше это были строки + `hrbox-*`, зашитые в шаблоны: при `nameOverride` они молча переставали + работать. Заданное значение по-прежнему используется как есть, поэтому + старые values работают без правок. Выключить `dragonfly` или `nats`, + не указав внешний адрес, теперь нельзя. #### Шаблоны - `_resources.tpl`: девять почти одинаковых define заменены одним - `hrbox.resources`; незаданные поля не рендерятся -- `_helpers.tpl`: убрано около сотни строк мёртвого кода (хелперы `nats.*`, - которыми ничего не пользовалось и которые ссылались на несуществующий - `nats.k8sClusterDomain`); добавлены `hrbox.component`, `hrbox.labels`, - `hrbox.selectorLabels`, `hrbox.secretEnv` и хелперы плеера + `hrbox.resources`, незаполненные поля не рендерятся +- `_helpers.tpl`: удалено около сотни строк неиспользуемого кода (хелперы + `nats.*`, которые ссылались на несуществующий `nats.k8sClusterDomain`); + добавлены `hrbox.component`, `hrbox.labels`, `hrbox.selectorLabels`, + `hrbox.secretEnv` и хелперы плеера - у всех объектов появились стандартные лейблы `app.kubernetes.io/*`; - лейбл `app` сохранён, привычные `kubectl get pods -l app=worker` работают -- селекторы Deployment не менялись — обновление 2.x → 3.0 не пересоздаёт + лейбл `app` остался, привычный `kubectl get pods -l app=worker` работает +- селекторы Deployment не менялись, обновление 2.x → 3.0 не пересоздаёт объекты -- аннотации `checksum/*` перезапускают поды при изменении конфигурации nginx - или секрета приложения +- аннотации `checksum/*` перезапускают поды при изменении конфигурации + nginx или Secret приложения - образы, порты и таймауты вынесены из шаблонов в values.yaml #### nginx -Набор `location` синхронизирован с продовым: +Набор `location` синхронизирован с облачным: -- `/healthz` и `/health-check` — под них настроены пробы пода `web` +- `/healthz` и `/health-check` — на них настроены пробы пода `web` (раньше проверялся только TCP-порт) -- `/api/v1/integration/commit` — потоковая загрузка оргструктуры из 1С без - буферизации тела, лимит 500 МБ +- `/api/v1/integration/commit` — потоковая загрузка оргструктуры из 1С + без буферизации, лимит 500 МБ - `/ai/` — увеличенные таймауты - `/.well-known/assetlinks.json` и `/.well-known/apple-app-site-association` -- `/frontend/sw.js` — корректные заголовки service worker +- `/frontend/sw.js` — правильные заголовки service worker - кеширование шрифтов -- убран TLSv1 и TLSv1.1 +- убраны TLSv1 и TLSv1.1 #### Версии образов @@ -132,56 +131,56 @@ On-premise-топология отличается от облачной: выд #### Джобы -- имя содержит номер ревизии релиза: в `kubectl get jobs` видно, какой выкат - запускал миграции, а повторный `upgrade` не спотыкается об уже существующий +- в имени появился номер ревизии релиза: в `kubectl get jobs` видно, какой + выкат запускал миграции, а повторный `upgrade` не спотыкается о старый объект -- `ttlSecondsAfterFinished` — час у обеих джоб (было 100 секунд у миграций и - ничего у обновления кластера): логи упавшей джобы успевают дожить до того, - как их посмотрят +- `ttlSecondsAfterFinished` — час у обеих джоб (было 100 секунд у миграций + и ничего у обновления кластера), логи упавшей джобы успевают дожить до + того, как их посмотрят #### Переименования файлов -- `03-websocket.yaml` → `03-chatbox.yaml` (содержимое было chatbox с версии 2.0) +- `03-websocket.yaml` → `03-chatbox.yaml` (внутри был chatbox с версии 2.0) - `09-geonames.yml` → `09-geonames.yaml` - `99-update-cluser-job.yaml` → `99-update-cluster-job.yaml` ### Fixed -- **Общий секрет в коде чарта.** `SERVICES_INTERNAL_SECRET` имел захардкоженное - значение по умолчанию, то есть все установки, где его не заполнили, работали +- **Общий секрет у всех установок.** У `SERVICES_INTERNAL_SECRET` было + значение по умолчанию, зашитое в чарт: все, кто его не заполнил, работали на одном и том же секрете межсервисной аутентификации. Теперь значение - обязательно. -- **Домен кластера игнорировался.** Маршруты кластера NATS были захардкожены - на `cluster.local`, хотя `cluster.domain` присутствовал в values и - использовался в соседней переменной. На кластере с нестандартным доменом - ноды не находили друг друга. -- **`CLUSTER_ADVERTISE` без сегмента `svc`.** Имя не резолвилось, и ноды NATS - объявляли соседям недостижимый адрес. -- **Рассинхрон реплик NATS.** `nats.replicas` и `nats.cluster.replicas` - задавались независимо; при расхождении часть нод не входила в кластер. - Теперь чарт требует их совпадения. + обязательное. +- **`cluster.domain` не использовался.** Маршруты кластера NATS были зашиты + на `cluster.local`, хотя параметр в values был и применялся в соседней + переменной. На кластере с другим доменом ноды не находили друг друга. +- **`CLUSTER_ADVERTISE` без сегмента `svc`.** Имя не резолвилось, ноды NATS + сообщали соседям недостижимый адрес. +- **Разные значения реплик NATS.** `nats.replicas` и `nats.cluster.replicas` + задавались независимо, при расхождении часть нод не входила в кластер. + Теперь чарт требует, чтобы они совпадали. - **Пробы `web` не проверяли приложение.** Проверялся TCP-порт 9000, поэтому - под с неработающим PHP считался готовым. Теперь readiness ходит в - `/health-check` через FPM. + под с неработающим PHP считался готовым. Теперь readiness ходит + в `/health-check` через FPM. - Пароли и ключи больше не видны в спеках Deployment. -- `imagePullPolicy` у части сервисов был `Always` на закреплённых тегах — +- У части сервисов `imagePullPolicy` был `Always` на фиксированных тегах — лишние обращения к реестру при каждом рестарте пода. ### Removed -- `secrets` больше не имеет значений по умолчанию — пустые ключи - останавливают установку вместо тихого запуска на предсказуемых секретах -- убраны неиспользуемые хелперы `nats.name`, `nats.fullname`, `nats.chart`, +- у `secrets` больше нет значений по умолчанию: с пустыми ключами установка + останавливается, а не запускается на предсказуемых секретах +- удалены неиспользуемые хелперы `nats.name`, `nats.fullname`, `nats.chart`, `nats.labels`, `nats.selectorLabels`, `nats.clusterAdvertise`, - `nats.clusterRoutes`, `nats.extraRoutes`, `nats.tlsConfig`, `tplvalues.render` -- `SENDGRID_API_KEY` — не используется в on-premise + `nats.clusterRoutes`, `nats.extraRoutes`, `nats.tlsConfig`, + `tplvalues.render` +- `SENDGRID_API_KEY` — в on-premise не используется ### Migration Guide #### Переход с 2.x на 3.0 -Все переехавшие ключи чарт распознаёт и сообщает, куда именно они переехали, -поэтому надёжнее всего запустить `helm template` и править по сообщениям. +Все переехавшие ключи чарт узнаёт и пишет, куда именно они переехали, +поэтому проще всего запустить `helm template` и править по сообщениям. **Шаг 1. Перенесите образы в блок `image`** @@ -225,20 +224,20 @@ app: | `app.chatbox.sentryDsn` | `app.sentry.chatboxDsn` | | `nats.image` | `image.nats` | -**Шаг 3. Заполните ключи плеера** +**Шаг 3. Сгенерируйте ключи плеера** ```bash openssl rand -base64 48 # app.nanabushPlayer.keys.ticketHs256 openssl rand -base64 48 # app.nanabushPlayer.keys.sessionHs256 ``` -Ключи должны отличаться друг от друга. +Ключи должны быть разными. **Шаг 4. Проверьте `secrets.servicesInternalSecret`** Если поле было пустым, установка работала на общем значении по умолчанию. Сгенерируйте своё: `openssl rand -base64 48`. После обновления перезапустятся -все сервисы, обменивающиеся внутренними вызовами, — это ожидаемо. +все сервисы, которые ходят друг к другу, — это нормально. **Шаг 5. Проверьте рендер и обновитесь** @@ -249,21 +248,21 @@ helm upgrade hrbox .helm -n hrbox -f values.yaml **Шаг 6. Включите плеер** -Порядок первого включения и проверки — в README, раздел «Nanabush Player». +Порядок включения и проверки — в README, раздел «Nanabush Player». #### Breaking Changes 1. **Ключи `values.yaml` переехали.** Установка останавливается с подсказкой, молча ничего не игнорируется. -2. **`secrets.*` стали обязательными.** Установка без них не пройдёт. +2. **`secrets.*` стали обязательными.** Без них установка не пройдёт. 3. **Появился Nanabush Player.** Это дополнительный под, Service, Secret и - маршрут; учтите ресурсы кластера. Плеер требует синхронизированного времени + маршрут, учтите ресурсы кластера. Плееру нужно синхронизированное время на нодах. 4. **Пробы `web` стали строже.** Под, где PHP-FPM не отвечает по HTTP, теперь не проходит readiness. Если приложение отдаёт `/health-check` иначе, проверьте это до обновления. -5. **`app.replicas` расширился** полями `goworker`, `kedoca`, `fileProcessor`, - `fileProcessorVideo`; соответствующие `replicas` внутри сервисов удалены. +5. **В `app.replicas` добавились** `goworker`, `kedoca`, `fileProcessor`, + `fileProcessorVideo`; поля `replicas` внутри самих сервисов удалены. --- diff --git a/README.md b/README.md index 8de1465..35b3fbf 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # HRBox Helm Chart -Helm-чарт для развёртывания HRBox в собственном Kubernetes-кластере. +Чарт для установки HRBox в свой Kubernetes-кластер. -Чарт лежит в каталоге [`.helm`](.helm), настройки — в [`.helm/values.yaml`](.helm/values.yaml). -Каждый параметр там прокомментирован; ниже — только то, что нужно сделать руками. +Сам чарт лежит в [`.helm`](.helm), настройки — в [`.helm/values.yaml`](.helm/values.yaml). +Каждый параметр там подписан, ниже — только то, что нужно сделать руками. --- @@ -11,32 +11,32 @@ Helm-чарт для развёртывания HRBox в собственном | Компонент | Что делает | Порт | |---|---|---| -| `web` | PHP-FPM + nginx: веб-интерфейс и API | 80 | +| `web` | PHP-FPM и nginx: веб-интерфейс и API | 80 | | `worker` | фоновые задачи: файлы, почта, отчёты | — | -| `conductor` | планировщик задач по расписанию (строго 1 реплика) | 8080 | -| `chatbox` | вебсокеты чата и real-time уведомлений | 80 → 9797 | -| `nanabush-player` | проигрыватель курсов: SCORM, cmi5, xAPI, нативные материалы | 8094 | -| `goworker` | мессенджеры, push, синхронизация оргструктуры с 1С | 8686 | +| `conductor` | задачи по расписанию, всегда 1 под | 8080 | +| `chatbox` | вебсокеты чата и уведомлений | 80 → 9797 | +| `nanabush-player` | плеер курсов: SCORM, cmi5, xAPI, нативные материалы | 8094 | +| `goworker` | мессенджеры, пуши, синхронизация оргструктуры с 1С | 8686 | | `kedoca` | электронная подпись: КЭДО, МЧД | 8558 | | `geonames` | справочник городов, стран, часовых поясов | 8181 | | `file-processor` | конвертация документов и изображений | — | -| `file-processor-video` | конвертация видео, выключена по умолчанию | — | +| `file-processor-video` | конвертация видео, по умолчанию выключен | — | | `dragonfly` | Redis-совместимый кеш и сессии | 6379 | | `nats` | брокер сообщений JetStream | 4222 | -Все три публичных маршрута живут на одном домене: `/` — приложение, +Публичных маршрутов три, и все на одном домене: `/` — приложение, `/chatbox/` — вебсокеты, `/nanabush-player` — плеер курсов. -## Что нужно от вас +## Что нужно подготовить - Kubernetes 1.21+ и ingress-контроллер (nginx или traefik); -- PostgreSQL 14+ (с расширением Citus, если планируется шардирование); +- PostgreSQL 14+ (с Citus, если планируется шардирование); - S3-совместимое хранилище: AWS S3, MinIO, Yandex Object Storage, VK Cloud; - SMTP-сервер; -- доступ к Docker-реестру HRBox и токен HRBox Hub — выдают сотрудники HRBox. +- доступ к реестру образов HRBox и токен HRBox Hub — выдают сотрудники HRBox. -PostgreSQL и S3 чарт не разворачивает: это состояние, которое переживает -установку и требует собственного резервного копирования. +Базу и хранилище чарт не разворачивает: это данные, которые живут дольше +установки и требуют своего резервного копирования. --- @@ -52,13 +52,13 @@ kubectl create secret docker-registry regsecret \ --docker-email=not@used.com ``` -### 2. TLS-сертификат домена +### 2. TLS-сертификат ```bash kubectl create secret tls hrbox-tls --cert=cert.pem --key=key.pem ``` -Или выпустите сертификат через cert-manager и укажите его Secret +Либо выпустите сертификат через cert-manager и укажите его Secret в `ingress.tls.secretName`. ### 3. Ключи @@ -72,15 +72,15 @@ openssl rand -hex 16 openssl rand -base64 48 ``` -`secrets.encryptionKey` — единственный ключ, потеря которого необратима: -им зашифрованы пароли интеграций в базе. Сохраните его отдельно от кластера. +`secrets.encryptionKey` теряется безвозвратно: им зашифрованы пароли +интеграций в базе. Сохраните копию отдельно от кластера. -Ключи плеера `ticketHs256` и `sessionHs256` должны отличаться друг от друга — -это ключи разных контуров, и чарт откажется ставить одинаковые. +Ключи плеера `ticketHs256` и `sessionHs256` должны быть разными, одинаковые +чарт не примет. ### 4. Заполните `values.yaml` -Обязательный минимум: +Минимум: ```yaml ingress: @@ -121,7 +121,7 @@ secrets: servicesInternalSecret: "..." ``` -Всё остальное имеет рабочие значения по умолчанию. +У остальных параметров есть рабочие значения по умолчанию. Проверить конфигурацию, ничего не устанавливая: @@ -129,8 +129,7 @@ secrets: helm template hrbox .helm -f values.yaml > /dev/null ``` -Если чего-то не хватает, чарт скажет об этом текстом, а не оставит -неработающие поды в кластере. +Если чего-то не хватает, команда остановится и напишет, что заполнить. ### 5. Установка @@ -144,7 +143,7 @@ helm upgrade --install hrbox .helm -n hrbox --create-namespace -f values.yaml kubectl -n hrbox wait --for=condition=complete --timeout=30m job -l app=jobs ``` -### 7. Первый администратор +### 7. Создайте администратора ```bash kubectl -n hrbox exec -it deploy/hrbox-worker -- \ @@ -155,14 +154,13 @@ kubectl -n hrbox exec -it deploy/hrbox-worker -- \ ## Секреты -Чувствительные значения из `values.yaml` чарт складывает в один Secret -`hrbox-secrets` и монтирует контейнерам через `secretKeyRef` — в спеке -Deployment паролей нет. +Пароли и ключи из `values.yaml` чарт кладёт в один Secret `hrbox-secrets`, +поды читают их оттуда. В спеке Deployment паролей нет. Если секретами управляет внешний инструмент, создайте Secret сами и укажите -его имя в `secrets.existingSecret`. Ключи должны называться так: +имя в `secrets.existingSecret`. Ключи должны называться так: -| Ключ | Откуда берётся при `existingSecret: ""` | +| Ключ | Что кладут | |---|---| | `cookie-validation-key` | `secrets.cookieValidationKey` | | `encryption-key` | `secrets.encryptionKey` | @@ -180,63 +178,63 @@ Deployment паролей нет. | `slack-client-secret` | `app.slack.clientSecret` | | `slack-verification-token` | `app.slack.verificationToken` | -Ключи плеера лежат в отдельном Secret `hrbox-nanabush-player` — им управляет -`app.nanabushPlayer.deployment.existingSecret`. +Ключи плеера лежат в отдельном Secret `hrbox-nanabush-player`, за него +отвечает `app.nanabushPlayer.deployment.existingSecret`. --- ## Nanabush Player -Проигрыватель учебных материалов: SCORM 1.2 и 2004, cmi5, xAPI, нативные -курсы, тесты и эссе. +Плеер учебных материалов: SCORM 1.2 и 2004, cmi5, xAPI, нативные курсы, +тесты и эссе. -В облаке HRBox плеер живёт на отдельном поддомене. В on-premise выделенного -поддомена нет, поэтому плеер монтируется на основной домен под путём -`/nanabush-player`. Что из этого следует: +В облаке HRBox плеер стоит на отдельном поддомене. В on-premise поддомена +нет, поэтому плеер живёт на основном домене по пути `/nanabush-player`. +Из этого следуют две вещи: -- **Изоляции cookie нет.** Плеер и HRBox делят один origin, поэтому cookie - сессии HRBox отправляются браузером и на запросы к содержимому курсов, - включая сторонние SCORM-пакеты. Если нужна настоящая изоляция — - запрашивайте у HRBox схему с отдельным поддоменом. -- **Часы должны быть синхронизированы.** Launch-тикет живёт 5 минут, сессия - проверяется с допуском 2 минуты. Настройте NTP на нодах и сервере БД; - если это невозможно, выключите плеер (`app.nanabushPlayer.enabled: false`). +- **Cookie общие.** Плеер и HRBox на одном домене, поэтому cookie сессии + HRBox уходят и в запросы к содержимому курсов, включая сторонние + SCORM-пакеты. Если такая изоляция нужна, запросите у HRBox схему + с отдельным поддоменом. +- **Часы должны быть синхронны.** Launch-тикет живёт 5 минут, у сессии + допуск 2 минуты. Настройте NTP на нодах и сервере базы. Если это + невозможно, выключите плеер: `app.nanabushPlayer.enabled: false`. Порядок первого включения: -1. Убедитесь, что миграции применились: плееру нужны таблицы проекций. -2. Дождитесь, пока **все** поды `web` перейдут на новую ревизию. Смешанный - выкат при `rollout.percent > 0` может создать вторую активную попытку по - одному назначению. -3. Проверьте, что плеер отвечает сам, а не HRBox через общий маршрут: +1. Убедитесь, что миграции применились: плееру нужны свои таблицы. +2. Дождитесь, пока **все** поды `web` перейдут на новую версию. Пока часть + подов старая, при `rollout.percent > 0` по одному назначению может + открыться вторая попытка. +3. Проверьте, что отвечает именно плеер, а не HRBox через общий маршрут: ```bash kubectl -n hrbox exec deploy/hrbox-worker -- \ curl -s http://hrbox-nanabush-player:8094/readyz ``` - Настоящий плеер вернёт JSON. HTML означает, что запрос ушёл в HRBox. + Плеер вернёт JSON. HTML означает, что запрос ушёл в HRBox. -4. Проверьте публичный путь: `https://<домен>/nanabush-player/start`, - статику и API плеера под этим путём. +4. Откройте `https://<домен>/nanabush-player/start` и проверьте статику + и API плеера по этому пути. -Служебные пути (`/healthz`, `/readyz`, метрики) живут в корне и не зависят -от `basePath`. Порт метрик наружу не публикуется намеренно. +`/healthz`, `/readyz` и метрики отвечают в корне и от `basePath` не зависят. +Порт метрик наружу не публикуется. -Аварийное отключение — три независимых переключателя: +Быстро выключить плеер можно тремя переключателями: ```yaml app: nanabushPlayer: - enabled: false # пользователи возвращаются на старый проигрыватель + enabled: false # пользователи вернутся на старый проигрыватель ingress: enabled: false # маршрут снимается deployment: enabled: false # под останавливается ``` -Выключать нужно сверху вниз: чарт не даст оставить включённым маршрут -без пода или раскатку без маршрута. +Выключать нужно сверху вниз: чарт не даст оставить маршрут без пода +или включённый плеер без маршрута. Подробности контракта — `docs/nanabush-player-deployment.md` в репозитории HRBox. @@ -250,9 +248,9 @@ app: helm upgrade hrbox .helm -n hrbox -f values.yaml ``` -Миграции и обновление кластера запускаются автоматически как хуки -`post-upgrade` и выполняются строго по порядку. Имя джобы содержит номер -ревизии релиза, поэтому в `kubectl get jobs` видно, какой выкат её запускал. +Миграции и обновление кластера запускаются сами как хуки `post-upgrade`, +строго по очереди. В имени джобы есть номер ревизии релиза, поэтому +в `kubectl get jobs` видно, к какому выкату она относится. ### Масштабирование @@ -264,16 +262,16 @@ app: chatbox: 4 # по числу открытых вкладок ``` -`conductor` всегда в одном экземпляре: второй планировщик запустил бы -каждую задачу дважды. +`conductor` всегда в одном экземпляре: два планировщика запустят каждую +задачу дважды. -Для `web` при двух и более репликах создаётся PodDisruptionBudget -(`minAvailable: 75%`), чтобы drain ноды не уронил приложение целиком. +Для `web` от двух реплик создаётся PodDisruptionBudget (`minAvailable: 75%`), +чтобы вывод ноды из обслуживания не уронил приложение целиком. ### Конвертация видео -Выключена по умолчанию: профиль ресурсов у неё на порядок тяжелее обычной -обработки файлов. Включение: +Выключена по умолчанию, потому что требует заметно больше ресурсов, чем +остальная обработка файлов. Включение: ```yaml app: @@ -297,7 +295,7 @@ app: tempoUrl: "http://tempo.monitoring:4317" ``` -Рядом с `web` поднимется коллектор OpenTelemetry, PHP начнёт слать трейсы. +Рядом с `web` появится коллектор OpenTelemetry, PHP начнёт слать трейсы. ### Внешние Redis и NATS @@ -323,18 +321,18 @@ kubectl -n hrbox logs -l app=nanabush-player --tail=200 kubectl -n hrbox get jobs ``` -| Симптом | Куда смотреть | +| Что видно | Куда смотреть | |---|---| | Поды `web` не готовы | пробы `/healthz` и `/health-check`, логи контейнера `fpm` | -| Джоба миграций висит | init-контейнер `wait-postgres`: доступность и права в БД | -| Курс не открывается | логи `nanabush-player`, совпадение домена в `ingress.host` и `app.defaultTenantHostname` | +| Джоба миграций висит | init-контейнер `wait-postgres`: доступность базы и права | +| Курс не открывается | логи `nanabush-player`, совпадают ли `ingress.host` и `app.defaultTenantHostname` | | Не приходят уведомления | логи `goworker`, очереди NATS, `app.launchpad.topics` | -| Пользователей разлогинивает | перезапуски `dragonfly`: сессии живут в памяти | +| Пользователей разлогинивает | перезапуски `dragonfly`: сессии хранятся в памяти | --- -## Миграция с версии 2.x +## Обновление с версии 2.x -Часть ключей `values.yaml` переехала. Чарт проверяет старые имена и -останавливает установку с подсказкой, поэтому незамеченным ничего не -останется. Полный список изменений и порядок перехода — в [CHANGELOG.md](CHANGELOG.md). +Часть ключей `values.yaml` переехала. Старые имена чарт узнаёт и +останавливает установку с подсказкой, поэтому ничего не потеряется. +Список изменений и порядок перехода — в [CHANGELOG.md](CHANGELOG.md). From 3882081b79d7c7a319da323236b0ac9c62ad8d30 Mon Sep 17 00:00:00 2001 From: Dmitrii Sashchenko Date: Sat, 5 Sep 2026 19:10:43 +0600 Subject: [PATCH 3/3] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=80=D0=B8=D0=B4=D0=BC=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 35b3fbf..d8c11ef 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,7 @@ - SMTP-сервер; - доступ к реестру образов HRBox и токен HRBox Hub — выдают сотрудники HRBox. -Базу и хранилище чарт не разворачивает: это данные, которые живут дольше -установки и требуют своего резервного копирования. +Базу и S3 чарт не разворачивает: эти хранилища содержат постоянные данные и должны быть развёрнуты вручную. --- @@ -72,11 +71,9 @@ openssl rand -hex 16 openssl rand -base64 48 ``` -`secrets.encryptionKey` теряется безвозвратно: им зашифрованы пароли -интеграций в базе. Сохраните копию отдельно от кластера. +`secrets.encryptionKey` шифруют пароли, в случае его утери придётся заново задавать пароли в настройках интеграций. -Ключи плеера `ticketHs256` и `sessionHs256` должны быть разными, одинаковые -чарт не примет. +Ключи плеера `ticketHs256` и `sessionHs256` должны быть разными, одинаковые чарт не примет. ### 4. Заполните `values.yaml`