Модуль 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: что и зачем

100%
🖱 Перетащите • Колесо для зума

Ключевые решения:

  • 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 Functions1M вызовов/мес~10 500 (1%)~42 000 (4%)~84 000 (8%)
API Gateway100k запросов/мес~2 000 (2%)~10 000 (10%)~20 000 (20%)
YDB Serverless10 RU/с~2 RU/с пик (20%)~5 RU/с (50%)~8 RU/с (80%) ⚠️
Message Queue1M запросов/мес~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 + KafkaServerless недопустим: Cold Start + strong consistency
Git-хостингCloud Functions + NFSYDB ServerlessСпорадическая нагрузка, free-tier покрывает
МессенджерK8s (WebSocket, long-lived)PostgreSQL + RedisServerless недопустим: long-lived соединения
ЛентаCloud Functions для индексацииYDB + Object StorageФоновые задачи, спорадическая нагрузка

Общий принцип: Serverless для спорадических, stateless нагрузок. K8s для постоянных, stateful, real-time.

1.7. Компромиссы (Trade-offs)

РешениеПлюсМинусКогда выбирать
Serverless (Cloud Functions)~50₽/мес; автомасштаб; минимум opsCold Start 10–100 мс; лимит 10 мин; 32 MB pushMVP, спорадическая нагрузка, < 1k RPS
VPS (500₽/мес)Нет Cold Start; полный контрольФиксированная стоимость; администрированиеСтабильная нагрузка > 1k RPS
K8s (3000₽/мес)Масштабирование; StatefulSetСложность; стоимостьProduction, > 10k RPS
YDB Serverless~0₽ при 10 RU/сНет FOR UPDATE; YQL ≠ SQLMVP, метаданные
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 — это стандарт».

Вопросы студенту:

  1. Почему K8s для 12 пользователей — антипаттерн? (Стоимость 3000₽/мес, сложность, загрузка 3–5%)
  2. Почему VPS за 500₽/мес — не оптимально? (Фиксированная стоимость, администрирование, нет автомасштабирования)
  3. Когда Serverless перестанет работать? (Cold Start критичен, push > 32 MB, WebSocket, > 1k RPS)

🧩 Проверь себя

1. Как автоматизировать локальную разработку?
2. Зачем concurrency_limit = 5 на git-gateway?
3. Какой компонент Serverless MVP Git-хостинга платный?
4. Лимит Cloud Functions на тело запроса?
5. При каком RPS мигрируем с Serverless на VPS?
6. Free-tier YDB Serverless — сколько RU/с?

🤖 Инструкция для 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)

Попроси студента:

  1. Рассчитать стоимость для 12, 50 и 100 пользователей (FinOps-калькулятор).
  2. Написать Terraform для всего стека (NFS, YDB, MQ + DLQ, 4 функции, Timer Triggers, API Gateway).
  3. Написать docker-compose для локальной разработки (local-ydb, elasticmq) и Makefile.
  4. Написать 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.»