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

Руководство по развёртыванию

Production-ready развёртывание системы OKTO: edge-сервис, центральный локальный сервер (далее — ЦЛС), PostgreSQL, центральная консоль, reverse-proxy, мониторинг, бэкапы и миграции.

Аудитория: DevOps, SRE, системные администраторы. Предпосылки: базовое знание Linux, systemd, Docker, PostgreSQL.


Полный A-to-Z сценарий развёртывания

От чистого Linux-сервера до 51 работающего шкафа — каждый шаг по имени, со временем и ответственным.

Глоссарий: что такое "factory URL"

Во всех примерах ниже и в cabinet-stamp.sh встречается https://factory.customer.ru — это плейсхолдер, подставляемый на конкретный customer. Это DNS-имя (или LAN IP) сервера, на котором админ запустил install-server.sh. Реальные значения могут быть любыми:

Ситуация у заказчика Значение factory URL
Есть публичный DNS с Let's Encrypt https://factory.mars-russia.ru
Только внутренний DNS на площадке https://okto-factory.mars.local
Только LAN IP, без DNS https://192.168.10.50 (self-signed cert)

Значение фиксируется один раз, на шаге 2 мастера первого запуска — мастер спрашивает "на каком хостнейме шкафы будут подключаться к серверу?" и записывает это в /etc/okto/server.env → OKTO_PUBLIC_HOSTNAME. После этого:

  1. Caddy автоматически выпускает TLS-сертификат на этот хостнейм.
  2. Дашборд в диалоге "Add Terminal" подставляет этот URL в генерируемую команду curl.
  3. OKTO-сборка прошивает тот же URL через cabinet-stamp.sh --factory-url ….

То есть "factory URL" — это то, что скажет сам заказчик при первичной настройке сервера; OKTO перед сборкой получает его от заказчика и использует в stamper'е.

Переназначение шкафа (unpair / reassign)

На странице Rollout, в карточке любого шкафа, у которого есть привязанный deviceId, доступна кнопка Unpair cabinet. Это:

  1. Отзывает device JWT того шкафа на стороне factory-server.
  2. Отправляет DeprovisionCmd по WebSocket в шкаф (best-effort — если шкаф офлайн, всё равно всё очищается на сервере).
  3. На шкафу edge-service очищает локальный токен, перезапускает PairingClientService — на kiosk'е через несколько секунд появляется свежий PIN.
  4. Слот в rollout переходит в статус BACK_ON_BENCH, deviceId = null.
  5. Шкаф появляется в панели "Cabinets waiting to pair" — админ либо даёт ему авто-восстановиться на тот же слот (default когда шкаф OKTO-stamped), либо вручную пэйрит его на другой слот через панель ожидания.

Сценарии, когда это нужно: - Шкаф ошибочно stamped'нут на неправильный слот на сборке. - Перестановка шкафа между линиями по решению заказчика. - Резервный шкаф временно занимает слот упавшего, после замены возвращается на свою стоянку. - Диагностика: "отцепить и перепривязать заново, посмотреть, проходит ли pairing чисто".

Endpoint: POST /api/v1/rollout/cabinets/{slotId}/unpair с JSON { "reason": "..." } (только ADMIN). См. RolloutService.unpair.

Шаг 0 · OKTO assembly (один раз за каждый шкаф)

Ответственный: OKTO engineer на сборочной линии. Время: ~5 секунд на шкаф.

sudo packaging/cabinet-stamp.sh \
  --slot LUZ-DRY-03 \
  --factory-url https://factory.customer.ru \
  --print-label

Пишет /etc/okto/cabinet-slot + /etc/okto/factory-url на образе шкафа перед отгрузкой. Больше никакой уникальной конфигурации на шкаф — один stamp на сборке, всё.

Шаг 1 · Установка factory-server (один раз на площадку)

Ответственный: DevOps / IT-администратор заказчика. Время: ~15 минут от момента "положили флешку" до "открыли мастер в браузере".

Два пути на выбор:

1A. Cloud-init autoinstall USB (новый, полностью unattended):

# На рабочей станции OKTO — собираем загрузочную флешку один раз:
./packaging/cloud-init/build-usb.sh ubuntu-24.04-live-server-amd64.iso
sudo dd if=dist/okto-factory-autoinstall.iso of=/dev/sdX bs=4M

Флешка едет к заказчику. Админ вставляет её в чистый сервер, включает питание, через ~12 минут сервер сам завершает установку Ubuntu → перезагружается → выполняет install-server.sh → показывает на логин-экране (/etc/issue) URL мастера + QR-код. Ноль SSH-сессий, ноль команд в терминале.

1B. Одна команда на живом Ubuntu (существующий путь):

curl -sSL https://get.okto.ru/server | sudo bash

В 1B админ сам устанавливает Ubuntu и вручную запускает команду. Быстрее, если Ubuntu уже стоит; требует SSH.

Шаг 2 · Мастер первого запуска (5 кликов, ~3 минуты)

Ответственный: admin заказчика. Время: ~3 минуты.

Открыть URL из Шага 1 → мастер проводит через: пароль админа → публичный хостнейм + TLS → токен OKTO Cloud → метаданные площадки → финиш. Мастер self-seal'ится, OTP инвалидируется, маршрут 404ит.

Опционально: импорт CSV rollout-манифеста, если какие-то шкафы НЕ были stamped на шаге 0 (кнопка Import manifest (unstamped) на странице Rollout).

Шаг 3 · Физический монтаж шкафа (ноль кликов)

Ответственный: полевой инженер заказчика. Время: ~10 секунд на шкаф, включая путь к шкафу.

  1. Подключить питание + Ethernet.
  2. Уйти.

Edge-service читает /etc/okto/cabinet-slot → представляется factory-server'у как LUZ-DRY-03 → factory-server автоматически апрувит pairing → kiosk открывается с operator UI. Админ в офисе видит в дашборде, как карточка LUZ-DRY-03 переходит PLANNED → ENROLLED за 3-5 секунд.

Шаг 4 · Эксплуатация (1 клик на релиз)

Ответственный: admin заказчика. Время: <1 минута на релиз.

  • FirmwareAutoUpdateService в factory-server каждые 5 минут опрашивает OKTO Cloud, скачивает новые релизы, показывает баннер в дашборде.
  • Админ нажимает Deploy, выбирает canary (1-2 шкафа), observe-окно (по умолчанию 30 мин) → factory-server отправляет UpdateFirmwareCmd на canary → если canary ок, автоматически промотит на остальной парк; если нет, автоматически откатывает canary (AUTO_ROLLED_BACK).
  • На любом шкафу в любой момент — кнопка Rollback в карточке релиза: factory-server шлёт RollbackFirmwareCmd → edge промотит /opt/okto/edge-service.jar.previous обратно → restart.

Суммарное человеческое внимание за весь rollout

Роль Действий за весь rollout
OKTO engineer (сборка) 51 × cabinet-stamp.sh ≈ 4 минуты
Customer admin (сервер) 1 × dd USB + 5-step wizard ≈ 9 минут
Customer admin (шкафы) 0 (авто-pair)
Field engineer (шкафы) 51 × подключил 2 кабеля ≈ 10 минут
Customer admin (firmware) ~1 клик на релиз
ИТОГО ~25 минут живого внимания на 51 шкаф

Важно: дефолтный путь — zero-touch (три модели)

Этот документ описывает advanced / air-gapped сценарий с полной ручной настройкой. В типовой установке используется одна из трёх zero-touch моделей:

1. Шкаф OKTO с установленным seal (рекомендуется для 51-шкафного rollout)

OKTO стампует /etc/okto/cabinet.seal на этапе сборки на своём производстве (см. packaging/cabinet-seal.sh). Шкаф приезжает к заказчику готовый: подключили питание + Ethernet → edge-service читает seal → auto-enroll в factory-server → киоск открывается.

# На стенде OKTO при сборке шкафа:
sudo ./packaging/cabinet-seal.sh \
  --cabinet-id  LUZ-DRY-01 \
  --factory-url https://factory.customer.ru \
  --enrollment-key $(cat /secure/luz-enrollment.key) \
  --variant DRY --site LUZ --industry tobacco

Первая загрузка шкафа на площадке: ноль ввода, ноль QR-кодов, ноль curl-ов. См. edge-service/.../CabinetSealService.kt.

2. Vanilla Ubuntu + installer-OTP из консоли (для добавления в существующий парк)

Когда у заказчика появляется 52-й шкаф не из поставки OKTO:

# В центральной консоли → «Устройства» → «+ Add Terminal» → QR/curl one-liner
curl -sSL https://<server>/i/ot_XXXXXXXX | sudo bash

3. Standalone без factory-server (для мелких клиентов)

# На отдельной Ubuntu-машине:
docker run -d -p 80:80 -v terminal-data:/app/data okto/terminal:latest
# → открыть http://localhost → Настройки → Активация → вход в OKTO Cloud

Сервер (factory-server) — один раз на площадку

curl -sSL https://get.okto.ru/server | sudo bash
# → печатает URL мастера первого запуска с одноразовым OTP

Zero-touch сценарии покрывают 95% того, что описано в этом документе (установка Docker, генерация секретов, compose + Caddy + auto-TLS, Flyway миграции, systemd unit, enrollment устройств, kiosk-автозапуск, hardware auto-detect, staged firmware rollout + rollback + auto-update из OKTO Cloud).

Единая концепция rollout — slot-name-as-identity

Самый простой путь для 51-шкафного парка. Один концепт вместо двух:

  • Имя шкафа в софте == имя rollout-слота в дашборде (например LUZ-DRY-03).
  • OKTO-сборка один раз за шкаф выполняет sudo packaging/cabinet-stamp.sh --slot LUZ-DRY-03 --factory-url https://factory.customer.ru.
  • На площадке заказчика: воткнули в розетку + Ethernet, шкаф phones home с именем LUZ-DRY-03, factory-server автоматически апрувит pairing, потому что такой slot уже есть в плане.

Ноль действий от админа заказчика. Ноль CSV. Ноль секретов в шкафу. Ноль ручного типирования где-либо.

Flow в деталях:

  1. OKTO на своей сборочной линии запускает stamper на каждом шкафу — два текстовых файла: /etc/okto/cabinet-slot (= имя rollout слота, e.g. LUZ-DRY-03) и /etc/okto/factory-url (HTTPS URL factory-server'а заказчика).
  2. Шкаф едет к заказчику. Ничего секретного внутри: только имя слота + URL, оба напечатаны на наклейке на корпусе.
  3. Заказчик разворачивает шкаф на линии, подключает Ethernet + питание.
  4. Edge-service читает /etc/okto/cabinet-slot → представляется factory-server'у как LUZ-DRY-03.
  5. Factory-server видит, что слот LUZ-DRY-03 уже в плане rollout'а, а deviceId = null → автоматически апрувит pairing, выдаёт device JWT, переводит слот в ENROLLED.
  6. Дашборд в Rollout Command Center показывает живой прогресс — карточка слота меняется с PLANNED на ENROLLED за 3-5 секунд, без единого клика админа.

Fallback пути (если stamp не выполнен или шкаф переименован в поле):

  • QR-скан — на kiosk'e шкафа есть QR-код рядом с PIN-кодом, админ сканирует с телефона, открывается дашборд с предзаполненной формой pairing, один тап.
  • PIN — шкаф показывает 6-значный PIN, админ видит карточку в "Cabinets waiting to pair" на Rollout-странице, пару кликов.
  • Hardware-id CSV (unstamped) — кнопка "Import manifest (unstamped)" на Rollout странице принимает CSV со строками slot_id,hardware_id для шкафов, которые не были stamped на сборке.

Все три fallback пути не требуют никакой настройки сверх factory-server'а — они работают из коробки.

MARS Rollout Command Center

Открывается в консоли по ссылке Развёртывание в левой панели (/rollout). Специализированный экран под контракт OKTO × MARS: 51 шкаф через LUZ / NOV / MIR / RND × DRY / WET × PLC / UPS_ONLY.

Что видит инженер эксплуатации:

  • KPI сверху — всего шкафов / % в production / подключено / требует внимания (FAILED, BACK_ON_BENCH).
  • Карточки по площадкам — прогресс-бар «в production / всего», разрез по DRY/WET, клик по карточке фильтрует список ниже.
  • Phase timeline — F4_PILOT / F5_WAVE_1 / F5_WAVE_2 / F5_WAVE_3 с долями «done / in-flight / planned».
  • Список шкафов — фильтры (площадка, вариант, статус), live-статус из DevicesTable + текущая firmware-версия, кнопка «Advance» одним кликом переводит в следующий статус.
  • Drawer по шкафу — полный lifecycle-timeline (sealed → shipped → powered_on → enrolled → observer → cutover → production), журнал событий (кто, когда, что сказал), кнопки всех возможных переходов, кнопка «Open in Fleet» (ведёт на /devices/{id} когда шкаф уже подключён).

Data-модель: RolloutCabinetsTable + RolloutEventsTable. Seed из 51 шкафа выполняется автоматически при первом старте factory-server (RolloutService.seedIfEmpty); повторные рестарты — no-op. Переходы статусов валидируются whitelist'ом в RolloutService.allowedTransitions — нельзя случайно перескочить из PLANNED сразу в PRODUCTION.

Firmware auto-update + staged rollout + rollback

Ключевая функциональность для 51-шкафного парка (§4.3 технического предложения):

  1. Авто-получение релизовFirmwareAutoUpdateService опрашивает https://releases.okto.ru/edge-service/manifest.json каждые 5 минут, проверяет SHA-256, загружает артефакт в локальный factory-server, на дашборде появляется баннер «New firmware available».
  2. Staged rollout — админ выбирает canary-устройства (1-2 шкафа), остальные идут автоматически после observe-окна (по умолчанию 30 мин). Если canary не прошёл threshold — автоматический rollback canary-среза. См. FirmwareService.stagedDeploy.
  3. Rollback — кнопка «Rollback» на карточке релиза в консоли. Edge-service промотирует /opt/okto/edge-service.jar.previous (бэкап, который okto-edge-swap-firmware сохраняет на каждом обновлении) через okto-edge-rollback-firmware — без повторной загрузки артефакта. Полный flow: команда RollbackFirmwareCmd → edge-helper → systemctl restart okto-edge → готово.

Когда нужны остальные разделы ниже

  • нужна собственная схема хранения секретов (vault, Kubernetes Secrets, AWS SSM);
  • требуется тонкая настройка PostgreSQL / PgBouncer / read-replica;
  • нужен air-gapped deploy без доступа к get.okto.ru и releases.okto.ru;
  • требуется интеграция с существующим observability-стэком (Grafana Cloud и т. п.);
  • делаете миграцию со старой системы или восстановление из бэкапа.

Ключевые файлы реализации zero-touch (для обзора — не меняйте вручную без необходимости):

Bootstrap: - install-server.sh — bootstrap сервера - install.sh — bootstrap терминала (OTP-вариант) - packaging/cabinet-seal.sh — стампер cabinet-seal для сборки OKTO - docker/docker-compose.server.yml — production compose-стэк - docker/Caddyfile — reverse-proxy + auto-TLS - packaging/systemd/okto-server.service — systemd unit сервера

Enrollment: - factory-server/.../SetupService.kt — state machine мастера первого запуска - factory-server/.../InstallerService.kt — генерация и разбор installer-OTP - edge-service/.../CabinetSealService.kt — first-boot auto-enroll по cabinet seal - management-dashboard/src/pages/Setup.tsx — UI мастера /setup - management-dashboard/src/ui/components/AddTerminalDialog.tsx — диалог «+ Add Terminal»

Firmware rollout + rollback: - factory-server/.../FirmwareService.ktdeploy, stagedDeploy, rollback - factory-server/.../FirmwareAutoUpdateService.kt — авто-поллинг OKTO Cloud manifest - packaging/systemd/okto-edge-swap-firmware — супервизор swap + backup prev - packaging/systemd/okto-edge-rollback-firmware — супервизор rollback с последнего backup - management-dashboard/src/pages/Firmware.tsx — баннер нового релиза, staged UI, Rollback-диалог


Содержание

  1. Варианты развёртывания
  2. Системные требования
  3. Подготовка хоста
  4. Развёртывание ЦЛС
  5. PostgreSQL: установка и настройка
  6. Развёртывание edge-сервиса
  7. Reverse-proxy и TLS
  8. Центральная консоль (статика)
  9. Docker Compose: полный стек
  10. Кибербезопасность и hardening
  11. Наблюдаемость: Prometheus + Grafana + Loki
  12. Бэкапы и восстановление
  13. Миграции схемы
  14. Обновление (rolling update)
  15. Масштабирование
  16. Чек-лист приёмки

1. Варианты развёртывания

Сценарий Описание Когда использовать
Standalone Terminal Один edge-сервис в режиме DIRECT_CLOUD шлёт напрямую в облако 1 терминал, простая интеграция, нет on-prem инфраструктуры
Factory Site ЦЛС + N edge-сервисов в режиме VIA_LOCAL_SERVER Завод 3–500 терминалов, требование аудита, OTA, группового управления
Hybrid Часть устройств DIRECT_CLOUD, часть VIA_LOCAL_SERVER Миграция / A-B rollout
High-Availability Active-Passive ЦЛС + shared PostgreSQL Критичные производства (роадмап)

В этом документе описан второй сценарий — Factory Site — как наиболее востребованный.


2. Системные требования

Центральный локальный сервер

Пункт Минимум Рекомендовано
CPU 2 vCPU 4 vCPU
RAM 4 GB 8 GB
Диск 40 GB SSD 200 GB NVMe (для логов устройств и прошивок)
ОС Ubuntu 22.04 / Debian 12 / RHEL 9 Ubuntu 24.04 LTS
Сеть 100 Mbps 1 Gbps
Java OpenJDK 17 OpenJDK 17 (Temurin)
PostgreSQL 14 16

Edge-сервис (граничное устройство)

Пункт Минимум Рекомендовано
CPU 2 vCPU 2 vCPU
RAM 1 GB 2 GB
Диск 8 GB 32 GB
ОС Debian 11, Ubuntu 20.04, RHEL 8 Debian 12
Java OpenJDK 17 OpenJDK 17

Сетевые порты

Сервис Порт Direction Примечание
ЦЛС HTTP 8081 Listen В продакшне за reverse-proxy на 443
ЦЛС Prometheus 9091 Listen Только loopback / VPN
ЦЛС PostgreSQL 5432 Outbound к БД
Edge HTTP 8080 Listen Локальный UI оператора
Edge → ЦЛС 443 Outbound WSS + HTTPS
Edge → OKTO Cloud 443 Outbound В DIRECT_CLOUD режиме
Принтеры / Сканеры Specific TCP in/out Обычно 9100, 4001 и т.п.
Modbus TCP 502 Outbound PLC

3. Подготовка хоста

3.1 Пользователь и директории

sudo useradd --system --home /var/lib/okto --shell /usr/sbin/nologin okto

sudo mkdir -p /opt/okto /var/lib/okto /var/log/okto /etc/okto
sudo chown -R okto:okto /opt/okto /var/lib/okto /var/log/okto
sudo chmod 750 /etc/okto

3.2 OpenJDK 17

# Ubuntu / Debian
sudo apt-get update
sudo apt-get install -y openjdk-17-jre-headless

# Проверка
java -version
# openjdk version "17.0.x"

3.3 Файервол (ufw пример)

sudo ufw default deny incoming
sudo ufw default allow outgoing

# Доступ по SSH только с bastion host
sudo ufw allow from 10.0.0.5 to any port 22

# HTTPS (через reverse-proxy)
sudo ufw allow 443/tcp

# Prometheus — только VPN
sudo ufw allow from 10.8.0.0/24 to any port 9091

sudo ufw enable

3.4 Временна́я зона и NTP

sudo timedatectl set-timezone Europe/Moscow
sudo systemctl enable --now systemd-timesyncd

Крайне важно: JWT и аудит полагаются на правильное время. Расхождение > 60 с может инвалидировать токены.


4. Развёртывание ЦЛС

4.1 Установка JAR

# Загрузите артефакт из CI
sudo -u okto curl -L -o /opt/okto/factory-server.jar \
  https://releases.okto.ru/factory-server/1.2.3/factory-server.jar

sudo chown okto:okto /opt/okto/factory-server.jar

4.2 Конфигурация

/etc/okto/factory-application.yaml:

server:
  host: "127.0.0.1"           # reverse-proxy завернёт публичный трафик
  port: 8081
  enableCors: false

database:
  host: "127.0.0.1"
  port: 5432
  database: "okto_factory"
  username: "okto"
  password: "${OKTO_DB_PASSWORD}"
  maxPoolSize: 20
  minIdle: 5

cloudSync:
  enabled: true
  cloudServerUrl: "https://app.okto.ru/api/v1"
  syncIntervalMs: 10000
  batchSize: 100
  maxRetries: 10
  retryDelayMs: 5000
  authToken: "${OKTO_CLOUD_AUTH_TOKEN}"

auth:
  jwtSecret: "${OKTO_JWT_SECRET}"
  jwtIssuer: "okto-factory"
  jwtAudience: "okto-edge"
  tokenExpirationMs: 86400000
  deviceEnrollmentKey: "${OKTO_ENROLLMENT_KEY}"
  allowAutoEnrollment: false        # в продакшне — false

firmware:
  storageDir: "/var/lib/okto/firmware"
  maxArtifactSizeBytes: 268435456

management:
  defaultCommandTimeoutMs: 30000
  maxGroupSize: 500
  heartbeatIntervalSeconds: 25

observability:
  metricsEnabled: true
  metricsPort: 9091
  tracingEnabled: true
  otlpEndpoint: "http://127.0.0.1:4317"
  serviceName: "okto-factory"

Секреты — в /etc/okto/factory-server.env (режим 640, owner okto):

OKTO_DB_PASSWORD=<random 40-char>
OKTO_JWT_SECRET=<random 64-char base64>
OKTO_ENROLLMENT_KEY=<random 32-char>
OKTO_CLOUD_AUTH_TOKEN=<from OKTO Cloud dashboard>

Генерация:

openssl rand -base64 48          # для JWT_SECRET
openssl rand -hex 20             # для остальных

4.3 systemd unit

/etc/systemd/system/okto-factory.service:

[Unit]
Description=OKTO Central Local Server (factory-server)
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service

[Service]
Type=simple
User=okto
Group=okto
EnvironmentFile=/etc/okto/factory-server.env
Environment=JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Djava.net.preferIPv4Stack=true"
ExecStart=/usr/bin/java $JAVA_OPTS -jar /opt/okto/factory-server.jar /etc/okto/factory-application.yaml
WorkingDirectory=/var/lib/okto
StandardOutput=append:/var/log/okto/factory-server.log
StandardError=append:/var/log/okto/factory-server.log
Restart=always
RestartSec=5
LimitNOFILE=65536

# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/okto /var/log/okto
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes

[Install]
WantedBy=multi-user.target

Запуск:

sudo systemctl daemon-reload
sudo systemctl enable --now okto-factory
sudo systemctl status okto-factory
sudo journalctl -u okto-factory -f

4.4 Смена admin-пароля

JWT=$(curl -sX POST https://factory.okto.ru/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"admin123"}' | jq -r .data.token)

curl -X POST https://factory.okto.ru/api/v1/auth/change-password \
  -H "Authorization: Bearer $JWT" -H 'Content-Type: application/json' \
  -d '{"currentPassword":"admin123","newPassword":"<new-strong>"}'

5. PostgreSQL: установка и настройка

5.1 Установка

sudo apt-get install -y postgresql-16
sudo systemctl enable --now postgresql

5.2 Создание БД и пользователя

sudo -u postgres psql <<SQL
CREATE USER okto WITH PASSWORD '<OKTO_DB_PASSWORD>';
CREATE DATABASE okto_factory OWNER okto ENCODING 'UTF8' LC_COLLATE 'en_US.UTF-8' LC_CTYPE 'en_US.UTF-8';
GRANT ALL PRIVILEGES ON DATABASE okto_factory TO okto;
SQL

5.3 Тюнинг (/etc/postgresql/16/main/postgresql.conf)

Для 4 vCPU / 8 GB RAM:

shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 32MB
maintenance_work_mem = 512MB
wal_buffers = 16MB
min_wal_size = 1GB
max_wal_size = 4GB

max_connections = 100
random_page_cost = 1.1                 # SSD

# Логирование
log_min_duration_statement = 1000       # 1 s slow queries
log_checkpoints = on
log_connections = on
log_disconnections = on
log_line_prefix = '%t [%p] %q%u@%d '
log_temp_files = 0

/etc/postgresql/16/main/pg_hba.conf — localhost по паролю:

local  all       all                             peer
host   okto_factory  okto  127.0.0.1/32          scram-sha-256
host   okto_factory  okto  ::1/128               scram-sha-256

Перезапуск:

sudo systemctl restart postgresql

5.4 Ролевой аудит

sudo -u postgres psql -c "\du"

5.5 Схема

При первом запуске Database.init() создаст все таблицы через SchemaUtils.create(...). См. §13 Миграции для эволюции.


6. Развёртывание edge-сервиса

6.1 Установка

sudo -u okto curl -L -o /opt/okto/edge-service.jar \
  https://releases.okto.ru/edge-service/1.2.3/edge-service.jar

6.2 Конфигурация /etc/okto/edge-application.yaml

server:
  host: "0.0.0.0"
  port: 8080
  corsHosts: ["http://localhost:3000","http://localhost:5180"]

database:
  path: "/var/lib/okto/edge.db"

device:
  identifier: "edge-01"
  name: "Line 1 terminal"
  companyId: "acme"
  productionLineId: "line-1"

factoryServer:
  host: "factory.okto.ru"
  port: 443
  useSsl: true
  enrollmentKey: "${OKTO_ENROLLMENT_KEY}"
  deviceName: "Line 1 terminal"
  companyId: "acme"
  productionLineId: "line-1"

cloud:
  host: "app.okto.ru"
  port: 443
  useSsl: true
  apiKey: "${OKTO_CLOUD_API_KEY}"     # для DIRECT_CLOUD

connection:
  defaultMode: "VIA_LOCAL_SERVER"
  allowOverride: false

sync:
  intervalMs: 15000
  batchSize: 50
  maxRetries: 10
  retryDelayMs: 5000

printers:
  - id: "videojet-1"
    type: "VIDEOJET"
    host: "10.0.1.10"
    port: 9100

scanners:
  - id: "honeywell-1"
    type: "TCP"
    host: "10.0.1.11"
    port: 4001

logging:
  level: "INFO"
  file: "/var/log/okto/edge-service.log"
  maxSizeMb: 100
  maxBackups: 10

6.3 systemd unit /etc/systemd/system/okto-edge.service

См. полный файл в packaging/systemd/okto-edge.service.

Важно:

Environment=OKTO_FIRMWARE_STAGING_DIR=/var/lib/okto/firmware/staging
ExecStart=/usr/bin/java -jar /opt/okto/edge-service.jar /etc/okto/edge-application.yaml
Restart=always
ReadWritePaths=/var/lib/okto /var/log/okto

6.4 OTA-support unit'ы

sudo cp packaging/systemd/okto-edge-update.service /etc/systemd/system/
sudo cp packaging/systemd/okto-edge-swap-firmware /usr/local/bin/
sudo chmod 755 /usr/local/bin/okto-edge-swap-firmware
sudo cp packaging/sudoers/okto /etc/sudoers.d/okto
sudo visudo -c -f /etc/sudoers.d/okto         # проверка синтаксиса

6.5 Запуск

sudo systemctl daemon-reload
sudo systemctl enable --now okto-edge
sudo systemctl status okto-edge

Первая итерация: edge обращается к POST /api/v1/devices/edge-01/token с X-Enrollment-Key, получает device JWT, сохраняет в /var/lib/okto/edge.db → device_auth.


7. Reverse-proxy и TLS

7.1 Caddy (рекомендуется для простоты)

/etc/caddy/Caddyfile:

factory.okto.ru {
    encode gzip zstd

    # REST API
    reverse_proxy /api/* 127.0.0.1:8081

    # Центральная консоль (статика)
    handle_path /* {
        root * /var/www/okto-console
        try_files {path} /index.html
        file_server
    }

    # WebSocket
    @ws path /ws/*
    reverse_proxy @ws 127.0.0.1:8081 {
        transport http {
            versions 1.1
            read_buffer 65536
        }
    }

    # Security headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options "nosniff"
        Referrer-Policy "strict-origin-when-cross-origin"
        X-Frame-Options "DENY"
    }

    # Скрываем device JWT из логов
    log {
        output file /var/log/caddy/factory.log
        format filter {
            wrap json
            fields {
                request>uri query_redact
            }
        }
    }
}

7.2 nginx (альтернатива)

server {
    listen 443 ssl http2;
    server_name factory.okto.ru;

    ssl_certificate     /etc/letsencrypt/live/factory.okto.ru/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/factory.okto.ru/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    client_max_body_size 256m;

    location /api/ {
        proxy_pass http://127.0.0.1:8081;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_read_timeout 300s;
    }

    location /ws/ {
        proxy_pass http://127.0.0.1:8081;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }

    location / {
        root /var/www/okto-console;
        try_files $uri /index.html;
    }

    # Скрыть device JWT из access log
    log_format custom '$remote_addr - $remote_user [$time_local] "$request_method $uri $server_protocol" '
                      '$status $body_bytes_sent "$http_referer" "$http_user_agent"';
    access_log /var/log/nginx/factory.access.log custom;
}

Важно: избегайте $request в log_format для WS-эндпоинтов — он содержит query-string с device JWT.


8. Центральная консоль (статика)

cd management-dashboard
npm ci
VITE_API_BASE=/api/v1 VITE_WS_BASE=/ws npm run build

sudo mkdir -p /var/www/okto-console
sudo cp -r dist/* /var/www/okto-console/
sudo chown -R www-data:www-data /var/www/okto-console   # или caddy:caddy

Vite собирает SPA в dist/. Reverse-proxy раздаёт с fallback на index.html (SPA routing).


9. Docker Compose: полный стек

docker/docker-compose.prod.yml:

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: okto
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
      POSTGRES_DB: okto_factory
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./postgres/postgresql.conf:/etc/postgresql/postgresql.conf:ro
    secrets:
      - db_password
    command: ["postgres","-c","config_file=/etc/postgresql/postgresql.conf"]
    healthcheck:
      test: ["CMD-SHELL","pg_isready -U okto -d okto_factory"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  factory:
    image: okto/factory-server:1.2.3
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      OKTO_DB_PASSWORD_FILE: /run/secrets/db_password
      OKTO_JWT_SECRET_FILE: /run/secrets/jwt_secret
      OKTO_ENROLLMENT_KEY_FILE: /run/secrets/enrollment_key
      OKTO_CLOUD_AUTH_TOKEN_FILE: /run/secrets/cloud_auth_token
    volumes:
      - ./factory/application.yaml:/etc/okto/application.yaml:ro
      - factory_firmware:/var/lib/okto/firmware
      - factory_logs:/var/log/okto
    secrets:
      - db_password
      - jwt_secret
      - enrollment_key
      - cloud_auth_token
    ports:
      - "127.0.0.1:8081:8081"
      - "127.0.0.1:9091:9091"
    restart: unless-stopped

  caddy:
    image: caddy:2
    depends_on: [factory]
    ports: ["80:80","443:443"]
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - ./console/dist:/var/www/okto-console:ro
      - caddy_data:/data
      - caddy_config:/config
    restart: unless-stopped

volumes:
  pgdata:
  factory_firmware:
  factory_logs:
  caddy_data:
  caddy_config:

secrets:
  db_password:
    file: ./secrets/db_password
  jwt_secret:
    file: ./secrets/jwt_secret
  enrollment_key:
    file: ./secrets/enrollment_key
  cloud_auth_token:
    file: ./secrets/cloud_auth_token

Генерация секретов:

mkdir -p secrets
openssl rand -base64 48 | tr -d '\n' > secrets/jwt_secret
openssl rand -hex 20 | tr -d '\n' > secrets/enrollment_key
openssl rand -base64 32 | tr -d '\n' > secrets/db_password
echo -n "<token-from-okto-cloud>" > secrets/cloud_auth_token
chmod 600 secrets/*

Запуск:

docker compose -f docker/docker-compose.prod.yml up -d
docker compose logs -f factory

10. Кибербезопасность и hardening

Checklist

  • TLS: только TLSv1.2+, HSTS включён, сертификаты автообновляются (Let's Encrypt / внутренний CA).
  • JWT secret хранится в env / vault, не в git.
  • Admin-пароль изменён сразу после первого запуска.
  • Auto-enrollment выключен (auth.allowAutoEnrollment: false) после первичной регистрации всех устройств.
  • Firewall: порт 8081 не открыт снаружи; только 443 через reverse-proxy.
  • Rate-limit на /api/v1/auth/login (nginx limit_req_zone или Ktor plugin).
  • fail2ban реагирует на LOGIN_FAILED события (паттерн в /var/log/okto/factory-server.log).
  • Prometheus слушает только на loopback / VPN.
  • Access-логи reverse-proxy стрипают query-string для /ws/* (device JWT!).
  • sudoers настроен минимально (только 4 команды).
  • SELinux/AppArmor профиль для okto-factory и okto-edge (опционально).
  • Обновления: unattended-upgrades включены для security patches.
  • SSH: login by key only, отключён root, port-knocking или VPN.
  • Container image scanning: Trivy / Grype перед каждым релизом.
  • Ed25519-подписание прошивок включено (см. SERVER_MANAGEMENT.ru.md §10.5).

Поворот секретов

Секрет Периодичность Процедура
auth.jwtSecret 90 дней Изменить env var → restart factory → все JWT invalidated, пользователи логинятся заново
auth.deviceEnrollmentKey 1 год / при компрометации Изменить env var → передать новый ключ всем edge (через push_config + restart_service)
cloudSync.authToken по правилам OKTO Cloud Request new token в OKTO Cloud → update env → restart
admin-пароль 90 дней UI → Настройки аккаунта → Сменить пароль
PostgreSQL password 180 дней ALTER ROLE okto WITH PASSWORD '…' → update env → restart

11. Наблюдаемость: Prometheus + Grafana + Loki

11.1 Prometheus

/etc/prometheus/prometheus.yml:

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'okto-factory'
    static_configs:
      - targets: ['127.0.0.1:9091']
  - job_name: 'okto-edge'
    static_configs:
      - targets: ['edge-01:9092','edge-02:9092']
  - job_name: 'node'
    static_configs:
      - targets: ['127.0.0.1:9100']
  - job_name: 'postgres'
    static_configs:
      - targets: ['127.0.0.1:9187']

11.2 Grafana дашборды

Рекомендуемые панели:

  1. Fleet health: okto_devices_online + heartbeat lag + CPU / memory по устройствам.
  2. Cloud queue: okto_cloud_sync_queue_size{status} + throughput.
  3. Commands: rate(okto_commands_total[5m]) by (type,status).
  4. Firmware: табло по rollout — % устройств на каждой версии.
  5. HTTP latency: histogram_quantile(0.95, …) для /api/v1/sync и /api/v1/devices/{id}/commands.
  6. PostgreSQL: connections, slow queries, replication lag.
  7. JVM: heap, GC pause, threads.

11.3 Loki для логов

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: okto-factory
    static_configs:
      - targets: [localhost]
        labels:
          job: okto-factory
          __path__: /var/log/okto/factory-server.log
  - job_name: okto-edge
    static_configs:
      - targets: [localhost]
        labels:
          job: okto-edge
          __path__: /var/log/okto/edge-service.log

11.4 Alerting

Рекомендованные правила:

groups:
  - name: okto.rules
    rules:
      - alert: FactoryServerDown
        expr: up{job="okto-factory"} == 0
        for: 1m
        labels: { severity: P1 }
        annotations:
          summary: "Central local server is down"

      - alert: CloudQueueDeadLetter
        expr: okto_cloud_sync_queue_size{status="DEAD_LETTER"} > 0
        for: 5m
        labels: { severity: P2 }

      - alert: DevicesOfflineSurge
        expr: delta(okto_devices_online[5m]) < -5
        labels: { severity: P1 }

      - alert: CommandTimeoutsHigh
        expr: rate(okto_commands_total{status="TIMEOUT"}[5m]) > 0.1
        for: 5m
        labels: { severity: P2 }

      - alert: PostgresHighConnections
        expr: pg_stat_activity_count > 80
        for: 5m
        labels: { severity: P3 }

12. Бэкапы и восстановление

12.1 Что бэкапить

  • PostgreSQL okto_factory (полностью).
  • Каталог прошивок /var/lib/okto/firmware (артефакты с SHA).
  • Конфигурация /etc/okto/*.yaml (без секретов — они в env / vault).
  • Audit log можно экспортировать отдельно для long-term retention.
  • Edge SQLite (/var/lib/okto/edge.db) — критично для операций, ещё не синхронизированных в ЦЛС.

12.2 PostgreSQL: логический бэкап

# Ежедневно в 02:00
0 2 * * * pg_dump -Fc -U okto okto_factory | gpg --encrypt --recipient backup@okto.ru \
          > /backup/factory-$(date +\%Y\%m\%d).dump.gpg

# Weekly: rsync в offsite

Retention: 7 daily + 4 weekly + 12 monthly.

12.3 PostgreSQL: физический бэкап (PITR)

Настройте WAL archiving:

archive_mode = on
archive_command = 'test ! -f /backup/wal/%f && cp %p /backup/wal/%f'
wal_level = replica

И базовый бэкап раз в неделю pg_basebackup -D /backup/base-$(date +%F) -Ft -z -P.

12.4 Восстановление

# Logical restore
sudo -u postgres createdb okto_factory
gpg --decrypt /backup/factory-20260417.dump.gpg | pg_restore -U okto -d okto_factory

# PITR
pg_basebackup restore + recovery.conf с restore_command + target time

После restore: рестарт ЦЛС, проверка /api/v1/health, smoke-тест GET /api/v1/devices.

12.5 Edge SQLite

Копирование файла во время работы — небезопасно. Используйте:

sqlite3 /var/lib/okto/edge.db ".backup /backup/edge-$(date +%F).db"

Cron-задача раз в час. Retention 48 часов локально + sync на ЦЛС.


13. Миграции схемы

Текущая модель

При старте ЦЛС вызывает SchemaUtils.create(DevicesTable, ...) — создаёт отсутствующие таблицы, но не меняет существующие.

Для продакшна

Варианты:

  1. Flyway (рекомендуется): миграции в factory-server/src/main/resources/db/migration/ (V1__init.sql, V2__add_groups.sql, …), запуск до Database.init().
  2. Liquibase: changelog в XML/YAML.
  3. Ручные SQL-скрипты + schema_version таблица, прогон через psql в деплой-процедуре.

Пример добавления колонки

-- V12__add_device_label.sql
ALTER TABLE devices ADD COLUMN label VARCHAR(100);
CREATE INDEX idx_devices_label ON devices(label);

И параллельно обновление factory-server/src/main/kotlin/ru/okto/factory/persistence/Tables.kt:

val label = varchar("label", 100).nullable()

Несовместимые изменения

При breaking change:

  1. Опубликуйте мажорную версию (2.0.0).
  2. Документируйте процедуру миграции в CHANGELOG.md.
  3. Включите флаг совместимости (legacy.enableV1Sync=true) на переходный период.

14. Обновление (rolling update)

14.1 Центральный сервер

# 1. Снимите бэкап БД.
pg_dump -Fc -U okto okto_factory > /backup/pre-upgrade.dump

# 2. Скачайте новый JAR.
sudo -u okto curl -L -o /opt/okto/factory-server-1.3.0.jar \
  https://releases.okto.ru/factory-server/1.3.0/factory-server.jar

# 3. Прогоните миграции (если Flyway).
java -jar /opt/okto/factory-server-1.3.0.jar --migrate /etc/okto/factory-application.yaml

# 4. Переключите symlink.
sudo ln -sfn /opt/okto/factory-server-1.3.0.jar /opt/okto/factory-server.jar

# 5. Рестарт.
sudo systemctl restart okto-factory

# 6. Проверка.
curl -sf https://factory.okto.ru/api/v1/health

Downtime: ~10 секунд (рестарт Ktor). Edge-устройства реконнектятся автоматически с backoff.

14.2 Edge-сервис

Используйте встроенный OTA-механизм:

  1. Upload новой версии через UI / API.
  2. Деплой на canary-группу (1–2 устройства).
  3. Мониторинг 30 минут: ошибки, метрики, logs.
  4. Деплой на stable группу поэтапно (10% → 50% → 100%).

См. SERVER_MANAGEMENT.ru.md §10.

14.3 PostgreSQL major upgrade

# Ubuntu пример 15 → 16
sudo apt-get install postgresql-16
sudo pg_dropcluster 16 main --stop
sudo pg_upgradecluster 15 main
sudo pg_dropcluster 15 main

# Проверка версии
sudo -u postgres psql -c "SELECT version();"

15. Масштабирование

Вертикальное

До 500 edge-устройств на одной площадке ЦЛС справляется на 4 vCPU / 8 GB. При росте:

  • Перейдите на NVMe-диск (IOPS критичны для cloud_sync_queue).
  • Добавьте PgBouncer между ЦЛС и PostgreSQL (pool mode=transaction).
  • Увеличьте hikari.maxPoolSize до 50.

Горизонтальное (roadmap)

На данный момент не поддерживается: DeviceConnectionRegistry — in-memory. При необходимости:

  1. Sticky-роутинг по deviceId (реверс-прокси → один из шардов).
  2. Или внешний координатор (Redis Streams / Postgres NOTIFY) для маршрутизации команд между репликами.

Разделение нагрузки

  • Read replica PostgreSQL для отчётов (Dashboard* endpoints).
  • CDN для центральной консоли (статика).
  • Dedicated artifact storage для прошивок (S3-compatible), если размеры > 100 MB.

16. Чек-лист приёмки

Перед выводом сайта в production:

Базовая доступность

  • GET /api/v1/health возвращает 200
  • GET /health на edge возвращает 200
  • Центральная консоль доступна по HTTPS
  • TLS сертификат валиден > 30 дней
  • Metric-endpoint :9091/metrics доступен только из VPN

Безопасность

  • Admin-пароль изменён
  • JWT secret уникален, не change-me-in-production
  • Enrollment key уникален
  • auth.allowAutoEnrollment=false
  • admin@okto.ru настроен как получатель security alert
  • Firewall закрывает всё кроме 443 (+ SSH с bastion)
  • Rate-limit на login активирован
  • sudoers валиден (visudo -c)

Функциональность

  • Тестовое edge-устройство прошло enrollment
  • WS-соединение в /ws/device установилось
  • Команда force_sync завершилась SUCCESS
  • Одна тестовая бутылка прошла путь edge → ЦЛС → OKTO Cloud
  • Upload тестовой прошивки работает
  • Деплой на canary-устройство успешен
  • Центральная консоль отображает live-события
  • Audit log содержит все выше действия

Наблюдаемость

  • Prometheus собирает метрики ЦЛС и edge
  • Grafana dashboard подключён
  • Alerting-правила активны
  • Loki получает логи
  • Логи едут с ротацией

Бэкапы

  • Ежедневный pg_dump запланирован и проверен
  • Восстановление из бэкапа выполнено на тест-стенде
  • Offsite-копия настроена
  • Runbook «Disaster recovery» опубликован

Документация

  • IP-адреса и имена хостов в internal wiki
  • Кто на on-call в эскалационном листе
  • Контакт OKTO Cloud support

Обновлено: апрель 2026. Связанные документы: OPERATIONS.ru.md, SERVER_MANAGEMENT.ru.md, DEVELOPER_GUIDE.ru.md.