Инфраструктура и CI/CD (Terraform)

Перед началом: Модули 1-8. Инструменты: Terraform, Yandex Cloud. Время: ~5 ч.

Из M8: мы настроили очередь (YMQ + триггер) через Terraform. Теперь опишем инфраструктуру целиком: YDB, API Gateway, Cloud Functions, IAM, Object Storage — и соберём CI/CD-пайплайн.

В этом модуле — «инфраструктура как код» (IaC) на Terraform и CI/CD-пайплайн для бэкенда чата. Фокус: инфраструктура — это код, воспроизводимый, проверяемый и откатываемый.

🔥 Зачем это вообще нужно

Инфраструктура создаётся через Web-консоль. Вчера кто-то удалил продакшен-базу «по ошибке». Восстановление из бэкапа — 2 часа даунтайма. Terraform: всё в Git, любое изменение — через PR, откат — git revert && terraform apply.

📖 Теория

Что описываем в Terraform

Все ресурсы Yandex Cloud — декларативно:

  • YDB (serverless БД), Object Storage (бакет под статику PWA);
  • API Gateway (HTTP/WebSocket-маршруты);
  • Cloud Functions (Auth, Ingestion, Broadcast, Notification);
  • YMQ + триггер (очередь и запуск Broadcast);
  • Serverless Container (WS Gateway);
  • IAM: сервисные аккаунты с минимальными ролями; Lockbox для секретов.
hcl
# ── providers.tf ──
terraform {
  required_providers {
    yandex = { source = "yandex-cloud/yandex", version = ">= 0.100" }
  }
  backend "s3" {
    bucket   = "tf-state-chat"
    key      = "backend/terraform.tfstate"
    endpoint = "storage.yandexcloud.net"
    region   = "ru-central1"
  }
}

provider "yandex" {
  cloud_id  = var.yc_cloud_id
  folder_id = var.yc_folder_id
  zone      = "ru-central1-a"
}

# ── main.tf ──

# Сервисный аккаунт для функций
resource "yandex_iam_service_account" "worker" {
  name = "chat-worker"
}
resource "yandex_resourcemanager_folder_iam_member" "worker_roles" {
  for_each = toset(["ydb.editor", "ymq.writer", "ymq.reader", "functions.functionInvoker"])
  folder_id = var.yc_folder_id
  role      = each.value
  member    = "serviceAccount:${yandex_iam_service_account.worker.id}"
}

# YDB (serverless)
resource "yandex_ydb_database_serverless" "chat" {
  name      = "chat-db"
  folder_id = var.yc_folder_id
}

# Object Storage (статика PWA)
resource "yandex_storage_bucket" "frontend" {
  bucket = "chat-frontend-${var.project_id}"
  acl    = "public-read"
  website {
    index_document = "index.html"
    error_document = "index.html"
  }
}

# Cloud Function (Ingestion)
resource "yandex_function" "ingestion" {
  name               = "ingestion"
  runtime            = "go121"
  entrypoint         = "main.HandleHTTP"
  memory             = 128
  execution_timeout  = 10
  service_account_id = yandex_iam_service_account.worker.id
  environment = {
    YDB_DATABASE    = yandex_ydb_database_serverless.chat.database_path
    YMQ_QUEUE_URL   = yandex_message_queue.chat_messages.id
  }
  content { zip_filename = "build/ingestion.zip" }
}

# API Gateway
resource "yandex_api_gateway" "chat" {
  name = "chat-gateway"
  spec = templatefile("openapi.yaml.tpl", {
    ingestion_fn   = yandex_function.ingestion.id
    auth_fn        = yandex_function.auth.id
    ws_container   = yandex_serverless_container.ws.id
    jwt_audience   = "chat"
    jwt_issuer     = "chat-auth"
    jwks_url       = "https://chat-auth.yandexcloud.net/.well-known/jwks.json"
  })
}

# Переменные (dev/prod)
variable "yc_cloud_id"   {}
variable "yc_folder_id"  {}
variable "project_id"    { default = "dev" }

CI/CD-пайплайн (GitHub Actions)

yaml
# .github/workflows/deploy.yml
name: Deploy Backend

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: '1.23' }
      - run: go test ./... -race -covermode=atomic

  terraform-plan:
    needs: test
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform plan -var-file="dev.tfvars"
        env: { YC_TOKEN: '${{ secrets.YC_TOKEN }}' }

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: '1.23' }
      - run: go build -o build/ingestion ./cmd/ingestion
      - run: zip -j build/ingestion.zip build/ingestion
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform apply -auto-approve -var-file="prod.tfvars"
        env: { YC_TOKEN: '${{ secrets.YC_TOKEN }}' }
      - name: Smoke test
        run: |
          curl -sf https://chat-api.yandexcloud.net/health || exit 1

State и окружения

  • State хранится в remote backend (Object Storage) с блокировкой (lock) — чтобы два инженера не применили изменения одновременно.
  • terraform plan — посмотреть diff до применения; terraform apply — применить.
  • Окружения dev / prod — отдельные стейты и переменные (один модуль, разные tfvars).

Не коммитьте terraform.tfstate в репозиторий — там секреты и его нельзя править руками.

CI/CD-пайплайн

Пайплайн: push → test → plan → merge → apply → deploy → smoke, с rollback
  1. push/PR → 2. lint + unit-test → 3. terraform plan (отчёт в PR) → 4. review & merge →
  2. terraform apply → 6. deploy функций + миграции → 7. smoke-тесты → ✅ релиз. На провале smoke — rollback: re-apply предыдущей версии функций и tf-state.

Миграции БД применяются в пайплайне после apply, до smoke-тестов (с возможностью отката).

Безопасность

  • Принцип минимальных привилегий: каждому сервисному аккаунту — только нужные роли (ydb.editor, functions.invoker, storage.admin и т.д.).
  • Секреты — в Yandex Lockbox, не в переменных окружения манифеста и не в коде.
  • TLS: принудительный HTTPS/WSS (через API Gateway + Certificate Manager).
  • Доступ к production-стейту — ограничен (отдельные права на backend-бакет).

Отказоустойчивость и DR

  • Зоны доступности: YDB serverless реплицирует автоматически; контейнеры/функции — многозонные.
  • Резервное копирование: регулярные бакапы критичных данных (YDB export в Object Storage).
  • План восстановления (DR): зафиксировать RTO/RPO и последовательность восстановления.

Runbook

Короткий документ «инцидент с чатом»: что проверить первым (метрики, логи), как диагностировать (Yandex Monitoring/Logging), куда эскалировать, как откатить. Runbook сокращает время восстановления — его пишут заранее, а не в момент аварии.

💱 Компромиссы

  • Terraform vs Web-консоль: воспроизводимо, reviewable, откатываемо — но порог входа и необходимость remote state.
  • Remote state с блокировкой: командная работа без конфликтов, но добавляет зависимость (бакет + механизм блокировки).
  • terraform plan в PR перед apply: медленнее деплой, но ловит регрессии до прода.
  • Least privilege IAM: больше сервисных аккаунтов с узкими ролями — безопаснее, но сложнее управлять.

Практика

  • Написать Terraform-модуль для YDB, Object Storage, одной Cloud Function и API Gateway.
  • Настроить remote backend с блокировкой и два окружения (dev/prod) через tfvars.
  • Собрать CI/CD (GitHub Actions/SourceCraft): plan в PR, apply после merge, smoke + rollback.
  • Написать одностаничный Runbook инцидента.

🔗 К следующему модулю: Инфраструктура в коде. Масштабируем систему.


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

1. terraform plan — показывает...
2. Principal of least privilege для SA приложения — что это?
3. CI/CD pipeline для serverless backend — обязательный шаг перед deploy...
4. Infrastructure as Code (Terraform) — это...
5. tfstate в Object Storage с блокировкой нужен для...

🔗 Мостик к Модулю 10

Инфраструктура в коде. Масштабируем систему: что делать при росте до 100k пользователей.