Представьте, что вы настраиваете облако руками: заходите в консоль AWS, кликаете «создать сервер», «создать хранилище», «открыть порт». Работает — пока серверов мало. Когда их десятки, а окружений несколько (тестовое, боевое), руками это уже не повторить одинаково: где-то забыли галочку, где-то регион не тот. Terraform решает эту боль: вы описываете нужную инфраструктуру текстом в файле, а Terraform сам создаёт её в облаке — одинаково, сколько угодно раз.
Terraform — это инструмент инфраструктуры как кода (infrastructure as code): вместо кликов вы пишете конфигурацию, кладёте её в git рядом с приложением и применяете командой. Его главная фишка — он работает почти с любым облаком (AWS, Google Cloud, Azure) и сотнями других систем через единый язык. Если вы только знакомитесь с тем, зачем всё это нужно, начните со статьи инфраструктура как код: основы. Здесь — про сам Terraform, с примерами на AWS.
Что такое Terraform и HCL
Terraform читает файлы с расширением .tf, написанные на языке HCL (HashiCorp Configuration Language) — это собственный язык конфигурации Terraform. HCL устроен просто: вся конфигурация состоит из блоков. Блок — это тип, опциональные метки в кавычках и тело в фигурных скобках.
тип_блока "метка1" "метка2" {
аргумент = значение
}
Главное, что нужно понять новичку: HCL — декларативный. Вы описываете не «как делать» (создай, потом измени, потом удали), а «что должно быть в итоге». Terraform сам вычисляет разницу между тем, что описано, и тем, что уже есть в облаке, и делает только нужные шаги. Это как список покупок: вы пишете, что должно лежать в холодильнике, а не маршрут по магазину.
Любой проект на Terraform обычно начинается с блока terraform {}, где указано, какие провайдеры и какая версия нужны:
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
Версию провайдера всегда указывайте явно: без неё Terraform возьмёт свежайшую, и однажды утром сборка сломается на чужом обновлении. Какая версия сейчас актуальна, смотрят на странице провайдера в Terraform Registry — мажорные версии там меняются раз в год-два (пятая вышла в 2023-м, шестая — в 2025-м), так что цифру из любой статьи стоит перепроверить.
Провайдеры
Сам по себе Terraform не знает, что такое «сервер AWS» или «бакет S3». За это отвечают провайдеры — плагины, которые переводят ваши блоки в вызовы API конкретного облака. Провайдер AWS умеет работать с AWS, провайдер Google Cloud — с Google, и так далее. Аналогия: Terraform — переводчик, а провайдер — словарь конкретного облака: без словаря переводчик понимает ваш текст, но сказать облаку ничего не может.
Провайдер настраивается отдельным блоком provider. Для AWS чаще всего достаточно указать регион:
provider "aws" {
region = "us-east-1"
}
Один и тот же провайдер можно подключить несколько раз — например, чтобы создавать ресурсы в двух регионах. Для этого второму экземпляру дают псевдоним через alias:
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "west"
region = "us-west-2"
}
Какие именно ресурсы AWS бывают и зачем они — это отдельная большая тема, начните с обзора основы AWS.
Ресурсы и зависимости
Ресурс — это конкретный объект инфраструктуры: бакет хранилища, виртуальный сервер, таблица базы данных. Описывается блоком resource с двумя метками: тип ресурса и ваше внутреннее имя (по нему вы будете ссылаться на ресурс внутри конфигурации).
resource "aws_s3_bucket" "my_bucket" {
bucket = "my-company-app-data-2026"
tags = {
Environment = "production"
Team = "backend"
}
}
Здесь aws_s3_bucket — тип (бакет в объектном хранилище S3), my_bucket — имя для ссылок внутри кода, bucket — реальное имя бакета в AWS.
Часто один ресурс зависит от другого: например, политику доступа нельзя создать, пока нет самого бакета. Terraform определяет такие зависимости автоматически — по ссылкам. Когда вы упоминаете один ресурс внутри другого в формате тип.имя.атрибут, Terraform понимает, что сначала надо создать первый:
resource "aws_s3_bucket" "my_bucket" {
bucket = "my-company-app-data-2026"
}
resource "aws_s3_bucket_versioning" "versioning" {
bucket = aws_s3_bucket.my_bucket.id
versioning_configuration {
status = "Enabled"
}
}
Ссылка aws_s3_bucket.my_bucket.id означает «возьми id бакета my_bucket» — и заодно говорит Terraform: бакет создавай первым.
Но иногда связь есть, а ссылки нет, и тогда Terraform о зависимости не догадывается. Живой пример: функция, которой нужно право писать логи. В коде она ссылается на роль, а не на привязку политики к этой роли, — для Terraform это два независимых ресурса, и он спокойно создаст функцию раньше, чем у неё появятся права. Такую скрытую связь указывают руками через depends_on:
resource "aws_lambda_function" "handler" {
function_name = "order-handler"
role = aws_iam_role.lambda.arn
filename = "handler.zip"
handler = "index.handler"
runtime = "nodejs20.x"
depends_on = [aws_iam_role_policy_attachment.lambda_logs]
}
Важно не увлекаться: depends_on нужен только там, где ссылки действительно нет. Если один ресурс упоминает другой — порядок Terraform построит сам, и дописывать зависимость руками не надо.
Цикл init, plan, apply, destroy
Работа с Terraform — это четыре команды, которые вы запускаете по очереди.
terraform init— подготовка папки к работе. Скачивает указанные провайдеры (тот самый AWS-плагин), подтягивает модули и настраивает хранение state. Запускается один раз в начале и повторно каждый раз, когда поменяли провайдеры, backend или подключили либо изменили модуль. Забытыйinitпосле правки модуля — самая частая причина непонятных ошибок у новичка.
terraform init
terraform plan— сухой прогон. Показывает, что именно изменится, ничего не трогая. В выводе+— создать,~— изменить,-— удалить. Это ваша страховка: всегда смотрите план перед применением.
terraform plan
terraform apply— применяет изменения в реальном облаке. Сначала снова покажет план и спросит подтверждение — нужно ввестиyes.
terraform apply
terraform destroy— удаляет всю инфраструктуру, которой управляет эта конфигурация. Полезно для временных тестовых окружений, чтобы не платить за неиспользуемые ресурсы. Тоже спросит подтверждение.
terraform destroy
Запомнить просто: init — подготовиться, plan — посмотреть, apply — сделать, destroy — убрать.
State и где он живёт
Чтобы понимать разницу между «что описано» и «что уже создано», Terraform ведёт state (состояние) — файл, где записано, какие реальные ресурсы соответствуют вашим блокам. По умолчанию это локальный файл terraform.tfstate в папке проекта.
Локальный файл годится, пока вы один. Как только над инфраструктурой работает команда, локальный state становится проблемой: у каждого своя копия, они расходятся, а если двое запустят apply одновременно — state может повредиться. Решение — remote backend: state хранится в общем месте, а не на чьём-то ноутбуке.
Классический вариант на AWS — backend s3: сам state лежит в бакете S3, а DynamoDB используется для блокировки (lock), чтобы два человека не меняли state одновременно.
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "my-app/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
Здесь bucket и key — где лежит файл состояния, dynamodb_table — таблица для блокировки (у неё должен быть ключ LockID типа String), encrypt — шифровать state в покое.
Начиная с Terraform 1.10 появилась нативная блокировка прямо в S3 без DynamoDB — флаг use_lockfile = true; DynamoDB-вариант со временем уберут, но в существующих проектах он встречается повсеместно, поэтому знать его стоит.
Если берёте use_lockfile, не забудьте поднять и required_version в блоке terraform {}: с объявленным >= 1.0 конфигурация может попасть на версию, которая про этот флаг ещё не знает, и упадёт на init.
State часто содержит чувствительные данные (например, пароли), поэтому общий бакет должен быть закрыт правами — про разграничение доступа смотрите IAM в AWS.
Модули
Когда конфигурация растёт, копировать одни и те же блоки в разные окружения утомительно и опасно. Модуль — это переиспользуемый набор .tf-файлов, как функция в обычном коде: один раз описали, дальше вызываете с разными параметрами. Любая папка с .tf-файлами уже является модулем; вызывают её блоком module с обязательным аргументом source — откуда брать код.
module "app_storage" {
source = "./modules/storage"
bucket_name = "my-company-app-data-2026"
environment = "production"
}
Откуда модуль знает про bucket_name и environment? Он объявляет их сам — блоками variable в своих файлах, а ресурсы внутри обращаются к ним как var.bucket_name. Передать модулю аргумент, которого он не объявил, нельзя: Terraform остановится с ошибкой «An argument named "bucket_name" is not expected here».
variable "bucket_name" {
type = string
}
variable "environment" {
type = string
default = "dev"
}
resource "aws_s3_bucket" "this" {
bucket = var.bucket_name
tags = {
Environment = var.environment
}
}
Обратно наружу модуль отдаёт значения блоками output — например, идентификатор созданного бакета, чтобы на него можно было сослаться из вызывающего кода как module.app_storage.bucket_id:
output "bucket_id" {
value = aws_s3_bucket.this.id
}
source указывает путь: локальную папку (./modules/storage), git-репозиторий или публичный реестр Terraform Registry, где лежат тысячи готовых модулей. Так из тестового и боевого окружения вы вызываете один модуль с разными значениями — и инфраструктура гарантированно одинаковой формы.
module "test_storage" {
source = "./modules/storage"
bucket_name = "my-app-test"
environment = "test"
}
module "prod_storage" {
source = "./modules/storage"
bucket_name = "my-app-prod"
environment = "production"
}
Где это применяется
Terraform встречается почти везде, где есть облако: команды держат описание инфраструктуры в git и применяют его автоматически из конвейера сборки. Если вы настраиваете CI/CD, шаги terraform plan и terraform apply обычно встроены в пайплайн — об устройстве конвейеров читайте принципы CI/CD-пайплайна, а про выкатку — стратегии релизов. Часто Terraform поднимает кластер Kubernetes, а уже внутрь кластера приложения деплоят и конфигурируют другими инструментами.
Типичные ошибки новичков:
- Применять без
plan. Всегда сначала смотрите план —applyменяет реальное облако и стоит денег. - Не хранить state удалённо в команде. Локальный
terraform.tfstateу каждого свой — окружения разъедутся. Переходите на remote backend сразу, как только над проектом больше одного человека. - Править ресурсы руками в консоли облака. Тогда реальность расходится со state (это называют дрейфом), и следующий
applyможет всё сломать. Меняйте инфраструктуру только через Terraform. - Класть state в git. В нём бывают секреты, а блокировки git не даёт. State — в S3 с шифрованием, не в репозитории.
Ещё одно имя, которое стоит знать заранее, — OpenTofu. В августе 2023 года HashiCorp сменила лицензию Terraform с открытой на BUSL, и сообщество отделило форк, который остался полностью открытым и живёт теперь под крылом Linux Foundation. По языку и командам он совместим: tofu init, tofu plan, tofu apply — всё, что вы прочитали выше, работает и там. Часть компаний перешла на OpenTofu именно из-за лицензии, поэтому встретить его в чужом проекте — обычное дело, и пугаться незнакомого слова не нужно.
Что учить дальше: Terraform — не единственный инструмент инфраструктуры как кода. У AWS есть свой родной CloudFormation и более «программистский» AWS CDK. Полезно понимать управление состоянием и доставку в целом, а также сравнить подходы в обзоре инфраструктура как код: основы. Когда освоитесь с базовыми ресурсами, посмотрите, как Terraform поднимает вычислительные мощности и управляемые базы данных.