Установка и развёртывание¶
Модуль 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"]
Ключевые шаги:
- Образы в tarball'ы. Образы
loyaltyиloyalty-uiвыгружаются черезskopeo copyилиdocker saveв файлы-архивы и переносятся в контур. - Самодостаточные чарты. Зависимости чарта (платформенный base-chart и, опционально,
PostgreSQL) вендорятся внутрь
.tgzкомандамиhelm dependency buildиhelm package, чтобы установка не обращалась к chart-репозиториям вендора. - Загрузка в реестр Заказчика через
skopeo copyилиdocker load/tag/push. Для приватного реестра в namespace создаётсяimagePullSecret. - Подготовка БД и Keycloak в контуре Заказчика (роль и БД
billing, ролиLOYALTY_*). - Установка чартов
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) — все зависимости приложения с версиями и лицензиями. Файл собирается вместе
с образом (а не отдельным сторонним обходом дерева зависимостей), поэтому всегда соответствует
тому, что реально попало в конкретный выкат:
Используйте его для подтверждения состава поставляемого ПО и лицензионных условий заказчику (в том числе для проверки отсутствия конкретных технологий, если это требование контролирующего органа).
Дополнительно в CI обоих компонентов — backend и веб-интерфейса — выполняется шаблонная джоба
generate-sbom-fs: перечень зависимостей по манифестам репозитория (у loyalty-ui — по
package-lock.json) в формате CycloneDX публикуется отчётом GitLab в пайплайнах веток и merge request.
Внутрь образа loyalty-ui файл состава не кладётся — для веб-интерфейса источник состава ПО этот отчёт CI.
Перевод контура с накопленными данными на партиционированный леджер¶
Леджер партиционирован помесячно (см. «Архитектура» → «Леджер: время проводки и партиционирование»). На пустой базе миграция применяется автоматически при старте — отдельных действий не требуется.
На контуре, где в таблице проводок уже есть данные (обновление с версии до перехода на эту модель),
схему переводит отдельный эксплуатационный скрипт 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: без него уровни клиентов не понижаются (см. «Конфигурация» → «Уровни: пересмотр и событие смены уровня»).