Skip to content

Автоматическая провизионинг (Cloud)

Этот документ описывает, как VM RoqueOS Cloud создаётся с нуля за ~3-5 минут после подписки клиента. Вам не нужно ничего из этого понимать для использования — это техническая справка для отладки, поддержки или для тех, кто хочет адаптировать поток под свой бизнес.

Диаграмма потока

1. Клиент кликает "Подписаться на Cloud Pro" в /pricing


2. Stripe Checkout (Бразилия) обрабатывает карту/Pix


3. Stripe webhook → Cloud Function `provisionServer`


4. Cloud Function вызывает Hetzner API (создаёт CX32 VM)
   + генерирует временный токен (TTL 30 мин)


5. VM загружается с cloud-init, который выполняет
   `curl https://roqueos.com.br/install.sh | bash`
   передавая --customer-id, --tier, --provisioning-token,
   --provisioning-callback


6. install.sh запускается внутри VM:
   - Подтягивает Docker-образы
   - Создаёт постоянные тома
   - Поднимает roqueos-server + sidecars (guacd, cloudflared)
   - Ждёт, пока /health не вернёт 200
   - Локальный POST /admin/setup для генерации admin key
   - POST callback в Firebase Functions с
     {customerUid, token, apiKey, apiSecret}


7. Cloud Function `provisioningCallback` валидирует токен,
   помечает подписку как `active`, сохраняет учётные данные (TTL 24ч),
   запускает приветственное письмо через SMTP


8. Клиент получает email с URL + учётными данными
   Логин в /app уже настроен для нового сервера

Задействованные компоненты

Frontend (roqueos-front)

  • src/pages/PricingPage.vue — кнопка "Подписаться" ведёт в Stripe Checkout
  • functions/index.js — Cloud Functions provisionServer и provisioningCallback
  • public/install.sh — зеркало канонического установщика roqueos-server

Backend (roqueos-server)

  • install/install.sh v1.1.0-cloud-callback — однострочный установщик с поддержкой callback
  • src/modules/admin/admin.controller.ts — идемпотентный эндпоинт POST /admin/setup (генерирует admin key)

Внешняя инфра

  • Hetzner Cloud API (https://api.hetzner.cloud/v1) — создание серверов
  • Cloudflare API — создание поддомена <id>.cloud.roqueos.com.br (планируется v1.2)
  • Firebase Secret Manager — хранит HCLOUD_TOKEN, PROVISIONER_SSH_KEY_ID, STANDARD_FIREWALL_ID
  • Stripe Brasil — checkout BRL через карту/Pix

Tier → spec Hetzner

RoqueOS TierHetzner SKUvCPURAMSSD дискCOGS €/мес
Startercx222 shared4 GB40 GB4.51
Procx324 shared8 GB80 GB7.55
Powerccx234 dedicated16 GB160 GB31.20
Enterprisead-hocvariavariavariavaria

Локация: Фалькенштайн, Германия (fsn1). Образ: ubuntu-22.04.

Эндпоинты Cloud Function

provisionServer (admin onCall)

Триггер: Stripe webhook customer.subscription.created. Внутри это https.onCall, требующий context.auth.token.admin === true.

Input:

json
{
  "customerUid": "abc123",
  "tier": "pro"
}

Output:

json
{
  "ok": true,
  "vmId": "12345678",
  "vmIp": "65.108.x.x",
  "provisioningToken": "<64 hex chars>",
  "callbackUrl": "https://southamerica-east1-roqueos.cloudfunctions.net/provisioningCallback",
  "tokenExpiresAt": 1716800000000
}

provisioningToken показывается admin только один раз (не сохраняется в логе). Его SHA-256 хеш хранится в Firestore в subscriptions/{uid}.provisioning.tokenHash с TTL 30 мин.

provisioningCallback (HTTP публичный)

Триггер: только что созданная VM вызывает POST этого эндпоинта через install.sh.

Публичный URL: https://southamerica-east1-roqueos.cloudfunctions.net/provisioningCallback

Почему публичный без Firebase auth: новая VM не имеет учётных данных Firebase. Аутентификация выполняется временным токеном (одноразовый, SHA-256 хеш сравнивается).

Input (POST JSON):

json
{
  "customerUid": "abc123",
  "token": "<64 hex chars>",
  "apiKey": "ros_xxxx",
  "apiSecret": "ros_secret_xxxx"
}

Валидации:

  • Форма payload (regex на token + apiKey + apiSecret)
  • tokenHash совпадает с записанным в Firestore
  • Токен не просрочен
  • Подписка всё ещё в provisioning (не была завершена ранее)

Output ok: 200 { "ok": true }Output ошибка: HTTP 400/401/404/409/410 + { "error": "<code>" }

Возможные ошибки:

  • 400 missing_fields — неполный payload
  • 400 invalid_* — невалидная форма
  • 401 token_mismatch — неправильный токен (потенциальная атака, логирует IP)
  • 404 subscription_not_found — customerUid не существует
  • 409 already_completed — callback уже был вызван успешно (dedup)
  • 409 no_pending_provisioning — подписка не в состоянии provisioning
  • 410 token_expired — прошло более 30 мин

install.sh флаги cloud-init

Cloud Function provisionServer генерирует user_data (cloud-init), выполняющий:

bash
#!/bin/bash
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get install -y curl ca-certificates

curl -fsSL https://roqueos.com.br/install.sh | bash -s -- \
  --customer-id "${CUSTOMER_UID}" \
  --tier "${TIER}" \
  --provisioning-token "${TOKEN}" \
  --provisioning-callback "${CALLBACK_URL}" \
  --skip-prompts

4 флага --customer-id, --tier, --provisioning-token, --provisioning-callback были добавлены в install.sh v1.1.0-cloud-callback (INSTALLER_VERSION в верху скрипта). Когда все четыре присутствуют, установщик запускает do_provisioning_callback() после того, как wait_for_server возвращает healthy.

Валидация со стороны оператора

Тестировать callback вручную (заменив customerUid/token фейковыми):

bash
curl -sS -X POST \
  "https://southamerica-east1-roqueos.cloudfunctions.net/provisioningCallback" \
  -H 'Content-Type: application/json' \
  -d '{
    "customerUid":"smoke-test",
    "token":"0000000000000000000000000000000000000000000000000000000000000000",
    "apiKey":"ros_smoketestkey",
    "apiSecret":"ros_secret_smoketest_secret_xyz"
  }'
# Ожидается: HTTP 404 {"error":"subscription_not_found"}

Устранение неполадок

СимптомВероятная причинаФикс
Приветственное письмо не пришлоSMTP_HOST/USER/PASS отсутствуют в Secret Managergcloud secrets list --project=roqueos | grep SMTP + переконфигурировать
VM создана, но подписка осталась provisioninginstall.sh упал ВНУТРИ VM (не вызвал callback)SSH в VM: journalctl -u cloud-init -f + docker logs roqueos-server
Callback возвращает token_expiredProvisioning занял >30 мин (VM медленная)Запустить provisionServer снова для регенерации токена + cloud-init
Callback возвращает token_mismatchБаг или попытка атакиПроверить IP источника в логе Function; если подозрительно, ротация и алерт
vmIp: null в Firestore docHetzner API timeout или ответ без public_netПовторить provisionServer; логи в gcloud functions logs read provisionServer

Не документировано здесь (но важно)

  • Автоматическое продление подписки: Stripe обрабатывает сам через customer.subscription.updated webhook. VM продолжает жить.
  • Отмена: webhook customer.subscription.deleted → будущая функция decommissionServer останавливает VM + финальный snapshot + email "ваши данные сохранены на 30 дней".
  • Апгрейд tier (Pro → Power): планируется v1.2 — вероятно migration через hcloud server change-type.
  • Cloudflare Tunnel per-customer: планируется — сегодня каждая VM экспонируется через публичный IP + порты 80/443. Tunnel добавляет TLS через edge SP (более низкая задержка для BR).

Перекрёстные ссылки

  • Обзор RoqueOS Cloud — планы, цены, обоснование
  • Self-host install — тот же install.sh, что запускается внутри cloud-init
  • Server Admin Panel — для управления только что провизионированным сервером
  • Техническая memory: hetzner_architecture_decision.md в репо (внутреннее)