Northflank: два ONNX-бота — установка, эксплуатация и первые замеры

Summary

Состояние на 17.08.2026: оба webhook-сервиса — northflank/kcp-one-ply-1 и northflank/hybrid-kcp-1 — остановлены, instances: 0. Они сняты с лестницы Play, и, в отличие от первоначального предположения, Developer Sandbox не делал их бесплатными: два бота стоили $13,07/мес (разбор в §5.1). План nf-compute-20 и образ сохранены, подъём — одной командой.

Замеры за 78 часов работы 14–17.08 остаются в силе: каждый сервис получал 0,2 shared vCPU, 512 MiB RAM, один экземпляр и одну одновременную партию. KCP One Ply упирается в CPU, но имеет большой запас памяти. Hybrid KCP использует CPU мягче, зато дошёл примерно до 408 MiB / 512 MiB — в nf-compute-10 (256 MiB) он бы не поместился.

Куда переносить ботов — открытый вопрос, решается отдельно.

Связанные заметки:


1. Зачем сделаны два бота

Модель kcp_nodice_serving_20260809.onnx появилась как аналитическая модель оценки позиции. При просмотре партий обнаружилось, что она иногда предпочитает ход, отличный от хода существующего Hunter book. Это породило два разных эксперимента:

  1. KCP One Ply — применить KCP-модель напрямую ко всем легальным ходам и выбрать лучший результат без expectimax-поиска.
  2. Hybrid KCP — сохранить rich-feature 2-ply expectimax, но пересчитать небольшой шортлист корневых ходов KCP-моделью.

Это две отдельные игровые гипотезы, поэтому им нужны отдельные имена и рейтинговые истории. При этом транспорт, HMAC, часы, книга, HTTP-сервер и контейнер одинаковы. Поддерживать два репозитория или два Dockerfile не требуется.

Публичный репозиторий кода: rabestro/dicechess-bot-gcp-onnx. Подготовка сделана в PR #17, который:

  • обновил движок до dicechess-engine-scala v1.14.0;
  • добавил SEARCH_MODE=expectimax|one-ply;
  • добавил выбор TIME_POLICY=empirical-v1|legacy-linear-v1;
  • вынес production opening book во внешний OPENING_BOOK_PATH;
  • оставил в публичном репозитории только пятистрочный пример TSV;
  • сохранил synthetic ONNX-модель, чтобы открытый проект собирался и тестировался без закрытых весов;
  • починил и проверил linux/amd64 Docker-сборку.

Почему версия находится в имени Play-бота

Play ранжирует идентичность, а не код. Суффикс -1 фиксирует первую итерацию алгоритма. Если существенно меняются модель, фичи, вес рескоринга или стратегия, корректнее зарегистрировать -2, чем смешивать две силы в одной рейтинговой истории.

2. Архитектура, которая работает сейчас

flowchart LR
    L["Play ladder\nplay.jc.id.lv"] -->|"signed HTTPS POST\n/api/webhook"| K["Northflank service\nkcp-one-ply\n0.2 vCPU / 512 MiB"]
    L -->|"signed HTTPS POST\n/api/webhook"| H["Northflank service\nhybrid-kcp\n0.2 vCPU / 512 MiB"]
    G["Private Google Artifact Registry\nlinux/amd64 image"] --> K
    G --> H
    P["Public GitHub repo\ncode + synthetic model\n+ 5 book rows"] --> G
    A["Private assets\n2 ONNX models\n+ 147-row book"] --> G

Northflank

ПараметрЗначение
Teamrabestros-team
ProjectDice Chess ONNX Bots (dice-chess-onnx-bots)
РегионEurope West — Frankfurt
Архитектураx86 / linux-amd64
Сервисыkcp-one-ply, hybrid-kcp
Compute plannf-compute-20
На сервис0,2 shared vCPU, 512 MiB RAM, 1 GiB ephemeral storage
Экземпляры1 на сервис
Публичный портHTTP 8080, автоматически выданный HTTPS-домен
Persistent volumes / databaseнет
Registry integrationDice Chess GAR

Публичные webhook URL:

  • https://http--kcp-one-ply--85xh4h7g8scd.code.run/api/webhook;
  • https://http--hybrid-kcp--85xh4h7g8scd.code.run/api/webhook.

Play

СервисЗарегистрированная идентичностьCapacityLadderHuman catalog
kcp-one-plynorthflank/kcp-one-ply-11включенавыключен
hybrid-kcpnorthflank/hybrid-kcp-11включенавыключен

maxConcurrentGames=1 выбран намеренно. Внутри JVM один Strategy сериализует chooseMoves, потому что ONNX-сессия не считается thread-safe. Второй параллельный webhook в тот же контейнер ждал бы первый и тратил часы другой партии. Ограничение Play не позволяет посадить бота во вторую игру, пока первая не закончилась.

3. Где проходит публично-приватная граница

Публично доступны:

  • Scala-код стратегии и webhook-рантайма;
  • Dockerfile;
  • synthetic signal-free ONNX-модель;
  • пять строк opening_book.tsv, показывающие формат;
  • все переменные конфигурации и описание алгоритмов.

Приватными остаются:

  • реальные обученные ONNX-веса;
  • полная production opening book;
  • Play bearer tokens;
  • per-bot HMAC webhook secrets;
  • ключ или credential, дающий Northflank право читать приватный GAR.

Снимок приватных артефактов в образе

Хэши позволяют проверить идентичность файлов, но не раскрывают их содержимое:

ФайлРазмерSHA-256Назначение
kcp_nodice_serving_20260809.onnx1 149 338 B3154c6e4d35292063ebf55141a90d2d09cb70418ef9035d4bb6aae7a4679434dKCP-13: one-ply и root rescore
oracle-3.onnx1 149 338 B5e3cd9a1fbeba2b762d4b914d134298fcc7e33d83b59ae37577700a8ca06f702rich-9: 2-ply expectimax
opening_book.tsv10 161 B, 147 строк2f00088525b2ca0485de4ae7b233a32caed9cff9f15330fa80eb2d484e675e68закрытая production book

Файлы находятся в контейнере под /models. Это приватный производный образ: публичная базовая сборка заканчивается JAR-файлом, а две верхние image-layer добавляют /models/ и полную книгу. История текущего образа подтверждает слои размером примерно 2,3 MB и 10,2 kB.

Не пушить приватный производный образ в публичный registry

Репозиторий публичный, а trained weights и production book — нет. Тег с приватными слоями разрешено публиковать только в закрытый GAR/другой закрытый registry. Перед docker push всегда проверять полное имя registry, project и repository.

4. Текущий контейнерный артефакт

ПолеЗначение
Registryeurope-west3-docker.pkg.dev
GCP projectdice-chess-lab
Repositorydicechess
Imagedicechess-bot-onnx:edccf9d
Manifest/image digestsha256:7458a22d5b18ef993c0a76f52d393798a879a021a9a112295111fd3f7351ebfc
Platformlinux/amd64
Локальный uncompressed size160 206 160 B (≈160,2 MB)
Исходный commitedccf9d из PR #17

Тег — commit SHA, а не latest. Это даёт воспроизводимый rollback и не позволяет Northflank неожиданно получить другой набор весов под старым именем.

Рекомендуемая двухступенчатая сборка

Публичную и приватную части лучше собирать раздельно:

# В публичном dicechess-bot-gcp-onnx.
GH_TOKEN="" gh auth status
sbt clean "testOnly *" assembly
 
PUBLIC_IMAGE="dicechess-bot-onnx-public:<commit-sha>"
docker build --platform linux/amd64 -t "$PUBLIC_IMAGE" .

Затем в отдельном приватном, gitignored или временном build context:

ARG BASE_IMAGE
FROM ${BASE_IMAGE}
COPY models/ /models/
COPY opening_book.tsv /models/opening_book.tsv
PRIVATE_IMAGE="europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/dicechess-bot-onnx:<commit-sha>"
 
docker build --platform linux/amd64 \
  --build-arg "BASE_IMAGE=$PUBLIC_IMAGE" \
  -t "$PRIVATE_IMAGE" \
  /path/to/private-build-context
 
docker run --rm --platform linux/amd64 --entrypoint /bin/sh "$PRIVATE_IMAGE" -c '
  sha256sum /models/*.onnx /models/opening_book.tsv
  wc -l /models/opening_book.tsv
'
 
gcloud auth configure-docker europe-west3-docker.pkg.dev
docker push "$PRIVATE_IMAGE"

Нельзя копировать private build context внутрь публичного репозитория даже «на минуту»: случайный git add, Docker build cache и архивы CI превращают временный файл в долгоживущую копию. Исходные модели должны иметь отдельный бэкап согласно runbook артефактов моделей.

5. Подготовка Northflank

5.1. Аккаунт и бесплатный план

Исправлено 17.08.2026: слоты Sandbox не защитили от списания

Первая редакция раздела утверждала, что два бота бесплатны, поскольку занимают два free-слота Developer Sandbox. На практике это не сработало: выделенные vCPU и RAM метрились по полным PAYG-ставкам с первой секунды жизни контейнера. За ~78 часов работы (деплой 14.08 07:51 UTC) два сервиса дали 13,07/мес. Оба заскейлены в instances: 0.

Должно ли было так быть — вопрос открытый: официально тариф Developer заявлен бесплатным. Разбор противоречия — ниже в этом же разделе.

Для создания ресурсов Northflank требует платёжную карту даже на Developer Sandbox, и это действительно проверка личности — формулировка официальной документации:

all users must add a payment method to start creating resources on Northflank, regardless of plan selection. This is to verify user identity, and prevent malicious usage of the platform.

То есть сам факт добавленной карты платного потребления не означает. Но и защитой от него не является: списание в нашем случае произошло (см. ниже).

На 14.08.2026 официальная документация Developer Sandbox даёт:

  • два service-слота;
  • два job/cron-слота;
  • один addon/database-слот;
  • один проект для обычного эксперимента.

Это лимиты на количество ресурсов, а не бесплатная вычислительная квота. Ни на странице прайса, ни в документации нет утверждения, что vCPU и RAM в Sandbox бесплатны, — но нет и обратного. Именно это молчание породило ошибку первой редакции. Формулировки прайса («2× free services», «Always-on-compute – no sleeping») ей активно способствуют; см. также лимиты бесплатных тарифов у платформ.

Как считают на самом деле

Тарифицируется выделено × время существования контейнера — не потребление и не число запросов. Простой оплачивается полностью: снятие ботов с лестницы Play и отсутствие живых игроков на счёт не влияют никак.

Ставки: 0,00833 / GiB-ч. Отсюда nf-compute-20 (0,2 vCPU / 512 MiB) = 10,80/мес, с латвийским НДС 21% — $13,07/мес.

Три независимых подтверждения, что считают именно выделение:

  1. У обоих сервисов счёт совпал до цента (15,56 vCPU-ч / 37,11 GiB-ч), хотя профили разные: KCP One Ply упирается в CPU, Hybrid KCP — в память.
  2. 37,11 GiB-ч ÷ 74 ч = ровно 0,5 GiB, то есть выделенные 512 MiB. По фактическому RSS у Hybrid (408 MiB) вышло бы ≈29,7 GiB-ч.
  3. 15,56 vCPU-ч ÷ 0,2 vCPU = 77,8 ч — совпадает со временем от деплоя с точностью до часа (расхождение объясняется задержкой снимка биллинга). Значит метр не прерывался ни на час.

Бесплатного compute-плана в каталоге не существует

northflank list plans (и публичный GET /v1/plans) отдаёт 20 планов, из них ни одного с нулевой ценой. Самый дешёвый — nf-compute-10 (0,1 vCPU / 256 MiB) за $2,70/мес, и Hybrid KCP в 256 MiB не поместился бы: он уже на 408 MiB. Гипотезы про регион, внешний GAR-образ и купоны подтверждения не нашли.

Важно не спутать два разных объекта: compute-план (nf-compute-*, привязан к сервису) и биллинговый план аккаунта (Sandbox / Pay-as-you-go). Отсутствие compute-плана за $0 само по себе не доказывает, что Sandbox платный, — Sandbox мог бы покрывать выделение отдельным механизмом.

Нерешённое противоречие: официально Developer заявлен бесплатным

Это надо знать, прежде чем считать вопрос закрытым. Официальные источники Northflank противоречат факту нашего списания.

За бесплатность compute:

  • матрица сравнения тарифов на странице цен: у колонки Developer строка Pricing = «Free», строка Compute = «Limited»;
  • в той же матрице две строки биллинга — «To the second billing» и «Invoice history» — для Developer помечены как неприменимые, тогда как у Pay-as-you-go включены;
  • карточка тарифа продаёт «free» именно применительно к ресурсам: «2× free services», «1× free database», плюс «Always-on-compute – no sleeping»;
  • changelog от 22.11.2021 (старое имя «free developer projects») говорит прямо: «Everything is free within a developer project… no risk of incurring cloud costs», «2 services, 2 jobs and 1 database with generous resources».

Против:

  • фактическое списание по полным PAYG-ставкам без единой скидочной строки;
  • ни одна действующая страница документации не утверждает, что vCPU и RAM в Sandbox бесплатны, — и не утверждает обратного. Размер «Limited» нигде не выражен в числах;
  • переименование «free developer projects» → «Developer Sandbox» в changelog не задокументировано, так что обещание 2021 года не переносится на текущий тариф автоматически. При этом нет и ни одной записи об урезании бесплатного тарифа.

Отсюда два возможных объяснения, различить которые изнутри не удалось:

  1. аккаунт фактически не на Developer/Sandbox, а на Pay-as-you-go — тогда всё корректно, и бесплатный тариф просто никогда не был активен;
  2. аккаунт на Developer, но nf-compute-20 выходит за необъявленный предел «Limited» — тогда заявленная «Free» и реальное поведение расходятся.

Что для этого сделать

Через API план аккаунта не виден: org-scoped эндпоинты требуют org-токена, а list usage и list invoices отдают 403. Поэтому:

  1. посмотреть в UI, какой тариф показан у аккаунта (Billing → план);
  2. если там Developer/Sandbox — задать вопрос поддержке, приложив строки счёта: при заявленной «Free» списание по полным ставкам требует объяснения;
  3. параллельно любопытная деталь: 403 «Feature disabled for your account» пришёл именно на usage и invoices — те самые две строки, что в матрице помечены неприменимыми для Developer. Это согласуется с гипотезой «аккаунт на Developer», но не доказывает её: те же эндпоинты могут быть просто org-only.

Как останавливать расходы

Метр останавливает только instances: 0 либо Pause. Wake-on-HTTP для scale-from-zero нет: автоскейл по RPS требует хотя бы одного живого инстанса. Даже будь он, холодный старт JVM с ONNX (30–60 с) не уложился бы в таймаут вебхука — play-api рвёт клиента на 45 с.

northflank scale service --project dice-chess-onnx-bots --service kcp-one-ply -i '{"instances": 0}'
northflank scale service --project dice-chess-onnx-bots --service hybrid-kcp -i '{"instances": 0}'

deploymentPlan в теле запроса не указан намеренно: план и образ сохраняются, обратный подъём — та же команда с "instances": 1.

По документации на нуле инстансов продолжает тарифицироваться только storage томов («When paused, you are only billed for volume storage»). У нас volumes в проекте нет — northflank list volumes отдаёт [], — поэтому остаётся ноль.

Ловушка: CI на нуле инстансов продолжает билдить

Раздел Scale документации: «If you leave CI enabled the service will still build the latest commit to the linked repository». Билды тратят compute-план, то есть сервис на instances: 0 с включённым CI может продолжать начислять. У нас оба сервиса развёрнуты из внешнего GAR-образа, а не из связанного репозитория, поэтому build-пути нет — но при переходе на сборку внутри Northflank это надо проверить заново. Сам раздел Scale о биллинге не упоминает вообще, так что предупреждения там не будет.

Чем проверять, что метр встал

Не кодом выхода команды и не тем, что панель показывает ноль инстансов. Фактическая проверка — Last 24 hours в Billing должно стать **0,35).

Почасовой API для этого не годится: list usage и list invoices отдают 403 «Feature disabled for your account», так что следить за расходами через CLI на этом аккаунте нельзя — только панель и billing alerts. Отдельно: list teams и get team дают другой 403 («requires an organisation-scoped token»), это лишь скоуп токена и об аккаунте ничего не говорит. Узнать план аккаунта через API не удалось.

Порог по умолчанию **13/мес счёт придёт молча в конце месяца. Billing alerts на аккаунте не настроены ни одного — незакрытый хвост.

Пригодность Sandbox по назначению

Официальное описание называет Sandbox средой для изучения платформы, hobby-проектов и проверки новых идей и отдельно говорит, что он не предназначен для production applications. Текущая работа — некоммерческий исследовательский эксперимент без SLA и без критической зависимости сайта от этих двух ботов — соответствует заявленному назначению по существу. Это эксплуатационная оценка, а не юридическая гарантия: если появятся коммерческая цель, SLA или production-зависимость, тариф и условия нужно пересмотреть.

Третий бот нельзя считать бесплатным продолжением этой схемы — но теперь по двум причинам: свободных service-слотов нет, и любой слот в любом случае платный.

5.2. CLI

На рабочей машине использовался Northflank CLI 0.12.1:

npm install --global @northflank/cli
northflank login
northflank --version

northflank login открывает одноразовую страницу авторизации. API token нельзя печатать в заметке, shell history или логе CI. Для автоматизации нужен отдельный token с минимальными правами на один team/project, а не личный admin-token.

5.3. Доступ к приватному GAR

В Northflank сохранена registry integration Dice Chess GAR и выбрана как credential у обоих сервисов. Она должна иметь только право читать образы из dice-chess-lab/dicechess.

Рекомендуемая долговечная схема:

  1. отдельный GCP service account для Northflank;
  2. роль roles/artifactregistry.reader, по возможности на один repository;
  3. JSON key загрузить только в Northflank registry credential;
  4. локальную временную копию key удалить после проверки;
  5. записать дату/владельца ключа и план ротации, но не само значение.

Команды для отдельного read-only service account:

GCP_PROJECT="dice-chess-lab"
GCP_REGION="europe-west3"
GAR_REPOSITORY="dicechess"
GAR_READER="northflank-gar-reader"
GAR_READER_EMAIL="${GAR_READER}@${GCP_PROJECT}.iam.gserviceaccount.com"
 
gcloud iam service-accounts create "$GAR_READER" \
  --project "$GCP_PROJECT" \
  --display-name "Northflank Artifact Registry reader"
 
gcloud artifacts repositories add-iam-policy-binding "$GAR_REPOSITORY" \
  --project "$GCP_PROJECT" \
  --location "$GCP_REGION" \
  --member "serviceAccount:${GAR_READER_EMAIL}" \
  --role roles/artifactregistry.reader
 
umask 077
gar_key_file=$(mktemp /tmp/northflank-gar-reader.XXXXXX.json)
gcloud iam service-accounts keys create "$gar_key_file" \
  --project "$GCP_PROJECT" \
  --iam-account "$GAR_READER_EMAIL"

Файл gar_key_file загружается в Northflank как GCP registry credential и после проверки удаляется с ноутбука. Это long-lived secret: его нужно ротировать и никогда не добавлять в git/iCloud/заметки. Если Northflank предложит keyless federation для обычного external image pull, она предпочтительнее JSON key.

Проверить credential до следующего обновления

При первом развёртывании нужно убедиться, что Dice Chess GAR использует долговечный read-only credential, а не часовой gcloud auth print-access-token. Уже запущенный pod продолжит работать с локально скачанным образом, но новый pod или redeploy после истечения временного token не сможет pull-нуть image. Проверка: сделать контролируемый redeploy одного сервиса до следующего релиза и убедиться, что image загружается без обновления credential.

5.4. Проект и сервисы

Проект создан в регионе Frankfurt. Оба сервиса — Deployment → External image, источник — приватный GAR image, registry credential — Dice Chess GAR.

Путь в UI для повторения:

  1. Team → Integrations → Registries → Add registry: имя Dice Chess GAR, тип Google, registry europe-west3-docker.pkg.dev, read-only credential из §5.3.
  2. Projects → Create project: Dice Chess ONNX Bots, регион Europe West — Frankfurt, Developer Sandbox.
  3. Project → Create → Service → Deployment → External image: вставить immutable GAR tag, выбрать Dice Chess GAR.
  4. Выбрать nf-compute-20, один instance и 1 GiB ephemeral storage.
  5. Открыть порт 8080 как public HTTP и оставить автоматически выданный домен.
  6. Добавить две TCP-пробы из таблицы ниже.
  7. Добавить несекретные runtime variables из §6; DICECHESS_WEBHOOK_SECRET появится только после регистрации webhook в §7.

GitHub/VCS к team намеренно не подключён. Northflank не собирает публичный репозиторий и не имеет доступа к GitHub: он только pull-ит уже собранный private image из GAR. Поэтому баннер team doesn't have a version control platform linked для этой архитектуры нормален.

Общие параметры:

{
  "billing": { "deploymentPlan": "nf-compute-20" },
  "deployment": {
    "instances": 1,
    "external": {
      "imagePath": "europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/dicechess-bot-onnx:edccf9d",
      "credentials": "<Dice Chess GAR credential id>"
    },
    "storage": {
      "ephemeralStorage": { "storageSize": 1024 }
    }
  },
  "ports": [
    {
      "name": "http",
      "internalPort": 8080,
      "protocol": "HTTP",
      "public": true
    }
  ]
}

Health checks:

ProbeProtocolPortInitial delayPeriodTimeoutFailure threshold
startupTCP808010 s10 s5 s18
readinessTCP808010 s30 s5 s3

Startup допускает до трёх минут на запуск JVM и загрузку ONNX. TCP readiness проверяет, что процесс слушает порт, но не доказывает, что модель выбирает легальный ход. Поэтому после каждого обновления обязательны Play handshake и реальный smoke turn.

6. Конфигурация двух стратегий

KCP One Ply

MODEL_PATH=/models/kcp_nodice_serving_20260809.onnx
OPENING_BOOK_PATH=/models/opening_book.tsv
ORACLE_FEATURES=kcp
SEARCH_MODE=one-ply
TIME_POLICY=empirical-v1
OVERHEAD_BUFFER_MS=1000
DEFAULT_THINK_MS=2000

Hybrid KCP

MODEL_PATH=/models/oracle-3.onnx
OPENING_BOOK_PATH=/models/opening_book.tsv
ORACLE_FEATURES=rich
SEARCH_MODE=expectimax
TIME_POLICY=empirical-v1
OVERHEAD_BUFFER_MS=1000
DEFAULT_THINK_MS=2000
ORACLE_CANDIDATE_LIMIT=4
RESCORE_MODEL_PATH=/models/kcp_nodice_serving_20260809.onnx
RESCORE_WEIGHT=0.5

Общее замечание: RESCORE_MODEL_PATH разрешён только при SEARCH_MODE=expectimax. One-ply с rescore должен упасть при старте, а не молча игнорировать ошибочную конфигурацию.

TIME_POLICY=empirical-v1 — новый алгоритм управления временем движка, полученный из реального распределения длительности партий Dice Chess. Старый legacy-linear-v1 сохранён для сравнения и rollback, но в этих двух deployments не используется.

OVERHEAD_BUFFER_MS=1000 больше кодового default 300 ms. На маленьком shared CPU это осознанный запас на последнюю неотменяемую ONNX-операцию, сериализацию и сетевой round-trip. Уменьшать его стоит только по фактическим измерениям, а не ради ещё одной кандидатной позиции.

Что должно быть в startup logs

KCP:

[bot] opening book: /models/opening_book.tsv (147 entries)
[bot] ONNX model: /models/kcp_nodice_serving_20260809.onnx (mode=one-ply, features=kcp, timePolicy=empirical-v1, ...)
[bot] ONNX custom handler listening on :8080/api/webhook

Hybrid:

[bot] opening book: /models/opening_book.tsv (147 entries)
[bot] ONNX model: /models/oracle-3.onnx (mode=expectimax, features=rich, timePolicy=empirical-v1, candidateLimit=4, rescore=/models/kcp_nodice_serving_20260809.onnx (weight=0.5))
[bot] ONNX custom handler listening on :8080/api/webhook

Если вместо реального пути видно MODEL_PATH not set — using the bundled synthetic model, deployment формально healthy, но играет без шахматного сигнала. Это блокирующая ошибка конфигурации.

7. Регистрация в Play без утечки секретов

7.1. Зарегистрировать долговечные identity

PLAY_BASE="https://play-api.jc.id.lv"
 
registration_json=$(curl --fail --silent --show-error \
  -X POST "$PLAY_BASE/bot/register" \
  -H "Content-Type: application/json" \
  -d '{"team":"northflank","name":"kcp-one-ply-1"}')
 
bot_token_value=$(printf '%s' "$registration_json" | jq -r .token)
security add-generic-password \
  -s dicechess-play-bot-token \
  -a northflank/kcp-one-ply-1 \
  -U -w "$bot_token_value"
unset bot_token_value registration_json

Повторить для northflank/hybrid-kcp-1. Значение token сервер показывает один раз. Команду нельзя запускать с set -x; переменную нельзя echo-ить. На общей машине лучше использовать интерактивный ввод Keychain, чтобы token даже кратковременно не был виден в аргументах процесса.

Проверка без вывода token:

security find-generic-password \
  -s dicechess-play-bot-token \
  -a northflank/kcp-one-ply-1 >/dev/null && echo OK

7.2. Зарегистрировать webhook

Сначала Northflank endpoint должен быть публичным и healthy. Verification handshake не требует HMAC secret: runtime возвращает присланный nonce. После успешного handshake Play один раз возвращает новый per-bot secret.

bot_token_value=$(security find-generic-password \
  -s dicechess-play-bot-token \
  -a northflank/kcp-one-ply-1 -w)
 
webhook_registration=$(curl --fail --silent --show-error \
  -X POST "$PLAY_BASE/bot/webhook" \
  -H "Authorization: Bearer ${bot_token_value}" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://http--kcp-one-ply--85xh4h7g8scd.code.run/api/webhook"}')
 
webhook_secret_value=$(printf '%s' "$webhook_registration" | jq -r .secret)

webhook_secret_value нужно сразу записать в Northflank runtime variable DICECHESS_WEBHOOK_SECRET, не печатая его и не добавляя в JSON внутри git. После обновления сервиса:

unset webhook_secret_value webhook_registration bot_token_value

Повторить с отдельными token, URL и secret для Hybrid. Один secret нельзя разделять между identity.

Повторная регистрация ротирует secret

Каждый новый POST /bot/webhook заменяет URL и HMAC key. Старое значение перестаёт подходить. После re-register нужно сразу обновить DICECHESS_WEBHOOK_SECRET и перезапустить deployment.

7.3. Capacity и ladder

Оставляем default capacity равным одному и проверяем:

bot_token_value=$(security find-generic-password \
  -s dicechess-play-bot-token \
  -a northflank/kcp-one-ply-1 -w)
 
curl --fail --silent --show-error "$PLAY_BASE/bot/capacity" \
  -H "Authorization: Bearer ${bot_token_value}" | jq
 
curl --fail --silent --show-error -X POST "$PLAY_BASE/bot/ladder/join" \
  -H "Authorization: Bearer ${bot_token_value}" | jq
 
unset bot_token_value

Ожидаем maxConcurrentGames: 1, ladderAllowance: 1, onLadder: true в profile. openToHumans пока оставлен false: текущая цель — чистый ladder experiment.

8. Проверка после развёртывания

Порядок важен:

  1. Northflank показывает Running, 1 / 1, startup/readiness passing.
  2. В runtime logs загружены реальная модель и книга из 147 строк.
  3. GET /bot/webhook показывает правильный URL и verifiedAt.
  4. GET /bot/capacity показывает одну разрешённую и не более одной активной партии.
  5. Один контролируемый unrated game завершается реальными ходами.
  6. В GET /bot/webhook/stats растёт applied, нет timed_out, unreachable, http_5xx или garbled.
  7. Только после этого бот входит в ladder.

Текущий service status:

  • оба deployments — COMPLETED;
  • по одному pod, 0 restarts;
  • у обоих 2/2 probes passing.

9. Первые реальные замеры

9.1. Northflank CPU и память

Окно: 14.08.2026 07:51:30–08:24:30 UTC (10:51–11:24 Europe/Riga), шаг 15 секунд. Метрика CPU — процент от выделенных 0,2 vCPU; память — процент от 512 MiB. В таблице приведён текущий pod каждого сервиса, а не старые pods от промежуточных deployments.

МетрикаKCP One PlyHybrid KCP
CPU average50,97% = 0,102 vCPU27,74% = 0,055 vCPU
CPU p5057,95% = 0,116 vCPU25,17% = 0,050 vCPU
CPU p9099,95% = 0,200 vCPU63,38% = 0,127 vCPU
CPU max100%98,16%
Memory average27,98% = 143,2 MiB57,12% = 292,5 MiB
Memory p5028,26% = 144,7 MiB76,35% = 390,9 MiB
Memory p9032,76% = 167,7 MiB78,50% = 401,9 MiB
Memory latest / max35,26% = 180,5 MiB79,58% = 407,4 MiB
Запас на последней точке≈331,5 MiB≈104,6 MiB

Интерпретация:

  • KCP One Ply CPU-bound. На p90 он уже выбирает весь shared CPU. Памяти много, поэтому увеличение RAM ему не поможет; возможный будущий рычаг — 0,5 vCPU или более лёгкое вычисление KCP.
  • Hybrid KCP memory-bound раньше KCP. У него две ONNX-сессии и тяжёлый поиск; вероятно, этим объясняется больший RSS. Это вывод по архитектуре и графику, а не доказанная декомпозиция памяти.
  • Память Hybrid росла в первой половине окна, поэтому 407 MiB нельзя пока считать окончательным plateau. Нужно наблюдать несколько часов и после нескольких GC.
  • Краткие пики CPU до 100% допустимы. Нежелательны устойчивые 100%, рост памяти выше 85–90%, рестарты/OOM и ухудшение webhook tail latency.

9.2. Доставка ходов Play

Снимок сделан сразу после ресурсного окна. Percentiles Play — приближения по логарифмическим latency buckets, а не точные миллисекунды.

Метрика за 24 часаKCP One PlyHybrid KCP
Deliveries88183
Applied88182
Refused01
p505 s5 s
p9060 s10 s
p9960 s20 s
Последняя ошибканетroom refused the moves, 07:19 UTC

Единственный refused Hybrid относится к контролируемому проверочному сценарию: партия была завершена resign, пока вычисление уже шло, и вернувшийся ход попал в закрытую room. Это не текущая недоступность endpoint. После него идут успешные applied deliveries.

У KCP нет ошибок, но p90/p99 в 60-секундном bucket согласуется с CPU saturation. Это главный показатель для наблюдения: даже легальный ход, пришедший через минуту, съедает слишком много игровых часов. По 88 deliveries нельзя решать вопрос о новом тарифе окончательно, но сигнал уже есть.

9.3. Ранний ladder snapshot — не оценка силы

На 14.08.2026 около 11:25 Europe/Riga:

БотRated gamesW-D-LRatingRDProvisional
KCP One Ply119-0-21918,3126,7да
Hybrid KCP187-0-111623,9102,5нет по текущему profile

Это только operational smoke: боты получают партии, ходы применяются и рейтинг обновляется. Выборка мала, соперники неодинаковы, Dice Chess содержит высокий шум. Сравнивать алгоритмы нужно после существенно большей серии и желательно через контролируемые mirrored pairings/SPRT, а не по этим первым W-L.

10. Как повторить замеры

Service configuration без вывода webhook secret

northflank get service \
  --projectId dice-chess-onnx-bots \
  --serviceId kcp-one-ply \
  --output json --quiet |
  jq 'del(.runtimeEnvironment.DICECHESS_WEBHOOK_SECRET)'

Runtime logs

northflank get service logs \
  --projectId dice-chess-onnx-bots \
  --serviceId hybrid-kcp \
  --types runtime \
  --lineLimit 100

root_search_stats появляется только у expectimax и только когда не сработала opening book. Поля candidatesCompleted, deadlineTruncated и fellBackToPreRank показывают, успевает ли Hybrid реально пересчитать свой candidate shortlist.

CPU/RAM за час, только текущий pod

northflank get service metrics \
  --projectId dice-chess-onnx-bots \
  --serviceId hybrid-kcp \
  --duration 3600 \
  --metricTypes cpu memory \
  --output json --quiet |
jq '
  def latest($m): ($m.values | max_by(.data[-1].ts));
  def stats($s):
    ($s.data | map(.value)) as $v |
    {
      container: $s.metadata.containerId,
      from: $s.data[0].ts,
      to: $s.data[-1].ts,
      samples: ($v | length),
      latest: $v[-1],
      average: ($v | add / length),
      p50: ($v | sort | .[((length * 0.50) | floor)]),
      p90: ($v | sort | .[((length * 0.90) | floor)]),
      max: ($v | max)
    };
  {cpu_pct: stats(latest(.cpu)), memory_pct: stats(latest(.memory))}
'

Play delivery stats

bot_token_value=$(security find-generic-password \
  -s dicechess-play-bot-token \
  -a northflank/hybrid-kcp-1 -w)
 
curl --fail --silent --show-error "$PLAY_BASE/bot/webhook/stats" \
  -H "Authorization: Bearer ${bot_token_value}" | jq
 
curl --fail --silent --show-error "$PLAY_BASE/bot/capacity" \
  -H "Authorization: Bearer ${bot_token_value}" | jq
 
unset bot_token_value

Публичный profile не требует token:

curl --fail --silent --show-error \
  "$PLAY_BASE/bots/northflank/hybrid-kcp-1" | jq

11. Стоимость и граница бесплатного использования

nf-compute-20 стоит 10,80/мес, с латвийским НДС 21% — $13,07/мес, плюс возможные egress и storage.

Исправлено 17.08.2026: фактически это не $0

Прежняя редакция утверждала, что два service-слота покрываются бесплатным планом и «ожидаемая compute-стоимость этой конфигурации — **1,14 + НДС по полным PAYG-ставкам, без единой скидочной строки.

Соответствует ли это заявленным условиям тарифа Developer — открытый вопрос: официально он значится бесплатным. Разбор противоречия и что с ним делать — в §5.1. Планировать в любом случае надо от $13,07/мес, а не от нуля.

Форма лимита описана верно: это не пул «N CPU-часов», который незаметно выгорает от частой игры, а два разрешённых сервиса фиксированного малого размера, 0,2 shared vCPU и 512 MiB на контейнер. Неверным был вывод из этой формы. Слот ограничивает сколько сервисов можно создать — но в нашем случае не сделал их бесплатными, и оплачивалось всё время жизни контейнера, а не время игры. Простаивающий бот стоил столько же, сколько играющий.

Карта — не индикатор тарифа, следим отдельно

Карта требуется на любом плане и служит проверкой личности (§5.1), поэтому по её наличию нельзя судить, платный ли текущий тариф, — но и защиты от списания она не даёт: в нашем случае списание произошло. Дисциплина: не подтверждать upgrade вслепую, не создавать третий сервис, включить billing alert (сейчас нет ни одного) и опустить порог с дефолтных 13,07/мес**, а не ноль; нулём она становится только при instances: 0.

Почему текущая работа подходит Sandbox

  • проект некоммерческий;
  • боты — исследовательские варианты модели, а не продаваемый сервис;
  • нет SLA и обещания доступности;
  • падение обоих ботов не ломает Play — перестают играть только две identity;
  • цель запуска — измерить качество алгоритмов и пригодность маленького runtime;
  • заняты ровно два предусмотренных service-слота (бесплатность которых опровергнута фактическим списанием — §5.1).

Сохраняем эту формулировку как контекст решения. Если статус проекта или назначение площадки изменятся, оценку условий нужно сделать заново.

12. Какие боты разумно размещать здесь дальше

Northflank Sandbox — дефицит из двух маленьких, но always-on слотов. Его лучше отдавать ботам, которые:

  • укладываются в 512 MiB с запасом хотя бы 25–30%;
  • отвечают быстро на 0,2 shared vCPU;
  • не хранят локальное состояние и не требуют volume/database;
  • играют только одну партию одновременно;
  • имеют linux/amd64 образ и публичный HTTPS webhook;
  • не являются критичной production-зависимостью.

Кандидаты на будущее — лёгкие book/greedy/raw-board/небольшие one-ply стратегии. Тяжёлый expectimax, полный KCP DFS и Monte Carlo логичнее держать там, где есть целое ядро или фиксированная машина: homelab, Cloud Run на время кредита, Azure/другой PaaS для canary либо дешёвый VPS.

Практические пороги пересмотра

Переносим или увеличиваем ресурс, если выполняется хотя бы одно:

  • память устойчиво выше 85% или продолжает монотонно расти;
  • есть OOM/restarts;
  • CPU p90 постоянно около 100% и webhook p90 остаётся в 60-секундном bucket;
  • появляются timed_out, http_5xx, unreachable;
  • алгоритм нужно пустить более чем в одну одновременную партию;
  • Developer Sandbox перестаёт соответствовать назначению проекта.

Текущая предварительная классификация:

  • KCP One Ply: по памяти подходит отлично, по CPU — на границе;
  • Hybrid KCP: по CPU подходит, по памяти — на границе;
  • поэтому пока оставляем оба, но не считаем sizing закрытым вопросом.

13. Обновление, rollback и остановка

Обновление

  1. Сначала merge публичного кода и зелёный CI.
  2. Собрать новый public linux/amd64 image.
  3. Добавить private assets отдельным слоем и проверить SHA-256.
  4. Push нового immutable commit tag в private GAR.
  5. Обновить один Northflank сервис как canary.
  6. Проверить pull, startup logs, 147-entry book, handshake, smoke game и метрики.
  7. Только затем обновить второй сервис.
  8. Старый tag/digest сохранить до успешной серии партий.

Остановка без рейтингового ущерба

Сначала убрать identity с ladder, затем остановить compute:

bot_token_value=$(security find-generic-password \
  -s dicechess-play-bot-token \
  -a northflank/kcp-one-ply-1 -w)
 
curl --fail --silent --show-error -X POST "$PLAY_BASE/bot/ladder/leave" \
  -H "Authorization: Bearer ${bot_token_value}" | jq
 
unset bot_token_value
 
northflank pause service \
  --projectId dice-chess-onnx-bots \
  --serviceId kcp-one-ply

Не делать наоборот: ladder продолжит создавать партии для недоступного endpoint, бот получит timeout losses и может быть auto-parked только после нескольких поражений.

Rollback

  1. ladder/leave для затронутой identity.
  2. В deployment вернуть предыдущий известный tag или digest.
  3. Проверить, что GAR credential по-прежнему pull-ит image.
  4. Проверить startup logs и webhook stats.
  5. ladder/join только после smoke game.

Webhook не нужно регистрировать заново, если публичный URL и secret не менялись.

14. Что ещё нужно довести до зрелого runbook

  • проверить долговечность Dice Chess GAR credential контролируемым redeploy;
  • поставить billing alert на минимальную сумму;
  • снять метрики после нескольких часов/суток, особенно memory plateau Hybrid;
  • сохранить manifest происхождения oracle-3.onnx рядом с моделью, как уже сделано для KCP;
  • через достаточную серию партий сравнить стратегии контролируемым методом;
  • после эксперимента решить, какие две identity достойны занимать бесплатные слоты.

Источники и дата актуальности

Снимок конфигурации и метрик — 14.08.2026. Тарифы и условия могут измениться.