Представьте, что вы настраиваете облако руками: заходите в консоль 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 — это четыре команды, которые вы запускаете по очереди.

  1. terraform init — подготовка папки к работе. Скачивает указанные провайдеры (тот самый AWS-плагин), подтягивает модули и настраивает хранение state. Запускается один раз в начале и повторно каждый раз, когда поменяли провайдеры, backend или подключили либо изменили модуль. Забытый init после правки модуля — самая частая причина непонятных ошибок у новичка.
terraform init
  1. terraform plan — сухой прогон. Показывает, что именно изменится, ничего не трогая. В выводе + — создать, ~ — изменить, - — удалить. Это ваша страховка: всегда смотрите план перед применением.
terraform plan
  1. terraform apply — применяет изменения в реальном облаке. Сначала снова покажет план и спросит подтверждение — нужно ввести yes.
terraform apply
  1. 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 поднимает вычислительные мощности и управляемые базы данных.