Terraform для начинающих: инфраструктура как код на Linux
Как поставить Terraform или OpenTofu, поднять первый проект без облака, хранить state удалённо, делить окружения и запускать изменения через CI.

Коротко: Terraform описывает серверы, сети, DNS и контейнеры в текстовых файлах, а потом сам приводит реальность к описанию. Рабочий цикл — init, plan, apply. Главное, что нужно понять с первого дня: файл состояния terraform.tfstate — это память проекта, его хранят удалённо, с блокировкой и без доступа посторонних. В России провайдеры ставят через зеркало или переходят на OpenTofu.
Пока сервер один, его удобно настраивать руками: зашёл по SSH, поставил пакеты, поправил конфиг. Когда серверов становится пять, появляется тестовый стенд, а DNS-записи и балансировщик живут в облачной консоли, ручная настройка превращается в набор «а кто это создал и зачем». Инфраструктура как код решает именно эту проблему: всё, что есть в проде, записано в репозитории, проходит ревью и воспроизводится одной командой.
Этот же вопрос всплывает, когда инфраструктуру передают из рук в руки. Если вы рассматриваете DevOps-сопровождение на аутсорсе или, наоборот, забираете проект у подрядчика, первым делом выясняйте, описаны ли серверы и сети в коде и где лежит state. Инфраструктуру в Terraform можно принять за день и проверить через plan; инфраструктуру «в головах» приходится восстанавливать неделями. Ниже — путь от установки до первого проекта, который не стыдно запускать в CI.
Что такое инфраструктура как код и зачем она нужна?
Infrastructure as Code (IaC) — подход, при котором инфраструктура описывается в файлах так же, как код приложения. Эти файлы лежат в git, у них есть история, ревью и откат. Вместо инструкции «зайди в консоль, нажми “Создать”, выбери 2 vCPU» у вас есть блок конфигурации, который одинаково отработает сегодня, через год и у нового сотрудника.
Что это даёт на практике:
- Воспроизводимость. Тестовый стенд собирается из того же кода, что и прод, с другими переменными. Расхождения «на стейдже работает, в проде нет» становятся реже.
- Ревью изменений. Правка инфраструктуры — это merge request с диффом и планом, а не действие в консоли, о котором никто не знает.
- Документация, которая не устаревает. Код и есть описание того, что развёрнуто. Схему на вики можно не рисовать.
- Быстрое восстановление. Если облачный проект удалили или регион лёг, базовую инфраструктуру можно поднять заново за минуты (данные при этом нужно восстанавливать из бэкапа отдельно).
- Контроль дрейфа. Ручные правки в обход кода видны в плане, их можно найти и отменить.
Terraform работает декларативно: вы описываете желаемое состояние («должна быть одна машина с таким-то образом»), а не последовательность шагов. Инструмент сам сравнивает описание с реальностью и вычисляет, что создать, изменить или удалить. Повторный запуск без изменений в коде ничего не делает — это и называется идемпотентностью.
Чем Terraform отличается от OpenTofu, Ansible и Pulumi?
Новички часто путают инструменты, потому что все они «автоматизируют серверы». Различаются они зоной ответственности и подходом.
| Инструмент | Что делает | Подход | Лицензия | Когда брать |
|---|---|---|---|---|
| Terraform | Создаёт и удаляет ресурсы: машины, сети, DNS, бакеты, кластеры | Декларативный, HCL | BSL 1.1 (с версии 1.6, 2023) | Стандарт рынка, больше всего примеров и провайдеров |
| OpenTofu | То же, что Terraform: форк версии 1.5 | Декларативный, HCL | MPL 2.0, Linux Foundation | Нужна свободная лицензия или нет доступа к реестру HashiCorp |
| Ansible | Настраивает уже созданные машины: пакеты, файлы, сервисы | Процедурный с идемпотентными модулями, YAML | GPL 3.0 | Конфигурация ОС и приложений |
| Pulumi | Создаёт ресурсы, как Terraform | Код на Python, Go, TypeScript | Apache 2.0 | Команда хочет циклы и тесты на привычном языке |
| Docker Compose | Поднимает набор контейнеров на одном хосте | Декларативный, YAML | Apache 2.0 | Локальная разработка и небольшие одиночные серверы |
Классическая связка выглядит так: Terraform создаёт сервер и сеть, Ansible этот сервер настраивает. Если вы уже прочитали наш разбор первых playbook в Ansible, Terraform закрывает предыдущий шаг — появление самой машины.
Про лицензию. В августе 2023 года HashiCorp перевела Terraform с MPL 2.0 на Business Source License. Для внутреннего использования в компании это ничего не меняет, ограничения касаются тех, кто строит на Terraform конкурирующий коммерческий продукт. В ответ сообщество сделало форк OpenTofu. Синтаксис и команды у них почти совпадают (terraform → tofu), поэтому всё, что написано ниже, применимо к обоим. Различия появились в новых функциях: например, в OpenTofu есть встроенное шифрование state.
Как установить Terraform на Linux и обойти блокировку из России?
HashiCorp ограничила доступ к своим загрузкам и реестру провайдеров с российских IP-адресов. Сам бинарник при этом можно поставить из пакетного репозитория дистрибутива или с GitHub, а проблема возникает на шаге terraform init, когда скачиваются провайдеры. Рабочих путей три.
Вариант 1. Terraform и зеркало провайдеров
На Debian и Ubuntu, если доступ к apt.releases.hashicorp.com есть:
wget -O- https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
terraform -version
Если репозиторий недоступен, скачайте архив с бинарником с зеркала облачного провайдера и положите его в /usr/local/bin. Чтобы провайдеры качались через зеркало, создайте ~/.terraformrc:
provider_installation {
network_mirror {
url = "https://terraform-mirror.yandexcloud.net/"
include = ["registry.terraform.io/*/*"]
}
direct {
exclude = ["registry.terraform.io/*/*"]
}
}
Зеркало Yandex Cloud проксирует публичный реестр, так что подходит не только для провайдера Yandex Cloud, но и для Docker, AWS и других.
Вариант 2. OpenTofu
OpenTofu использует свой реестр registry.opentofu.org и ставится скриптом или пакетом из репозитория проекта. Если вы начинаете с нуля, это самый беспроблемный путь: команды те же, просто вместо terraform пишете tofu.
Вариант 3. Локальный кэш провайдеров
Для изолированных контуров без интернета провайдеры заранее скачивают командой terraform providers mirror /opt/tf-mirror на машине с доступом и переносят каталог. В .terraformrc вместо network_mirror указывают filesystem_mirror с этим путём.
Отдельно включите кэш плагинов, чтобы каждый проект не качал одни и те же провайдеры заново: строка plugin_cache_dir = "$HOME/.terraform.d/plugin-cache" в том же .terraformrc и созданный каталог.
Первый проект: контейнер Nginx без облака за 15 минут
Учиться на облаке неудобно: каждая ошибка стоит денег, а для старта нужна учётная запись. Поэтому первый проект сделаем на провайдере Docker — он управляет контейнерами на вашей же Linux-машине. Логика при этом ровно та же, что с виртуальными машинами в облаке. Если Docker ещё не стоит, начните с нашего вводного гайда по контейнерам.
Создайте каталог проекта и файл main.tf:
terraform {
required_version = ">= 1.6"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
provider "docker" {}
resource "docker_image" "nginx" {
name = "nginx:1.27-alpine"
keep_locally = false
}
resource "docker_container" "web" {
name = "sllug-web"
image = docker_image.nginx.image_id
ports {
internal = 80
external = 8080
}
}
Здесь три типа блоков. terraform фиксирует версии самого инструмента и провайдеров. provider настраивает подключение (для Docker — локальный сокет по умолчанию). resource описывает объект, которым управляем: первое слово — тип ресурса, второе — локальное имя в проекте. Обратите внимание на ссылку docker_image.nginx.image_id: из неё Terraform понимает, что контейнер зависит от образа, и создаст их в правильном порядке. Так строится граф зависимостей, который вручную описывать не нужно.
Как работают terraform init, plan и apply?
Весь повседневный цикл укладывается в несколько команд:
| Команда | Что делает | Когда запускать |
|---|---|---|
terraform init | Скачивает провайдеры и модули, настраивает backend, создаёт .terraform.lock.hcl | В новом проекте и после смены провайдеров, модулей или backend |
terraform fmt -recursive | Форматирует код по единому стилю | Перед каждым коммитом |
terraform validate | Проверяет синтаксис и типы без обращения к API | Перед plan, в CI |
terraform plan -out=tfplan | Показывает, что будет создано, изменено и удалено, и сохраняет план | Перед каждым изменением |
terraform apply tfplan | Применяет ровно сохранённый план | После проверки плана |
terraform destroy | Удаляет всё, что описано в проекте | Только для учебных и временных стендов |
terraform state list / state show | Показывает ресурсы в state и их атрибуты | При отладке |
Запустите по порядку:
terraform init
terraform plan -out=tfplan
terraform apply tfplan
curl -sI http://localhost:8080 | head -1
В выводе plan ищите три символа: + — ресурс будет создан, ~ — изменён на месте, -/+ — удалён и создан заново. Последний вариант самый опасный: для базы данных или диска пересоздание означает потерю данных. Строку forces replacement рядом с атрибутом нужно читать каждый раз.
Теперь поменяйте внешний порт на 8081 и снова выполните plan. Вы увидите, что контейнер будет пересоздан, потому что порт у Docker нельзя изменить на работающем контейнере. Это и есть суть Terraform: вы меняете описание, а инструмент сам решает, как к нему прийти. Учебный стенд в конце удаляется командой terraform destroy.
Почему apply лучше запускать с файлом плана, а не «голым»? Между вашим plan и apply коллега мог что-то изменить. Сохранённый план гарантирует, что применится ровно то, что вы проверили, а если состояние за это время изменилось, Terraform откажется его применять.
Переменные, locals и outputs: как убрать хардкод
Захардкоженные имена и порты мешают переиспользовать код. Вынесите их в variables.tf:
variable "env" {
type = string
description = "Окружение: dev, stage или prod"
validation {
condition = contains(["dev", "stage", "prod"], var.env)
error_message = "env должен быть dev, stage или prod."
}
}
variable "external_port" {
type = number
default = 8080
}
locals {
name = "sllug-web-${var.env}"
}
В ресурсе пишите name = local.name и external = var.external_port. Блок validation ловит опечатку ещё до обращения к API. locals — это вычисляемые значения внутри проекта, чтобы не повторять одно и то же выражение в десяти местах.
Значения переменных передают несколькими способами. Если одна переменная задана в нескольких местах, побеждает последний источник в этом списке:
- переменные окружения
TF_VAR_env=dev; - файл
terraform.tfvars; - файлы
*.auto.tfvarsв алфавитном порядке; - флаги
-varи-var-fileв порядке их указания в командной строке.
Чтобы получить результат наружу — IP-адрес машины, имя бакета, адрес балансировщика — используйте outputs.tf:
output "url" {
value = "http://localhost:${var.external_port}"
}
После apply значение выводится в конце, а terraform output -raw url отдаёт его скриптам и CI. Пароли и токены помечайте sensitive = true — тогда Terraform не напечатает их в консоль. Но помните: в state они всё равно лежат открытым текстом, об этом следующий раздел.
Что такое state и почему его нельзя хранить на ноутбуке?
Файл terraform.tfstate — это JSON-карта соответствия между ресурсами в коде и реальными объектами: «ресурс docker_container.web — это контейнер с таким-то ID». Без него Terraform не знает, что уже создано, и при следующем apply попытается создать всё заново или потеряет управление ресурсами.
У локального state три проблемы:
- Он один. Если файл лежит на ноутбуке, коллега не может работать с той же инфраструктурой, а потеря диска означает потерю управления.
- Нет блокировки. Два одновременных
applyиз разных мест испортят state, и разбирать это придётся руками. - В нём секреты. Пароли баз данных, приватные ключи и токены, которые прошли через ресурсы, записаны в state открытым текстом. Поэтому
*.tfstateникогда не коммитят в git. Подробнее о том, как такие данные протекают, — в материале про секреты в проде.
Решение — remote backend: state хранится в общем месте с блокировкой и правами доступа. Варианты, которые доступны без зарубежных облаков:
- S3-совместимое хранилище (Yandex Object Storage, VK Cloud, собственный MinIO). С Terraform 1.11 блокировка работает через lock-файл в том же бакете (
use_lockfile = true), если хранилище поддерживает условную запись; иначе нужна отдельная таблица блокировок. - PostgreSQL — встроенный backend
pgс блокировками на уровне базы. Удобно, если надёжный Postgres у вас уже есть. - GitLab — встроенное хранилище Terraform state через HTTP-backend, с блокировкой и правами по ролям проекта. Хороший выбор, если CI у вас и так в GitLab.
Пример для S3-совместимого бакета:
terraform {
backend "s3" {
bucket = "infra-state"
key = "web/prod.tfstate"
region = "ru-central1"
endpoints = { s3 = "https://storage.yandexcloud.net" }
use_lockfile = true
skip_region_validation = true
skip_credentials_validation = true
skip_requesting_account_id = true
}
}
Ключи доступа к бакету в код не пишите — передавайте через переменные окружения AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY или через профиль. На бакете включите версионирование: если state испортится, вы откатитесь на предыдущую версию за минуту. После добавления блока выполните terraform init -migrate-state, и локальный файл переедет в бакет.
Ещё два правила работы со state: не редактируйте JSON руками и не удаляйте ресурсы из него без понимания последствий. Для переименований и переносов есть штатные инструменты, о них ниже.
Модули: как не копировать один и тот же код
Модуль — это просто каталог с .tf-файлами, который вызывают из другого кода. Любой проект уже модуль (корневой). Свои модули выделяют, когда один и тот же набор ресурсов повторяется: «виртуальная машина + диск + DNS-запись + правила файрвола».
Типовая структура модуля:
modules/web-server/
├── main.tf # ресурсы
├── variables.tf # входные параметры
├── outputs.tf # что модуль отдаёт наружу
└── versions.tf # required_providers
Вызов из корневого проекта:
module "web" {
source = "./modules/web-server"
env = "prod"
external_port = 80
}
module "vpc" {
source = "git::https://git.example.ru/infra/tf-vpc.git?ref=v1.4.0"
cidr = "10.10.0.0/16"
}
Модуль из git всегда закрепляйте на тег или коммит через ?ref=, иначе очередной init подтянет чужие свежие изменения прямо в ваш прод. Признаки, что пора выделять модуль: блок ресурсов скопирован третий раз, у него есть понятные входы и выходы, и его хочется протестировать отдельно. Не делайте модуль на каждый ресурс: обёртка над одним resource только добавляет слой без пользы.
Как разделять окружения dev, stage и prod?
Есть два подхода.
Workspaces — несколько state для одного и того же кода: terraform workspace new stage, а в коде используется terraform.workspace. Это просто, но у подхода серьёзные минусы: окружения делят один backend и одни права доступа, а перепутать текущий workspace и применить изменения к проду вместо стенда очень легко.
Отдельные каталоги — рекомендуемый вариант для продакшена:
envs/
├── dev/
│ ├── main.tf # вызывает modules/web-server с env = "dev"
│ └── backend.tf # key = "web/dev.tfstate"
├── stage/
└── prod/ # отдельный бакет или отдельные права
modules/
└── web-server/
Каждое окружение — отдельный корневой проект со своим state и своими правами. Общая логика лежит в модулях, различаются только параметры. Чтобы применить что-то к проду, нужно явно зайти в envs/prod — случайно ошибиться окружением сложнее. Если каталогов становится много и в них повторяется обвязка backend, на помощь приходит Terragrunt, но начинать с него не нужно.
Terraform в CI/CD: plan в merge request, apply по кнопке
Пока Terraform запускают с ноутбуков, у каждого инженера свои версии, свои ключи и своё понимание «что сейчас в проде». Зрелая схема переносит запуск в CI:
- В merge request автоматически выполняются
fmt -check,validateиplan, а план выкладывается артефактом или комментарием к MR. - Ревьюер смотрит и дифф кода, и план — второй важнее.
- После слияния в основную ветку
applyсохранённого плана запускается вручную (кнопкой) для prod и автоматически для dev. - Ключи облака лежат в защищённых переменных CI, доступных только защищённым веткам.
Минимальный кусок .gitlab-ci.yml:
stages: [validate, plan, apply]
validate:
stage: validate
script:
- terraform init -backend=false
- terraform fmt -check -recursive
- terraform validate
plan:
stage: plan
script:
- terraform init
- terraform plan -out=tfplan
artifacts:
paths: [tfplan]
apply:
stage: apply
script:
- terraform init
- terraform apply tfplan
when: manual
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Как устроен сам GitLab CI, раннеры и переменные, мы разбирали в статье GitLab CI с нуля. Полезное дополнение — ночная проверка дрейфа: задача по расписанию запускает terraform plan -detailed-exitcode. Код выхода 0 означает, что расхождений нет, 2 — что кто-то поменял инфраструктуру в обход кода, и стоит прислать алерт.
Импорт, переименование и защита от удаления
Три механизма, о которых новички узнают обычно после первой аварии.
Импорт существующих ресурсов. Инфраструктура почти никогда не начинается с чистого листа. Чтобы взять под управление машину, созданную руками, с версии 1.5 используется блок import:
import {
to = docker_container.web
id = "c2f6a1d0e9b4"
}
Команда terraform plan -generate-config-out=generated.tf сгенерирует черновик описания ресурса. Его нужно вычистить и довести до вида, при котором plan показывает «No changes».
Переименование без пересоздания. Если переименовать ресурс в коде, Terraform решит, что старый нужно удалить, а новый создать. Чтобы этого не случилось, оставьте блок moved:
moved {
from = docker_container.web
to = module.web.docker_container.this
}
Защита критичных ресурсов. Для баз данных, дисков с данными и бакетов с бэкапами добавьте в ресурс:
lifecycle {
prevent_destroy = true
}
Теперь любой план, который удаляет такой ресурс, завершится ошибкой. Это дешёвая страховка от -/+, который проглядели на ревью.
Типичные ошибки новичков
- State в git или на ноутбуке. Секреты утекают, коллеги перезаписывают изменения друг друга. Лечение — remote backend с блокировкой с первого дня.
- Нет фиксации версий. Без
required_providersс ограничением версии и без закоммиченного.terraform.lock.hclмажорное обновление провайдера однажды сломает план. Lock-файл коммитят, каталог.terraform/— нет. - Ручные правки в консоли. «Быстро поправил правило файрвола руками» — и следующий
applyмолча откатит правку. Всё через код или хотя бы с последующим переносом в код. - Apply без чтения плана. Особенно строк
forces replacementи количества удаляемых ресурсов в итоговой строкеPlan: 3 to add, 0 to change, 2 to destroy. - Один огромный проект на всё. Сеть, базы, Kubernetes и DNS в одном state:
planидёт десять минут, а ошибка в одном месте блокирует всех. Делите по слоям и жизненному циклу. - Секреты в
.tfи.tfvars. Пароли передают через переменные окружения CI или берут из менеджера секретов источником данных, а файлы с секретами добавляют в.gitignore. - Модуль из git без версии. Сегодня работает, завтра автор модуля сделал рефакторинг — и у вас в плане пересоздание половины инфраструктуры.
Чек-лист перед первым apply в прод
- State в удалённом backend, включены блокировка и версионирование бакета.
- Версии Terraform и провайдеров зафиксированы,
.terraform.lock.hclзакоммичен. fmtиvalidateпроходят, план прочитан целиком и сохранён в файл.- В плане нет неожиданных
destroyиforces replacement. - У критичных ресурсов стоит
prevent_destroy, бэкапы данных проверены восстановлением. - Ключи доступа не в коде, у CI минимально нужные права.
- Понятно, как откатиться: предыдущий коммит, предыдущая версия state, ручные шаги для данных.
Официальная документация с полным списком команд, функций HCL и настроек backend — на developer.hashicorp.com. Для OpenTofu есть аналог на opentofu.org, отличия перечислены там же.
Частые вопросы
Что учить первым: Terraform или Ansible?
Зависит от задачи. Если серверы уже есть и их нужно настраивать, начните с Ansible. Если вы работаете с облаком, где машины, сети и DNS создаются через API, начните с Terraform. В зрелых проектах используют оба: Terraform создаёт инфраструктуру, Ansible её настраивает.
Terraform или OpenTofu — что выбрать в 2026 году?
Для внутреннего использования подходят оба, синтаксис и команды почти совпадают. OpenTofu распространяется под свободной лицензией MPL 2.0 и не зависит от доступа к реестру HashiCorp, поэтому в России с ним меньше хлопот. Terraform выбирают, если нужны функции HCP Terraform или так устроено в команде.
Можно ли использовать Terraform без облака?
Да. Есть провайдеры для Docker, libvirt и Proxmox, для DNS-серверов, PostgreSQL, GitLab, Kubernetes и десятков других систем. Для обучения удобнее всего провайдер Docker: он работает на локальной Linux-машине и ничего не стоит.
Что будет, если потерять файл terraform.tfstate?
Сами ресурсы продолжат работать, но Terraform перестанет понимать, что ими управляет. Придётся заново импортировать каждый ресурс блоком import. Поэтому state хранят в удалённом backend с включённым версионированием, а не на одном диске.
Как хранить пароли в Terraform?
Не в .tf и не в закоммиченных .tfvars. Пароли передают через переменные окружения TF_VAR_ в CI или читают из менеджера секретов через источник данных, а переменные помечают sensitive = true. Помните, что значения всё равно попадают в state, поэтому доступ к нему ограничивают.
Нужен ли Terraform для одного сервера?
Для одного сервера на VPS обычно хватит Ansible или даже Docker Compose. Terraform окупается, когда появляются несколько окружений, облачные ресурсы вроде сетей, DNS и бакетов или нужно быстро поднимать одинаковые стенды.
Сколько времени нужно, чтобы освоить Terraform?
Базовый цикл init, plan, apply, переменные и outputs осваиваются за пару вечеров. На remote backend, модули, разделение окружений и CI уходит ещё одна-две недели практики на реальном проекте. Импорт и рефакторинг существующей инфраструктуры приходят с опытом.