Модуль 1. Setup: Serverless-окружение, free-tier лимиты, первый health-check
Цель модуля: Развернуть минимальное Serverless-окружение на Yandex Cloud для Git-хостинга MVP. Понять free-tier лимиты и рассчитать стоимость для 12, 50 и 100 пользователей. Написать первый health-check endpoint. Зафиксировать в ADR-SV1, почему Serverless для MVP, а не VPS или K8s, и что делать при росте.
Стадия роста проекта: Идея / прототип. Проект ещё не написан. Есть 12 разработчиков, которым нужен внутренний Git-хостинг. Бюджет: < 200₽/мес. Первый вопрос: «На чём запускать?»
Перед началом: Go 1.22+, Yandex Cloud аккаунт, Terraform, git, docker
Инструменты: Yandex Cloud (Cloud Functions, API Gateway, YDB Serverless, Message Queue, Object Storage, NFS), Terraform, Go 1.22+, Docker, local-ydb (эмулятор)
Трудоёмкость: ~4 ч (теория 1 ч · практика 2.5 ч · ADR 0.5 ч)
🔥 Инцидент: «Просто поднимем VPS»
Стартап из 12 человек решает поднять внутренний Git-хостинг. Первый вариант: «Арендуй VPS за 500₽/мес, поставь Gitea». Через неделю: VPS работает на 3–5% загрузку (12 человек делают 50 push/день и 200 clone/день), но ты платишь 500₽ фиксированно. Администрирование: обновления, бэкапы, мониторинг — всё на тебе. Через месяц: VPS не масштабируется, при 100 одновременных clone диск умирает.
Второй вариант: «Давайте Kubernetes». Три ноды по 4 vCPU, 16 GB RAM. Стоимость: ~3000₽/мес минимум. Для 12 пользователей это абсурд.
Третий вариант: Serverless. Cloud Functions + YDB Serverless + Message Queue + NFS. Стоимость: ~50₽/мес (только NFS платный, остальное в free-tier). Масштабирование от 0 до N автоматически. Администрирование: минимум.
Но Serverless — не серебряная пуля. Лимиты: 10 минут на вызов функции, 32 MB на тело запроса, Cold Start 10–100 мс. Для MVP с 12 пользователями — приемлемо. Для 1000+ пользователей или WebSocket — нет.
В этом модуле мы развернём Serverless-окружение, посчитаем стоимость для трёх сценариев нагрузки и зафиксируем в ADR, при каком RPS мигрируем на VPS.
Сквозной User Story (начало): «Пользователь Вася делает git push origin main. В этом модуле мы разворачиваем окружение, которое примет его push. В Модуле 2 мы посмотрим, что происходит на этапе Git Gateway. В Модуле 3 — как метаданные push сохраняются в YDB. В Модуле 4 — как событие RepositoryPushed доставляется в CI/CD через Outbox. В Модуле 5 — как блокировка защищает от конкурентных push.»
📖 Теория: Serverless-стек Yandex Cloud
Глоссарий модуля:
Cloud Function — бессерверная функция: код запускается по запросу, масштабируется от 0 до N, оплата за вызовы. Лимиты: 10 мин таймаут, 32 MB тело запроса, 1 GB RAM.
API Gateway — точка входа: маршрутизация, Rate Limiting, JWT-валидация. Free-tier: 100k запросов/мес.
YDB Serverless — управляемая БД: YQL вместо SQL, оптимистичные транзакции, нет FOR UPDATE. Free-tier: 10 RU/с.
Message Queue (SQS-совместимая) — очередь сообщений: Visibility Timeout, DLQ, Redrive Policy. Free-tier: 1M запросов/мес.
Object Storage — S3-совместимое хранилище: для артефактов функций, кэша, бэкапов.
NFS (Network File System) — файловое хранилище: для bare-репозиториев Git. Единственный платный компонент MVP (~50₽/мес за 50 GB).
Cold Start — задержка при первом запросе к «холодному» контейнеру функции. Для Go: 10–100 мс. Для JVM: 1–5 с.
Free-tier — бесплатный лимит Yandex Cloud: Cloud Functions 1M вызовов/мес, API Gateway 100k запросов/мес, YDB 10 RU/с, Message Queue 1M запросов/мес.
RU (Request Unit) — единица вычислительных ресурсов YDB Serverless. 1 RU ≈ 1 мс CPU + 1 МБ I/O.
1.1. Архитектура MVP: что и зачем
Ключевые решения:
- API Gateway — единая точка входа, Rate Limiter, JWT.
- Cloud Functions — 4 функции: api, git-gateway, outbox-relay, saga-orchestrator.
- YDB Serverless — метаданные (репозитории, пользователи, PR, outbox_events).
- Message Queue — события Outbox → консьюмеры.
- NFS — bare-репозитории Git (packfile, refs). Единственный платный компонент.
- Object Storage — артефакты функций (ZIP), кэш git ls-tree.
1.2. Free-tier лимиты и FinOps-калькулятор
Free-tier Yandex Cloud покрывает MVP с запасом. Но важно понимать, когда лимит кончится.
Расчёт нагрузки:
- 12 юзеров: 50 push/день × 3 функции + 200 clone/день × 1 функция = ~10 500 вызовов/мес.
- 50 юзеров: ×4 → ~42 000 вызовов/мес.
- 100 юзеров: ×8 → ~84 000 вызовов/мес.
| Сервис | Free-tier лимит | 12 юзеров | 50 юзеров | 100 юзеров |
|---|---|---|---|---|
| Cloud Functions | 1M вызовов/мес | ~10 500 (1%) | ~42 000 (4%) | ~84 000 (8%) |
| API Gateway | 100k запросов/мес | ~2 000 (2%) | ~10 000 (10%) | ~20 000 (20%) |
| YDB Serverless | 10 RU/с | ~2 RU/с пик (20%) | ~5 RU/с (50%) | ~8 RU/с (80%) ⚠️ |
| Message Queue | 1M запросов/мес | ~500 (0.05%) | ~2 500 (0.25%) | ~5 000 (0.5%) |
| NFS | Платный | 50 GB = ~50₽/мес | 50 GB = ~50₽/мес | 100 GB = ~100₽/мес |
Стоимость:
- 12 юзеров: ~50₽/мес (только NFS). Остальное в free-tier.
- 50 юзеров: ~50₽/мес (всё ещё free-tier, NFS тот же).
- 100 юзеров: ~100–200₽/мес (NFS 100 GB + возможное превышение YDB RU/с).
Точка миграции: при > 1000 RPS или push > 32 MB (лимит Cloud Function) — миграция на VPS. Паттерны те же — инфраструктура меняется.
1.3. Terraform: инфраструктура как код
Весь стек описан в Terraform. ClickOps запрещён.
hcl# terraform/main.tf # === NFS для Git-объектов === resource "yandex_storage_filesystem" "git-repos" { name = "git-repos" type = "nfs" zone = "ru-central1-a" size = 50 # ~50₽/мес } # === YDB Serverless === resource "yandex_ydb_database_serverless" "main" { name = "git-hosting-db" } # === Message Queue + DLQ === resource "yandex_message_queue" "outbox-events" { name = "outbox-events" visibility_timeout_seconds = 30 message_retention_seconds = 86400 } resource "yandex_message_queue" "outbox-events-dlq" { name = "outbox-events-dlq" visibility_timeout_seconds = 60 message_retention_seconds = 1209600 # 14 дней } # Redrive Policy: после 5 неудач → DLQ resource "yandex_message_queue_redrive_policy" "outbox" { queue_name = yandex_message_queue.outbox-events.name dead_letter_queue = yandex_message_queue.outbox-events-dlq.name max_receive_count = 5 } # === Cloud Functions === # API Function resource "yandex_function" "api" { name = "git-hosting-api" runtime = "golang122" memory = 256 timeout = 30 concurrency_limit = 100 # API без жёсткого лимита service_account_id = yandex_iam_service_account.functions.id environment = { YDB_CONNECTION = "grpcs://..." NFS_PATH = "/var/git" } mount_points { name = "git-repos" file_system_id = yandex_storage_filesystem.git-repos.id path = "/var/git" mode = "rw" } package { bucket = yandex_storage_bucket.functions.id object = "api.zip" } } # Git Gateway Function (Bulkhead: concurrency_limit = 5) resource "yandex_function" "git-gateway" { name = "git-hosting-git-gateway" runtime = "golang122" memory = 512 timeout = 600 concurrency_limit = 5 # Bulkhead из Модуля 5 service_account_id = yandex_iam_service_account.functions.id mount_points { name = "git-repos" file_system_id = yandex_storage_filesystem.git-repos.id path = "/var/git" mode = "rw" } package { bucket = yandex_storage_bucket.functions.id object = "git-gateway.zip" } } # Outbox Relay Function resource "yandex_function" "outbox-relay" { name = "git-hosting-outbox-relay" runtime = "golang122" memory = 256 timeout = 60 service_account_id = yandex_iam_service_account.functions.id package { bucket = yandex_storage_bucket.functions.id object = "outbox-relay.zip" } } # Saga Orchestrator Function resource "yandex_function" "saga-orchestrator" { name = "git-hosting-saga-orchestrator" runtime = "golang122" memory = 256 timeout = 60 service_account_id = yandex_iam_service_account.functions.id package { bucket = yandex_storage_bucket.functions.id object = "saga-orchestrator.zip" } } # === Timer Triggers === # Outbox Relay: каждые 5 секунд resource "yandex_function_trigger" "outbox-timer" { name = "outbox-relay-timer" timer { cron_expression = "*/5 * * * * ?" } function { id = yandex_function.outbox-relay.id } } # Saga Orchestrator: каждые 5 секунд resource "yandex_function_trigger" "saga-timer" { name = "saga-orchestrator-timer" timer { cron_expression = "*/5 * * * * ?" } function { id = yandex_function.saga-orchestrator.id } } # === API Gateway === resource "yandex_api_gateway" "main" { name = "git-hosting-gateway" spec = <<-EOF openapi: 3.0.0 paths: /api/{path+}: x-yc-apigateway-any-method: x-yc-apigateway-integration: type: cloud_functions function_id: ${yandex_function.api.id} x-yc-apigateway-plugin-rate-limiter: requests_per_second: 2 burst: 10 key_type: ip /git/{repo}.git/{path+}: x-yc-apigateway-any-method: x-yc-apigateway-integration: type: cloud_functions function_id: ${yandex_function.git-gateway.id} EOF } # === Service Account (least privilege) === resource "yandex_iam_service_account" "functions" { name = "git-hosting-functions" } resource "yandex_resourcemanager_folder_iam_member" "functions-ydb" { folder_id = var.folder_id role = "ydb.databases.owner" member = "serviceAccount:${yandex_iam_service_account.functions.id}" } # === Cloud Monitoring Alerts === resource "yandex_monitoring_alert" "ydb_ru" { name = "ydb-ru-exceeds-free-tier" conditions { metric { name = "ydb.serverless.ru_per_second" threshold = 8 # 80% от 10 RU/с duration = 300 } } notification_channels = [yandex_monitoring_notification_channel.telegram.id] } resource "yandex_monitoring_alert" "functions_invocations" { name = "functions-invocations-exceed-free-tier" conditions { metric { name = "functions.invocations.count" threshold = 800000 # 80% от 1M/мес duration = 3600 } } notification_channels = [yandex_monitoring_notification_channel.telegram.id] }
1.4. Локальная разработка: docker-compose + Makefile
Для локальной разработки используем эмуляторы. Всё поднимается через make up.
yaml# docker/docker-compose.yml services: # YDB Serverless эмулятор ydb: image: ydbplatform/local-ydb:latest ports: - "2136:2136" # gRPC API - "8765:8765" # Web UI environment: - YDB_USE_IN_MEMORY_PDISKS=true # Message Queue эмулятор (SQS-совместимый) elasticmq: image: softwaremill/elasticmq:latest ports: - "9324:9324" # SQS API # NFS-эмулятор (локальная директория) git-repos: image: alpine:latest command: ["sleep", "infinity"] volumes: - git-repos-data:/var/git volumes: git-repos-data:
Makefile для автоматизации:
makefile# Makefile .PHONY: up down test migrate deploy build upload apply # Поднять локальное окружение up: docker-compose -f docker/docker-compose.yml up -d @echo "YDB UI: http://localhost:8765" @echo "ElasticMQ: http://localhost:9324" # Остановить локальное окружение down: docker-compose -f docker/docker-compose.yml down # Запустить тесты test: go test ./... -race -coverprofile=coverage.out # Применить миграции YDB migrate: go run ./cmd/migrate --database=grpc://localhost:2136/?database=/local --dir=./migrations # Полный деплой deploy: build upload apply build: cd cmd/api && GOOS=linux GOARCH=amd64 go build -o bootstrap main.go && zip api.zip bootstrap cd cmd/git-gateway && GOOS=linux GOARCH=amd64 go build -o bootstrap main.go && zip git-gateway.zip bootstrap cd cmd/outbox-relay && GOOS=linux GOARCH=amd64 go build -o bootstrap main.go && zip outbox-relay.zip bootstrap cd cmd/saga-orchestrator && GOOS=linux GOARCH=amd64 go build -o bootstrap main.go && zip saga-orchestrator.zip bootstrap upload: yc storage object upload --bucket $(FUNCTIONS_BUCKET) --name api.zip --file cmd/api/api.zip yc storage object upload --bucket $(FUNCTIONS_BUCKET) --name git-gateway.zip --file cmd/git-gateway/git-gateway.zip yc storage object upload --bucket $(FUNCTIONS_BUCKET) --name outbox-relay.zip --file cmd/outbox-relay/outbox-relay.zip yc storage object upload --bucket $(FUNCTIONS_BUCKET) --name saga-orchestrator.zip --file cmd/saga-orchestrator/saga-orchestrator.zip apply: terraform -chdir=terraform apply -auto-approve
1.5. Health-check: первая Cloud Function
Первый код — health-check endpoint. Проверяет, что функция запускается, NFS примонтирован, YDB доступен.
go// cmd/api/main.go package main import ( "context" "encoding/json" "net/http" "os" "time" ) func main() { http.HandleFunc("/api/health", healthHandler) http.ListenAndServe(":8080", nil) } func healthHandler(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() _ = ctx // используется для проверки YDB в полной версии status := map[string]string{ "status": "ok", "timestamp": time.Now().UTC().Format(time.RFC3339), } // Проверяем NFS if _, err := os.Stat("/var/git"); err != nil { status["nfs"] = "unavailable" w.WriteHeader(http.StatusServiceUnavailable) } else { status["nfs"] = "ok" } // Проверяем YDB (упрощённо) if os.Getenv("YDB_CONNECTION") != "" { status["ydb"] = "configured" } else { status["ydb"] = "not configured" } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(status) }
1.6. Этот паттерн в архетипах
Setup проявляется по-разному в четырёх архетипах проектов:
| Архетип | Compute | Хранилище | Почему Serverless для MVP |
|---|---|---|---|
| Финтех | K8s (нет Cold Start для платежей) | PostgreSQL + Kafka | Serverless недопустим: Cold Start + strong consistency |
| Git-хостинг | Cloud Functions + NFS | YDB Serverless | Спорадическая нагрузка, free-tier покрывает |
| Мессенджер | K8s (WebSocket, long-lived) | PostgreSQL + Redis | Serverless недопустим: long-lived соединения |
| Лента | Cloud Functions для индексации | YDB + Object Storage | Фоновые задачи, спорадическая нагрузка |
Общий принцип: Serverless для спорадических, stateless нагрузок. K8s для постоянных, stateful, real-time.
1.7. Компромиссы (Trade-offs)
| Решение | Плюс | Минус | Когда выбирать |
|---|---|---|---|
| Serverless (Cloud Functions) | ~50₽/мес; автомасштаб; минимум ops | Cold Start 10–100 мс; лимит 10 мин; 32 MB push | MVP, спорадическая нагрузка, < 1k RPS |
| VPS (500₽/мес) | Нет Cold Start; полный контроль | Фиксированная стоимость; администрирование | Стабильная нагрузка > 1k RPS |
| K8s (3000₽/мес) | Масштабирование; StatefulSet | Сложность; стоимость | Production, > 10k RPS |
| YDB Serverless | ~0₽ при 10 RU/с | Нет FOR UPDATE; YQL ≠ SQL | MVP, метаданные |
| Managed PostgreSQL | Полный SQL; FOR UPDATE SKIP LOCKED | От 500₽/мес | Рост > 100 юзеров |
Вывод: для MVP Git-хостинга — Serverless. При росте > 1k RPS или push > 32 MB — миграция на VPS. Паттерны те же — инфраструктура меняется. См. ADR-SV1.
🛠 Практические задания
Сценарий: Развернуть Serverless-окружение для Git-хостинга MVP. Цель: health-check отвечает 200, NFS примонтирован, YDB доступен, стоимость < 100₽/мес.
Задача 1: Terraform — весь стек
Написать Terraform для: NFS, YDB Serverless, Message Queue + DLQ с Redrive Policy, 4 Cloud Functions, Timer Triggers, API Gateway, Service Account, Cloud Monitoring алерты.
✅ Готово, когда:
[ ] Terraform описывает весь стек (ClickOps запрещён)
[ ] terraform plan в CI для Drift Detection
[ ] Service Account с least privilege (не editor)
[ ] NFS 50 GB, YDB Serverless, Message Queue + DLQ с Redrive Policy (max_receive_count = 5)
[ ] 4 Cloud Functions: api (concurrency 100), git-gateway (concurrency 5), outbox-relay, saga-orchestrator
[ ] Timer Triggers: outbox-relay и saga-orchestrator каждые 5 секунд
[ ] API Gateway: Rate Limiter 2 req/s, burst 10
[ ] Cost-алерты: YDB RU/с > 8, Cloud Functions invocations > 800k
Задача 2: Локальная разработка — docker-compose + Makefile
Написать docker-compose с local-ydb и elasticmq. Makefile с командами make up, make test, make migrate, make deploy.
✅ Готово, когда:
[ ] make up поднимает local-ydb (Web UI на localhost:8765) и elasticmq
[ ] make test запускает Go-тесты с race detector
[ ] make migrate применяет миграции к local-ydb
[ ] make deploy собирает ZIP-артефакты и загружает в Object Storage
Задача 3: Health-check endpoint
Написать health-check Cloud Function: проверяет NFS, YDB, возвращает JSON со статусом.
✅ Готово, когда:
[ ] GET /api/health возвращает 200 с JSON: {"status": "ok", "nfs": "ok", "ydb": "configured"}
[ ] При недоступности NFS → "nfs": "unavailable", статус 503
[ ] Функция запускается за < 500 мс (Cold Start)
Задача 4: FinOps-калькулятор
Рассчитать стоимость для 12, 50 и 100 пользователей. Заполнить таблицу: вызовы/мес, RU/с, стоимость/мес. Определить точку миграции на VPS.
✅ Готово, когда: [ ] Таблица стоимости для 12, 50, 100 пользователей [ ] Указана точка миграции (> 1k RPS или push > 32 MB) [ ] ADR-SV1 «Serverless vs VPS vs K8s» написан
⚠️ Антипаттерн для разбора
Студенту дается описание для анализа:
Команда решает поднять Git-хостинг для 12 человек.
Вариант 1: «Арендуй VPS за 500₽/мес, поставь Gitea».
Вариант 2: «Давайте Kubernetes, три ноды».
Вариант 3: «Serverless: Cloud Functions + YDB + NFS».
Выбираем Вариант 2, потому что «K8s — это стандарт».
Вопросы студенту:
- Почему K8s для 12 пользователей — антипаттерн? (Стоимость 3000₽/мес, сложность, загрузка 3–5%)
- Почему VPS за 500₽/мес — не оптимально? (Фиксированная стоимость, администрирование, нет автомасштабирования)
- Когда Serverless перестанет работать? (Cold Start критичен, push > 32 MB, WebSocket, > 1k RPS)
🧩 Проверь себя
🤖 Инструкция для LLM (Coding Plan Assistant) — Режим Модуля 1
Привет! Я начинаю Модуль 1 курса «Git Hosting на Go: Serverless MVP на Yandex Cloud». Выступи в роли Наставника-Архитектора.
Сначала проверь понимание: дай короткий ответ (1–2 предложения) — почему Serverless подходит для MVP Git-хостинга на 12 пользователей, и при каком RPS нужно мигрировать на VPS.
Затем выдай сценарий:
- 12 пользователей, 50 push/день, 200 clone/день
- Бюджет < 200₽/мес
- Стек: Yandex Cloud (Cloud Functions, YDB Serverless, Message Queue, NFS)
Попроси студента:
- Рассчитать стоимость для 12, 50 и 100 пользователей (FinOps-калькулятор).
- Написать Terraform для всего стека (NFS, YDB, MQ + DLQ, 4 функции, Timer Triggers, API Gateway).
- Написать docker-compose для локальной разработки (local-ydb, elasticmq) и Makefile.
- Написать health-check endpoint.
НЕ ПИШИ за меня Terraform и docker-compose. Дождись моего ответа с архитектурным планом. После этого дай фидбек по чек-листу ментора и укажи на слепые зоны (например, «нет Rate Limiter», «нет DLQ с Redrive Policy», «concurrency_limit не настроен», «нет cost-алертов»).
📝 Чек‑лист ментора
- Terraform описывает весь стек: NFS, YDB, MQ + DLQ, 4 Cloud Functions, Timer Triggers, API Gateway.
- ClickOps запрещён: все ресурсы в Terraform,
terraform planв CI. - Service Account с least privilege (не
editor). - concurrency_limit = 5 на git-gateway (Bulkhead).
- Rate Limiter на API Gateway (2 req/s, burst 10).
- DLQ с Redrive Policy (max_receive_count = 5).
- Timer Triggers: outbox-relay и saga-orchestrator каждые 5 секунд.
- Cost-алерты: YDB RU/с > 8, Cloud Functions invocations > 800k.
- Локальная разработка:
make upподнимает local-ydb и elasticmq. - Health-check: проверяет NFS, YDB, возвращает JSON.
- FinOps-калькулятор: стоимость для 12, 50, 100 пользователей.
- Точка миграции на VPS указана (> 1k RPS или push > 32 MB).
- ADR-SV1: «Serverless vs VPS vs K8s» с количественными метриками.
🔗 Мостик к Модулю 2 (Git Smart HTTP)
В конце ревью скажи студенту:
«Отлично, мы развернули Serverless-окружение и посчитали стоимость. Но как Git-клиент общается с нашим хостингом? Git использует протокол Smart HTTP: git clone отправляет POST на /repo.git/git-upload-pack, git push — POST на /repo.git/git-receive-pack. В Модуле 2 мы реализуем Git Smart HTTP в Cloud Function: разберём CGI-окружение, стриминг через io.Copy (не io.ReadAll — лимит 32 MB!), и как exec.Command запускает git-http-backend. А ещё — как защитить Git-репозиторий от Directory Traversal через assertSafeRepo, и как протестировать это через моки os/exec.»
Сквозной User Story: «Пользователь Вася делает git push origin main. В Модуле 2 мы посмотрим, что происходит на этапе Git Gateway: как Cloud Function принимает POST, парсит CGI-заголовки и стримит packfile в git-http-backend. В Модуле 3 — как метаданные push сохраняются в YDB. В Модуле 4 — как событие RepositoryPushed доставляется в CI/CD через Outbox. В Модуле 5 — как блокировка защищает от конкурентных push.»