Перейти к содержанию

Установка и развёртывание

Модуль CM Ocean Loyalty поставляется как набор контейнерных образов и Helm-чартов и разворачивается в Kubernetes. Поддерживается классическая установка в контуре с доступом к реестрам вендора, а также вариант on-prem (закрытый контур, air-gap), когда внешние реестры и CI/CD недоступны и поставка переносится оффлайн.

Состав поставки

Компонент Назначение Артефакт Ingress
loyalty-billing Backend: балльный леджер, акции, мониторинг, API /v1 образ loyalty + Helm-чарт loyalty-api префикс /v1
loyalty-ui Веб-интерфейс: разделы «Акции», «Клиенты», «Ручные операции», «Конфигурации программы», «Настройка ПЛ», «Мониторинг», «Отчёты», «Администрирование» образ loyalty-ui + Helm-чарт loyalty-ui префикс /
PostgreSQL БД леджера (миграции Liquibase при старте) управляемая Заказчиком или bundled-subchart (не для PROD) —
Keycloak OIDC: realm и клиентские роли LOYALTY_* управляемый Заказчиком (общий контур) —

Веб-интерфейс работает в одном из двух режимов (выбирается при установке):

  • Standalone — loyalty-ui как самостоятельный SPA на собственном ingress, с собственным входом в Keycloak. Минимальная поставка «только Loyalty».
  • Embedded в Core UI — loyalty-ui регистрируется микрофронтендом-ремоутом в портале Core UI, токен берётся от хоста. Требует развёрнутого Core UI (отдельная поставка контура CM Ocean). Разделы продукта попадают в левое меню хоста из манифеста навигации ремоута; «Администрирование» — раскрываемый пункт с подменю разделов («Справочники», «Профиль клиента», «Параметры уровня», «Партнёрские предложения», «Очередь приёма», «Журнал аудита», «Субсчета баллов», «Валюты», «Достижения», «Настройка акции»). В standalone-режиме те же разделы открывает подменю «Администрирование» собственного меню loyalty-ui; подписи в нём — заголовки экранов (например, «Настройка профиля клиента», «Настройка параметров уровня»).

Пререквизиты

Внешние зависимости

PostgreSQL и Keycloak — это инфраструктура контура Заказчика. Модуль предполагает, что они уже развёрнуты и доступны из кластера; их установка в объём поставки модуля не входит.

Инфраструктура:

  • Kubernetes ≥ 1.27, выделенный namespace; kubectl и Helm 3.12+ с доступом к кластеру.
  • Container registry, доступный из кластера (Harbor / Nexus / Docker Registry Заказчика).
  • Ingress-controller (nginx или иной) + DNS-записи на хосты loyalty-billing и loyalty-ui + TLS-сертификаты.
  • StorageClass (RWO PV) — только если БД ставится bundled.

PostgreSQL 15 или новее — на 15 работают стенды и выполняются интеграционные тесты в CI; миграции не используют возможностей новее PostgreSQL 15. Работа на более ранних версиях не проверяется:

  • БД billing (имя настраивается) и роль-владелец (например loyalty) с правами DDL — приложение само применяет Liquibase-миграции при старте.
  • Сетевая доступность из кластера; учётные данные передаются через k8s Secret.

Keycloak (общий realm контура):

  • Realm с OIDC issuer, доступным из кластера и из браузера.
  • Клиентские роли LOYALTY_* на клиенте входа frontend, группа LOYALTY_ADMINS и назначение операторам.
  • JWKS-эндпоинт доступен backend'у для проверки подписи JWT.

Подготовка ролей Keycloak

Полный перечень ролей LOYALTY_*, состав группы администраторов и порядок их заведения — в разделе «Ролевая модель» → «Полный перечень ролей». Для интеграции Promo → Loyalty сервисному клиенту Promo выдаётся роль LOYALTY_INTEGRATION_WRITE (client-credentials).

Каждый релиз с новыми ролями требует повторного импорта

Новая версия модуля может добавлять роли LOYALTY_* (новые экраны и действия). На уже провижиненном realm заведение ролей и добавление их в группу LOYALTY_ADMINS выполняется повторным запуском импортирующего скрипта Keycloak — сам по себе helm upgrade роли не заводит. Если после обновления модуля новое действие интерфейса отвечает 403 access_denied при формально достаточных правах — сначала проверьте, заведена ли роль в Keycloak.

Redis и Kafka (опционально)

Обе зависимости — не часть обязательной поставки: модуль стартует и работает без них, подключаются контур за контуром, когда нужны. Исключение — ручные операции: для них подключение к Kafka рекомендуется (см. предупреждение ниже).

Зависимость Зачем Как включается
Redis Кеш баланса счёта (снижает нагрузку на БД при чтении баланса под высокой частотой запросов) LOYALTY_BALANCE_CACHE_ENABLED=true + строка подключения Spring Data Redis (spring.data.redis.*)
Kafka Приём команд изменения баланса из очереди сообщений; транспорт чанков пакетов ручных операций; отправка событий смены уровня и уведомлений клиентам LOYALTY_INTAKE_KAFKA_ENABLED=true / LOYALTY_MANUAL_OPERATIONS_KAFKA_ENABLED=true / LOYALTY_TIER_EVENTS_ENABLED=true / LOYALTY_NOTIFICATIONS_ENABLED=true + LOYALTY_KAFKA_BOOTSTRAP и SASL-учётные данные (см. «Конфигурация»)

Без этих переменных модуль не пытается подключиться ни к Redis, ни к Kafka при старте. Каждый Kafka-транспорт включается отдельно; для него на брокере заводятся топик и ACL (для потребителей — и на группу потребителя). В примерах значений чарта для dev, qa и prod приём команд и транспорт чанков ручных операций включены, строка JAAS берётся из k8s-секрета loyalty-kafka.

Ручные операции: рекомендуется подключение к Kafka

Без Kafka (LOYALTY_MANUAL_OPERATIONS_KAFKA_ENABLED=false, значение по умолчанию) пакеты ручных операций выполняются через очередь в памяти приложения в один поток. При перезапуске пода неисполненные части пакета пропадают вместе с процессом; их заново публикует подхват брошенных пакетов (LOYALTY_MANUAL_OPERATIONS_RESUME_ENABLED, включён по умолчанию) — на старте приложения и затем раз в минуту, когда пакет не подаёт признаков жизни дольше 5 минут. Уже проведённые строки повторно не проводятся, но выполнение пакета прерывается и продолжается с задержкой. Для контуров, где используются ручные операции, включите транспорт через Kafka: LOYALTY_MANUAL_OPERATIONS_KAFKA_ENABLED=true, LOYALTY_KAFKA_BOOTSTRAP и SASL-учётные данные (см. «Конфигурация», раздел «Ручные операции»).

Базовые параметры чарта

Дефолтные значения чарта loyalty-api (переопределяются в values-prod.yaml):

Параметр Значение по умолчанию Назначение
service.image.repository образ loyalty из реестра вендора образ backend (в PROD — реестр Заказчика)
service.image.tag latest тег релиза
service.ingress.uriPrefix /v1 глобальный префикс API; ingress форвардит путь без обрезки
service.ingress.host loyalty-billing-{{ .Values.env }} хост публикации
service.resources.requests CPU 50m / RAM 500Mi запрос ресурсов на под backend
service.resources.limits CPU 2000m / RAM 2Gi лимит ресурсов на под backend — с запасом на разбор файла ручных операций (до 20 МБ) и вставку сотен тысяч строк в том же поде, что и онлайн-операции
service.securityContext.readOnlyRootFilesystem true корневая файловая система контейнера только на чтение (требование к приложениям в закрытом контуре)
service.persistentVolumes.tmpVolume emptyDir (tmpfs) на /tmp записываемый /tmp поверх readOnlyRootFilesystem: без него не работает multipart-загрузка файла ручных операций; содержимое занимает память пода в пределах spring.servlet.multipart.max-request-size
service.livenessProbe / readinessProbe / startupProbe GET /health/liveness / /health/readiness на порту 9090 readiness включает проверку БД; startup даёт запас на применение миграций Liquibase при старте на засеянном стенде
service.terminationGracePeriodSeconds 120 плавная остановка (см. предупреждение ниже)
service.metrics.enabled true включает ServiceMonitor и порт 9090 в Service — без этого метрики физически не попадают в Prometheus
postgresql.enabled true bundled-PostgreSQL только для стендов; в PROD — false

Bundled-PostgreSQL не для PROD

Встроенный subchart PostgreSQL с литеральными паролями — это удобство для dev/qa-стендов. Для PROD задавайте postgresql.enabled=false, направляйте приложение на управляемую БД Заказчика и передавайте учётные данные через k8s Secret. Литеральные пароли из дефолтного values.yaml в PROD использовать запрещено.

Плавная остановка ограничена фактическим grace period пода

Приложение поддерживает плавную остановку (server.shutdown: graceful) и ждёт завершения начатого до LOYALTY_SHUTDOWN_TIMEOUT (по умолчанию 20 с). Чарт зависит от платформенного base-chart uniform-service версии 1.0.29, которая рендерит terminationGracePeriodSeconds (120 с) в под; на более ранних версиях base-chart ключ не рендерился, и под получал 30 с по умолчанию Kubernetes. Если на контуре используется другая версия base-chart, проверьте значение на живом поде (kubectl get pod -o jsonpath='{.spec.terminationGracePeriodSeconds}'): поднимать LOYALTY_SHUTDOWN_TIMEOUT выше фактического значения нельзя — иначе Kubernetes завершит процесс посреди остановки.

Чарт поставляется с примерами values.dev.yaml / values.qa.yaml / values.prod.yaml — шаблонами структуры extra_vars под три типовых контура; для контура Заказчика они не подставляются автоматически и служат образцом для собственного values-prod.yaml. В примерах явно задан пояс среды исполнения TZ=UTC и пояс программы LOYALTY_PROGRAM_TIMEZONE, расписания сгорания и предупреждений (0 30 0 * * * и 0 0 9 * * * — в поясе программы), шаги предупреждения P7D,P1D и уборка журналов идемпотентности и сегментов — см. «Конфигурация».

Установка через Helm

Backend подключается к БД и Keycloak через переменные окружения, заданные в values-prod.yaml:

env: prod
service:
  image:
    repository: registry.client.local/loyalty/loyalty   # реестр Заказчика
    tag: "1.1.0"
  ingress:
    host: "loyalty-billing.client.local"                # + TLS
  extra_vars:
    - { name: LOYALTY_DB_URL,          value: "jdbc:postgresql://pg.client.local:5432/billing" }
    - { name: LOYALTY_DB_USERNAME,     valueFrom: { secretKeyRef: { name: loyalty-db, key: username } } }
    - { name: LOYALTY_DB_PASSWORD,     valueFrom: { secretKeyRef: { name: loyalty-db, key: password } } }
    - { name: LOYALTY_OIDC_ISSUER_URI, value: "https://auth.client.local/realms/cmocean" }
    - { name: LOYALTY_OIDC_CLIENT_ID,  value: "frontend" }
    - { name: LOYALTY_CORS_ALLOWED_ORIGINS, value: "https://loyalty-ui.client.local" }
    - { name: LOYALTY_PROGRAM_TIMEZONE, value: "Europe/Moscow" }   # пояс программы (IANA)
    - { name: TZ,                      value: "UTC" }              # пояс среды исполнения
postgresql:
  enabled: false        # используем управляемую БД Заказчика

Установка backend и UI:

helm upgrade --install loyalty-api ./loyalty-api-1.0.0.tgz \
  -n loyalty-billing --create-namespace -f values-prod.yaml

helm upgrade --install loyalty-ui ./loyalty-ui-1.0.0.tgz \
  -n loyalty-billing -f values-ui-prod.yaml

Развёртывание on-prem (air-gap)

Для закрытого контура без доступа к реестрам и CI/CD вендора поставка переносится оффлайн.

flowchart LR
  A["Образы и чарты<br/>вендора"] --> B["tarball'ы образов<br/>+ self-contained .tgz"]
  B -->|носитель / шлюз| C["Реестр Заказчика<br/>+ namespace"]
  C --> D["helm upgrade --install<br/>backend + UI"]

Ключевые шаги:

  1. Образы в tarball'ы. Образы loyalty и loyalty-ui выгружаются через skopeo copy или docker save в файлы-архивы и переносятся в контур.
  2. Самодостаточные чарты. Зависимости чарта (платформенный base-chart и, опционально, PostgreSQL) вендорятся внутрь .tgz командами helm dependency build и helm package, чтобы установка не обращалась к chart-репозиториям вендора.
  3. Загрузка в реестр Заказчика через skopeo copy или docker load/tag/push. Для приватного реестра в namespace создаётся imagePullSecret.
  4. Подготовка БД и Keycloak в контуре Заказчика (роль и БД billing, роли LOYALTY_*).
  5. Установка чартов loyalty-api и loyalty-ui из локальных .tgz (см. раздел выше).

Соответствие требованиям закрытого контура

Путь air-gap не требует облачных SaaS: образы и чарты переносятся оффлайн, БД и Keycloak — в контуре Заказчика, секреты хранятся в k8s Secret / Vault, авторизация — deny-by-default с ролями LOYALTY_*.

Проверка после установки

kubectl -n loyalty-billing get pods,ingress

# health / metrics backend (порт управления 9090):
kubectl -n loyalty-billing port-forward deploy/loyalty-billing 9090:9090 &
curl -s localhost:9090/health         # {"status":"UP"} — readiness включает проверку БД
curl -s localhost:9090/metrics | head # Prometheus-метрики

Признак успешной миграции — запись Liquibase: Update has been successful в логах backend. Веб-интерфейс открывается по адресу loyalty-ui (standalone) — экран «Акции» отображает данные из backend.

Демо-данные только для стендов

В PROD не задавайте контекст Liquibase demo — он сидит демонстрационные данные и предназначен исключительно для тестовых стендов. На базе, уже переведённой на единый справочник валют (см. «Модель данных»), контекст demo падает при попытке вставки в таблицу point_currency — этой таблицы больше нет. Включайте демо-контекст только на чистой базе.

Индекс по валюте счёта на засеянном контуре

На контуре с уже накопленными данными индекс account(currency_id) (появился вместе с единым справочником валют) строится с блокировкой записи в таблицу счетов. На большой базе создавайте его заранее конкурентно (CREATE INDEX CONCURRENTLY) вне окна пиковой нагрузки, а не полагайтесь на автоматическую миграцию при старте.

Состав ПО (SBOM)

Образ backend несёт внутри себя перечень состава ПО в формате CycloneDX (файл /opt/app/sbom.json) — все зависимости приложения с версиями и лицензиями. Файл собирается вместе с образом (а не отдельным сторонним обходом дерева зависимостей), поэтому всегда соответствует тому, что реально попало в конкретный выкат:

kubectl -n loyalty-billing exec deploy/loyalty-billing -- cat /opt/app/sbom.json

Используйте его для подтверждения состава поставляемого ПО и лицензионных условий заказчику (в том числе для проверки отсутствия конкретных технологий, если это требование контролирующего органа).

Дополнительно в CI обоих компонентов — backend и веб-интерфейса — выполняется шаблонная джоба generate-sbom-fs: перечень зависимостей по манифестам репозитория (у loyalty-ui — по package-lock.json) в формате CycloneDX публикуется отчётом GitLab в пайплайнах веток и merge request. Внутрь образа loyalty-ui файл состава не кладётся — для веб-интерфейса источник состава ПО этот отчёт CI.

Перевод контура с накопленными данными на партиционированный леджер

Леджер партиционирован помесячно (см. «Архитектура» → «Леджер: время проводки и партиционирование»). На пустой базе миграция применяется автоматически при старте — отдельных действий не требуется.

На контуре, где в таблице проводок уже есть данные (обновление с версии до перехода на эту модель), схему переводит отдельный эксплуатационный скрипт deploy/sql/ledger-time-model-swap.sql, выполняемый до деплоя новой версии, в согласованное окно обслуживания:

psql "$LOYALTY_DB_URL" -v ON_ERROR_STOP=1 -f deploy/sql/ledger-time-model-swap.sql

Обязательное окно обслуживания

Финальный шаг скрипта (подмена родительской таблицы) блокирует запись не только в леджер, но и в таблицу счетов на всё время выполнения — то есть полностью останавливает приём операций. Длительность окна не зависит от объёма накопленных данных (переливки строк нет — таблица отцепляется от старого родителя и прицепляется к новому), но окно нужно планировать вне нагрузки. Все проверки, которые можно выполнить заранее, скрипт делает до блокирующего шага; после него отката на предыдущую схему нет.

Если запустить деплой на контуре с данными, минуя скрипт, — старт приложения останавливается сразу с понятным сообщением (проверка на старте требует наличия колонки posted_at), а не зависает и не даёт логическую ошибку впоследствии.

Обновление и откат

  • Обновление: загрузить новый образ и выполнить helm upgrade --install с новым тегом — Liquibase накатит новые миграции при старте. Если релиз добавляет роли LOYALTY_* — повторно выполнить импорт ролей Keycloak (см. предупреждение выше).
  • Откат: helm rollback <release> <REVISION>. При несовместимой миграции потребуется восстановление БД из резервной копии.
  • Резервное копирование БД (pg_dump billing) выполняется по расписанию согласно требованиям к RTO/RPO контура Заказчика.

Umbrella-чарт — в дорожной карте

Сейчас чарты loyalty-api и loyalty-ui устанавливаются раздельно. Объединяющий umbrella-чарт cm-ocean-loyalty, ставящийся одной командой с едиными values, находится в дорожной карте и пока не реализован.

Что учесть при обновлении на текущую версию

  • Новые роли. Версия добавляет роли LOYALTY_PROMOTION_ACTIVATE, LOYALTY_PROMOTION_APPROVE, LOYALTY_PROGRAM_CONFIG_*, LOYALTY_CLIENT_PROFILE_ACTIVATE, LOYALTY_CLIENT_PROFILE_DELETE, LOYALTY_SEGMENT_*, LOYALTY_NOTIFICATION_*, LOYALTY_CITY_*, LOYALTY_STORE_*, LOYALTY_MERCHANT_*, LOYALTY_PRODUCT_CATALOG_*, LOYALTY_ACHIEVEMENT_* — перезапустите импорт ролей Keycloak до выкатки. Запуск акции теперь требует LOYALTY_PROMOTION_ACTIVATE (права LOYALTY_PROMOTION_WRITE недостаточно) — выдайте её тем, кто запускает акции. LOYALTY_SEGMENT_INTAKE в группу LOYALTY_ADMINS не входит и выдаётся только сервисной учётной записи системы, заливающей сегменты.
  • Согласование запуска акции. Настройка включается парой переменных: LOYALTY_PROMOTION_APPROVAL_ENABLED бэкенда и PROMOTION_APPROVAL_ENABLED веб-интерфейса — API значение флага не отдаёт, и интерфейс узнаёт его только из своего runtime-конфига (см. «Конфигурация»).
  • Изменения контракта для интеграций. Код торговой точки в покупке стал обязательным (storeCode в чеке POS, externalStoreCode в POST /v1/events/purchase), поле segments события покупки больше не учитывается, отказ валидации тела интеграционных запросов приходит кодом validation_failed вместо bad_request — предупредите владельцев каналов до выкладки (см. «Интеграции»).
  • Расписания в поясе программы. LOYALTY_EXPIRY_CRON, LOYALTY_EXPIRY_NOTIFY_CRON и расписания выгрузок теперь трактуются в поясе программы, а не в поясе среды исполнения (в образе — UTC): значение 0 0 9 * * * теперь означает 09:00 по поясу программы. Пересмотрите расписания контура.
  • Разведённые системные счета. Миграция заводит счета списания RDM и сгорания BRK, и новые проводки списания и сгорания идут на них, если номера счетов не переопределены переменными окружения или в операционной конфигурации — переопределения миграция не трогает; прежние проводки остаются на MRK (см. «Конфигурация»). Свойства loyalty.promotion.budget.point-currency и customer.default.currency больше не читаются — их можно убрать из значений контура.
  • Окно раскатки. Версия удаляет из схемы неиспользуемые колонки. Пока в кластере живут поды прежней версии, часть запросов у них отвечает ошибкой — возвраты по чеку и их отмена, слияние клиентов, поиск, архивация, удаление и заливка сегментов, синхронные пакеты ручных операций, справочник «Системные счета». Окно закрывается готовностью новых подов; планируйте раскатку вне пиковой нагрузки.
  • Безвозвратная очистка значений атрибутов. Миграция связывает значения кастомных полей карточки клиента со схемой полей и удаляет значения по ключам, которых в схеме нет. Перед выкаткой проверьте, что будет удалено: SELECT a.field_key, count(*) FROM customer_attribute a WHERE NOT EXISTS (SELECT 1 FROM client_profile_field f WHERE f.field_key = a.field_key) GROUP BY 1.
  • Короткое ожидание блокировки. Миграция удаления колонки баланса ждёт блокировку таблицы счетов не дольше 5 секунд: если в этот момент идёт долгое чтение (например, выгрузка остатков в DWH), старт пода прерывается и повторяется перезапуском, частично ничего не применяется.
  • Проверки данных, отложенные до исправления. Если в базе уже есть партия баллов, у которой источник срока не согласован с моментом сгорания, или вручную заполнена колонка иерархии плана счетов, сервис стартует, а в журнал старта пишется сообщение о том, какие строки нужно исправить; проверка повторяется на каждом старте, пока данные не исправлены.
  • Фоновые задачи после выкладки. Разовый прогон открывает счета клиентам во всех включённых валютах (см. «Конфигурация» → «Открытие счетов в новых валютах»). Сегментные акции не применяются ни к кому до первой заливки сегментов из внешней системы: на демо-стенде демо-набор сегментов загружается скриптом deploy/demo/load-demo-segments.sh сразу после выкладки (нужна учётная запись с ролью LOYALTY_SEGMENT_INTAKE). Пересмотр уровней (LOYALTY_TIER_REVIEW_CRON) по умолчанию выключен и в примерах чарта задан только для dev: без него уровни клиентов не понижаются (см. «Конфигурация» → «Уровни: пересмотр и событие смены уровня»).