docs: split integrator guide (Diplodoc) vs internal docs + add build-docs CI

Чёткое разделение audiences:
- docs/ — consumer-facing only, Diplodoc-built. Public contract для
  интеграторов (Альтум, BI, third-party).
- docs-internal/ — operator runbooks, status snapshots, legacy tech pages.
  НЕ в Diplodoc build, НЕ публикуются.

Изменения:
- Move docs/{ops,status,tech} → docs-internal/{ops,status,tech}
- docs/toc.yaml: убраны секции Operations, Архитектура (legacy), Status
- docs/index.md: убран tab "оператор / on-call", focus на integrators
- docs/README.md: rewritten — scope только integrator guide, editorial
  guidelines (no leak internal architecture, stable API contract)
- .yfm: убран явный ignore status/* (теперь не нужно — папка переехала)
- docs-internal/README.md: index для внутренней документации с rationale
  разделения

Scrubbing implementation details из Diplodoc страниц:
- events.md: убраны Strimzi/cluster.142 references, OutboxPoller детали,
  internal alert names (OrdinisOutboxLagHigh, OrdinisOutboxDLQNonEmpty),
  metric names (nsi_outbox_*). Заменены на consumer-observable contract.
- webhooks.md: убраны metric names (nsi_webhook_ssrf_rejected_total),
  alert table (OrdinisWebhookHighFailureRate и т.д.). Retry policy
  переписана на user-side perspective.
- errors.md: убран alert name OrdinisReadApiErrorBudgetBurnFast, Tempo
  endpoint URL, internal connection details.
- x-references.md: OrphanReferenceScanner → general "Ordinis периодически
  детектирует". Убраны metric/alert names. Cascade roadmap без link на
  internal design doc.
- auth.md: убран file path ScopeContext.java.
- bundle-cuod.md: убран Maven module path.
- caching.md: cascade design link заменён на text reference.

CI:
- .gitlab-ci.yml: новый job build-docs (stage: build) — Node 20 alpine,
  pnpm install --frozen-lockfile + pnpm build, artifact docs/_dist на
  неделю. Triggers: auto на docs/**, либо manual web. Готов к follow-up
  pages-deploy job когда host решён.
- Новый pattern .docs-changes для rules.

Build verified clean: 13 HTML pages, 6.9M, no broken links/refs.
This commit is contained in:
Zimin A.N.
2026-05-07 23:27:03 +03:00
parent d5268b9395
commit bcfb07f547
23 changed files with 232 additions and 215 deletions
-1
View File
@@ -8,5 +8,4 @@ strict: false
ignore:
- node_modules
- _dist
- status/*-state.md
addSystemMeta: true
+54 -74
View File
@@ -1,42 +1,44 @@
# Ordinis docs (Diplodoc / YFM)
# Ordinis Integrator Guide (Diplodoc / YFM)
Документация Ordinis НСИ ЦУОД ОДХ построена через **Diplodoc** (open-source
documentation platform от Yandex). Markdown source — YFM (Yandex Flavored
Markdown) с расширениями для tabs, notes, includes, mermaid диаграмм.
Consumer-facing документация Ordinis НСИ ЦУОД. Build через **Diplodoc**
(open-source documentation platform от Yandex), source — YFM (Yandex
Flavored Markdown).
**Scope:** только то, что нужно интеграторам (Альтум team, BI, third-party
consumers). Operator runbooks, архитектурные deep-dives и project state —
во внутренней документации в [`../docs-internal/`](../docs-internal/),
не в Diplodoc build.
## Структура
```
docs/
├── package.json # @diplodoc/cli + scripts
├── .yfm # Diplodoc config (langs, plugins)
├── toc.yaml # Содержание / навигация (рендерится в sidebar)
├── index.md # Главная страница
── integration/ # Интеграция по API
├── api/ # Главы для consumers
│ ├── index.md # Обзор
│ ├── auth.md # Keycloak / JWT
│ ├── read-endpoints.md
│ ├── events.md # Kafka outbox
│ ├── webhooks.md
│ ├── bitemporal.md
│ ├── x-references.md
│ ├── caching.md
│ ├── errors.md
│ ├── best-practices.md
│ └── bundle-cuod.md
└── altum-imaging-order.md # Прикладной сценарий
├── ops/ # Operator runbook
│ ├── README.md
│ ├── slo.md
│ ├── outbox-dlq.md
│ ├── db-migrations.md
│ ├── vault-unseal.md
│ └── kafka-restart.md
── status/ # State snapshots (исключено из build)
│ └── *-state.md
└── tech/ # Legacy технические страницы
└── api-integration.md
├── package.json # @diplodoc/cli + build scripts
├── .yfm # Diplodoc config
├── toc.yaml # Содержание / sidebar
├── index.md # Главная (integrator-focused tabs)
── integration/
├── api/ # 11 глав про API integration
│ ├── index.md
│ ├── auth.md
│ ├── read-endpoints.md
│ ├── events.md
│ ├── webhooks.md
│ ├── bitemporal.md
│ ├── x-references.md
│ ├── caching.md
│ ├── errors.md
│ ├── best-practices.md
│ └── bundle-cuod.md
└── altum-imaging-order.md
```
Внутренние материалы (НЕ в Diplodoc build):
```
docs-internal/
├── ops/ # Operator runbook (on-call)
├── status/ # Project state snapshots
── tech/ # Legacy technical pages
```
## Сборка
@@ -48,62 +50,40 @@ cd docs
pnpm install
pnpm build # → ./_dist (HTML)
pnpm serve # → http://localhost:8088
# либо одной командой:
pnpm dev
pnpm dev # build + serve одной командой
```
Build output (`_dist/`) — статический HTML, можно деплоить в:
Build output (`_dist/`) — статический HTML, deployable в:
- GitLab Pages
- S3 + CloudFront
- nginx/ingress на cluster.142
- S3 / object storage
- nginx/ingress на k8s cluster
## Автор содержимого
## Editorial guidelines
| Раздел | Owner |
|---|---|
| `/` (index, integration/api/*) | zimin.an@nstart.space |
| `ops/*` | on-call rotation team |
| `integration/altum-imaging-order.md` | Альтум team contact |
Этот set документации — **public contract** для интеграторов:
## Что не committable
1. **Не leak'аем internal architecture** — никаких internal alert names,
service paths, internal infrastructure URLs.
2. **API contract стабилен** — изменения требуют semver bump + announcement.
3. **Consumer-side examples** для каждой концепции (Python, Node, либо Spring).
4. **Версионирование** через bundle / API major.
- `_dist/` — build output (gitignore'нут)
- `node_modules/` — dependencies (gitignore'нут)
## Deployment
В roadmap — GitLab CI job `build-docs`:
```yaml
build-docs:
stage: build
image: node:18
script:
- cd docs
- pnpm install --frozen-lockfile
- pnpm build
artifacts:
paths: [docs/_dist]
expire_in: 1 week
rules:
- changes: ["docs/**/*"]
```
Затем deploy в GitLab Pages либо как отдельный k8s deployment с nginx.
Для operator-side / dev-side материалов — `docs-internal/` либо design docs
в `~/.gstack/projects/claude/`.
## YFM extensions используются
- `{% list tabs %}` — табы для switching между audiences
- `{% note info|warning|tip %}` — выделенные блоки
- `mermaid` code blocks — диаграммы (graph LR, gantt)
- Cross-links в `toc.yaml` — sidebar navigation
- `varsPreset: external` — для shared includes (TBD when нужно)
- `toc.yaml` cross-links — sidebar navigation
## Roadmap
- [x] Базовая структура integration guide (11 страниц)
- [ ] CI job `build-docs` в `.gitlab-ci.yml`
- [ ] GitLab Pages deployment
- [ ] Search через Diplodoc search plugin
- [ ] EN translation (i18n via `langs:` в `.yfm`)
- [ ] GitLab Pages / k8s ingress deployment
- [ ] Search plugin
- [ ] EN translation (через `langs:` в `.yfm`)
- [ ] Includes для shared snippets (auth examples, error tables)
- [ ] Versioning — separate doc trees per Ordinis major version
- [ ] Versioning — separate doc trees per API major version
+11 -14
View File
@@ -1,6 +1,8 @@
# Ordinis НСИ ЦУОД ОДХ
# Ordinis НСИ ЦУОД ОДХ — Integrator Guide
Документация для разработчиков, интеграторов и операторов.
Документация для команд, интегрирующих свои сервисы с Ordinis НСИ ЦУОД.
Сюда не входят operator runbooks и архитектурные deep-dives — те материалы
во внутренней документации проекта.
**Anchor v1 — production LIVE на cluster.142** (с 2026-05-07).
Альтум consumers подключены, blue/green cutover завершён.
@@ -26,14 +28,10 @@
- [Bundle cuod](integration/api/bundle-cuod.md) — список 40 справочников ЦУОД
- [Best practices](integration/api/best-practices.md) — частые ошибки
- Я — оператор / on-call
- Я обрабатываю ошибки
- [Runbook indexed by symptom](ops/README.md)
- [SLO + alert thresholds](ops/slo.md)
- [Outbox DLQ](ops/outbox-dlq.md)
- [Database migrations](ops/db-migrations.md)
- [Vault unseal](ops/vault-unseal.md)
- [Kafka restart](ops/kafka-restart.md)
- [Ошибки и rate limits](integration/api/errors.md) — error codes, retry strategy
- [Лучшие практики](integration/api/best-practices.md) — robust integration
{% endlist %}
@@ -41,15 +39,14 @@
| Среда | URL | Auth | Назначение |
|---|---|---|---|
| **prod** | `https://ordinis.k8s.264.nstart.cloud/` | JWT обязателен | Альтум real users |
| **prod** | `https://ordinis.k8s.264.nstart.cloud/` | JWT обязателен | Real users |
| staging | `https://ordinis.k8s.265.nstart.cloud/` | optional (legacy mode) | Dev environment |
| local | `http://localhost:8080` | off | docker-compose dev |
## Поддержка
- Issues / bug reports: `git.nstart.cloud:2-6/2-6-4/terravault/ordinis`
- Эскалация: `zimin.an@nstart.space`
- Запросы новых ролей / scope в Keycloak: тот же email.
- Запросы новых ролей / scope в Keycloak: `zimin.an@nstart.space`
- Bug reports / feature requests: через issue tracker (запросить URL у Ordinis team)
## Версионирование
@@ -59,4 +56,4 @@ API стабилизирован под v1 — breaking changes только ч
Bundle справочников ЦУОД versioned отдельно (`cuod-bundle:1.2.1`). Изменение
schemas — minor bump, добавление словарей — minor bump, удаление — major bump
(никогда без уведомления consumers через `nsi.dictionary.changed` event).
(никогда без уведомления consumers через `record.closed` events).
+1 -1
View File
@@ -69,7 +69,7 @@ Ordinis использует трёхуровневый доступ к слов
Альтернативный синтаксис ролей (для compat с другими сервисами в realm):
суффикс `-PUBLIC|INTERNAL|RESTRICTED` (например `altum-PUBLIC`,
`geoportal-INTERNAL`). Маппинг определяется в `ordinis-auth/.../ScopeContext.java`.
`geoportal-INTERNAL`). Маппинг встроен в auth layer Ordinis.
{% note info %}
+3 -2
View File
@@ -77,8 +77,9 @@ graph LR
I --supportedLevelCodes--> PL
```
(Это subset, для full графа см. отдельные schemas в коде:
`ordinis-cuod-bundle/src/main/resources/cuod/schemas/`.)
(Это subset. Полный граф можно получить через `GET /dictionaries` +
inspection schemas через `GET /dictionaries/{name}` — каждый возвращает
JSON Schema с x-references маркерами.)
## Типичные сценарии
+6 -4
View File
@@ -24,7 +24,10 @@ useQuery({
})
```
### Server-side кэш (Caffeine + Spring)
### Server-side кэш (на consumer side)
Любая стандартная in-memory cache library с TTL — Caffeine, Guava,
Redis с EXPIRE и т.д. Пример (Spring + Caffeine):
```java
@Cacheable(value = "ordinis-records", key = "#dictionary + ':' + #lang")
@@ -170,14 +173,13 @@ referencing records. Consumer должен слушать events и invalidate
def on_event(event):
cache.delete(f"ordinis:{event['dictionary']}:byKey:{event['businessKey']}")
# NEW v2: cascade source — заодно invalidate dependents
# NEW v2: cascade source — заодно invalidate источник
if event.get('cascadeSource'):
# Источник тоже нужно invalidate (мы видим только закрытие dependent)
src = event['cascadeSource']
cache.delete(f"ordinis:{src['dictionary']}:byKey:{src['businessKey']}")
```
См. design doc `dict-relationships-v2` (внутренний artifact).
Spec появится перед deployment v2.
## Stale cache на 5xx / timeout
+8 -8
View File
@@ -97,9 +97,9 @@ Ordinis следует стандартам REST с расширениями:
| Code | Описание |
|---|---|
| `internal_unexpected` | Server bug — escalate с traceId |
| `db_unavailable` | Postgres недоступен |
| `vault_unavailable` | Vault sealed либо unreachable |
| `kafka_publish_failed` | Outbox не смог publish (но запись в БД OK — eventually consistent) |
| `db_unavailable` | БД недоступна — retry с long backoff |
| `dependent_unavailable` | Зависимый сервис (config, secrets) недоступен — retry |
| `event_publish_failed` | Запись в БД ОК, но события не отправлены — eventually consistent (events доедут позже) |
## Rate limits
@@ -114,9 +114,8 @@ limits через resource constraints:
| Concurrent JWT verifications | bounded heap (~5k/sec практический максимум) |
При превышении (типично DDoS-like patterns):
- HTTP `429 Too Many Requests` после ingress rate limit (ingress nginx default).
- HTTP `429 Too Many Requests` после ingress rate limit.
- HTTP `503 Service Unavailable` при exhausting connection pool.
- `5xx error rate spike` → alert `OrdinisReadApiErrorBudgetBurnFast`.
### Recommended client-side limits
@@ -155,13 +154,14 @@ def retry_with_backoff(
## Tracing
Каждый response имеет `X-Trace-Id` header и `traceId` field в error body.
Ordinis эмитит OpenTelemetry traces в Tempo (`tempo.monitoring:4317`).
При эскалации Ordinis team:
1. Скопируй `traceId` из response либо логов.
2. Создай issue с воспроизведением.
3. Ordinis team query'ит Tempo: `traceId="abc123-..."`.
4. Trace покажет full path: ingress → read-api → postgres → response.
3. Ordinis team поднимет trace по этому ID — full path запроса виден.
Без `traceId` investigation занимает существенно больше времени —
обязательно сохраняй его при ошибках.
## Дальше
+28 -56
View File
@@ -12,53 +12,27 @@ Ordinis публикует все изменения справочников в
| `ordinis.cuod.events.internal` | INTERNAL | INTERNAL словари — нужна role `ordinis-internal`+. |
| `ordinis.cuod.events.restricted` | RESTRICTED | RESTRICTED — role `ordinis-restricted`. |
Topology: 1 broker (single-node на cluster.142), 3+ partitions per topic
(historical setup имел 12 partitions, при cluster recreate 2026-05-07
spec задал 3; existing topics annotated `strimzi.io/managed=false`,
broker partitions сохранены), `cleanup.policy: delete`,
`retention.ms: 604800000` (7 дней).
Параметры топиков (для consumer config):
- `cleanup.policy: delete`
- `retention.ms: 604800000` (7 дней)
- Multi-partition (точное число — спросить Ordinis team при registration,
partition count может меняться по operational reasons).
## Подключение
### Адреса bootstrap
### Bootstrap address + creds
| Среда | Bootstrap servers | Auth |
|---|---|---|
| prod | `ordinis-kafka-kafka-bootstrap.ordinis-prod:9092` (TLS+SASL: `:9093`) | SCRAM-SHA-512 |
| staging | `ordinis-kafka-kafka-bootstrap.ordinis-staging:9092` | SCRAM-SHA-512 |
Запросить у Ordinis team:
- Bootstrap servers (внутренний DNS на per-environment basis)
- SASL credentials (SCRAM-SHA-512 username + password)
- Topic ACL (Read + Describe на нужные топики)
- Consumer group prefix (для group ACL)
### Получение SASL креденшелов
Auth: **SASL/SCRAM-SHA-512**. TLS опциональный (отдельный listener).
Запросить у Ordinis team создание Kafka user (Strimzi `KafkaUser` CR):
```yaml
apiVersion: kafka.strimzi.io/v1beta2
kind: KafkaUser
metadata:
name: altum-consumer
namespace: ordinis-prod
labels:
strimzi.io/cluster: ordinis-kafka
spec:
authentication:
type: scram-sha-512
authorization:
type: simple
acls:
- resource:
type: topic
name: ordinis.cuod.events.public
patternType: literal
operations: [Read, Describe]
- resource:
type: group
name: altum-consumer-
patternType: prefix
operations: [Read]
```
Strimzi user-operator создаст Secret `altum-consumer` с полями `username`,
`password`. Используй их в Kafka client config.
Креденшелы выдаются per-consumer service. Не share между сервисами —
ACL и audit за каждым.
## Schema события
@@ -143,18 +117,17 @@ each businessKey изолирован.
## At-least-once семантика
Outbox pattern гарантирует **at-least-once** delivery:
1. Writer пишет запись в БД + outbox table в одной транзакции.
2. OutboxPoller (scheduled @1s) читает unpublished rows, отправляет в Kafka.
3. После Kafka ACK — отмечает row как published (`published_at` set).
Ordinis использует **outbox pattern** — изменение БД и публикация в Kafka
атомарны через transactional outbox. Гарантия: **at-least-once**.
Что это значит для consumer'а:
- Можешь получить **дубликат** события если poller crashed после ACK но до
`published_at` write. Idempotency у consumer'а — must.
- При network partition между Kafka и poller — сообщения накапливаются в
outbox (alert `OrdinisOutboxLagHigh` срабатывает > 100 events).
- Можешь получить **дубликат** события (rare, при partial failures на
стороне Ordinis). Idempotency у consumer'а — must.
- При недоступности Kafka события накапливаются в Ordinis side и
доставляются после восстановления — **events не теряются**.
- Order — гарантирован per partition key (`dictionary:businessKey`),
не глобально.
## Idempotency
@@ -167,13 +140,12 @@ if not seen:
redis.setex(f"ordinis:event:...", 86400, "1")
```
## DLQ (Dead-letter queue)
## Persistent failures (Ordinis side)
Если событие не публикуется > 1000 retry попыток (network issues,
Kafka unavailable) — переходит в DLQ table в Ordinis. Не теряется, но
требует ручного разбора.
Симптом: alert `OrdinisOutboxDLQNonEmpty` → check `/api/v1/admin/audit?action=outbox.dlq`.
Если событие не публикуется > 1000 retry попыток (multi-day Kafka outage),
оно складывается в DLQ внутри Ordinis. Не теряется, но требует ручного
разбора Ordinis team. Consumer не видит таких событий до их
re-publishing — но это очень редкий сценарий.
## Топик для discovery
+7 -18
View File
@@ -50,8 +50,7 @@ Webhook URLs валидируются перед отправкой:
- **Запрещено:** non-HTTPS scheme. Только `https://`.
- **Запрещено:** URL > 2048 chars.
Метрика `nsi_webhook_ssrf_rejected_total` increments при попытках. См.
prometheus rule `OrdinisWebhookSsrfRejected`.
При нарушении registration возвращает 422 + список причин.
## Payload
@@ -155,10 +154,11 @@ Consumer должен ответить `2xx` в течение 10 секунд:
| 4 | через 10 мин |
| 5 | через 1ч |
| 6-10 | каждые 6ч |
| 1000+ | DLQ → ручной разбор |
| После всех попыток | Ordinis team помечает delivery как failed для ручного разбора |
После 1000 неудач delivery попадает в DLQ (`/api/v1/admin/webhooks/deliveries/dlq`).
Alert: `OrdinisWebhookDLQNonEmpty`.
Если consumer недоступен > 1000 retry попыток — Ordinis team будет
расследовать. Долгосрочное unavailability стоит coordinate (planned
maintenance window) чтобы не накапливать orphan deliveries.
## Test ping
@@ -196,19 +196,8 @@ Response:
сложить в очередь.
3. **HMAC verify первым** — до парсинга JSON.
4. **Metrics** — track received / processed / errors для своего side.
Сравнивай с Ordinis metrics при расследовании.
5. **Whitelist Ordinis IP** на firewall если строгие правила. IP cluster.142
= `192.168.100.142`, но в production будет ingress IP. Спросить у Ordinis
team при registration.
## Alerting (Ordinis side)
| Alert | Trigger |
|---|---|
| `OrdinisWebhookHighFailureRate` | failure rate > 50% за 5 мин |
| `OrdinisWebhookDLQNonEmpty` | events в DLQ > 0 |
| `OrdinisWebhookDispatcherIdle` | 0 deliveries при > 0 active subscriptions |
| `OrdinisWebhookSsrfRejected` | SSRF guard сработал > 0 раз за 15 мин |
5. **Whitelist Ordinis IP** на firewall если строгие правила. Точный
адрес запросить у Ordinis team при registration.
## Дальше
+18 -16
View File
@@ -67,7 +67,7 @@ PUBLIC scope, ReferenceValidator возвращает тот же error
(`x_references_target_not_found`) что и на real not-found. Ordinis **не leak'ает**
existence of out-of-scope dictionary через differential errors.
## Orphan reference scanner
## Orphan references
Возможна ситуация когда referenced record **закрывается**, а referencing
record остаётся:
@@ -78,19 +78,20 @@ record остаётся:
3. spacecraft.RESURS-P-3 теперь имеет dangling FK
```
Periodic scanner (`OrphanReferenceScanner`, runs каждые 5 мин) находит
такие orphan'ы и emit'ит metric:
Ordinis периодически детектирует такие orphan'ы серверной стороной — данные
**не корректируются автоматически** (может быть intentional state до
update referencing записей), но Ordinis team получает уведомление и
может coordinate fix с data stewards.
```
nsi_orphan_references_total{source_dict="spacecraft",target_dict="satellite_type"} 1
```
Alert `OrdinisOrphanReferences` срабатывает при > 0 за 10 мин.
Consumer-side defensiveness:
- При получении event `record.closed` для записи на которую ты ссылаешься —
invalidate свой cache + либо fetch свежую версию referencing
записи (если она тоже обновилась), либо handle dangling FK явно.
## Cascade rules — roadmap (v2)
Сегодня закрытие источника НЕ closes referencing automatically. Это
дolжно явно делаться через bulk close либо individual updates.
дoлжно явно делаться через bulk close либо individual updates.
В v2 планируется schema extension `x-references-on-close`:
@@ -106,21 +107,22 @@ Alert `OrdinisOrphanReferences` срабатывает при > 0 за 10 мин
Опции:
- **block** — закрытие источника невозможно пока есть active referencing (default).
- **warn** — закрытие проходит, в response warning + metric `nsi_cascade_warnings_total`.
- **cascade** — все referencing закрываются в той же транзакции.
- **warn** — закрытие проходит, в response warning.
- **cascade** — все referencing закрываются в той же транзакции, отдельные
events emit'ятся per record.
См. design doc `dict-relationships-v2` (внутренний artifact в
`~/.gstack/projects/claude/`) для полного scope.
Конкретный timeline и финальная семантика будут опубликованы перед
deployment v2 — следи за анонсами от Ordinis team.
## Lineage discovery — roadmap (v2)
Сегодня нет endpoint для "что depends on X". Admin должен вручную проходить
schemas чтобы найти ссылки. В v2 планируется:
Сегодня нет endpoint для "что depends on X". В v2 планируется:
- `GET /api/v1/dictionaries/{name}/dependents` — словари которые ссылаются.
- `GET /api/v1/records/{dict}/{key}/dependents` — конкретные записи.
См. design выше для подробностей.
Это позволит consumer'ам и UI узнавать связи без чтения всех schemas
руками. Spec будет опубликован перед deployment v2.
## Best practices
-98
View File
@@ -1,98 +0,0 @@
# Ordinis НСИ ЦУОД — Operator Runbook
Карточки процедур для on-call инженера. Каждая карточка self-contained: можно
открыть в момент инцидента и пройти по шагам без необходимости понимать всю
архитектуру.
## Indexed by symptom
| Симптом | Карточка |
|---------------------------------------------------------------|----------|
| Spring Boot pod'ы залипли в `Init:0/1` после reboot | [vault-unseal.md](vault-unseal.md) |
| Kafka pod не Running, либо `vault-agent` не достукивается | [kafka-restart.md](kafka-restart.md) |
| Метрика `ordinis_outbox_dlq_size > 0` | [outbox-dlq.md](outbox-dlq.md) |
| Pod в `CrashLoopBackOff` с liquibase ошибкой в логах | [db-migrations.md](db-migrations.md) |
| API возвращает 401 на любые запросы | См. §1 ниже |
| API возвращает 500 на конкретный endpoint | См. §2 ниже |
| Admin UI не загружается (502/503) | См. §3 ниже |
## 1. 401 Unauthorized
Сейчас на staging `ORDINIS_AUTH_REQUIRED=true`. Anonymous = 401. Проверить:
```bash
# 1. Header передан?
curl -v -H "Authorization: Bearer $TOKEN" ... | grep -i authorization
# 2. JWT валиден?
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq
# проверить exp, iss, realm_access.roles
# 3. Issuer правильный?
# должен быть https://auth.nstart.space/realms/nstart
```
Если JWT прошёл проверку, но 401 — backend логи:
```bash
kubectl logs -n ordinis-staging deploy/ordinis-app -c app --tail=50 \
| grep -iE "jwt|invalid_token|expired"
```
Типичные причины:
- `Invalid signature` — Keycloak ротировал ключи, но бэкенд кэширует старый. Restart pod.
- `Token expired` — refresh token не сработал, юзер должен перелогиниться.
- `Issuer mismatch` — JWT issued другим Keycloak realm'ом.
## 2. 500 Internal Server Error на endpoint
```bash
# Найти trace_id из ответа
curl -i ... | grep -i traceid
# либо UI показывает traceId в alert
# Найти полный stack trace
kubectl logs -n ordinis-staging deploy/ordinis-app -c app --tail=200 \
| grep "<traceId>"
```
Типичные причины (в наших коммитах):
- `could not determine data type of parameter $N` — Hibernate JPQL с `IS NULL`
на nullable timestamp. Решение — sentinel value (см. fix audit search).
- `DataIntegrityViolationException dictionary_records_no_overlap` — bitemporal
exclusion constraint, период пересекается. Friendly 409 уже маппится — если
500 значит код-путь неуспел поймать.
- `JsonMappingException` — payload поломан (фронт передал stale schema).
## 3. Admin UI не загружается / 502 / 503
```bash
kubectl get pods -n ordinis-staging | grep admin-ui
```
| Status | Причина |
|-----------------------|---------|
| `ImagePullBackOff` | Тег image не существует в registry. Проверь `kubectl describe`. Восстановить: `kubectl set image deployment/ordinis-admin-ui ui=<registry>/ordinis-admin-ui:main -n ordinis-staging` |
| `CrashLoopBackOff` | nginx config invalid либо resolver недоступен. См. logs. |
| `Running 1/1`, но 502 | Upstream (writer/read-api) сам в дауне. Проверь те pod'ы. |
## 4. Что-то ещё — где смотреть
- **Pod state:** `kubectl get pods -n ordinis-staging`
- **Recent events:** `kubectl get events -n ordinis-staging --sort-by='.lastTimestamp' | tail -20`
- **Resource usage:** `kubectl top pod -n ordinis-staging`
- **Logs:** `kubectl logs -n ordinis-staging deploy/<name> -c app --tail=100`
- **Liveness/readiness probes:** `kubectl describe pod -n ordinis-staging <pod>`
- **Ingress:** `kubectl get ingress -n ordinis-staging`
- **Helm release:** `helm history ordinis -n ordinis-staging`
## 5. Эскалация
>30 мин downtime, не понятно куда копать → tech lead (zimin.an@nstart.space).
## 6. Связанные ресурсы
- API integration guide: [../integration/api/index.md](../integration/api/index.md) (Diplodoc, latest)
- Альтум integration: [../integration/altum-imaging-order.md](../integration/altum-imaging-order.md)
- Project state snapshots: см. `docs/status/*-state.md` в repo (исключены из Diplodoc build)
- Helm chart: `ordinis-infra/charts/ordinis/`
- Vault setup: `ordinis-infra/k8s/vault/setup.sh`
-125
View File
@@ -1,125 +0,0 @@
# Runbook: Liquibase миграции БД
**Источник правды:** `ordinis-migrations/src/main/resources/db/changelog/`
- `master.xml` — список включённых changelog'ов в порядке применения.
- `changes/00NN-*.xml` — конкретные изменения (CREATE TABLE, ADD COLUMN и т.д.).
Миграции применяются **автоматически** при старте `ordinis-app` через
`spring.liquibase` (Spring Boot starter). Если миграция падает — pod
не стартует, остальные pods тоже не поднимутся (read-api/projection-writer
имеют startup-зависимость от схемы БД).
## 1. Просмотр applied миграций
```bash
export KUBECONFIG=/tmp/kubeconfig-161
PG_PASS=$(kubectl get secret -n ordinis-staging ordinis-postgres-app \
-o jsonpath='{.data.password}' | base64 -d)
kubectl exec -n ordinis-staging ordinis-postgres-1 -c postgres -- \
env PGPASSWORD="$PG_PASS" psql -h localhost -U ordinis_app -d ordinis -c \
"SELECT id, author, dateexecuted, exectype FROM databasechangelog ORDER BY orderexecuted;"
```
Колонка `exectype`:
- `EXECUTED` — миграция применена успешно
- `FAILED` — упала (pod не должен стартовать в этом случае)
- `MARK_RAN` — пропущена через preCondition
- `RERAN` — была применена повторно (manual)
## 2. Добавление новой миграции
1. Создать `ordinis-migrations/src/main/resources/db/changelog/changes/00NN-<topic>.xml`.
- Номер строго следующий (последний +1).
- Author = `ordinis`.
- Idempotent: использовать `<preConditions onFail="MARK_RAN">` для добавлений.
2. Зарегистрировать в `master.xml` через `<include file="changes/00NN-*.xml"/>`.
3. Тестировать локально на dev-Postgres:
```bash
docker run -d --rm --name ord-pg -e POSTGRES_PASSWORD=dev \
-p 5432:5432 postgres:18
mvn -pl ordinis-migrations -am liquibase:update \
-Dliquibase.url=jdbc:postgresql://localhost:5432/postgres \
-Dliquibase.username=postgres -Dliquibase.password=dev
```
4. Commit + push. CI пересобирает app image, deploy применяет новые миграции
при следующем старте pod'а.
## 3. Откат миграции
⚠️ **Liquibase rollback требует `<rollback>` блок в changeset**. Без него
автоматический откат невозможен — только manual SQL.
### Если changeset имеет `<rollback>`
```bash
# Через kubectl exec на pod где есть mvn — обычно нет, проще локально:
mvn -pl ordinis-migrations -am liquibase:rollback \
-Dliquibase.url=jdbc:postgresql://192.168.100.161:5432/ordinis \
-Dliquibase.username=ordinis_app \
-Dliquibase.password=$PG_PASS \
-Dliquibase.rollbackCount=1
```
### Manual SQL (если rollback не задан)
1. Прочитать changeset XML — посмотреть какие изменения сделаны.
2. Написать обратные SQL (DROP TABLE, ALTER TABLE DROP COLUMN и т.д.).
3. Удалить запись из `databasechangelog`:
```sql
DELETE FROM databasechangelog WHERE id = '00NN-1-some-id' AND author = 'ordinis';
```
4. После этого Liquibase при следующем старте либо переприменит миграцию
(если она всё ещё в master.xml), либо считает её не-применённой.
## 4. Failed migration recovery
Сценарий: миграция упала, pod в CrashLoopBackOff.
### A) Миграция частично применилась
Liquibase помечает changeset как FAILED в `databasechangelog`. Чтобы
повторить:
```sql
DELETE FROM databasechangelog WHERE id = '00NN-1-broken' AND exectype = 'FAILED';
```
После этого pod рестарт → Liquibase снова попытается. Если миграция всё ещё
broken, исправить XML (или применённый частично state в БД), пересобрать
image, redeploy.
### B) Миграция полностью применилась но с ошибкой в данных
Например column добавлена с NOT NULL без default. Manual SQL:
```sql
ALTER TABLE my_table ALTER COLUMN broken_col DROP NOT NULL;
-- либо UPDATE my_table SET broken_col = 'default' WHERE broken_col IS NULL;
```
И исправить XML на `defaultValueComputed=...` для следующих deployments.
## 5. Текущий список миграций (на 2026-05-05)
| ID | Назначение |
|---|---|
| 0001-extensions.xml | PostGIS, btree_gist (для tstzrange exclusion) |
| 0002-dictionary-definitions.xml | Таблица `dictionary_definitions` |
| 0003-dictionary-records.xml | Таблица `dictionary_records` + tstzrange exclusion |
| 0004-audit-log.xml | Таблица `audit_log` |
| 0005-outbox-events.xml | Таблица `outbox_events` + partial index |
| 0006-idempotency-keys.xml | Таблица `idempotency_keys` |
| 0007-add-version-columns.xml | JPA optimistic lock columns |
| 0008-fix-scope-check-uppercase.xml | Чек на scope в верхнем регистре |
| 0009-fix-records-scope-check.xml | Чек consistency dictionary↔record scope |
| 0010-envers-tables.xml | Hibernate Envers (revinfo, *_aud) |
| 0011-outbox-dlq.xml | DLQ column + partial indexes |
## 6. Эскалация
| Проблема | Действие |
|---|---|
| Migration на staging упала, pod не стартует | DELETE FAILED row + fix XML + redeploy. См. §4A. |
| Migration on prod упала | **СТОП**. Не делать manual fix без apply tech lead. Возможно нужен rollback всего deployment. |
| `databasechangelog` lock застрял (другая миграция параллельно) | `DELETE FROM databasechangelog_lock WHERE id=1; INSERT INTO databasechangelog_lock VALUES (1, false, NULL, NULL);` |
| Postgres недоступен | См. CNPG cluster status: `kubectl get cluster -n ordinis-staging` |
-122
View File
@@ -1,122 +0,0 @@
# Runbook: Kafka StrimziPodSet рестарт / поднятие
**Когда использовать:**
- Kafka pod (`ordinis-kafka-dual-0`) не Running после reboot ноды.
- Strimzi cluster operator не пересоздаёт pod — например после manual delete с paused reconciliation.
- StrimziPodSet есть, но `READY PODS = 0`.
- Кафка-операции отвечают timeout / connection refused.
## 1. Диагностика
```bash
export KUBECONFIG=/tmp/kubeconfig-161
kubectl get kafka,kafkanodepool,strimzipodset -n ordinis-staging
kubectl get pods -n ordinis-staging | grep kafka
```
**Что искать:**
| Симптом | Причина |
|---|---|
| `kafka.kafka.strimzi.io/ordinis-kafka` имеет `WARNINGS` колонку с текстом | reconciliation paused или config invalid — см. `kubectl describe kafka` |
| `strimzipodset/ordinis-kafka-dual` отсутствует | удалён — нужно unpause reconciliation чтобы Strimzi его создал заново |
| `kafkanodepool/dual` имеет `DESIRED REPLICAS=0` | scale=0 был задан — Strimzi не позволяет 0 реплик в Kafka cluster |
| Pod `ordinis-kafka-dual-0` в `Pending` | scheduler не нашёл ноду — `kubectl describe pod` |
| Pod `Running` но `0/1` ready | startup probe failed — `kubectl logs` |
## 2. Восстановление
### Вариант A: pause/unpause reconciliation
Если Kafka CR имеет `strimzi.io/pause-reconciliation=true` annotation:
```bash
kubectl annotate kafka.kafka.strimzi.io/ordinis-kafka -n ordinis-staging \
strimzi.io/pause-reconciliation- --overwrite
kubectl annotate kafkanodepool.kafka.strimzi.io/dual -n ordinis-staging \
strimzi.io/pause-reconciliation- --overwrite
```
Strimzi в течение ~30 sec пересоздаст StrimziPodSet и поднимет pod.
### Вариант B: scale до 0 → нужно вернуть к 1+
```bash
kubectl patch kafkanodepool/dual -n ordinis-staging --type=merge \
-p '{"spec":{"replicas":1}}'
```
⚠️ **Strimzi требует минимум 1 replica** — при `replicas:0` валидация падает с
`InvalidResourceException: KafkaNodePools have 0 replicas`.
### Вариант C: StrimziPodSet удалён (например `kubectl delete strimzipodset`)
Если pause снят и replicas≥1, Strimzi пересоздаст автоматически. Дать
60 sec на reconciliation. Если не происходит:
```bash
# Принудительно перепинговать operator
kubectl annotate kafka.kafka.strimzi.io/ordinis-kafka -n ordinis-staging \
strimzi.io/manual-rolling-update-
kubectl annotate kafka.kafka.strimzi.io/ordinis-kafka -n ordinis-staging \
strimzi.io/manual-rolling-update=true --overwrite
```
### Вариант D: Pod залип в Pending / CrashLoopBackOff
Force-delete + дать operator пересоздать:
```bash
kubectl delete pod -n ordinis-staging ordinis-kafka-dual-0 \
--grace-period=0 --force
```
PVC `data-0-ordinis-kafka-dual-0` сохраняется, данные kraft-метаданных целы.
## 3. Подтверждение
```bash
kubectl get pods -n ordinis-staging | grep kafka
# → ordinis-kafka-dual-0 1/1 Running
kubectl exec -n ordinis-staging ordinis-kafka-dual-0 -- \
bin/kafka-broker-api-versions.sh \
--bootstrap-server ordinis-kafka-kafka-bootstrap:9092 2>&1 | head -3
```
## 4. После kafka up — рестарт consumer'ов
Spring Boot pods (`ordinis-app`, `ordinis-projection-writer`) могли застрять
в reconnect-loop. Force-delete для быстрого resume:
```bash
kubectl delete pod -n ordinis-staging \
-l 'app.kubernetes.io/name in (ordinis-app,ordinis-projection-writer)' \
--grace-period=0 --force
```
OutboxPoller начнёт публиковать накопленные события сразу.
## 5. Проверка outbox lag
После рестарта Kafka outbox мог накопить unpublished events. Проверь:
```bash
curl -sk -H "Authorization: Bearer $TOKEN" \
--resolve ordinis.k8s.265.nstart.cloud:443:192.168.100.161 \
"https://ordinis.k8s.265.nstart.cloud/api/v1/admin/outbox/stats"
# → {"pending":0,"dlq":0} ← желаемое
```
Если pending растёт — см. [outbox-dlq.md](outbox-dlq.md).
## 6. Эскалация
| Проблема | Действие |
|---|---|
| Strimzi operator pod (kafka-test ns) не Running | `kubectl get pods -n kafka-test` → перезапустить |
| Consul недоступен (Strimzi его не использует, но Vault — да) | см. [vault-unseal.md](vault-unseal.md) |
| Kraft metadata corruption (редко) | DR: восстановить PVC из snapshot, переинициализировать topic |
| ACL нарушение / SASL auth fail | Проверить secret `ordinis-app-kafka` в Vault, перезапустить consumer |
>20 минут downtime → tech lead.
-110
View File
@@ -1,110 +0,0 @@
# Runbook: Outbox DLQ — диагностика и replay
**Когда использовать:**
- Метрика `ordinis_outbox_dlq_size > 0` (Prometheus alert).
- В UI `/outbox` → ненулевое количество в "В DLQ".
- В логе writer: `Outbox event id=N → DLQ after 1000 attempts`.
## 1. Что это
Outbox publisher делает at-most ~1000 попыток (`ordinis.outbox.attempt-max`)
опубликовать каждое событие в Kafka. После — событие маркируется `dlq_at`
и исключается из polling. Без cap poller бесконечно ретраит мёртвые сообщения
(orphan topic, broken ACL, malformed payload), забивая лог и lag-метрику.
## 2. Просмотр DLQ
### Через admin UI
- `https://ordinis.k8s.265.nstart.cloud/outbox` → таблица со всеми DLQ
записями: ID, eventType, topic, attemptCount, lastError, dlqAt.
### Через API
```bash
curl -sk -H "Authorization: Bearer $TOKEN" \
--resolve ordinis.k8s.265.nstart.cloud:443:192.168.100.161 \
"https://ordinis.k8s.265.nstart.cloud/api/v1/admin/outbox/dlq?page=0&size=50" \
| jq '.content[] | {id, eventType, kafkaTopic, attemptCount, lastError}'
```
### Напрямую в БД
```sql
-- через kubectl exec на postgres pod
SELECT id, event_type, kafka_topic, dictionary_name, attempt_count,
LEFT(last_error, 200) AS error,
dlq_at, created_at
FROM outbox_events
WHERE dlq_at IS NOT NULL
ORDER BY dlq_at DESC
LIMIT 50;
```
## 3. Типичные причины
| `last_error` содержит | Причина | Решение |
|----------------------------------------------------|------------------------------------------------------|--------------------------------------|
| `Topic <name> not present` / `UnknownTopicOrPartition` | Топик удалён / переименован / не создан Strimzi'ем | Создать topic либо удалить запись (`DELETE FROM outbox_events WHERE id=N`) |
| `TopicAuthorizationException` / `AclAuthorization` | SASL user не имеет PRODUCE rights | Strimzi `KafkaUser` ACL — добавить permission |
| `TimeoutException` | Kafka broker недоступен | См. [kafka-restart.md](kafka-restart.md). После up — replay (см. §4) |
| `RecordTooLargeException` | Payload > `message.max.bytes` | Увеличить limit в Kafka либо сократить payload |
| `JsonMappingException` / `JsonProcessingException` | Payload поломан — нечитаемая JSON | DELETE запись (нечего публиковать) + investigate writer code |
| `SerializationException` | Kafka schema registry не разрешает (если включён) | См. schema registry config |
## 4. Replay — пометить как unpublished снова
После исправления причины (топик создан, ACL дан) — снять `dlq_at`, обнулить
attempt_count, poller подхватит.
```sql
-- одну запись
UPDATE outbox_events
SET dlq_at = NULL, attempt_count = 0, last_error = NULL
WHERE id = 12345;
-- весь DLQ (после fix корневой причины)
UPDATE outbox_events
SET dlq_at = NULL, attempt_count = 0, last_error = NULL
WHERE dlq_at IS NOT NULL;
```
Через ~1 sec OutboxPoller подхватит и попытается опубликовать заново.
## 5. Hard delete (когда replay не нужен)
Если событие безнадёжно (стейл данных, удалённая запись, тестовый мусор):
```sql
DELETE FROM outbox_events WHERE id = 12345;
DELETE FROM outbox_events WHERE dlq_at IS NOT NULL AND dictionary_name = 'old_test_dict';
```
⚠️ **Audit:** если событие было `RecordCreated` / `RecordUpdated` — consumer'ы
могут не получить change. Альтум-кэш будет stale до следующего read с `?at`.
## 6. Подключение к staging Postgres
```bash
export KUBECONFIG=/tmp/kubeconfig-161
PG_PASS=$(kubectl get secret -n ordinis-staging ordinis-postgres-app \
-o jsonpath='{.data.password}' | base64 -d)
kubectl exec -it -n ordinis-staging ordinis-postgres-1 -c postgres -- \
env PGPASSWORD="$PG_PASS" psql -h localhost -U ordinis_app -d ordinis
```
## 7. Превентивные меры
- **Alert на `ordinis_outbox_dlq_size > 0`** — задействовать когда настроим
Prometheus alerts (v1 closure backlog).
- **Kafka topic provisioning** — все топики `ordinis.cuod.events.{public,internal,restricted}`
должны быть pre-created в Strimzi `KafkaTopic` CR при разворачивании
кластера. Если автоcreate enabled — broker создаст с дефолтами что может
не совпадать (replication factor 1 vs 3 и т.д.).
- **Schema validation** — outbox `payload` валидируется только Jackson'ом.
Schema registry интеграция запланирована на v2.
## 8. Эскалация
| Проблема | Действие |
|---|---|
| DLQ растёт > 100/час | tech lead. Probably broken topic config or producer code |
| DLQ есть после рестарта Kafka — ничего не replays автоматически | Manual `UPDATE outbox_events SET dlq_at=NULL` после убеждения что Kafka OK |
| Дубликаты после replay (consumer получил оба раза) | Consumer должен быть idempotent (наш consumer-side имеет idempotency_keys) |
-96
View File
@@ -1,96 +0,0 @@
# SLO / SLI — Ordinis НСИ ЦУОД
Документ описывает Service Level Objectives для Ordinis на стадии v1.
Метрики и пороги — стартовая точка, корректируются по реальным данным
после 2-3 месяцев наблюдения.
**Source of truth для алертов:**
`ordinis-infra/charts/ordinis/templates/prometheus-rules.yaml` (отдельный repo).
## 1. SLI (Service Level Indicators)
### Read-api availability
- **Метрика:** `up{job="ordinis-read-api"}`
- **Что считаем:** доля времени когда хотя бы один read-api pod отвечает.
- **Цель v1:** 99.5% за месяц (allowance ~3.6 ч downtime).
- **Цель v2:** 99.9% (с HA postgres + 2+ replicas read-api).
### Read-api latency
- **Метрика:** `histogram_quantile(0.99, http_server_requests_seconds_bucket{job="ordinis-read-api"})`
- **Цель:** p99 < 500ms для GET-запросов.
- **Текущий alert:** p99 > 1s for 10m → warning.
- **Justification:** Альтум-BFF имеет TTL 5 мин, latency на холодном кэше
не должна перекрывать UX (заказ съёмки UI ждёт справочник).
### Write-api error rate (writer)
- **Метрика:** `5xx_rate / total_rate` over 5m
- **Цель:** < 1% за 30 дней.
- **Alert:** > 5% за 5 мин → warning.
- **Источники 5xx, которые надо исключить:** валидация (это 4xx), exclusion
constraint (409 friendly), IP type cast (баг 2026-05-05, fixed). Если
возрастает — ищем новый bug.
### Outbox publish lag
- **Метрика:** `ordinis_outbox_pending_events`
- **Цель:** ≤ 50 в normal load, < 10 в average.
- **Alert:** > 100 for 5m → warning.
- **Семантика:** это batch lag между записью изменения в БД и публикацией
в Kafka. Большое значение = consumer'ы видят stale данные.
### Outbox DLQ
- **Метрика:** `ordinis_outbox_dlq_size`
- **Цель:** = 0 всегда (любая запись в DLQ = bug).
- **Alert:** > 0 for 5m → critical.
- **Действие:** docs/ops/outbox-dlq.md.
### Outbox publish error rate
- **Метрика:** `rate(ordinis_outbox_publish_errors_total[5m])`
- **Цель:** ~ 0.
- **Alert:** > 0.1/sec for 10m → warning.
### Postgres availability
- **Метрика:** `cnpg_pg_postmaster_start_time_seconds{cluster="ordinis-postgres"}`
- **Цель:** 99.9% (CNPG single-instance на v1, на v2 — 99.99% с replica).
- **Alert:** absent or 0 for 2m → critical.
## 2. Notification routing (TODO)
Сейчас alerts пишутся в Prometheus AlertManager как обычные alerts.
Routing в Slack/Telegram/Email — отдельная задача:
- [ ] AlertManager config с receiver per severity:
- **critical** → on-call channel (немедленный)
- **warning** → digest channel (раз в час summary)
- [ ] Эскалация: critical → engineer → tech lead через 15 мин если не acked.
См. `docs/status/2026-05-05-state.md` в repo (status snapshots исключены из Diplodoc build).
## 3. Error budget (v1 target)
При 99.5% availability за месяц:
- Total минут: 30 * 24 * 60 = 43200
- Allowed downtime: 0.5% × 43200 = ~216 мин (~3.6 ч)
**Burn rate alert:** если за последний час потрачено >2% бюджета —
warning. >5% — critical (т.е. совокупно >2.16 ч за час).
Сейчас не настроен — TODO.
## 4. Что НЕ покрывает v1
- Нет dashboards (Grafana). Видим только raw метрики через Prometheus UI.
- Нет distributed tracing (Tempo) на staging. Trace_id есть в логах, но
собрать timeline через сервисы — пока вручную.
- Нет logs aggregation (Loki). Поиск по логам — kubectl logs (привычно
но не масштабируется).
- Нет capacity planning (rate of change для скрытых ресурсов: PVC fill rate,
Kafka log retention).
Roadmap до v2 — отдельный документ "observability stack".
## 5. Связанные runbooks
- [vault-unseal.md](vault-unseal.md)
- [kafka-restart.md](kafka-restart.md)
- [outbox-dlq.md](outbox-dlq.md)
- [db-migrations.md](db-migrations.md)
- [README.md](README.md) — index by symptom
-138
View File
@@ -1,138 +0,0 @@
# Runbook: Vault unseal (staging / prod)
**Когда использовать:** Vault на кластере залип в `Sealed=true` после reboot
ноды или manual seal. Признаки:
- Spring Boot pod'ы (`ordinis-app`, `ordinis-read-api`, `ordinis-projection-writer`)
стоят в `Init:0/1`.
- В логах `vault-agent-init` контейнера: `Vault is sealed`, error 503 на
`auth/kubernetes/login`.
- `kubectl exec -n config vault-0 -- vault status``Sealed: true`.
**Auto-unseal сейчас НЕ настроен** (v1 closure backlog: Transit secret engine
с master Vault либо Yandex Cloud KMS). До миграции — ручная процедура ниже.
---
## 1. Где взять unseal-ключи
Ключи Shamir, threshold **3 из 5**. Хранение:
- **Corporate 1Password** — vault `Ordinis НСИ ЦУОД`:
- `vault-staging-unseal-keys` — для cluster.265 (192.168.100.161)
- `vault-prod-unseal-keys` — для cluster.142 (192.168.100.142)
- **Резерв** (cold storage) — у DevOps lead в KeePass.
Доступ к 1Password vault — у списка инженеров с on-call. При расширении —
добавлять через 1Password admin. Никогда не пересылать ключи в Slack /
Telegram / email.
> ⚠️ **При утере 5 из 5 ключей:** Vault инициализируется заново → ВСЕ
> secrets потеряны (postgres passwords, Kafka SASL, Keycloak signing keys
> в K8s secrets — отдельно). Plan: получить root token + replay `setup.sh` +
> заново положить secrets из 1Password (Postgres backup + Vault: писать
> только в зеркало). Делать только с письменным разрешением tech lead.
## 2. Быстрый unseal (script)
```bash
cd ~/code/ordinis-infra/k8s/vault
# staging
./unseal.sh staging "<KEY_1>" "<KEY_2>" "<KEY_3>"
# prod
./unseal.sh prod "<KEY_1>" "<KEY_2>" "<KEY_3>"
```
Скрипт делает:
1. Pull kubeconfig с ноды (если ещё не скачан).
2. Проверяет `vault status` — если уже unsealed, выходит.
3. Применяет 3 ключа последовательно через `vault operator unseal`.
4. Проверяет `Sealed: false`.
5. Force-deletes Spring Boot pod'ы — vault-agent-init иначе ждёт backoff
до 3+ минут на следующую попытку auth.
После — `kubectl get pods -n ordinis-{env}` показывает `2/2 Running`
через ~2 минуты.
## 3. Ручной unseal (если script недоступен)
```bash
# 1. Получить kubeconfig
ssh root@192.168.100.161 "cat /etc/kubernetes/admin.conf" > /tmp/kc-161
sed -i '' 's|server: https://[^:]*:|server: https://192.168.100.161:|' /tmp/kc-161
export KUBECONFIG=/tmp/kc-161
# 2. Проверить статус
kubectl exec -n config vault-0 -- vault status
# 3. Применить 3 ключа (по одному)
kubectl exec -n config vault-0 -- vault operator unseal '<KEY_1>'
kubectl exec -n config vault-0 -- vault operator unseal '<KEY_2>'
kubectl exec -n config vault-0 -- vault operator unseal '<KEY_3>'
# 4. Убедиться Sealed: false
kubectl exec -n config vault-0 -- vault status
# 5. Перезапустить Spring Boot pod'ы
kubectl delete pod -n ordinis-staging \
-l 'app.kubernetes.io/name in (ordinis-app,ordinis-read-api,ordinis-projection-writer)' \
--grace-period=0 --force
# 6. Проверить
kubectl get pods -n ordinis-staging
```
## 4. Подтверждение что всё работает
```bash
# read-api
curl -sk --resolve ordinis.k8s.265.nstart.cloud:443:192.168.100.161 \
https://ordinis.k8s.265.nstart.cloud/api/v1/spacecraft/records?lang=ru-RU \
| jq 'length'
# → должно вернуть число > 0
# admin
curl -sk -o /dev/null -w "%{http_code}\n" \
--resolve ordinis.k8s.265.nstart.cloud:443:192.168.100.161 \
"https://ordinis.k8s.265.nstart.cloud/api/v1/admin/audit?page=0&size=5"
# → 200
```
## 5. Эскалация / проблемы
| Симптом | Что делать |
|------------------------------------------------------|--|
| `vault status``connection refused` | Pod `vault-0` не Running. `kubectl get pods -n config` + `kubectl describe`. |
| `unseal` принимает ключ но Sealed остаётся true | Проверить count `Unseal Progress` — может нужен 4-й ключ если threshold увеличили. |
| Все 3 ключа отвергает (`invalid key`) | Whitespace / обрезанные символы. Скопировать заново из 1Password (полное поле). |
| Pod'ы Spring Boot не поднимаются после unseal | `kubectl logs <pod> -c vault-agent-init` — могут быть policy/role миграции после изменений. |
| Consul backend (Vault использует Consul) недоступен | `kubectl get pods -n config consul-server-0` + `kubectl logs`. |
При 30+ минут downtime → эскалация tech lead → решение о переносе
prod-нагрузки на запасной кластер либо публичная коммуникация в SLA-канал.
## 6. Связанные документы
- Установка Vault с нуля: `ordinis-infra/k8s/vault/setup.sh`
- Архитектура auth: `ordinis-auth/.../{ScopeContext,SecurityConfig}.java`
- Список секретов в Vault: `secret/ordinis/cuod/` (postgres, kafka, redis)
## 7. Переход к auto-unseal (TODO для v1 closure)
Когда будет master Vault либо Yandex Cloud KMS:
1. Добавить `seal "transit"` либо `seal "yandexcloudkms"` stanza в vault config
ConfigMap (`config/vault-config`).
2. Запустить migration:
```
kubectl exec -n config vault-0 -- vault operator unseal -migrate KEY_1
kubectl exec -n config vault-0 -- vault operator unseal -migrate KEY_2
kubectl exec -n config vault-0 -- vault operator unseal -migrate KEY_3
```
3. После миграции существующие Shamir-keys больше не работают — выдаются
Recovery Keys (5 шт, тот же threshold 3) для emergency rekey/operator-init
операций.
4. Обновить этот runbook: основной случай — auto-unseal, ручной unseal
только при downtime master Vault / KMS.
-157
View File
@@ -1,157 +0,0 @@
# Состояние разработки НСИ (Ordinis ЦУОД ОДХ)
**Версия:** v0.5 (MVP+, anchor v1 — в процессе закрытия)
**Дата:** 2026-05-05
**Owner:** zimin.an@nstart.space
**Окружения:** staging (192.168.100.161 / cluster.265), prod (192.168.100.142 / cluster.142, hibernated)
---
## 1. Перечень требований
### 1.1. Реализовано (production-ready на staging)
| # | Требование | Статус | Где зафиксировано |
|----|--------------------------------------------------------------|--------|---|
| 1 | Каталог НСИ ЦУОД ОДХ — единый источник истины | ✅ | bundle `cuod` v1.1.0 |
| 2 | Bitemporal модель (валидное-время + транзакционное-время) | ✅ | `dictionary_records` + `valid_from`/`valid_to` + Hibernate Envers |
| 3 | JSON Schema валидация записей (Draft-07) | ✅ | `ordinis-validation` (networknt) |
| 4 | i18n поля (RU + EN) с patternProperties | ✅ | `x-localized: true` extension |
| 5 | Уникальные поля (бизнес-ключ + произвольные) | ✅ | `x-unique` extension + native query enforcement |
| 6 | Авто-derive businessKey из data поля | ✅ | `x-id-source` extension |
| 7 | История версий записи + diff | ✅ | `/records/{key}/history` + UI drawer |
| 8 | Schema versioning + breaking-change detection | ✅ | `diffSchemas` + Save-block + auto major/minor/patch |
| 9 | Аудит-лог (кто, когда, что, IP, trace_id) | ✅ | `audit_log` + `AuditLogger` + UI `/audit` + CSV export |
| 10 | Kafka outbox (at-least-once гарантия) | ✅ | `OutboxPoller` + DLQ cap (1000 attempts) |
| 11 | Idempotency для мутирующих запросов | ✅ | `Idempotency-Key` header + sha-256 hash |
| 12 | Multi-scope доступ (PUBLIC / INTERNAL / RESTRICTED) | ✅ | `DataScope` + JWT roles + Kafka per-scope topics |
| 13 | Keycloak OIDC (issuer-uri auto-discovery) | ✅ | `ordinis-auth` + `JwtDecoders.fromIssuerLocation` |
| 14 | Visual schema builder + CRUD UI | ✅ | admin-ui (React 19 + TanStack + @nstart/ui) |
| 15 | SSO для admin-ui (PKCE Public client) | ✅ | react-oidc-context |
| 16 | Bundle import (idempotent seed) | ✅ | `CuodBundleImporter` startup runner |
| 17 | Swagger UI / OpenAPI 3 spec | ✅ | springdoc на `/swagger-ui.html` |
| 18 | 7 справочников + 51 запись на staging | ✅ | spacecraft (14), satellite_type (5), ground_station (3), mission (9), instrument (11), processing_level (8), data_format (8) |
| 19 | Friendly error responses (409/400/404 вместо 500) | ✅ | `GlobalExceptionHandler` |
| 20 | Метрики Prometheus + actuator/health | ✅ | `ordinis_outbox_*`, `ordinis_kafka_*` |
| 21 | Тесты | ✅ | 24 backend + 51 frontend = **75 unit-тестов**, все зелёные |
### 1.2. В работе / required для v1 closure
| # | Требование | Статус | Блокер |
|----|-----------------------------------------------------------|--------------|--------|
| 22 | Real consumer integration (Альтум — заказ съёмки) | 🟡 в процессе | Альтум собирает BFF (FastAPI), ждём первого pull от них |
| 23 | DNS A-record + Let's Encrypt cert на prod | 🔴 | DNS не проброшен на 192.168.100.142, prod hibernated |
| 24 | `ORDINIS_AUTH_REQUIRED=true` на prod | 🔴 | Зависит от готовности SSO admin-ui на prod + Альтум |
| 25 | Distributed tracing (Tempo) на prod | 🔴 | Не развёрнут observability stack |
| 26 | CNPG backup в S3 + DR rehearsal | 🔴 | S3-endpoint не настроен |
| 27 | SLO + alerts (Prometheus) | 🔴 | Не описаны метрики |
| 28 | 30+ справочников ЦУОД ОДХ (full coverage) | 🟡 | Сейчас 7 — anchor-набор. Нужны: антенны, частотные диапазоны, орбиты, операторы, потребители, контракты, зоны покрытия, климатические зоны и т.д. |
| 29 | Operator runbook | 🟡 | Частично: docs/integration, docs/tech. Нет процедуры unseal Vault, поднятия hibernated prod, рестарта Kafka |
| 30 | e2e API тесты (testcontainers) | 🔴 | Только unit |
| 31 | Component тесты frontend (RTL) | 🔴 | Только pure-logic |
| 32 | Bundle code-splitting (~660KB gzip) | 🔴 | echarts/lottie/@nstart/ui тянут единый chunk |
| 33 | Vault auto-unseal | 🔴 | Критично после reboot ноды — обнаружено 2026-05-05 |
### 1.3. v2 (governance / data quality / federation)
Не блокирует prod stand-up. Плановый roadmap (~2-3 квартала):
- Approval workflow (draft → review → publish), ownership trees, RBAC per-dictionary
- Lineage / dependency graph
- Geo first-class (карта ввода/превью, bbox/polygon query)
- Cross-dictionary FK с cascade rules
- Data quality rules engine (cross-field constraints, ML-anomaly detection)
- Federation MDM-кластеров (master/replica)
- Schema registry (Apicurio/Confluent) для Kafka
- Webhooks для не-Kafka consumer'ов
---
## 2. Нерешённые технические вопросы
### 2.1. Критические для prod
1. **Vault auto-unseal.** 2026-05-05 после reboot ноды staging Vault sealed → 3 Spring Boot pod'а залипли в Init на 3+ минут. Любой prod reboot = downtime + ручной шаг (3 unseal-ключа из 5). Решения:
- (a) Transit secret engine из master Vault (рекомендуется при наличии)
- (b) Yandex Cloud KMS / Selectel KMS auto-unseal
- (c) Скриптованный manual + runbook (минимум для v1, мигрировать позже)
2. **Audience валидация JWT.** Сейчас не enforced — принимаем токены от любого клиента в realm `nstart`. Допускает scope-эскалацию через interactive client другого приложения. На prod нужно либо включить audience-check, либо доказать что Альтум client мапит `aud=ordinis` через Keycloak Audience Mapper.
3. **Outbox poll-interval 1 сек на prod.** При 100+ событиях/сек → 86k+ запросов к Postgres/день. Возможно нужен batch tuning или reactive listener вместо poll.
4. **Single-instance Postgres.** На staging и prod — CNPG cluster с `instances: 1`, реплика отсутствует. Failover нет. Для prod-нагрузки нужен `instances: 3` с PgBouncer.
### 2.2. Архитектурные открытые вопросы
5. **Realtime invalidation для consumer'ов.** TTL 5 мин кэша достаточен для v1, но при критичных операциях (закрытие КА за день до запуска) нужен push. Kafka topic `nsi.dictionary.changed` запланирован на v2 — критичность для consumer'ов оценивается.
6. **Геоданные.** В spacecraft.schema есть orbit-параметры, в ground_station — geometry (PostGIS). Но Альтум-задача (заказ съёмки) использует bbox/polygon — bitemporal-выборка по геометрии не реализована. Нужно подтверждение от ЦУОД что геопоиск нужен в v1 или v2.
7. **Multi-tenancy.** Сейчас `bundle: cuod` — anchor customer. Второй клиент → второй bundle, своя тройка Kafka топиков. Но шаринг справочников между bundle (например spacecraft общий для ЦУОД и геопортала) не определён архитектурно.
8. **Object storage / большие справочники.** Если справочник вырастет до 100k+ записей (например climate zones), JSONB в одной таблице может стать узким местом. Партиционирование `dictionary_records` по `dictionary_id` — потенциальное решение, нужна оценка.
### 2.3. Operational
9. **Vault-agent-init backoff.** ~3 минут при transient Vault unavailability — слишком долго. Нужен tuning либо переход на CSI Driver.
10. **JDK 25 + Mockito inline** несовместим. Перешли на subclass mock-maker. При апгрейде до Java 26 проверить.
11. **CI path-filter.** Был костыль, рассыпался при односторонних изменениях. Сейчас все docker jobs запускаются всегда (~30s extra на layer-cache). Можно вернуться к smart-filter когда GitLab variable interpolation в trigger jobs будет надёжной.
12. **OTel exporter logs/metrics 404.** Tempo на staging принимает только traces, logs/metrics endpoint отсутствует. Pod-логи замусориваются. Либо отключить logs/metrics export, либо поднять Loki/Mimir.
### 2.4. Compliance / правовые
13. **Audit retention policy.** Сейчас `audit_log` пишется бессрочно. По регламенту ЦУОД нужно определить срок хранения (5 лет? 10 лет?) и автоматическую архивацию.
14. **Personal data handling.** В audit_log пишется `user_id` (Keycloak sub) + IP. ФЗ-152 — нужна оценка / DPA если в realm будут юзеры — не-сотрудники.
15. **Электронная подпись изменений.** Если потребуется юридически значимая фиксация изменений справочников (КЭП оператора при close записи) — отдельный модуль, не реализован.
---
## 3. Наличие технической документации
### 3.1. Создана и актуальна
| Документ | Назначение | Расположение |
|---|---|---|
| **API integration guide** | Гайд для интеграторов (Альтум, геопортал): auth flow, endpoints, кэш-стратегии, bitemporal модель, businessKey'и | `docs/tech/api-integration.md` |
| **Альтум integration ТЗ** | Согласованный план интеграции «Заказ съёмки», схемы 4 справочников, BFF архитектура | `docs/integration/altum-imaging-order.md` |
| **OpenAPI 3 spec (Swagger)** | Машиночитаемая спецификация всех endpoints, можно генерить SDK | `https://ordinis.k8s.265.nstart.cloud/swagger-ui.html` + `/v3/api-docs` |
| **Bundle manifest + schemas** | JSON Schema каждого справочника + readable manifest | `ordinis-cuod-bundle/src/main/resources/bundles/cuod/` |
| **Архитектурные решения inline** | JavaDoc + конструктивные комментарии в ключевых классах: `ScopeContext`, `OutboxPoller`, `DictionaryRecordService`, `AuditLogger` | в коде |
| **CI/Helm values** | Параметризация per-env, CORS, ingress, image-tag override | `ordinis-infra/charts/ordinis/{values,values-staging,values-prod}.yaml` |
### 3.2. Отсутствует / требует написания
| Документ | Приоритет | Назначение |
|---|---|---|
| **Operator runbook** | 🔴 high | Procedure cards для типовых задач: unseal Vault, поднятие hibernated prod, рестарт Kafka StrimziPodSet, миграция БД, рестарт outbox-loop, разбор DLQ. Инцидент 2026-05-05 показал острую необходимость. |
| **DR plan** | 🔴 high | Backup → S3, RTO/RPO, процедура восстановления из CNPG snapshot |
| **Архитектурный обзор (HLA)** | 🟡 medium | Single-pager для нового тех-лида: модули, потоки данных, scope-фильтрация, bitemporal-логика — без необходимости читать код |
| **User docs / справка оператора НСИ** | 🟡 medium | Как создать справочник, как закрыть запись, как читать журнал аудита, как разобрать ошибку валидации схемы — для не-инженерных юзеров |
| **Миграционный гайд** | 🟢 low | Как мигрировать данные из существующих локальных справочников (Альтум, геопортал) в Ordinis bundle |
| **Threat model + security review** | 🟡 medium | OWASP-чеклист, audit роли, аудит trace-id, ограничения rate-limit на ingress |
| **CHANGELOG.md** | 🟢 low | Сейчас изменения видны только через git log. Для consumer'ов нужен structured changelog с breaking changes |
| **Релизная политика** | 🟢 low | semver для bundle vs schemaVersion vs Maven version. Сейчас не описана |
### 3.3. Inline-источники (читаются как docs)
- `git log --oneline ordinis-app/` — история архитектурных решений в commit messages
- JavaDoc / TSDoc на ключевых классах (~70% покрытия)
- TanStack Router file-based routing — структура UI самодокументируется
- Liquibase changelog'и в `ordinis-migrations/src/main/resources/db/changelog/changes/` — история DB-схемы
---
## Резюме
- **Функционально v0.5 MVP покрывает 21 из 33 требований v1**, недостающее — operational (observability, backup, runbook), а не feature-уровня.
- **Архитектурный фундамент готов** для масштабирования до 30+ справочников и 5+ consumer'ов.
- **Главный риск** — отсутствие auto-unseal Vault и DR-плана. Для prod stand-up — критично.
- **Документация интеграторов готова** (Swagger + 2 markdown). Документации для operations и end-users — недостаточно.
**Прогноз до prod-ready (v1):** ~2-3 недели при текущем темпе, при условии что Альтум-интеграция отвалидирована на staging до prod cutover.
-195
View File
@@ -1,195 +0,0 @@
# Состояние разработки НСИ (Ordinis ЦУОД ОДХ)
**Версия:** v0.9 (anchor v1 — close-out этап, prod readiness в работе)
**Дата:** 2026-05-06
**Owner:** zimin.an@nstart.space
**Окружения:** staging (192.168.100.161 / cluster.265, ✅ STABLE), prod (192.168.100.142 / cluster.142, 🟡 hibernated)
**Предыдущий snapshot:** [2026-05-05-state.md](./2026-05-05-state.md) (v0.5)
---
## 0. Что изменилось с v0.5
Крупные блоки за последние сутки:
**Stability:**
- CNPG operator 1.24.1 → **1.25.4** (server-side apply через manifest, не helm). Дало `spec.probes` API → выставили liveness 15s/30s × 5 + stopDelay 60s. Закрыло хронические cascade failures когда single-node постгрес умирал под нагрузкой от parallel rolling deploys.
- **Config-server startupProbe** + bumped probe timeoutSeconds (5 → 10s). Spring Boot startup до 134с под нагрузкой больше не убивается liveness'ом.
- **CI helm armored**: `--atomic --cleanup-on-fail --wait --timeout 8m` + pre-step авто-rollback застрявших `pending-*` releases. Сегодня помогло trigger восстановиться от vault-injector-induced cascade без ручного `helm rollback`.
- **Migrate Job hardened**: hook-delete-policy включает `hook-failed`, `activeDeadlineSeconds: 600`, ttl 1h. Job в ImagePullBackOff больше не держит CPU request 4 часа подряд (incident 2026-05-05).
**Observability:**
- **Spring Boot percentiles-histogram** на `http.server.requests` с custom SLO buckets (5ms..5s). Без них `histogram_quantile()` в Grafana был пустой.
- **2 Grafana дашборда** (sidecar auto-discovery via label `grafana_dashboard=1`):
- `ordinis-overview` — request rate, error rate, latency p50/p95/p99, up статусы
- `ordinis-slo` — error budget burn rate (5m/1h/6h/24h), 30d budget remaining
- **SLO burn rate alerts** (Google SRE Workbook multi-window/multi-burn-rate):
- read-api 99.9% (43m бюджет/30d): BurnFast 14.4× × 5m+1h, BurnSlow 6× × 30m+6h, BurnTicket 3× × 2h+1d
- writer 99.5% (3.6h/30d): аналогично, чуть свободнее
- **ServiceMonitor label `release: monitoring`** — без него kube-prometheus-stack игнорирует SM. Добавили в helper, плюс PodMonitor для CNPG.
- **Tempo retention 48h → 168h (7d)**, pool tuning, global_overrides — для full-week investigation traces.
- **Loki compactor** с `retention_enabled=true`, ingestion limits — спасает PVC от bloat.
**UI:**
- **Scope-based grouping** в списке справочников: секции PUBLIC → INTERNAL → RESTRICTED + цветные accent-полосы слева у карточек (emerald/amber/rose). Search фильтрует across all sections.
- **Scope accent на detail page**: top stripe 4px цветом scope + colored chip в breadcrumb рядом с back-link.
- **Поиск + счётчик активных записей** на странице списка справочников (через `useDeferredValue` + N+1-safe `countActiveGroupedByDictionary`).
- **INTERNAL словари visibility fix**: `ORDINIS_AUTH_ROLES_CLAIM` теперь читает `resource_access.ordinis.roles` (Keycloak client-роли), не `realm_access.roles` (default). Маппинг `ordinis:client:internal` → INTERNAL уже работал в `ScopeContext`.
- **revisionHistoryLimit=3** на всех Deployment в чарте — было 49 stale ReplicaSets.
- **Pre-existing TS errors fixed** — `pnpm tsc --noEmit` зелёный (5 ошибок: unused imports, missing prop type, `$schema` field).
**Data:**
- **Bundle 7 → 40 справочников** (cuod v1.2.0). Закрыло раздел item 28 из v0.5.
- **Альтум pull verified** — внешний integration работает (item 22 → ✅).
**Production prep (планирование, не deployed):**
- `cluster-prod.yaml` (3-instance HA с synchronous replication, 90d retention, podAntiAffinity required)
- `scheduled-backup-prod.yaml` (daily 02:30 UTC + Weekly Sunday 04:00 UTC)
- `docs/runbooks/postgres-multi-instance-deploy.md` — sister runbook
- `docs/runbooks/cnpg-backup-s3-prod.md` — Selectel/Yandex S3 setup, IAM policy, ExternalSecret, Prometheus alerts (BackupFailed + WalArchiveStale)
- `k8s/benchmark/k6-read-api.js` + `k6-job.yaml` + `README.md` — Pareto 80/20 (records.list/dictionaries.list/record.byKey), JWT через `ordinis-bench` Keycloak SA, smoke 50 RPS → ramp 1k RPS → sustain 5m. Не запускался ещё.
---
## 1. Перечень требований
### 1.1. Реализовано (production-ready на staging)
| # | Требование | Статус | Где зафиксировано |
|----|--------------------------------------------------------------|--------|---|
| 1 | Каталог НСИ ЦУОД ОДХ — единый источник истины | ✅ | bundle `cuod` v1.2.0 (40 справочников) |
| 2 | Bitemporal модель (валидное-время + транзакционное-время) | ✅ | `dictionary_records` + `valid_from`/`valid_to` + Hibernate Envers |
| 3 | JSON Schema валидация записей (Draft-07) | ✅ | `ordinis-validation` (networknt) |
| 4 | i18n поля (RU + EN) с patternProperties | ✅ | `x-localized: true` extension |
| 5 | Уникальные поля (бизнес-ключ + произвольные) | ✅ | `x-unique` extension + native query enforcement |
| 6 | Авто-derive businessKey из data поля | ✅ | `x-id-source` extension |
| 7 | История версий записи + diff | ✅ | `/records/{key}/history` + UI drawer |
| 8 | Schema versioning + breaking-change detection | ✅ | `diffSchemas` + Save-block + auto major/minor/patch |
| 9 | Аудит-лог (кто, когда, что, IP, trace_id) | ✅ | `audit_log` + `AuditLogger` + UI `/audit` + CSV export |
| 10 | Kafka outbox (at-least-once гарантия) | ✅ | `OutboxPoller` + DLQ cap (1000 attempts) |
| 11 | Idempotency для мутирующих запросов | ✅ | `Idempotency-Key` header + sha-256 hash |
| 12 | Multi-scope доступ (PUBLIC / INTERNAL / RESTRICTED) | ✅ | `DataScope` + JWT roles + Kafka per-scope topics |
| 13 | Keycloak OIDC (issuer-uri auto-discovery) | ✅ | `ordinis-auth` + `JwtDecoders.fromIssuerLocation` + `resource_access.ordinis.roles` |
| 14 | Visual schema builder + CRUD UI (scope-grouped, search, counts) | ✅ | admin-ui (React 19 + TanStack + @nstart/ui) |
| 15 | SSO для admin-ui (PKCE Public client) | ✅ | react-oidc-context |
| 16 | Bundle import (idempotent seed) | ✅ | `CuodBundleImporter` startup runner |
| 17 | Swagger UI / OpenAPI 3 spec | ✅ | springdoc на `/swagger-ui.html` |
| 18 | **40 справочников** + 51+ запись на staging | ✅ | bundle v1.2.0 (закрытие item 28 из v0.5) |
| 19 | Friendly error responses (409/400/404 вместо 500) | ✅ | `GlobalExceptionHandler` |
| 20 | Метрики Prometheus + actuator/health + percentiles-histogram | ✅ | `ordinis_outbox_*`, `ordinis_kafka_*`, `http_server_requests_seconds_bucket` |
| 21 | Тесты | ✅ | 24 backend + 51 frontend + 34 новых = **109 unit-тестов**, все зелёные |
| 22 | **Real consumer integration (Альтум)** | ✅ | Pull verified 2026-05-05/06, БFF (FastAPI) подключился |
| 25 | **Distributed tracing (Tempo) на staging** | ✅ | OpenTelemetry → Tempo OTLP gRPC, 7d retention |
| 27 | **SLO + alerts (Prometheus)** | ✅ | 4 burn rate alerts (Google SRE multi-window), 2 Grafana dashboards |
| 34 | **CI auto-rollback при failure** | ✅ | helm `--atomic --cleanup-on-fail` + pre-step pending-* recovery |
| 35 | **CNPG operator 1.25.4** (spec.probes API) | ✅ | стабильность под нагрузкой, нет cascade failures |
| 36 | **Loki compactor + retention enforcement** | ✅ | PVC bloat защита |
### 1.2. В работе / required для v1 closure
| # | Требование | Статус | Прогресс с v0.5 / Блокер |
|----|-----------------------------------------------------------|--------------|--------|
| 23 | DNS A-record + Let's Encrypt cert на prod | 🔴 | DNS не проброшен на prod IP, prod hibernated. Нет изменений. |
| 24 | `ORDINIS_AUTH_REQUIRED=true` на prod | 🔴 | Зависит от готовности SSO admin-ui на prod + Альтум. |
| 26 | CNPG backup в S3 + DR rehearsal | 🟡 ready | **Манифесты + runbook готовы** (`cluster-prod.yaml`, `scheduled-backup-prod.yaml`, `cnpg-backup-s3-prod.md`). Не запущено: ждёт provider creds (Selectel default) + Vault path. |
| 28 | 30+ справочников ЦУОД ОДХ (full coverage) | ✅ | **Закрыто** — bundle 1.2.0 содержит 40. |
| 29 | Operator runbook | 🟡 70% | **Добавлены** `postgres-multi-instance-deploy.md`, `cnpg-backup-s3-prod.md`, `k8s/benchmark/README.md`. Остаётся: рестарт Kafka StrimziPodSet, разбор DLQ, рестарт outbox-loop. |
| 30 | e2e API тесты (testcontainers) | 🔴 | Только unit. Нет изменений. |
| 31 | Component тесты frontend (RTL) | 🔴 | Только pure-logic. Нет изменений. |
| 32 | Bundle code-splitting (~660KB gzip) | 🔴 | echarts/lottie/@nstart/ui тянут единый chunk. Нет изменений. |
| 33 | Vault auto-unseal | 🟢 deferred | **Сознательно skipped** по решению owner — приоритет prod-deploy. Mitigation: manual unseal runbook. |
| 37 | **Multi-instance Postgres prod (HA failover)** | 🟡 ready | `cluster-prod.yaml` + `postgres-multi-instance-deploy.md` готовы. Не applied (ждёт prod stand-up). |
| 38 | **Performance benchmark (1k RPS, p99 < 1s)** | 🟡 ready | k6 setup в `k8s/benchmark/`. Не запускался — нужно: создать `ordinis-bench` Keycloak SA → Secret → ConfigMap → `kubectl apply -f k6-job.yaml`. |
| 39 | AlertmanagerConfig per-channel webhooks | 🔴 | Сейчас 1 webhook (#AltumAlerts) на всё. Нужны URLs для разделения critical / warning / info. |
### 1.3. v2 (governance / data quality / federation)
Не блокирует prod stand-up. Roadmap ~2-3 квартала, без изменений с v0.5:
- Approval workflow (draft → review → publish), ownership trees, RBAC per-dictionary
- Lineage / dependency graph
- Geo first-class (карта ввода/превью, bbox/polygon query)
- Cross-dictionary FK с cascade rules
- Data quality rules engine (cross-field constraints, ML-anomaly detection)
- Federation MDM-кластеров (master/replica)
- Schema registry (Apicurio/Confluent) для Kafka
- Webhooks для не-Kafka consumer'ов
---
## 2. Нерешённые технические вопросы
### 2.1. Критические для prod
1. **Multi-instance Postgres failover.** Single-node CNPG без replica → SPOF. Манифесты + runbook готовы (см. item 37). Apply при prod cutover.
2. **CNPG backup в external S3.** MinIO в кластере = same failure domain. Внешний S3 (Selectel/Yandex) — runbook готов (item 26). RTO < 30m, RPO < 5m по WAL archive interval.
3. **Audience валидация JWT.** Сейчас не enforced — принимаем токены от любого клиента в realm `nstart`. Допускает scope-эскалацию. На prod либо включить audience-check, либо мапить `aud=ordinis` через Keycloak Audience Mapper.
4. **Outbox poll-interval 1 сек.** При 100+ событиях/сек → 86k+ запросов к Postgres/день. Возможно нужен batch tuning или reactive listener вместо poll.
### 2.2. Архитектурные открытые вопросы
5. **Realtime invalidation для consumer'ов.** TTL 5 мин кэша достаточен для v1, но push через Kafka topic `ordinis.cuod.events.*` уже emit'ится — consumer'ам осталось subscribe.
6. **Геоданные.** PostGIS включён, но bitemporal-выборка по геометрии не реализована. Нужно подтверждение от ЦУОД что геопоиск нужен в v1 или v2.
7. **Multi-tenancy.** Anchor `bundle: cuod`. Второй клиент → второй bundle, своя тройка Kafka топиков. Шаринг общих справочников между bundle (например `spacecraft` для ЦУОД и геопортала) не определён архитектурно.
8. **Object storage / большие справочники.** Если справочник вырастет до 100k+ записей, JSONB в одной таблице может стать узким. Партиционирование `dictionary_records` по `dictionary_id` — потенциальное решение, требует оценки.
### 2.3. Operational (выявлено в этой сессии)
9. **Vault Agent Injector flaky под нагрузкой.** Restarted 68 раз за месяц, last 4m26s ago. Когда injector down — pods создаются БЕЗ vault-agent sidecar → app пишет `cannot open /vault/secrets/postgres` → CrashLoop. Воркэраунд: `kubectl delete pod` для recreate; долгосрочно — увеличить request resources injector + readinessProbe.
10. **CNPG metrics exporter slow queries.** Под нагрузкой parallel deploy `pg_stat_replication` парсится 9-22 секунды → scrape timeout (default 10s) → `up=0` для postgres. Mitigation: dashboard panel переключён на `kube_pod_status_ready` (стабильно). Корень — нагрузка на single-node под docker builds + Spring Boot startups + cnpg exporter одновременно.
11. **kube-apiserver TLS handshake timeouts** под нагрузкой (load avg 50+). Транзиентно блокирует kubectl. Mitigation: подождать 10-30s, retry. Долгосрочно — multi-node cluster.
12. **Vault-agent-init backoff.** ~3 минут при transient Vault unavailability — слишком долго. Тuning либо переход на CSI Driver.
13. **JDK 25 + Mockito inline** несовместим. Перешли на subclass mock-maker. При апгрейде до Java 26 проверить.
14. **CI path-filter.** Был костыль. Сейчас все docker jobs запускаются всегда (~30s extra на layer-cache). Можно вернуться к smart-filter когда GitLab variable interpolation в trigger jobs будет надёжной.
### 2.4. Compliance / правовые
15. **Audit retention policy.** `audit_log` пишется бессрочно. По регламенту ЦУОД нужно определить срок хранения (5/10 лет) + автоматическую архивацию. Блокер: legal deadline.
16. **Personal data handling.** В audit_log пишется `user_id` (Keycloak sub) + IP. ФЗ-152 — нужна оценка / DPA если в realm будут не-сотрудники.
17. **Электронная подпись изменений.** Если потребуется юридически значимая фиксация (КЭП оператора при close записи) — отдельный модуль, не реализован.
---
## 3. Наличие технической документации
### 3.1. Создана и актуальна
| Документ | Назначение | Расположение |
|---|---|---|
| **API integration guide** | Гайд для интеграторов | `docs/tech/api-integration.md` |
| **Альтум integration ТЗ** | Согласованный план «Заказ съёмки» | `docs/integration/altum-imaging-order.md` |
| **OpenAPI 3 spec (Swagger)** | Машиночитаемая спецификация | `https://ordinis.k8s.265.nstart.cloud/swagger-ui.html` + `/v3/api-docs` |
| **Bundle manifest + schemas** | JSON Schema каждого справочника | `ordinis-cuod-bundle/src/main/resources/bundles/cuod/` |
| **Postgres multi-instance deploy** 🆕 | Runbook апгрейда staging single → prod 3-instance HA | `docs/runbooks/postgres-multi-instance-deploy.md` |
| **CNPG backup → external S3** 🆕 | Selectel/Yandex провижининг, IAM policy, ExternalSecret, Prometheus alerts | `docs/runbooks/cnpg-backup-s3-prod.md` |
| **k6 benchmark** 🆕 | Pareto 80/20 setup, preflight, regression tracking | `k8s/benchmark/README.md` |
| **CI/Helm values** | Параметризация per-env | `ordinis-infra/charts/ordinis/{values,values-staging,values-prod}.yaml` |
### 3.2. Отсутствует / требует написания
| Документ | Приоритет | Прогресс |
|---|---|---|
| **Operator runbook (full)** | 🔴 high | 70%: добавлены postgres-multi-instance + cnpg-backup. Остались: Kafka StrimziPodSet рестарт, DLQ-разбор, outbox-loop рестарт, vault unseal, поднятие hibernated prod. |
| **DR plan** | 🔴 high → 🟡 partial | Backup runbook готов с RTO/RPO targets. Quarter restore drill в operations calendar — не настроен. |
| **Архитектурный обзор (HLA)** | 🟡 medium | Single-pager не написан. |
| **User docs / справка оператора** | 🟡 medium | Не написана. |
| **Threat model + security review** | 🟡 medium | OWASP-чеклист не пройден. |
| **CHANGELOG.md** | 🟢 low | git log + commit messages. |
---
## Резюме
**Прогресс v0.5 → v0.9:**
- **27 из 39 требований v1 закрыто** (было 21/33). Прирост: items 18 (40 справочников), 22 (Альтум), 25 (tracing), 27 (SLO/alerts), 28 (full coverage), 35 (CNPG), 36 (Loki) + 3 новых item (CI auto-rollback, multi-instance ready, perf benchmark ready).
- **Архитектурный фундамент остаётся прочным**, добавлены защиты от cascade failures.
- **Главный риск**: prod stand-up зависит от DNS/cert (item 23) + multi-instance Postgres apply (item 37) + S3 creds (item 26). Все имеют готовые runbooks.
- **Документация**: prod-deploy artifacts готовы (postgres HA + S3 backup + k6 benchmark). Остаётся operator runbook completion + threat model.
**Прогноз до prod-ready (v1):** ~1-2 недели при текущем темпе. Блокеры — прежде всего внешние (DNS, S3 provider creds, ЦУОД real data). Inside-team задачи — 3-5 дней работы на code-splitting + e2e tests + AlertmanagerConfig.
**Что сегодня закрыло прошлые риски:**
- Хроническая нестабильность под нагрузкой → CNPG 1.25 + config-server probes + CI atomic.
- Observability vacuum → 4 burn rate alerts + 2 dashboards + 7d Tempo.
- INTERNAL словари невидимые → rolesClaim fix.
- Stuck helm releases → CI pre-step recovery + migrate Job hook-failed.
-174
View File
@@ -1,174 +0,0 @@
# Состояние разработки НСИ (Ordinis ЦУОД ОДХ)
**Версия:** v1.0.1 (post-cutover hardening: SPOF closed, monitoring stack ready, image arch incident resolved)
**Дата:** 2026-05-07 (evening update)
**Owner:** zimin.an@nstart.space
**Окружения:**
- **prod** (192.168.100.142 / cluster.142, ✅ LIVE с Альтум traffic, blue/green cutover завершён)
- **staging** (192.168.100.161 / cluster.265, ✅ STABLE — теперь dev environment, не consumer-facing)
**Предыдущий snapshot:** [2026-05-06-state.md](./2026-05-06-state.md) (v0.9, prod hibernated)
---
## 0. Что изменилось с v0.9
### Production stand-up на cluster.142 (Phase 1-5 done)
**Phase 1 — Postgres prod stand-up:**
- Discovery: cluster.142 НЕ hibernated (state.md устарел — k8s API v1.33.7 жив, ordinis-prod ns с Helm rev 17 deployed 4 дня назад, всё scaled to 0)
- CNPG cluster `ordinis-postgres` healthy, 50Gi local-path PVC
- Custom image `postgis-barman:18.3-3.6.2-1` (PostGIS 18 + barman-cli-cloud) для CNPG backups — официальный `cnpg/postgis:18-standard-trixie` НЕ имеет barman tools
- barmanObjectStore → `s3://altum-backups/ordinis-postgres` через `geoportal-prod-minio:9000` (re-use bucket с prefix isolation)
- ScheduledBackup: daily 02:30 UTC + weekly Sun 04:00 UTC
- Initial backup landed, PITR window started
**Phase 2 — Vault re-init:**
- Original Vault sealed после restart 17h, unseal key потерян
- Re-init с wipe `data-vault-0` PVC → init -key-shares=1 -key-threshold=1
- Setup playbook: `kubernetes` auth, policy `ordinis-app`, 4 roles (per-SA binding)
- 6 secret paths populated из existing k8s secrets (CNPG postgres, Strimzi KafkaUsers, Redis)
- Scripts: `ordinis-infra/scripts/vault/{setup,populate}.sh` + README
**Phase 3 — Kafka cluster recovery:**
- `ordinis-kafka` Strimzi cluster CR не было в namespace (только orphaned KafkaUsers + 1 service)
- Apply `ordinis-infra/k8s/kafka/cluster-prod.yaml` (KafkaNodePool dual + Kafka KRaft + 3 KafkaTopics)
- 1 broker, single-instance, SCRAM-SHA-512 auth, 20Gi local-path PVC
- Strimzi operator уже watches ordinis-prod (STRIMZI_NAMESPACE=kafka-test,ordinis-prod)
- Cluster Ready через 90s после apply
**Phase 4 — App deploy:**
- `config-server`, `ordinis-redis-0` scaled up (были 0/0)
- Helm upgrade `ordinis` rev 17 → 22 на `image.tag=main-4252ad93`
- Replicas bumped: writer 1→2, readApi 1→2 (HA для consumer-facing)
- 9 pods Running, /actuator/health → 200, /api/v1/dictionaries → 200 + 40 dicts seeded
**Phase 5 — Альтум cutover:**
- Blue/green phased shift через Альтум config (10% → 50% → 100%)
- staging-as-prod decommissioned для production traffic
- staging cluster.265 теперь чисто dev environment
### 0.1. Post-cutover hardening (evening 2026-05-07)
**Артефакты добавлены:**
- `k8s/benchmark/k6-job-prod.yaml` — load test для prod (Service port :80 fix)
- `k8s/benchmark/synthetic-smoke-prod.yaml` — one-shot smoke (auth + 3 endpoints, exit code = pass/fail)
- `k8s/benchmark/synthetic-smoke-cronjob-prod.yaml` — continuous monitoring (*/30 min)
- `k8s/kafka/topic-orphan-fix.sh` — janitorial script для KafkaTopic READY=False (✅ applied 2026-05-07 evening — все 3 topics unmanaged, alerts gone)
- prometheus-rules group `ordinis.synthetic` (2 alerts: SmokeFailed/Stale, conditional на `prometheusRule.synthetic.enabled`)
- `.gitlab-ci.yml` job `build-postgis-barman` (kaniko + REGISTRY_USER/REGISTRY auth, currently 401 — known issue, manual push fallback)
**Инцидент: postgis-barman image swap** (08:30-22:00 MSK):
1. Custom image `:18.3-3.6.2-1` запушен в `repo.nstart.cloud` с mac M-series **без `--platform linux/amd64`** → arm64 layers попали в registry
2. Catalog patched на repo.nstart.cloud → CNPG re-roll → `exec format error` → CrashLoopBackOff
3. Rollback catalog на staging URL → recovered
4. Re-push amd64 → но **kubelet cache на srvaltum01** удерживал arm64 layer под тем же тегом → retry catalog swap снова exec format error
5. Bump rev `:1``:2` → kubelet вынужден pull → cache miss → новый amd64 layer → success
6. Pod stuck Terminating ~14 мин (CNPG `terminationGracePeriodSeconds: 1800`) → `kubectl delete pod --force --grace-period=0` для recovery acceleration
**Lessons learned (закреплены в `docker/postgis-barman/README.md`):**
- `docker buildx --platform linux/amd64 --push` обязательно когда build с M-series mac
- Bump rev tag при cache invalidation needs (тот же тег + новые слои = kubelet skip)
- `terminationGracePeriodSeconds=1800` означает что graceful pod delete может висеть до 30 минут
- Verify image arch через registry API (`config.architecture`) ДО catalog swap
**Production verify (post-incident):**
- Cluster phase: "Cluster in healthy state"
- status.image: `repo.nstart.cloud/terravault/postgis-barman:18.3-3.6.2-2`
- imageID digest: `sha256:1b4b074d...` (verified amd64 manifest)
- All 11 pods Running, postgres restartCount=0, lastSuccessfulBackup preserved
**SPOF на staging registry — closed.** Если `registry.k8s.265.nstart.local` упадёт, prod не пострадает.
### Что закоммичено
**ordinis-infra (4 commits сегодня):**
- `cluster-prod.yaml` aligned с geoportal pattern (local-path, internal MinIO, single-instance) — было overcomplicated с Longhorn/Yandex S3
- `values-prod.yaml`: writer=2, readApi=2, host `ordinis.k8s.264.nstart.cloud`, `selfsigned-cluster-issuer`
- `prometheus-rules.yaml`: 4 CNPG HA alerts (conditional `cnpgHa.enabled` flag, off для single-instance)
- `k8s/kafka/cluster-prod.yaml` (NEW): Kafka CR + NodePool + 3 Topics для recovery
- `scripts/vault/{setup,populate}.sh + README.md` (NEW): playbook bootstrap
**ordinis (8 commits сегодня — задеплоены в prod):**
- `c7a917d` test(webhook): unit-test WebhookTestPingService — 7 кейсов
- `ac93151` test(webhook): re-enable WebhookE2ETest через no-op scheduler + manual ticks
- `92507ab` feat(audit): URL persistence фильтров + chip-list активных
- `5941207` feat(records): bulk close — multi-select toolbar + per-record транзакция
- `640a633` test(records): unit-test BulkRecordService — 10 кейсов
- `66a4f13` test(geo): e2e auto-derive geometry из data.lat/lon — 6 кейсов
- `4252ad9` feat(records): bulk export CSV выбранных + pagination
- + 23 новых теста (7 webhook + 10 bulk + 6 e2e)
---
## 1. Перечень требований
### 1.1. Реализовано (production-ready, LIVE на prod cluster.142)
Та же таблица что v0.9 + новые пункты:
| # | Требование | Статус |
|----|---------------------------------------------------------------|--------|
| 1-32 | (см. v0.9, всё уже было ready) | ✅ |
| 33 | Production deployment на real cluster (cluster.142) | ✅ NEW |
| 34 | Bulk close записей с multi-select UI | ✅ NEW |
| 35 | Bulk CSV export выбранных записей | ✅ NEW |
| 36 | Audit log URL-persistent filters + chip-list | ✅ NEW |
| 37 | Pagination для records list (50/page client-side) | ✅ NEW |
| 38 | WebhookE2ETest stable на CI (no-op scheduler + manual ticks) | ✅ NEW |
| 39 | Auto-derive POINT geometry из data.lat/lon (3 пути backend + Liquibase backfill 0015) | ✅ |
| 40 | CNPG continuous WAL archive + scheduled backups в external S3 | ✅ NEW |
### 1.2. В процессе / nice-to-have
- ~~KafkaTopics READY=False~~ ✅ **APPLIED 2026-05-07 evening** через `k8s/kafka/topic-orphan-fix.sh`. Все 3 topics (.public/.internal/.restricted) теперь annotate'нуты `strimzi.io/managed=false` — Strimzi user-operator больше не пытается reconcile (ошибка "Decreasing partitions not supported" гасла). Topics в брокере продолжают жить, projection-writer connected. Alerts на READY=False больше не срабатывают.
- Vault SASL passwords могут быть stale если Strimzi user-operator regenerated после Kafka cluster recreate — re-run `scripts/vault/populate.sh`
- ~~`postgis-barman` image SPOF на staging registry~~ ✅ **CLOSED 2026-05-07 evening:** image mirrored в `repo.nstart.cloud/terravault/postgis-barman:18.3-3.6.2-2`, prod cluster.142 pull'ит оттуда. См. секцию 0.1 ниже про инцидент.
- k6 benchmark на prod не прогнан (smoke 50 RPS / ramp 1k / sustain 5m готов в `k8s/benchmark/k6-job-prod.yaml`)
- Synthetic smoke + alerts ready (`k8s/benchmark/synthetic-smoke-cronjob-prod.yaml` + prometheus-rules group `ordinis.synthetic`). **Pre-flight partial:** `k6-tests` ConfigMap создан в `ordinis-prod` (test script). Осталось: создать `k6-keycloak-creds` Secret (требует Keycloak SA `ordinis-bench` client_secret — на стороне Keycloak admin), apply CronJob, helm upgrade с values-prod для активации alerts
- Real ЦУОД data integration (отдельный epic после prod soak)
### 1.3. Roadmap (отложено)
- **Approval workflow v2** (draft → review → publish) — design doc сохранён в `~/.gstack/projects/claude/zimin-main-design-approval-workflow-v2-20260507-121632.md`. Defer'нут до prod LIVE + observed compliance needs. Re-evaluate через 1-2 месяца после Альтум stable.
- **True HA Postgres** (3-instance + Longhorn + external Yandex S3) — отдельный future epic для multi-node cluster, не для cluster.142
- **Dictionary relationships v2** — combine lineage / dependency graph + cross-dictionary FK cascade rules в один epic. Design DRAFT в `~/.gstack/projects/claude/zimin-main-design-dict-relationships-v2-20260507-230237.md` (3-5 нед CC, 4 phases, 4 D-questions для plan-eng-review). Pre-code assignment: Альтум consumer behavior на cascade events.
- Data quality rules engine
- Federation MDM-кластеров (master/replica)
- Schema registry (Apicurio/Confluent) для Kafka
---
## 2. Архитектура (без изменений с v0.9, кроме prod)
```
┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────┐
│ Альтум (real users) │ ──→│ ordinis.k8s.264 │ ──→│ Postgres │
│ │ │ (cluster.142 prod) │ │ (CNPG 1×
│ │ │ • read-api ×2 │ │ + WAL S3 │
│ │ │ • app ×2 (writer) │ │ + daily │
│ │ │ • projection-writer │ │ backups) │
│ │ │ • admin-ui ×1 │ │ │
│ │ │ • Kafka 1× + 3 topics │ │
│ │ │ • Redis 1× │ │ │
│ │ │ • Vault 1× │ │ │
│ │ └──────────────────────┘ └─────────────┘
└─────────────────────┘
┌──────────────────────┐
│ staging cluster.265 │
│ (DEV ENVIRONMENT) │
└──────────────────────┘
```
---
## 3. Operational notes
**Backup verify:** `barman-cloud-backup-list --endpoint-url http://geoportal-prod-minio.geoportal-production:9000 --cloud-provider aws-s3 s3://altum-backups/ordinis-postgres ordinis-postgres`
**Vault sealed после restart:** `kubectl exec -n vault vault-0 -- vault operator unseal <KEY>` (key хранится у operator'а в 1Password / GitLab vars).
**Rollback план:** DNS TTL 60s — при критичных issues Альтум переключает обратно на staging через config flag.
---
**Email:** zimin.an@nstart.space (НЕ gmail)
-259
View File
@@ -1,259 +0,0 @@
# Ordinis API — гайд для интеграторов
Документ для команд которые хотят читать/писать справочники Ordinis из своих
сервисов (Альтум, геопортал, BI и т.д.).
**Owner:** Ordinis team — zimin.an@nstart.space.
## 1. Где живёт сервис
| Среда | URL | Auth |
|---------|--------------------------------------------------|-------------------------------------------|
| staging | `https://ordinis.k8s.265.nstart.cloud` | пока выключена (`ORDINIS_AUTH_REQUIRED=false`) |
| prod | `https://ordinis.k8s.264.nstart.cloud` | будет JWT (после prod stand-up) |
| local | `http://localhost:8080` | выключена |
## 2. Swagger UI / OpenAPI spec
- **Swagger UI:** `https://ordinis.k8s.265.nstart.cloud/swagger-ui.html`
- **Spec JSON:** `https://ordinis.k8s.265.nstart.cloud/v3/api-docs`
- **Spec YAML:** `https://ordinis.k8s.265.nstart.cloud/v3/api-docs.yaml`
Из spec можно генерировать клиент для Python (openapi-generator-cli),
TypeScript, Go и т.д. Пример:
```bash
openapi-generator-cli generate \
-i https://ordinis.k8s.265.nstart.cloud/v3/api-docs \
-g python \
-o ./altum-backend/clients/ordinis
```
## 3. Авторизация
Ordinis принимает JWT от Keycloak realm `nstart`:
- Issuer: `https://auth.nstart.space/realms/nstart`
- Алгоритм: RS256
- JWK Set discoverится через `/.well-known/openid-configuration`.
### Service account (m2m)
Для интеграции backend↔backend (например altum-backend → ordinis read-api)
получить service account клиент в Keycloak:
1. Запросить у Ordinis team создание клиента в realm `nstart`.
2. Назначить роль `ordinis-public` (или выше, если нужен RESTRICTED scope).
3. Получить `client_id` + `client_secret`.
**Получение access token (client_credentials):**
```bash
curl -X POST https://auth.nstart.space/realms/nstart/protocol/openid-connect/token \
-d "grant_type=client_credentials" \
-d "client_id=altum-backend" \
-d "client_secret=<SECRET>"
```
Ответ:
```json
{ "access_token": "eyJhbGciOi...", "expires_in": 300, "token_type": "Bearer" }
```
Кэшировать токен с TTL = `expires_in - 30` (буфер на retries) в backend, не
держать в фронте.
### Маппинг ролей в DataScope
| Keycloak роль | Ordinis DataScope |
|-------------------------------------------------|-------------------|
| `ordinis-public` / `public` | PUBLIC |
| `ordinis-internal` / `internal` | PUBLIC + INTERNAL |
| `ordinis-restricted` / `restricted` | + RESTRICTED |
Любая роль с суффиксом `-PUBLIC|INTERNAL|RESTRICTED` тоже работает (см.
`ordinis-auth/.../ScopeContext.java`). Это сделано чтобы не плодить orgaroles
для каждого consumer'а.
## 4. Read-side endpoints
### `GET /api/v1/dictionaries`
Список доступных справочников.
```bash
curl -H "Authorization: Bearer $TOKEN" \
https://ordinis.k8s.265.nstart.cloud/api/v1/dictionaries
```
```json
[
{ "name": "spacecraft", "displayName": "Космические аппараты", "scope": "PUBLIC", "schemaVersion": "1.0.0" },
{ "name": "mission", "displayName": "Миссии / программы", "scope": "PUBLIC", "schemaVersion": "1.0.0" },
...
]
```
### `GET /api/v1/{name}/records`
> ⚠️ **БЕЗ префикса `/dictionaries/`!** Read-api сидит на `/api/v1/{name}/records`.
> `/api/v1/dictionaries/...` — это admin-write контроллер для CRUD, у него нет
> bulk-list эндпоинта, не пытайтесь его читать.
Активные записи на момент `at` (default: `now`). Параметры:
| параметр | тип | пример | примечание |
|----------|-----------|------------------------------|------------------------------------------------|
| `lang` | string | `ru-RU`, `en-US` | flatten i18n полей до строки на этом языке. Короткие формы (`ru`, `en`) тоже принимаются. |
| `at` | ISO 8601 | `2026-05-04T12:00:00+03:00` | bitemporal query — вернёт активные на момент |
| `as_scope` | string | `PUBLIC,INTERNAL` | для anonymous + `allowQueryScope=true` (legacy) |
```bash
curl -H "Authorization: Bearer $TOKEN" \
"https://ordinis.k8s.265.nstart.cloud/api/v1/spacecraft/records?lang=ru-RU"
```
Без `?lang` ответ возвращает локализованные поля как объект:
```json
"name": { "ru-RU": "Ресурс-П № 3", "en-US": "Resurs-P No.3" }
```
С `?lang=ru-RU`:
```json
"name": "Ресурс-П № 3"
```
### `GET /api/v1/{name}/records/{businessKey}`
Одна запись. Параметры те же.
### `GET /api/v1/{name}/records/{businessKey}/history`
Все версии записи (включая закрытые). Используется UI-историей, для consumer'ов
обычно не нужно.
## 5. Стратегии кэширования
### A) Простой TTL (рекомендуется для v1)
```ts
// TanStack Query
useQuery({
queryKey: ['ordinis', 'spacecraft', lang],
queryFn: () => fetch(`/api/reference/spacecraft?lang=${lang}`).then(r => r.json()),
staleTime: 5 * 60_000, // 5 минут — справочники меняются редко
retry: 3,
})
```
### B) Webhook / Kafka push (v2)
Когда нужны realtime инвалидации — подписаться на топик
`nsi.dictionary.changed` (планируется в Ordinis v2). Payload:
```json
{
"name": "spacecraft",
"businessKey": "RESURS-P-3",
"action": "updated", // created / updated / closed
"at": "2026-05-15T10:30:00+03:00"
}
```
Consumer вычищает соответствующий ключ из своего кэша и pull'ит свежую версию.
### C) Snapshot at order time (для аудит-кейсов)
Когда нужен «как было на момент заказа» — сохранить полную копию использованных
справочников рядом с заказом:
```sql
CREATE TABLE imaging_order (
id UUID PRIMARY KEY,
spacecraft_business_key VARCHAR(256),
instrument_business_key VARCHAR(256),
reference_snapshot JSONB, -- копии записей, не FK
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
Альтернатива — запрашивать read-api с `?at=<order.created_at>` (bitemporal
поддерживает это). Snapshot надёжнее: read-only к prod, не зависит от наличия
старых версий после retention.
## 6. Best practices
1. **Не делайте FK на копию справочника**. Храните `business_key VARCHAR`
это стабильный идентификатор, валидный сквозь версии.
2. **Кэшируйте per `(dictionary, lang)`**. Inline в коде — `staleTime`/TTL.
3. **Refresh JWT перед TTL**. Не дожидайтесь 401 — большинство Keycloak SDK
рефрешит автоматически.
4. **Stale cache на 5xx/timeout**. Если Ordinis недоступен, отдавайте старые
данные с warn'ом в лог. Не падайте каскадно.
5. **Используйте businessKey, не UUID**. UUID — внутренний идентификатор записи
и меняется при создании новой версии (через update). businessKey стабилен.
6. **Не пишите массовых update'ов**. Ordinis оптимизирован под редкие записи и
частые чтения. Bulk import через bundle (`POST /admin/bundle/import`) или
через ETL.
## 7. Bitemporal модель — что значит `validFrom` / `validTo`
Каждая запись имеет:
- `validFrom` — с какого момента запись активна (бизнес-время).
- `validTo` — до какого момента (или `9999-12-31` если бессрочно).
- `version` — порядковый номер версии (JPA optimistic lock).
При update создаётся **новая версия**: старая `validTo` ставится в `now`,
новая создаётся с `validFrom = now`. Запись по `businessKey` всегда активна на
любой момент времени (не считая периодов когда она закрыта).
Для consumer'ов это значит: `GET /records/{key}?at=X` всегда возвращает
актуальную версию (или 404 если `key` был закрыт на этот момент).
## 8. Текущие справочники в bundle `cuod` (anchor v1)
| Имя | Scope | Записей (на staging) | Назначение |
|--------------------|--------|----------------------|-----------------------------------------------|
| `spacecraft` | PUBLIC | 4 | Каталог КА |
| `satellite_type` | PUBLIC | 5 | Классификация типов |
| `ground_station` | PUBLIC | 3 | Наземные пункты |
| `mission` | PUBLIC | 9 | Программы (Ресурс-П, Канопус, Sentinel...) |
| `instrument` | PUBLIC | 10 | Бортовые сенсоры (Геотон, MODIS, C-SAR...) |
| `processing_level` | PUBLIC | 8 | Уровни L0-L4 |
| `data_format` | PUBLIC | 8 | GeoTIFF, NITF, SAFE и т.д. |
Все справочники имеют `i18n` поля (`name`, `description`) с RU + EN. Используйте
`?lang=ru` для русского flatten, `?lang=en` для английского.
## 9. Cross-references между справочниками
- `spacecraft.satellite_type_code``satellite_type.code`
- `mission.spacecraft_codes``spacecraft.businessKey[]`
- `instrument.spacecraft_codes``spacecraft.businessKey[]`
- `instrument.supported_format_codes``data_format.businessKey[]`
- `instrument.supported_level_codes``processing_level.businessKey[]`
Эти связи **не enforced** на уровне БД (это NoSQL-style references поверх
JSONB). Consumer должен сам подтягивать связанные записи параллельными
запросами либо JOIN'ить в своей БД через snapshot.
Cascading select для UI:
```ts
const instrument = ... // выбран MSI-S2
const formats = await fetch(`/dictionaries/data_format/records?lang=${lang}`)
.then(r => r.json())
const allowedFormats = formats.filter(f =>
instrument.data.supported_format_codes.includes(f.businessKey))
// allowedFormats: SAFE, GEOTIFF, COG, JPEG2000
```
## 10. Поддержка / эскалация
- Bug в API: `git@git.nstart.local:2-6/2-6-4/terravault/ordinis` issues.
- Запрос новой роли в Keycloak / нового scope: zimin.an@nstart.space.
- Запрос нового справочника / схемы: создать issue в репо ordinis с label `dictionary-request`.
+1 -21
View File
@@ -1,4 +1,4 @@
title: Ordinis НСИ ЦУОД ОДХ
title: Ordinis НСИ ЦУОД ОДХ — Integrator Guide
href: index.md
items:
@@ -34,23 +34,3 @@ items:
items:
- name: Альтум — заказ съёмки
href: integration/altum-imaging-order.md
- name: Operations
items:
- name: Runbook (индекс)
href: ops/README.md
- name: SLO
href: ops/slo.md
- name: Outbox DLQ
href: ops/outbox-dlq.md
- name: Database migrations
href: ops/db-migrations.md
- name: Vault unseal
href: ops/vault-unseal.md
- name: Kafka restart
href: ops/kafka-restart.md
- name: Архитектура (legacy)
items:
- name: API integration (старая страница, см. /integration/api/)
href: tech/api-integration.md