Внутренний сервис управления задачами АНО «Линка». Приложение на Nuxt 4 разворачивается только в production-контуре Yandex Cloud. Локальные и серверные сборки Docker запрещены: production image собирает только GitHub Actions и публикует только в Yandex Container Registry (YCR).
Browser / Telegram
|
v
tasks.nkolinka.ru
Certificate Manager + API Gateway (HTTP and WebSocket)
|
v
private Serverless Container
|
+--> YDB Serverless
+--> private Object Storage bucket
+--> Lockbox runtime secrets
Timer triggers --> job bridge Function --> POST /api/jobs/*
GitHub Actions --OIDC--> deploy service account --> YCR + container revisions
Telegram --> bot.todos.nkolinka.ru legacy nginx --> tasks.nkolinka.ru/api/telegram/webhook
Production resources находятся в новой папке linka-tasks-prod облака aacidov-main. Единственное исключение: существующая DNS-зона nkolinka.ru. остаётся в папке nko-linka.
API Gateway проксирует обычный HTTP-трафик и операции WebSocket /api/realtime в один private Serverless Container. Сертификат tasks.nkolinka.ru выпускается Certificate Manager через DNS challenge.
Bootstrap создаёт три отдельных service account:
| Account | Назначение | Минимальные права |
|---|---|---|
linka-tasks-runtime |
Код приложения | ydb.editor на базе, storage.editor в изолированной production-папке, lockbox.payloadViewer на одном секрете, api-gateway.websocketWriter на gateway |
linka-tasks-deploy |
GitHub Actions | push image в одном registry, update одного container, use runtime SA, чтение метаданных одного Lockbox secret |
linka-tasks-gateway |
API Gateway и timer triggers | invoke одного private container и job bridge Function |
GitHub не хранит JSON key или иной долговременный ключ. Workflow получает короткоживущий GitHub OIDC JWT. Federated credential принимает только subject:
repo:OWNER/REPOSITORY:environment:production
ycс авторизованным профилемdefaultи правами создавать ресурсы вaacidov-main, а также DNS records вnko-linka.jq,openssl,gh,ssh,scpиcurl.- Созданный GitHub repository с default branch
main. gh auth statusуспешно работает для этого repository.- Доступ по SSH к
USER@circlestickers.ruи известен абсолютный путь Compose project. - BotFather token пока не вводится и нигде не сохраняется до этапа финализации.
Скрипты используют только yc --profile default. Они идемпотентно находят ресурсы по имени и не удаляют существующие объекты. При конфликте DNS или потерянном секрете Object Storage key скрипт останавливается вместо перезаписи.
Запуск создаёт реальные production-ресурсы. В этом репозитории он только подготовлен и автоматически не запускался.
./scripts/bootstrap-production.shBootstrap создаёт:
- папку
linka-tasks-prod; - YDB Serverless с deletion protection;
- private Object Storage bucket без public ACL, с CORS только для
https://tasks.nkolinka.ru(PUT,GET,HEAD, любые request headers, exposedETag); - единственный YCR registry;
- private Serverless Container без обязательной первой revision;
- пустую job bridge Function без version: код и Lockbox binding появятся только при финализации;
- пустой Lockbox secret с deletion protection, без Telegram token и без payload version;
- runtime, deploy и gateway service accounts и resource-scoped bindings;
- HTTP/WebSocket API Gateway из
infra/api-gateway.yaml.tpl; - managed certificate и DNS records для
tasks.nkolinka.ru.
Локальные IDs и единственный доступный при создании secret Object Storage key записываются с mode 0600 в infra/.env.production.local. Файл игнорируется правилом .env.*. Его необходимо хранить как production credential и не прикладывать к issue, CI artifact или сообщению.
Certificate Manager может некоторое время оставаться в VALIDATING. В этом случае bootstrap завершится без token и попросит повторить ту же команду после DNS propagation.
Успешный bootstrap останавливается до BotFather token и печатает точную следующую команду финализации:
./scripts/finalize-production.shСначала нужно подключить GitHub и получить первую container revision.
После создания repository выполните:
./scripts/configure-github-oidc.sh OWNER/REPOSITORYСкрипт идемпотентно создаёт workload identity federation, federated credential с environment subject, GitHub environment production и несекретные repository variables. Затем отправьте main в GitHub.
PR quality выполняет npm ci, lint, typecheck, Vitest и Nuxt build. Deploy production повторяет quality job, обменивает GitHub OIDC JWT на короткоживущий Yandex IAM token, собирает Docker image в GitHub-hosted runner, отправляет immutable SHA tag и latest в YCR и создаёт revision. Workflow передаёт repository variable TELEGRAM_BOT_USERNAME в NUXT_TELEGRAM_BOT_USERNAME; до финализации значение ожидаемо пустое.
Первый deploy выполняется до появления Lockbox version. В revision нет Telegram и Object Storage credentials; приложение и публичная оболочка могут стартовать, а Telegram-аутентификация остаётся недоступной. Это ожидаемый безопасный промежуточный режим. Workflow не подставляет пустые секреты и не блокирует deployment.
Нельзя выполнять docker build, docker compose build или сборку image на circlestickers.ru. Изменения Dockerfile проверяются только PR checks и собираются production workflow.
После успешного первого Deploy production выполните ровно:
./scripts/finalize-production.shПервый запуск выполняет только SSH/Compose audit и останавливается до чтения token и любых изменений Lockbox, container revision, proxy или Telegram. После проверки используйте напечатанную команду с --apply-proxy и явными подтверждениями.
Apply-запуск:
- До token проверяет все аргументы, локальный proxy env, GitHub access, SSH target, remote paths и выполняет read-only audit: service names, image names и
docker compose ps. Полныйdocker compose configне выводится, поскольку он может содержать secrets. - При отсутствии Lockbox version скрыто читает BotFather token из stdin. Token не передаётся аргументом процесса и не печатается.
- Вызывает Telegram
getMe, проверяет username, передаётNUXT_TELEGRAM_BOT_USERNAMEв revision и записываетTELEGRAM_BOT_USERNAMEв GitHub repository variables. - Генерирует отдельные random secrets для Telegram webhook, legacy proxy, session и internal jobs и создаёт первую Lockbox version вместе с Object Storage credentials.
- Создаёт новую container revision того же image с точными Lockbox bindings.
- Создаёт version job bridge Function с
internal-job-secretиз Lockbox и идемпотентно создаёт или обновляет два timer trigger. Каждый раз в минуту bridge делаетPOSTк/api/jobs/outboxи/api/jobs/recurrencesс headerx-internal-job-secret. - Проверяет и перезагружает legacy nginx, затем вызывает Telegram
setWebhook.
Если в Lockbox уже есть current version, обычный apply повторно использует её независимо от локального LOCKBOX_VERSION_ID: token читается из Lockbox, новая version не создаётся. Ротация возможна только с явным --rotate-secrets и полным proxy preflight/apply, чтобы не менять связанные app/proxy secrets до успешного аудита.
Для неинтерактивной передачи token допустим только защищённый stdin из secret manager. Не помещайте token в shell argument или environment history.
Подготовьте локальный ignored-файл по примеру infra/legacy-proxy/proxy.env.example. Все пути должны быть абсолютными. PROXY_REMOTE_CONFIG указывает на отдельный host-level nginx virtual host для bot.todos.nkolinka.ru. До запуска finalizer создайте A-record на legacy server, установите certbot и выпустите Let's Encrypt certificate через webroot /var/www/linka-tasks-acme. Nginx template сохраняет ACME path доступным для автоматического renewal. Finalizer откатывает config, если nginx -t или reload завершается ошибкой.
После ручной проверки audit выполните команду, напечатанную скриптом:
./scripts/finalize-production.sh \
--legacy-host USER@circlestickers.ru \
--legacy-compose-dir /ABSOLUTE/COMPOSE/PATH \
--apply-proxy \
--proxy-env-file /ABSOLUTE/PATH/proxy.env.local \
--confirm-environment production \
--confirm-host bot.todos.nkolinka.ruБез всех трёх явных значений --legacy-host, --confirm-host и --confirm-environment production изменения не применяются. Apply атомарно обновляет отдельный host-level nginx virtual host, выполняет nginx -t, reload и только после успешной проверки вызывает Telegram setWebhook. При ошибке предыдущий config восстанавливается. Bot token остаётся в Lockbox и во временном файле 0600, удаляемом trap.
Revision использует поддерживаемый текущим yc флаг --zone-instances-limit 1 как временную границу масштабирования для логики с process-local serialization. Это лимит на availability zone, а не строгий глобальный singleton; долговременное решение должно обеспечивать распределённую сериализацию через YDB. Глобального --max-instances в локальном yc 1.16.0 нет.
Merge в main автоматически создаёт новый SHA-tagged image и revision. Если Lockbox version уже существует, workflow сохраняет все bindings. Ручной Docker build не нужен и запрещён.
yc serverless container revision list \
--container-id "$CONTAINER_ID" \
--folder-id "$FOLDER_ID" \
--profile defaultПроверьте https://tasks.nkolinka.ru, login flow и WebSocket reconnect. Не выводите Lockbox payload или полный Compose config в terminal recording и CI logs.
Добавьте --rotate-secrets к полной команде --apply-proxy из раздела выше. Команда сначала повторит все локальные, GitHub и SSH проверки и audit, только затем запросит BotFather token, создаст новую Lockbox version, revision и согласованно обновит proxy/webhook. Отдельный запуск --rotate-secrets без proxy apply останавливается до token и ничего не ротирует.
YCR image помечен commit SHA. Для rollback экспортируйте значения из infra/.env.production.local в ожидаемые scripts/deploy-production-revision.sh переменные и передайте предыдущий immutable image URI. Не перемещайте latest вручную и не собирайте image локально.
IDs можно восстановить через yc list, но Object Storage secret access key восстановить нельзя. Bootstrap обнаружит существующий key и остановится, не создавая бесконтрольно новый. Отзовите потерянный key, создайте новый по процедуре ротации и обновите Lockbox.
Docker для разработки не используется.
npm ci
npm run devПроверки:
npm run lint
npm run typecheck
npm test
npm run buildVitest запускает unit suite и настроен с passWithNoTests, чтобы временно пустой suite не блокировал infrastructure-only ветку.