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 провайдер, декларативный подход (проще для аудита) и огромное сообщество. Команда из 4 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 до полного совпадения.

Импорт 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 серверов, созданных кликами в консоли за 3 года. Что стало: вся инфраструктура описана в 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 }
  • Обязательный code review — минимум 2 approve для production-изменений
  • 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 по расписанию и алертить при обнаружении изменений.

Несколько уровней защиты: 1) Remote backend (S3/Object Storage) вместо локального файла; 2) Версионирование бакета — даже при перезаписи state старые версии сохраняются; 3) State lock через DynamoDB — предотвращает параллельные операции; 4) Регулярный бэкап бакета в другой регион. В 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