DEVOPS-117: чарт on-premise 3.0 — Nanabush Player, секреты в Secret, валидация values

Актуализация чарта по продовому .helm из HRBOX.

Nanabush Player перенесён из прода и упрощён под one-release-топологию
on-premise: без карт env/ci_dc, но с сохранёнными инвариантами (audience =
https://<host>, 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 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 16:37:42 +06:00
co-authored by Claude Opus 5
parent 6363382f09
commit efc506e5de
35 changed files with 3150 additions and 1118 deletions
+14 -8
View File
@@ -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
+38
View File
@@ -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 }}
@@ -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 }}
+151
View File
@@ -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 }}
+19 -6
View File
@@ -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 }}
+46
View File
@@ -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": [
{
"/": "/*"
}
]
}
]
}
}
+155 -70
View File
@@ -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;
}
}
}
}
+38
View File
@@ -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 }}
+9 -3
View File
@@ -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
sleep 2
done
+44 -22
View File
@@ -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 }}
- name: dragonfly
port: 6379
{{- end }}
+72 -59
View File
@@ -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
- name: client
port: 4222
- name: cluster
port: 6222
- name: monitor
port: 8222
- name: metrics
port: 7777
{{- end }}
+21
View File
@@ -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 }}
+100 -21
View File
@@ -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
protocol: TCP
+61
View File
@@ -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
-51
View File
@@ -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
+22 -9
View File
@@ -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
- name: VIDEO_CONVERTER_ENABLED
value: {{ .Values.app.videoConverter.enabled | quote }}
{{- include "hrbox.resources" (dict "ctx" . "component" "worker") | indent 10 }}
terminationGracePeriodSeconds: 600
+57 -19
View File
@@ -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 }}
{{- 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"
@@ -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 }}
+19 -10
View File
@@ -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 }}
+60
View File
@@ -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
-50
View File
@@ -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
+27 -12
View File
@@ -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
+41 -17
View File
@@ -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
+232
View File
@@ -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 }}
+44 -8
View File
@@ -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 }}
{{- end }}
{{- end }}
+26 -11
View File
@@ -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
-24
View File
@@ -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
@@ -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 }}
+72
View File
@@ -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 }}
+255 -166
View File
@@ -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 }}
{{- 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 }}
+211 -90
View File
@@ -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://<ingress.host> (origin без пути)
* basePath = отдельная переменная, в aud не входит
* issuer тикета = https://<ingress.host>/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 -}}
+35 -111
View File
@@ -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 }}
{{- end -}}
+582 -308
View File
File diff suppressed because it is too large Load Diff
+263
View File
@@ -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
+309 -43
View File
@@ -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).