Ansible прекрасно справляется с настройкой уже существующего сервера, но кто-то должен этот сервер сначала создать: заказать виртуальную машину, выделить сеть, настроить security group, привязать диск. Делать это руками через веб-консоль облачного провайдера или гипервизора работает, пока серверов немного — а дальше начинается та же проблема, что и с ручной настройкой: расхождения между «тем, что должно быть» и «тем, что реально создано». Terraform закрывает именно этот этап — управление инфраструктурой как кодом (Infrastructure as Code), только не настройками внутри сервера, а самим фактом его существования. В этой статье разберём базовые концепции Terraform и опишем инфраструктуру для лабораторного стенда в сети 192.168.10.0/24.

В отличие от Ansible, который выполняет последовательность шагов, Terraform работает декларативно: ты описываешь желаемое конечное состояние («должно быть три сервера с такими-то параметрами»), а Terraform сам вычисляет, что нужно создать, изменить или удалить, чтобы прийти к этому состоянию. Он умеет работать практически с любым облаком — AWS, Azure, Google Cloud, а также с локальной виртуализацией вроде Proxmox или VMware через соответствующие провайдеры.

Шаг 1. Установка Terraform

wget -O- https://apt.releases.hashicorp.com/gpg | 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"   | tee /etc/apt/sources.list.d/hashicorp.list

apt update
apt install -y terraform

terraform -version

Terraform, как и Ansible, — это утилита командной строки, а не сервис. Устанавливать её можно на любую рабочую машину или на выделенный сервер автоматизации, например тот же srv-ansible01, если хочется держать весь инструментарий IaC в одном месте.

Шаг 2. Структура проекта

Terraform-проект — это каталог с файлами .tf. Стандартная практика — разносить конфигурацию по смыслу:

infra/
  providers.tf
  variables.tf
  main.tf
  outputs.tf
  terraform.tfvars

В providers.tf указывается, с каким облаком или гипервизором работаем. Возьмём для примера провайдер Proxmox — частый выбор для локальной лаборатории с доменом itpages.lab.local:

terraform {
  required_providers {
    proxmox = {
      source  = "bpg/proxmox"
      version = "0.61.1"
    }
  }
}

provider "proxmox" {
  endpoint  = "https://192.168.10.5:8006/"
  api_token = var.proxmox_api_token
  insecure  = true
}

Шаг 3. Переменные

Секреты и изменяемые параметры выносятся в переменные, чтобы не хардкодить их в основной конфигурации:

# variables.tf
variable "proxmox_api_token" {
  type      = string
  sensitive = true
}

variable "vm_count" {
  type    = number
  default = 2
}

Сами значения задаются в terraform.tfvars (этот файл в .gitignore, если содержит секреты) или передаются через переменные окружения вида TF_VAR_proxmox_api_token.

Шаг 4. Описываем сервер

Теперь основная часть — main.tf, где описываются ресурсы, которые Terraform должен создать. Например, виртуальная машина для будущего srv-web02:

resource "proxmox_virtual_environment_vm" "web" {
  name      = "srv-web02"
  node_name = "pve-node01"

  cpu {
    cores = 2
  }

  memory {
    dedicated = 4096
  }

  disk {
    datastore_id = "local-lvm"
    size          = 32
  }

  network_device {
    bridge = "vmbr0"
  }

  initialization {
    ip_config {
      ipv4 {
        address = "192.168.10.23/24"
        gateway = "192.168.10.1"
      }
    }
  }
}

Это декларативное описание: Terraform сравнит его с текущим состоянием инфраструктуры и поймёт, что нужно создать новую машину с указанными параметрами. Если позже увеличить memory.dedicated до 8192, Terraform на следующем прогоне сам определит, что нужно изменить именно память, не пересоздавая всю машину с нуля — если, конечно, тип ресурса это позволяет без пересоздания.

Шаг 5. Инициализация и применение

terraform init

Эта команда скачивает нужный провайдер (в нашем случае — плагин для Proxmox) и настраивает рабочий каталог. Выполняется один раз при старте проекта и повторно — при добавлении новых провайдеров.

terraform plan

План — это, пожалуй, самая важная команда во всём Terraform. Она показывает, что именно изменится, ничего при этом не применяя: какие ресурсы будут созданы (зелёным, со знаком +), какие изменены (жёлтым, ~), а какие удалены (красным, -). Прежде чем применять что-либо к реальной инфраструктуре, план стоит прочитать внимательно — особенно строки с удалением, которые иногда возникают неожиданно из-за изменения параметра, требующего пересоздания ресурса.

terraform apply

После подтверждения (ввод yes) Terraform создаст описанные ресурсы и сохранит их состояние в файл terraform.tfstate. Этот файл — по сути карта соответствия между кодом и реальной инфраструктурой, и относиться к нему нужно бережно.

Шаг 6. Состояние (state) и его хранение

Файл terraform.tfstate хранит актуальное представление о том, что Terraform создал и с какими параметрами. Именно по нему на следующем прогоне сравнивается желаемое состояние (код) с фактическим. Если этот файл потерять, Terraform перестанет понимать, какие ресурсы уже существуют, и может попытаться создать дубликаты. Поэтому для командной работы state никогда не хранится локально на ноутбуке — его выносят в удалённый backend, например:

terraform {
  backend "s3" {
    bucket = "itpages-terraform-state"
    key    = "lab/terraform.tfstate"
    region = "eu-central-1"
  }
}

Удалённый backend с блокировкой (locking) решает ещё одну проблему — если два инженера одновременно запустят terraform apply, второй получит ошибку блокировки вместо того, чтобы применить изменения поверх незавершённого прогона первого.

Шаг 7. Модули: переиспользуемые блоки

Когда нужно создать несколько однотипных серверов — скажем, три веб-сервера с одинаковой конфигурацией, но разными именами и IP — вместо копирования блока resource используется count или for_each, а для более сложной переиспользуемой логики — модули:

module "web_servers" {
  source   = "./modules/vm"
  for_each = { "srv-web01" = "192.168.10.21", "srv-web02" = "192.168.10.23" }

  name = each.key
  ip   = each.value
}

Модуль ./modules/vm при этом инкапсулирует все детали создания машины (CPU, память, диск, сеть), а снаружи остаётся только простой интерфейс из имени и IP-адреса. Это тот же принцип, что и роли в Ansible — не повторять одну и ту же логику в десяти местах, а описать её один раз и переиспользовать.

Шаг 8. Terraform и Ansible вместе

На практике Terraform и Ansible почти всегда работают в связке: Terraform создаёт сервер и выводит его IP-адрес через output, а Ansible следом настраивает то, что на этом сервере должно быть установлено.

# outputs.tf
output "web_server_ip" {
  value = proxmox_virtual_environment_vm.web.initialization[0].ip_config[0].ipv4[0].address
}

Дальше этот вывод можно подставить прямо в инвентарь Ansible — вручную, через скрипт или с помощью динамического inventory-плагина, который читает terraform.tfstate напрямую и сам формирует список хостов для следующего шага автоматизации. Получается сквозной цикл: Terraform поднимает сервер srv-web02 с нужным IP в сети 192.168.10.0/24, а Ansible следом ставит на него nginx, node_exporter и Zabbix-агент — без единого ручного клика на всём пути.

Workspaces: одна конфигурация, несколько окружений

Часто одна и та же конфигурация нужна в нескольких вариантах — например, лабораторный стенд itpages.lab.local и отдельное тестовое окружение с теми же ресурсами, но другими IP-адресами и размерами машин. Дублировать код ради этого не нужно — у Terraform есть workspaces:

terraform workspace new staging
terraform workspace new production
terraform workspace select staging

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

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

Не всегда Terraform внедряется с чистого листа — часто в компании уже есть серверы, созданные вручную, которые хочется взять под управление кодом, не пересоздавая их. Для этого есть команда import, которая привязывает уже существующий ресурс к описанию в конфигурации:

terraform import proxmox_virtual_environment_vm.web "pve-node01/100"

После импорта состояние ресурса появляется в state, но конфигурацию в main.tf всё равно придётся дописать вручную так, чтобы она точно соответствовала реальным параметрам — иначе следующий plan покажет расхождения и предложит «исправить» сервер, откатив параметры, которые импорт не подтянул автоматически. На практике это самый муторный этап внедрения Terraform в уже существующую инфраструктуру, но именно он окупается в дальнейшем — после него вся инфраструктура становится видна и воспроизводима из кода.

Destroy и работа с ресурсами, которые нельзя терять

Команда terraform destroy удаляет все ресурсы, описанные в конфигурации, — это удобно для одноразовых тестовых окружений, но крайне опасно, если случайно выполнить её не в том каталоге или не в том workspace. Для критичных ресурсов, которые никогда не должны быть удалены автоматически (например, диск с данными базы), в Terraform есть защита:

resource "proxmox_virtual_environment_vm" "db" {
  # ...

  lifecycle {
    prevent_destroy = true
  }
}

При попытке удалить такой ресурс Terraform откажется выполнять план, пока флаг явно не будет снят из кода — это простая, но действенная страховка от случайной команды не в том терминале.

Частые ошибки и как их избежать

  • Ручные правки инфраструктуры в обход Terraform. Если кто-то зашёл в консоль облака и поменял параметры сервера напрямую, состояние в terraform.tfstate разойдётся с реальностью. При следующем apply Terraform либо попытается откатить ручные изменения, либо, что хуже, пересоздать ресурс. Любые изменения инфраструктуры, управляемой Terraform, должны идти только через код.
  • Локальный state без блокировки в команде. Если несколько человек работают с одной инфраструктурой, а state лежит на чьём-то локальном диске, рано или поздно кто-то применит план поверх устаревшего состояния и получит конфликт или дубликаты ресурсов. Удалённый backend с locking — не опция, а обязательное условие для командной работы.
  • terraform apply без предварительного plan. Даже опытные инженеры иногда запускают apply сразу, полагаясь на память о том, что именно должно измениться. В реальности конфигурация могла разойтись с ожиданиями — например, из-за чужого коммита. Привычка всегда сначала смотреть на plan и только потом подтверждать apply экономит от неприятных сюрпризов, особенно от строк с пометкой «будет пересоздано».

Что дальше

Автоматизация и мониторинг обычно идут рука об руку: