30 серверов и ни одного описания: внедряем Terraform для SaaS-компании

Задача клиента: инфраструктура без документации

В феврале 2026 года к нам обратилась SaaS-компания CloudMetrics из Санкт-Петербурга — разработчик платформы аналитики для e-commerce. На тот момент у них крутилось 30 серверов в Yandex Cloud: 8 production, 5 staging, 3 базы данных, 4 узла Kubernetes-кластера и ещё десяток вспомогательных машин — мониторинг, CI/CD, VPN, логи.

Вся эта инфраструктура три года собиралась руками через веб-консоль Yandex Cloud. Никакой документации, никакого версионирования. Четыре DevOps-инженера настраивали серверы каждый по-своему — кто во что горазд.

«У нас staging отличался от production. Мы думали, что конфигурации одинаковые, пока баг не проявился только в проде — оказалось, на staging другие security groups» — DevOps-лид CloudMetrics.

К чему это привело на практике:

  • Дрифт конфигураций — staging и production расходились по настройкам
  • Время создания нового окружения — 2–3 дня ручной работы
  • Аудит невозможен — никто не знал, кто и когда менял настройки
  • Bus factor = 1 — только один инженер знал все тонкости сетевой конфигурации
  • Расходы на облако — забытые ресурсы (старые диски, неиспользуемые IP) стоили ~40 000 руб/мес

Мы предложили поэтапно внедрить Terraform — инструмент Infrastructure as Code от HashiCorp. Суть проста: описываешь инфраструктуру декларативно, кладёшь в Git и больше не кликаешь мышкой в консоли.

Почему Terraform, а не Pulumi или CloudFormation

Клиент рассматривал три варианта. Мы разложили их по полочкам:

КритерийTerraformPulumiYandex Cloud CLI
Язык описанияHCL (декларативный)Python/Go/TypeScriptBash-скрипты
Провайдер Yandex CloudОфициальный, зрелыйCommunity, отстаётНативный, но нет state
Кривая обученияСредняя (HCL прост)Высокая (нужен код)Низкая
Экосистема модулейTerraform Registry (тысячи)Pulumi Registry (сотни)Нет
State managementВстроенныйВстроенныйНет
Импорт существующих ресурсовterraform importpulumi importНевозможен

Terraform выиграл по совокупности факторов. Зрелый провайдер Yandex Cloud, декларативный подход — удобный для аудита, и огромное сообщество с готовыми модулями. Команда из четырёх DevOps-инженеров освоила HCL примерно за неделю.

План внедрения по этапам

Мы разбили работу на четыре этапа:

  • Этап 1 (неделя 1–2): Установка Terraform, настройка remote state, импорт базовых ресурсов (сети, подсети, security groups)
  • Этап 2 (неделя 3–4): Описание compute-ресурсов, баз данных, дисков. Создание модулей
  • Этап 3 (неделя 5–6): Workspaces для staging/production, переменные окружений
  • Этап 4 (неделя 7–8): Интеграция с GitLab CI/CD, code review для инфраструктурных изменений

Установка Terraform и настройка провайдера Yandex Cloud

Первым делом поставили Terraform на рабочие станции всех инженеров и на CI/CD-сервер.

Установка Terraform 1.8

# Установка Terraform на Ubuntu/Debian
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update && sudo apt-get install terraform=1.8.*

# Проверяем установку
terraform version
# Terraform v1.8.5

# Включаем автодополнение
terraform -install-autocomplete
source ~/.bashrc

Структура проекта и провайдер

Структуру проекта выстроили по принципу «каждый слой — отдельная директория со своим state»:

# Структура проекта
infrastructure/
├── modules/              # Переиспользуемые модули
│   ├── compute/
│   ├── network/
│   ├── database/
│   └── k8s-cluster/
├── environments/
│   ├── production/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   └── staging/
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       ├── terraform.tfvars
│       └── backend.tf
├── global/               # Общие ресурсы (DNS, IAM)
│   ├── main.tf
│   └── backend.tf
└── .gitlab-ci.yml

Конфигурация провайдера Yandex Cloud:

# environments/production/main.tf

terraform {
  required_version = ">= 1.8.0"

  required_providers {
    yandex = {
      source  = "yandex-cloud/yandex"
      version = "~> 0.120.0"
    }
  }
}

provider "yandex" {
  cloud_id  = var.cloud_id
  folder_id = var.folder_id
  zone      = var.default_zone

  # Аутентификация через сервисный аккаунт (для CI/CD)
  service_account_key_file = var.sa_key_file
}
# environments/production/variables.tf

variable "cloud_id" {
  description = "Yandex Cloud ID"
  type        = string
}

variable "folder_id" {
  description = "Yandex Cloud Folder ID"
  type        = string
}

variable "default_zone" {
  description = "Default availability zone"
  type        = string
  default     = "ru-central1-a"
}

variable "sa_key_file" {
  description = "Path to service account key file"
  type        = string
  default     = "~/.yc/sa-terraform-key.json"
  sensitive   = true
}

variable "environment" {
  description = "Environment name"
  type        = string
  default     = "production"
}

Remote state в Yandex Object Storage

Локальный state — классическая ошибка, на которой обжигаются почти все новички. Если файл terraform.tfstate лежит на ноутбуке одного инженера, остальные не видят актуальное состояние инфраструктуры. Параллельная работа моментально приводит к конфликтам. Мы настроили remote backend в Yandex Object Storage — он S3-совместимый, так что конфигурация стандартная:

# Создаём бакет для state (однократно через CLI)
yc iam service-account create --name terraform-state-sa
yc resource-manager folder add-access-binding \
  --id $FOLDER_ID \
  --role storage.admin \
  --subject serviceAccount:$SA_ID

yc storage bucket create \
  --name cloudmetrics-tf-state \
  --default-storage-class standard \
  --max-size 1073741824

# Включаем версионирование бакета (защита от случайного удаления)
yc storage bucket update \
  --name cloudmetrics-tf-state \
  --versioning versioning-enabled
# environments/production/backend.tf

terraform {
  backend "s3" {
    endpoints = {
      s3 = "https://storage.yandexcloud.net"
    }
    bucket = "cloudmetrics-tf-state"
    key    = "production/terraform.tfstate"
    region = "ru-central1"

    skip_region_validation      = true
    skip_credentials_validation = true
    skip_requesting_account_id  = true
    skip_s3_checksum            = true

    # Блокировка state через DynamoDB-совместимую таблицу (YDB)
    dynamodb_table = "terraform-locks"
  }
}

Блокировку реализовали через DynamoDB-таблицу — точнее, через Yandex Database в Serverless-режиме. Это гарантирует, что два инженера не запустят terraform apply одновременно и не затрут изменения друг друга.

Импорт существующей инфраструктуры

Перенос 30 живых серверов под управление Terraform — самый нервный этап. Просто описать ресурсы и запустить apply не выйдет: Terraform решит, что их нет, и попытается создать заново. Здесь нужен terraform import.

Инвентаризация ресурсов облака

Начали с полной инвентаризации:

# Получаем список всех VM
yc compute instance list --format json | jq '.[] | {name, id, status, zone: .zone_id, cores: .resources.cores, memory_gb: (.resources.memory / 1073741824)}'

# Результат (фрагмент):
# {"name": "prod-app-1",  "id": "fhm1abc...", "status": "RUNNING", "zone": "ru-central1-a", "cores": 4, "memory_gb": 8}
# {"name": "prod-app-2",  "id": "fhm2def...", "status": "RUNNING", "zone": "ru-central1-a", "cores": 4, "memory_gb": 8}
# {"name": "prod-db-1",   "id": "fhm3ghi...", "status": "RUNNING", "zone": "ru-central1-a", "cores": 8, "memory_gb": 32}
# ... (ещё 27 серверов)

# Список сетей и подсетей
yc vpc network list
yc vpc subnet list

# Security groups
yc vpc security-group list --format json | jq '.[] | {name, id, rules_count: (.rules | length)}'

# Диски
yc compute disk list --format json | jq '.[] | {name, id, size_gb: (.size / 1073741824), type: .type_id}'

И вот тут начались сюрпризы. Инвентаризация вскрыла 6 «забытых» ресурсов: 3 диска по 100 ГБ, которыми никто не пользовался, 2 статических IP без привязки и 1 snapshot месячной давности. Удалили — сэкономили 12 000 руб/мес сразу.

Процесс импорта ресурсов

Для каждого ресурса действовали по одной схеме: 1) описывали в HCL, 2) импортировали в state, 3) запускали plan и смотрели на расхождения:

# Шаг 1: Описываем сеть в Terraform
# modules/network/main.tf

resource "yandex_vpc_network" "main" {
  name        = "cloudmetrics-network"
  description = "Main VPC network"
}

resource "yandex_vpc_subnet" "app" {
  name           = "app-subnet"
  zone           = "ru-central1-a"
  network_id     = yandex_vpc_network.main.id
  v4_cidr_blocks = ["10.10.1.0/24"]
}

resource "yandex_vpc_subnet" "db" {
  name           = "db-subnet"
  zone           = "ru-central1-a"
  network_id     = yandex_vpc_network.main.id
  v4_cidr_blocks = ["10.10.2.0/24"]
}

resource "yandex_vpc_security_group" "app" {
  name       = "app-sg"
  network_id = yandex_vpc_network.main.id

  ingress {
    protocol       = "TCP"
    port           = 443
    v4_cidr_blocks = ["0.0.0.0/0"]
    description    = "HTTPS"
  }

  ingress {
    protocol       = "TCP"
    port           = 80
    v4_cidr_blocks = ["0.0.0.0/0"]
    description    = "HTTP"
  }

  ingress {
    protocol          = "TCP"
    port              = 22
    v4_cidr_blocks    = ["10.10.0.0/16"]
    description       = "SSH internal"
  }

  egress {
    protocol       = "ANY"
    v4_cidr_blocks = ["0.0.0.0/0"]
    description    = "Allow all outbound"
  }
}
# Шаг 2: Импортируем существующие ресурсы
cd environments/production

# Импорт сети
terraform import module.network.yandex_vpc_network.main enp1abc2def3ghi

# Импорт подсетей
terraform import module.network.yandex_vpc_subnet.app e9b1abc2def3ghi
terraform import module.network.yandex_vpc_subnet.db e9b4jkl5mno6pqr

# Импорт security group
terraform import module.network.yandex_vpc_security_group.app enp7stu8vwx9yz0

# Шаг 3: Проверяем, что plan показывает "No changes"
terraform plan
# module.network.yandex_vpc_network.main: Refreshing state...
# module.network.yandex_vpc_subnet.app: Refreshing state...
# No changes. Your infrastructure matches the configuration.

Если plan показывает изменения — описание в HCL не совпадает с реальностью. Вот так выглядит обнаружение дрифта на практике. Правили HCL до тех пор, пока plan не показывал «No changes».

Импорт 30 серверов: автоматизация через скрипт

Импортировать 30 серверов руками — то ещё удовольствие. Написали скрипт, который генерировал HCL и команды импорта автоматически:

#!/bin/bash
# generate-import.sh — генерирует HCL и команды импорта для всех VM

FOLDER_ID="b1g1abc2def3ghi"

# Получаем все VM в формате JSON
VMs=$(yc compute instance list --folder-id $FOLDER_ID --format json)

# Генерируем HCL-ресурсы
echo "$VMs" | jq -r '.[] | @base64' | while read VM_B64; do
    VM=$(echo "$VM_B64" | base64 -d)
    NAME=$(echo "$VM" | jq -r '.name')
    ID=$(echo "$VM" | jq -r '.id')
    CORES=$(echo "$VM" | jq -r '.resources.cores')
    MEMORY=$(echo "$VM" | jq -r '.resources.memory / 1073741824 | floor')
    DISK_SIZE=$(echo "$VM" | jq -r '.boot_disk.disk_size / 1073741824 | floor' 2>/dev/null || echo "20")
    ZONE=$(echo "$VM" | jq -r '.zone_id')
    SUBNET_ID=$(echo "$VM" | jq -r '.network_interfaces[0].subnet_id')
    INTERNAL_IP=$(echo "$VM" | jq -r '.network_interfaces[0].primary_v4_address.address')

    TF_NAME=$(echo "$NAME" | tr '-' '_')

    echo "# terraform import yandex_compute_instance.${TF_NAME} ${ID}"
    echo "resource \"yandex_compute_instance\" \"${TF_NAME}\" {"
    echo "  name        = \"${NAME}\""
    echo "  zone        = \"${ZONE}\""
    echo "  platform_id = \"standard-v3\""
    echo "  resources {"
    echo "    cores  = ${CORES}"
    echo "    memory = ${MEMORY}"
    echo "  }"
    echo "}"
    echo ""
done

За один рабочий день импортировали все 30 серверов, 5 подсетей, 8 security groups, 3 managed-базы данных и 12 дисков. Вся инфраструктура теперь живёт в коде.

Модули Terraform: переиспользуемые компоненты

Одна из главных ценностей Terraform — модули. Без них вы рано или поздно начнёте копировать блоки кода для каждого нового сервера, и это быстро превратится в кашу. Мы создали параметризованные модули сразу.

Модуль compute для типовых серверов

# modules/compute/main.tf

resource "yandex_compute_instance" "this" {
  name        = var.name
  hostname    = var.name
  zone        = var.zone
  platform_id = var.platform_id

  resources {
    cores         = var.cores
    memory        = var.memory
    core_fraction = var.core_fraction
  }

  boot_disk {
    initialize_params {
      image_id = var.image_id
      size     = var.disk_size
      type     = var.disk_type
    }
  }

  network_interface {
    subnet_id          = var.subnet_id
    security_group_ids = var.security_group_ids
    nat                = var.nat_enabled

    dynamic "dns_record" {
      for_each = var.internal_dns_zone != "" ? [1] : []
      content {
        fqdn = "${var.name}.${var.internal_dns_zone}."
      }
    }
  }

  metadata = merge(
    {
      ssh-keys  = "${var.ssh_user}:${file(var.ssh_public_key)}"
      user-data = var.cloud_init_config
    },
    var.extra_metadata
  )

  scheduling_policy {
    preemptible = var.preemptible
  }

  labels = merge(
    {
      environment = var.environment
      managed_by  = "terraform"
      team        = var.team
    },
    var.extra_labels
  )

  lifecycle {
    ignore_changes = [
      metadata["user-data"],  # Не пересоздавать при изменении cloud-init
    ]
  }
}

resource "yandex_dns_recordset" "this" {
  count   = var.create_dns_record ? 1 : 0
  zone_id = var.dns_zone_id
  name    = "${var.name}.${var.dns_domain}."
  type    = "A"
  ttl     = 300
  data    = [yandex_compute_instance.this.network_interface[0].ip_address]
}
# modules/compute/variables.tf

variable "name"               { type = string }
variable "zone"               { type = string; default = "ru-central1-a" }
variable "platform_id"        { type = string; default = "standard-v3" }
variable "cores"              { type = number; default = 2 }
variable "memory"             { type = number; default = 4 }
variable "core_fraction"      { type = number; default = 100 }
variable "disk_size"          { type = number; default = 20 }
variable "disk_type"          { type = string; default = "network-ssd" }
variable "image_id"           { type = string }
variable "subnet_id"          { type = string }
variable "security_group_ids" { type = list(string); default = [] }
variable "nat_enabled"        { type = bool; default = false }
variable "preemptible"        { type = bool; default = false }
variable "ssh_user"           { type = string; default = "deploy" }
variable "ssh_public_key"     { type = string; default = "~/.ssh/id_ed25519.pub" }
variable "cloud_init_config"  { type = string; default = "" }
variable "environment"        { type = string }
variable "team"               { type = string; default = "platform" }
variable "extra_labels"       { type = map(string); default = {} }
variable "extra_metadata"     { type = map(string); default = {} }
variable "create_dns_record"  { type = bool; default = false }
variable "dns_zone_id"        { type = string; default = "" }
variable "dns_domain"         { type = string; default = "" }
variable "internal_dns_zone"  { type = string; default = "" }

Использование модуля: описание 8 серверов в 40 строк

В итоге создание production-серверов выглядело вот так:

# environments/production/main.tf

locals {
  app_servers = {
    "prod-app-1" = { cores = 4, memory = 8 }
    "prod-app-2" = { cores = 4, memory = 8 }
    "prod-app-3" = { cores = 4, memory = 8 }
    "prod-app-4" = { cores = 4, memory = 8 }
  }
}

module "app_server" {
  source   = "../../modules/compute"
  for_each = local.app_servers

  name               = each.key
  cores              = each.value.cores
  memory             = each.value.memory
  disk_size          = 40
  disk_type          = "network-ssd"
  image_id           = data.yandex_compute_image.ubuntu_2204.id
  subnet_id          = module.network.app_subnet_id
  security_group_ids = [module.network.app_sg_id]
  environment        = "production"
  nat_enabled        = false

  cloud_init_config = templatefile("${path.module}/cloud-init/app.yaml", {
    docker_version = "24.0"
    node_exporter  = true
  })
}

# Managed PostgreSQL
module "database" {
  source = "../../modules/database"

  name        = "prod-postgresql"
  environment = "production"
  version     = "16"
  flavor      = "s3-c8-m32"      # 8 vCPU, 32 GB RAM
  disk_size   = 200
  disk_type   = "network-ssd"
  network_id  = module.network.network_id
  subnet_ids  = [module.network.db_subnet_id]

  databases = [
    { name = "cloudmetrics_prod", owner = "app" },
    { name = "analytics",         owner = "analytics" },
  ]

  users = [
    { name = "app",       password = var.db_app_password },
    { name = "analytics", password = var.db_analytics_password },
    { name = "readonly",  password = var.db_readonly_password },
  ]
}

Было: 30 серверов, нарощенных кликами в консоли за три года, без единой строчки документации. Стало: вся инфраструктура описана в 500 строках кода, лежит в Git, и любой инженер команды может её понять и воспроизвести с нуля.

Workspaces и окружения

Расхождение staging и production — это, пожалуй, главная боль, с которой к нам пришёл клиент. Terraform решает её двумя способами: через workspaces или через раздельные директории с общими модулями. Мы выбрали второй вариант — он даёт больше гибкости, когда окружения реально отличаются по конфигурации.

Staging как зеркало production

Staging использовал те же модули — просто с другими параметрами:

# environments/staging/main.tf

locals {
  app_servers = {
    "stg-app-1" = { cores = 2, memory = 4 }
    "stg-app-2" = { cores = 2, memory = 4 }
  }
}

module "app_server" {
  source   = "../../modules/compute"
  for_each = local.app_servers

  name               = each.key
  cores              = each.value.cores
  memory             = each.value.memory
  disk_size          = 20
  image_id           = data.yandex_compute_image.ubuntu_2204.id
  subnet_id          = module.network.app_subnet_id
  security_group_ids = [module.network.app_sg_id]
  environment        = "staging"
  preemptible        = true  # Прерываемые VM для экономии
}

# Staging использует меньший инстанс PostgreSQL
module "database" {
  source = "../../modules/database"

  name        = "stg-postgresql"
  environment = "staging"
  version     = "16"          # Та же версия, что в production!
  flavor      = "s3-c2-m8"    # 2 vCPU, 8 GB RAM (меньше)
  disk_size   = 50
  # ... остальные параметры
}

Главное правило, которое мы зафиксировали и не нарушали: security groups и сетевая топология в staging идентичны production. Меняются только «железные» параметры — CPU, RAM, диски — и количество реплик. Именно это убило целый класс багов из серии «на staging всё работало, в production упало в первую же ночь».

Terraform workspaces для временных окружений

Feature-окружения под тестирование новых фич поднимали через workspaces — создал, проверил, удалил:

# Создание временного окружения для feature-ветки
cd environments/staging
terraform workspace new feature-payments-v2

# workspace автоматически добавляет префикс к state key
# Теперь state хранится в:
# s3://cloudmetrics-tf-state/staging/feature-payments-v2/terraform.tfstate

# Создаём окружение
terraform apply -var="instance_count=2" -var="environment=feature-payments-v2"

# После завершения тестирования — удаляем
terraform destroy -auto-approve
terraform workspace select default
terraform workspace delete feature-payments-v2

Интеграция Terraform с GitLab CI/CD

Последний шаг — автоматизация. Теперь любое изменение инфраструктуры идёт только через merge request, и план генерируется автоматически. Никаких «я быстро поправлю руками».

Pipeline для Terraform

# .gitlab-ci.yml

stages:
  - validate
  - plan
  - apply

variables:
  TF_ROOT: "${CI_PROJECT_DIR}/environments/${ENVIRONMENT}"
  TF_STATE_NAME: "${ENVIRONMENT}"

.terraform_base:
  image: hashicorp/terraform:1.8
  before_script:
    - cd ${TF_ROOT}
    - terraform init -input=false
  rules:
    - changes:
        - "environments/${ENVIRONMENT}/**/*"
        - "modules/**/*"

# Валидация синтаксиса на каждый коммит
validate:
  extends: .terraform_base
  stage: validate
  script:
    - terraform validate
    - terraform fmt -check -recursive
  parallel:
    matrix:
      - ENVIRONMENT: [staging, production]

# Plan на merge request
plan:staging:
  extends: .terraform_base
  stage: plan
  variables:
    ENVIRONMENT: staging
  script:
    - terraform plan -out=plan.tfplan -input=false
    - terraform show -no-color plan.tfplan > plan.txt
  artifacts:
    paths:
      - "${TF_ROOT}/plan.tfplan"
      - "${TF_ROOT}/plan.txt"
    expire_in: 7 days
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

plan:production:
  extends: .terraform_base
  stage: plan
  variables:
    ENVIRONMENT: production
  script:
    - terraform plan -out=plan.tfplan -input=false
    - terraform show -no-color plan.tfplan > plan.txt
  artifacts:
    paths:
      - "${TF_ROOT}/plan.tfplan"
      - "${TF_ROOT}/plan.txt"
    expire_in: 7 days
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

# Apply только после approve MR и merge в main
apply:staging:
  extends: .terraform_base
  stage: apply
  variables:
    ENVIRONMENT: staging
  script:
    - terraform apply -auto-approve -input=false
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      changes:
        - "environments/staging/**/*"
        - "modules/**/*"

apply:production:
  extends: .terraform_base
  stage: apply
  variables:
    ENVIRONMENT: production
  script:
    - terraform apply -auto-approve -input=false
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      changes:
        - "environments/production/**/*"
        - "modules/**/*"
  when: manual  # Production apply только вручную!

Политики безопасности для Terraform

Мы выстроили несколько рубежей, которые защищают инфраструктуру от случайных (и не очень) изменений:

# .tflint.hcl — правила линтера
rule "terraform_naming_convention" {
  enabled = true
  format  = "snake_case"
}

rule "terraform_unused_declarations" {
  enabled = true
}

rule "terraform_documented_outputs" {
  enabled = true
}

rule "terraform_documented_variables" {
  enabled = true
}

Плюс несколько дополнительных мер, которые сэкономили нам не одну ночь:

  • Защита от удаления — все критические ресурсы получили lifecycle { prevent_destroy = true }. Да, иногда это мешает, зато случайный destroy базы продакшна исключён
  • Обязательный code review — production-изменения проходят минимум 2 approve. Один человек не должен в одиночку менять боевую инфраструктуру
  • Plan в комментариях MR — бот сам публикует terraform plan прямо в merge request, и ревьюеры видят точно, что изменится
  • Sensitive variables — пароли и ключи живут в GitLab CI/CD Variables с маской. В коде им делать нечего
  • State lock — DynamoDB-блокировка не даёт двум инженерам одновременно запустить apply. Звучит банально, но мы видели, чем это заканчивается без блокировки

Результаты внедрения

Проект занял 8 недель. Вот конкретные цифры по внедрению Terraform, которое специалисты АйТи Фреш сделали для компании CloudMetrics:

МетрикаДо TerraformПосле Terraform
Время создания нового окружения2–3 дня15 минут (terraform apply)
Дрифт конфигураций staging/prodПостоянный, не контролируемый0 (одни и те же модули)
Аудит изменений инфраструктурыНевозможенПолная история в Git
Bus factor1 человекВся команда (4 инженера)
Расходы на забытые ресурсы~40 000 руб/мес0 руб (всё в коде)
Время на onboarding нового DevOps2–3 недели2–3 дня (читает код)

Через 2 месяца после завершения проекта клиент самостоятельно — без нашего участия — поднял новый кластер Kubernetes через наш модуль k8s-cluster. От merge request до работающего кластера прошло 4 часа, включая code review. Это и есть настоящий результат.

«Terraform изменил наш подход к инфраструктуре. Раньше серверы были как домашние животные — каждый уникален, каждого жалко. Сейчас они как скот — одинаковые, заменяемые, описанные в коде» — DevOps-лид CloudMetrics.

Часто задаваемые вопросы

Да, и это вполне рабочая история. Команда terraform import затягивает уже существующие ресурсы в state без пересоздания. Схема простая: описываете ресурс в HCL, выполняете terraform import <resource> <id>, потом смотрите через terraform plan, что описание совпадает с реальностью. Если расхождений нет — готово. Начиная с Terraform 1.5 появился блок import {} прямо в коде — стало ещё удобнее, особенно при массовом импорте.

Дрифт, или configuration drift — это когда реальная инфраструктура начинает расходиться с тем, что описано в коде. Классический сценарий: кто-то быстро поправил security group через веб-консоль и забыл. Terraform ловит это при каждом terraform plan: сравнивает state с тем, что есть на самом деле, и честно показывает разницу. Чтобы мониторить постоянно, мы запускаем terraform plan по расписанию и настраиваем алерт, если что-то изменилось. Никаких сюрпризов.

Защита строится в несколько слоёв, и убирать ни один из них не стоит. Первое — remote backend на S3 или Object Storage вместо локального файла. Второе — версионирование бакета: даже если state перезаписался, старые версии никуда не делись. Третье — State lock через DynamoDB, чтобы параллельные операции не устроили кашу. Четвёртое — регулярный бэкап бакета в другой регион. В Yandex Cloud мы ещё дополнительно режем доступ к бакету через IAM-политики: писать в него может только сервисный аккаунт Terraform — и никто больше.

Это инструменты для разных задач, и путать их не стоит. Terraform занимается инфраструктурой: создаёт серверы, сети, базы данных — декларативно описывает конечное состояние. Ansible работает с конфигурацией внутри уже существующих машин: ставит пакеты, настраивает сервисы. В хорошем стеке они дополняют друг друга: Terraform поднял VM, Ansible её настроил. В проекте для CloudMetrics мы использовали cloud-init для базовой инициализации, а для дальнейшего управления конфигурацией рекомендовали Ansible — логичное продолжение.

Типовой проект занимает 6–10 недель. Внутри — аудит текущей инфраструктуры, проектирование модулей, импорт ресурсов, настройка CI/CD и обучение команды. Сам Terraform бесплатен, это open source. Основные затраты — работа инженеров плюс remote backend: S3 и DynamoDB обходятся обычно меньше 1000 руб/мес. Окупается всё за 2–3 месяца: уходит время на рутину, исчезают забытые ресурсы, которые просто тихо стоили денег.

Нужна помощь с настройкой?

Специалисты АйТи Фреш возьмут внедрение и настройку под ключ — за плечами 15+ лет практики, обслуживание от 15 000 ₽/мес

📞 Связаться с нами
#Terraform настройка#Infrastructure as Code#Terraform модули#Terraform state management#Terraform workspaces#IaC внедрение#Terraform CI/CD#Terraform import