☁️ Облачный стенд для хакатона — архитектура и план

О чём это

Проект эфемерного публичного стенда на Oracle Cloud под хакатон «Dice Chess AI» (23–24.07.2026): играть/смотреть партии против AI-ботов + витрина статистики. Облако = одноразовый клон демо-поверхности, независимый от homelab и домашнего интернета во время выступления. Настоящий прод остаётся на aurora (см. dicechess-analytics-na-aurora-deploy-i-sizing); сюда мы кладём снапшот данных, а не второй источник истины.

Статус — черновик/обсуждение. Решено 07-16: IaC = Terraform, код в отдельном репо dicechess-infra (см. раздел ниже). Механика облака уже проверена на KCP-прогоне — см. Oracle-Cloud-zhurnal-nastroyki-CLI-avtorizatsiya-set.

Зачем облако, а не homelab

  • Публичность — чистый URL, который открывают жюри, без проброса домашней сети.
  • Независимость — во время демо не зависим от домашнего аплинка/питания.
  • Изоляция — не рискуем настоящим продом на aurora.
  • Одноразовость — поднял → показал → terminate. Стоит центы (см. бюджет ниже).

Охват (scope B): play + боты + аналитика

Полный нарратив темы «Dice Chess AI»: посетитель играет против oracle-ботов и тут же видит почему модель сильная (openings explorer, winrate-by-position).

Ключевой плюс

Бо́льшая часть уже существует как docker-compose (у play-api свой compose, у analytics — db+api+ui). Стенд — это редеплой того же compose на публичную VM + обёртка наружу (прокси/TLS/снапшот), а не стройка с нуля.

Архитектура: одна A1.Flex VM

flowchart TD
  U["Игрок / жюри"] -->|HTTPS| C["Caddy · авто-TLS"]
  C -->|"play.домен"| PF["play-frontend · статика"]
  C -->|"/api + WS"| PA["play-api · JVM"]
  C -->|"stats.домен"| AU["analytics-ui · статика"]
  C -->|"/api"| AA["analytics-api · JVM"]
  PA --> PDB[("Postgres · play")]
  AA --> ADB[("Postgres · analytics · снапшот")]
  B["oracle-боты 1/2/3 + KCP · JVM+ONNX"] -->|"вызовы"| PA
  subgraph VM["Одна A1.Flex VM · docker-compose · aarch64"]
    C
    PF
    PA
    AU
    AA
    PDB
    ADB
    B
  end

Всё — JVM / статика / Postgres, то есть arch-нейтрально; ONNX-боты уже работают на arm64 (rpi4), так что Ampere A1 (aarch64) подходит без эмуляции.

КомпонентЧто этоАрх / форматНаружу
CaddyОбратный прокси + авто-TLS (Let’s Encrypt)любой:443
play-frontendSvelteKit adapter-staticстатикачерез Caddy
play-apiИгровой сервер + WebSocketJVM (arm64 образ)через Caddy
Postgres [play]Партии/сессии playarm64внутр.
analytics-uiСтатистика/openings explorerстатика (или arm64 образ)через Caddy
analytics-apiREST над БД игрJVM (arm64 образ)через Caddy
Postgres [analytics]Снапшот игрarm64внутр.
oracle-боты 1/2/3 + KCPONNX-value-ботыarm64 (как на rpi4)внутр. → play-api

Почему это влезает в €250

РесурсКонфиг€/мес (24/7)
A1.Flex4 OCPU / 24 GB~€50
A1.Flex8 OCPU / 48 GB~€100
Block storage (БД+тома)~50 GB~€1
Egress10 ТБ/мес бесплатно€0

Реально за окно хакатона (пара недель тестов + событие) — ~€25–50 из €250. Подробнее про биллинг: Oracle-Cloud-rannbuk.

IaC: Terraform в репо dicechess-infra

Решено 2026-07-16

Инфраструктуру стенда описываем Terraform, код — в отдельном репо dicechess-infra (не смешиваем с приложениями). Мотив: воспроизводимость + «поднял → показал → снёс одной командой». Terraform выбран в т.ч. как повод освоить инструмент (раньше не работали с ним).

🎓 Terraform в двух словах (для первого раза)

Terraform — декларативный IaC: описываешь желаемое состояние (какие ресурсы должны существовать), а не шаги. Весь жизненный цикл стенда закрывают три команды:

  • terraform plan — сухой прогон: показывает, что создаст / изменит / удалит; ничего не трогает;
  • terraform apply — приводит облако к конфигу (создаёт VM, публичный IP и т.д.);
  • terraform destroy — удаляет ровно то, что создал → это и есть «погасить, когда не нужно», без ручного клика в консоли.

Отличие от bootstrap-скрипта (который был на KCP-прогоне): Terraform хранит state — файл terraform.tfstate, карту «мой конфиг ↔ реальные ID ресурсов». Поэтому он знает, что именно снести, и повторный apply не плодит дубли (идемпотентность). ⚠️ State может содержать секреты/ID → в git не коммитим (.gitignore). Для соло-стенда локального state достаточно; удалённый (OCI Object Storage) — оверкилл. Аутентификация OCI-провайдера — та же, что у CLI (профиль DEFAULT / security-token), отдельного сетапа не нужно.

Разделение слоёв: Terraform vs cloud-init

  • Terraform = инфраструктура: VM (oci_core_instance), переиспользование готовых VCN/subnet (через data-источники), публичный IP, правило security-list на 80/443.
  • cloud-init (передаётся в metadata.user_data) = приложение: ставит Docker, логинится в GHCR, кладёт docker-compose.hackathon.yaml + Caddyfile, делает docker compose up -d.

Terraform поднимает «железо», внутри него cloud-init разворачивает тот же compose-стек, что на схеме выше.

Раскладка репо

dicechess-infra/
  README.md                    # что это + как запускать (apply/destroy)
  hackathon-stand/
    versions.tf                # пины: terraform >= 1.x, provider oracle/oci
    variables.tf               # входы: tenancy/compartment OCID, region,
                               #        ssh_public_key, subnet_ocid, shape,
                               #        ocpus, memory_gb, ghcr_token (sensitive)
    main.tf                    # data-источники (образ Ubuntu arm64, subnet) +
                               #        oci_core_instance с user_data
    cloud-init.yaml            # docker + GHCR login + compose up
    outputs.tf                 # public_ip, ssh_command, готовые URL
    terraform.tfvars.example   # шаблон (коммитим); реальный tfvars — gitignore
    .gitignore                 # *.tfstate*, terraform.tfvars, .terraform/

Иллюстративно (не финальный код) — инстанс с cloud-init:

resource "oci_core_instance" "stand" {
  compartment_id      = var.compartment_ocid
  availability_domain = var.availability_domain
  shape               = "VM.Standard.A1.Flex"        # Ampere aarch64, бесплатная квота
  shape_config { ocpus = var.ocpus, memory_in_gbs = var.memory_gb }
  create_vnic_details { subnet_id = var.subnet_ocid, assign_public_ip = true }
  source_details {
    source_type = "image"
    source_id   = data.oci_core_images.ubuntu_arm.images[0].id
  }
  metadata = {
    ssh_authorized_keys = var.ssh_public_key
    user_data           = base64encode(templatefile("cloud-init.yaml", { ghcr_token = var.ghcr_token }))
  }
}

Секреты — вне git

ghcr_token, пароль Postgres, токены ботов → Terraform-переменные с sensitive = true; значения даём через gitignored terraform.tfvars или TF_VAR_*-переменные окружения. В репо — только terraform.tfvars.example с плейсхолдерами. ⚠️ Образ ghcr.io/rabestro/dicechess-house-bots приватный → на VM нужен docker login ghcr.io с read:packages-токеном (делает cloud-init, токен приходит переменной). Публичным образам (play-api/play/analytics) логин не нужен.

Рабочий цикл

cd dicechess-infra/hackathon-stand
terraform init                 # один раз: качает провайдер oci
terraform apply                # VM+IP, cloud-init разворачивает стек (~пара минут)
# terraform output public_ip  → прописать в DNS-поддомен → Caddy берёт TLS сам
# ... демо ...
terraform destroy              # сносит ровно созданное; биллинг останавливается

Сеть (VCN/subnet) переиспользуется через data-источники → destroy её не трогает (в простое бесплатна).

Что нетривиально (разовые задачи)

  1. arm64-образы — УЖЕ ГОТОВЫ (проверено 07-16, снят с критического пути). Все CD-воркфлоу (play-api, analytics-api, analytics-ui) уже собирают и пушат мультиарх через QEMU+Buildx (platforms: linux/amd64,linux/arm64); Dockerfile’ы — build-стадия --platform=$BUILDPLATFORM + мультиарх-рантайм-базы. Проверка опубликованных манифестов: dicechess-play-api (sha-776e25e), dicechess-analytics-api (v0.5.6), dicechess-analytics-ui (v0.6.8) — во всех linux/amd64, linux/arm64. Боты (house-bots) — arm64 доказан работой на rpi4; play-фронт — статика (образа нет). Менять файлы сборки НЕ нужно.
  2. Снапшот данных: pg_dump игр с aurora → restore в облако. Решить: вся история (~140k партий) или курированный срез. ← теперь фактический критический путь вместе с Terraform-скелетом.
  3. DNS + TLS: поддомен jc.id.lv → public IP инстанса → Caddy сам берёт сертификат.
  4. Секреты / вход: redirect-URI Google-OAuth под новый домен — либо запустить analytics-ui в анонимном read-only режиме, чтобы не возиться с логином на публичном демо.

План по фазам

Фаза 0 — подготовка (заранее, вне критического пути)

  • ✅ arm64-образы — уже готовы (все образы мультиарх, проверено 07-16; см. «Что нетривиально» п.1).
  • ✅ Репо dicechess-infra (private) + Terraform-скелет hackathon-stand/ — сделано 07-16: self-contained VCN + A1.Flex VM + cloud-init + docker-compose.hackathon.yaml (Caddy + play-api in-memory + oracle-3) + Caddyfile. terraform validate ✓ (провайдер oci v8.23.0). Осталось: заполнить terraform.tfvarsplan → репетиция.
  • Скрипт pg_dump (aurora) → restore (облако).
  • Поддомен jc.id.lv + подготовка DNS.

Фаза 1 — генеральная репетиция (~за неделю)

launch VM → bootstrap → compose up → restore данных → DNS+TLS → сквозной smoke (сыграть партию против бота, открыть статистику) → terminate. Проверяет весь стенд за ~час.

Фаза 2 — событие

Поднять из готовых артефактов за ~10 мин (или держать с репетиции). Мониторить логи/ресурсы.

Фаза 3 — после

terminate инстанса (сеть VCN остаётся, в простое бесплатна).

Открытые решения (обсудить в отдельной сессии)

  • Объём данных: полная история vs курированный срез (свежие/высокорейтинговые) — чтобы дамп был лёгкий, а openings explorer выглядел богато. Склоняюсь к срезу.
  • Вход в аналитику: анонимный read-only (проще, ничего не светим) vs полноценный OAuth с новым доменом.
  • Размер VM: 4 OCPU/24 GB для демо с запасом; 8 — только при многих одновременных партиях.
  • IaC — решено (07-16): Terraform в отдельном репо dicechess-infra (см. раздел «IaC: Terraform» выше).

Следующий шаг

Прогресс 07-16:

  • ✅ репо dicechess-infra (private) + Terraform-скелет hackathon-stand/ (validate ✓, запушен);
  • ✅ arm64-образы сняты с критического пути (все мультиарх);
  • репетиция ядра пройдена (Фаза 1, core-scope): planapply (A1.Flex VM + сеть) → cloud-init развернул Caddy+play-api+oracle-3 → сквозной smoke (/health→play-api 200, oracle-3 постит seek, приватный образ+bot-token работают) → destroy (6 ресурсов снесены, биллинг 0). Весь цикл apply→smoke→destroy вживую. Репетиция выловила 2 бага Caddyfile (пустой STAND_HOSTNAME-адрес → крэш; respond перехватывал /health вместо proxy) — оба исправлены в репо (commit ff3c111), cloud-init теперь воспроизводит рабочую конфигурацию.

Дальше (scope-B, чтобы стенд стал полным демо):

  1. фронт play-статика — собрать SPA под URL стенда и отдавать через Caddy (сейчас / = заглушка);
  2. скрипт снапшота данных (pg_dump aurora → restore) + персистентность play (PLAY_DB_URL + postgres);
  3. analytics-трио (api+ui+snapshot db);
  4. на событие — DNS-поддомен + stand_hostname (Caddy авто-TLS) + генеральный прогон полного scope.

⚠️ Гоча Terraform (поймана в репетиции): terraform.tfvars перебивает TF_VAR_* из окружения — не класть плейсхолдеры в tfvars, если реальные значения приходят из env (иначе плейсхолдер выигрывает; на этом cloud-init сделал docker login пустышкой → unauthorized).

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