Перейти к основному содержимому

8.04. Terraform

Разработчику Архитектору Инженеру

Terraform

Общее определение и назначение

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

Основная задача Terraform — унифицировать управление разнородными системами через единый интерфейс. Поддержка осуществляется через провайдеры — специализированные плагины, которые взаимодействуют с API конкретных платформ. Благодаря этому один и тот же рабочий процесс применим к AWS, Azure, Google Cloud, Kubernetes, базам данных, сетевому оборудованию и многим другим компонентам.

Архитектурные принципы

Декларативный стиль описания

Конфигурации Terraform написаны на языке HCL (HashiCorp Configuration Language) или в формате JSON. В них пользователь указывает желаемое состояние системы, а не последовательность команд для его достижения. Например, вместо «создай виртуальную машину, затем настрой сеть, потом подключи диск» пишется: «должна существовать виртуальная машина с такими параметрами, сетевой интерфейс с такими настройками и диск такого объёма». Terraform сам определяет порядок действий на основе зависимостей.

Пример простого описания:

resource "aws_instance" "web" {
ami = "ami-0c02fb55956c7d316"
instance_type = "t3.micro"
}

Этот блок говорит: «в облаке AWS должна существовать EC2-инстанция с указанным AMI и типом». Terraform проверит текущее состояние, сравнит его с описанием и при необходимости создаст, изменит или удалит ресурс.

Иммутабельность и идемпотентность

Terraform стремится к идемпотентности: повторное применение одной и той же конфигурации не приводит к побочным эффектам. Если ресурс уже соответствует описанию, он остаётся без изменений. Если параметры отличаются, Terraform заменяет ресурс или обновляет его, в зависимости от возможностей провайдера.

Многие ресурсы в облачных средах являются иммутабельными: их нельзя изменить после создания. В таких случаях Terraform создаёт новый экземпляр с новыми параметрами и удаляет старый. Это снижает риск частичных обновлений и несогласованности.

Граф зависимостей

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

Пример явной зависимости:

resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
}

Здесь подсеть зависит от VPC, потому что использует её идентификатор. Terraform сначала создаст VPC, дождётся его появления, получит id, а затем создаст подсеть. Такой порядок гарантирует корректность развёртывания.

Состояние (State)

Назначение состояния

Файл состояния (обычно terraform.tfstate) хранит метаданные о текущем состоянии управляемых ресурсов. Он содержит идентификаторы, атрибуты и связи между ресурсами в реальной инфраструктуре. Этот файл позволяет Terraform понимать, какие ресурсы уже созданы, и сравнивать их с описанием в конфигурации.

Без состояния Terraform не смог бы отличить отсутствие ресурса от его наличия. Например, если в конфигурации описан бакет S3, но в облаке его нет, Terraform создаст его. Если бакет уже существует и совпадает с описанием — ничего не произойдёт. Состояние делает этот процесс возможным.

Локальное и удалённое хранение

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

Для решения этой проблемы Terraform поддерживает удалённое хранение состояния через бэкенды. Наиболее распространённые варианты — Amazon S3, Azure Blob Storage, Google Cloud Storage, HashiCorp Consul. Удалённое состояние автоматически блокируется во время применения, предотвращая параллельные изменения.

Пример настройки бэкенда для S3:

terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "prod/web/terraform.tfstate"
region = "us-west-2"
}
}

Такая конфигурация перенаправляет все операции со состоянием в указанный бакет. Terraform будет использовать его как единственный источник правды о текущей инфраструктуре.

Чувствительность состояния

Файл состояния может содержать конфиденциальные данные: пароли, ключи доступа, IP-адреса. По этой причине его нельзя хранить в открытых репозиториях. Terraform предоставляет механизмы для шифрования состояния (например, через AWS KMS при использовании S3) и исключения чувствительных полей из вывода (sensitive = true в переменных).

Провайдеры

Роль провайдеров

Провайдер — это плагин, который реализует взаимодействие с API конкретной платформы. Terraform не содержит встроенного кода для работы с AWS или Kubernetes. Вместо этого он загружает нужные провайдеры динамически и вызывает их функции для создания, чтения, обновления и удаления ресурсов (операции CRUD).

Каждый провайдер определяет набор ресурсов и источников данных (data sources). Ресурсы управляются Terraform, источники данных только читаются. Например, можно запросить информацию о существующем образе ОС без попытки его изменения.

Настройка и версионирование

Провайдеры требуют конфигурации: учётные данные, регион, endpoint и другие параметры. Эта информация передаётся в блоке provider.

Пример:

provider "aws" {
region = "eu-central-1"
profile = "production"
}

Важно фиксировать версии провайдеров. Новые версии могут вносить несовместимые изменения. Terraform позволяет указать допустимый диапазон версий:

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}

Это гарантирует, что все участники проекта используют одинаковую версию провайдера, избегая расхождений в поведении.

Модули

Композиция и переиспользование

Модуль — это пакет конфигураций Terraform, который можно использовать как единый блок. Любой каталог с .tf-файлами является модулем, но термин обычно применяют к переиспользуемым компонентам. Модули позволяют инкапсулировать сложные паттерны: например, «полностью настроенная веб-инфраструктура с балансировщиком, группой инстансов и мониторингом».

Модуль принимает входные переменные и возвращает выходные значения. Это делает его похожим на функцию в программировании.

Пример вызова модуля:

module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"

name = "my-vpc"
cidr = "10.0.0.0/16"
azs = ["eu-central-1a", "eu-central-1b"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
}

Этот вызов использует официальный модуль от сообщества AWS, который создаёт полноценную VPC с подсетями, таблицами маршрутизации, шлюзами и другими компонентами. Без модуля пришлось бы писать десятки блоков вручную.

Иерархия и организация

Проекты на Terraform часто организуются в виде дерева модулей. Корневой модуль вызывает дочерние, те — свои, и так далее. Такая структура упрощает поддержку: изменения в одном компоненте не затрагивают другие, если интерфейс остаётся неизменным.

Хорошая практика — выделять модули по доменным границам: сеть, вычисления, хранилище, безопасность. Это соответствует принципам разделения ответственности и упрощает тестирование.

Жизненный цикл управления

Инициализация

Команда terraform init подготавливает рабочий каталог. Она загружает указанные провайдеры, инициализирует бэкенд состояния и устанавливает зависимости модулей. Эта команда безопасна для повторного запуска и должна выполняться при каждом клонировании репозитория или смене окружения.

Планирование

Команда terraform plan анализирует конфигурацию и текущее состояние, строит план изменений и выводит его в человекочитаемом виде. План показывает, какие ресурсы будут созданы, изменены или удалены, и почему. Это ключевой этап для проверки перед применением.

План можно сохранить в файл и передать на утверждение. Это особенно полезно в CI/CD-конвейерах, где человек должен подтвердить изменения до их выполнения.

Применение

Команда terraform apply выполняет ранее сгенерированный план. Она отправляет запросы к API провайдеров, дожидается завершения операций и обновляет файл состояния. Все действия атомарны: либо весь план применяется успешно, либо возвращается ошибка, и состояние остаётся прежним.

Если во время применения происходит сбой (например, недоступен API), Terraform помечает ресурс как «зависший» и предлагает восстановить состояние вручную. Это редкая, но важная ситуация, требующая внимания оператора.

Уничтожение

Команда terraform destroy удаляет все управляемые ресурсы. Она строит план, аналогичный plan, но с единственной целью — полное уничтожение. Эта команда используется при завершении проекта или очистке тестовых окружений.

Уничтожение следует выполнять осторожно: оно необратимо. Для защиты от случайного удаления можно использовать механизм prevent_destroy в настройках ресурса.

Переменные и выходные значения

Гибкость конфигураций

Переменные позволяют параметризовать конфигурации. Вместо жёстко заданных значений используются символические имена, которым присваиваются конкретные данные при запуске.

Объявление переменной:

variable "instance_count" {
description = "Number of EC2 instances to launch"
type = number
default = 2
}

Использование:

resource "aws_instance" "app" {
count = var.instance_count
# ...
}

Такой подход делает конфигурацию универсальной: одну и ту же структуру можно использовать в dev, staging и prod, меняя только значения переменных.

Типизация и валидация

Terraform поддерживает строгую типизацию переменных: строки, числа, булевы значения, списки, карты, объекты, кортежи. Можно задавать пользовательские типы и правила валидации.

Пример валидации:

variable "region" {
type = string
validation {
condition = contains(["us-east-1", "eu-west-1", "ap-southeast-1"], var.region)
error_message = "Region must be one of the approved locations."
}
}

Это предотвращает ошибки на раннем этапе и обеспечивает соответствие политике безопасности.

Выходные значения

Выходные значения экспортируют информацию из модуля или корневой конфигурации. Они полезны для передачи данных между модулями или для отображения итоговых параметров после применения.

Пример:

output "vpc_id" {
value = aws_vpc.main.id
}

output "public_subnet_ids" {
value = aws_subnet.public[*].id
}

Эти значения появятся в выводе apply и могут использоваться внешними системами, например, для настройки CI/CD-пайплайнов.


Безопасность и управление доступом

Учётные данные и секреты

Terraform требует учётных данных для взаимодействия с API провайдеров. Эти данные могут включать ключи доступа, токены, сертификаты и другие формы аутентификации. Хранение таких данных в открытом виде в конфигурационных файлах недопустимо. Terraform предоставляет несколько механизмов для безопасной передачи секретов:

  • Переменные окружения: большинство провайдеров автоматически читают значения из переменных вроде AWS_ACCESS_KEY_ID, AZURE_CLIENT_SECRET и подобных.
  • Внешние источники: можно использовать специализированные инструменты — HashiCorp Vault, AWS Secrets Manager, Azure Key Vault — и запрашивать секреты через data-блоки или внешние модули.
  • Локальные файлы с исключением из контроля версий: например, secrets.auto.tfvars добавляется в .gitignore.

Пример безопасного использования:

provider "aws" {
region = "us-east-1"
# credentials не указаны явно — будут взяты из ~/.aws/credentials или ENV
}

Явное указание секретов в коде считается грубой ошибкой, даже если репозиторий приватный.

Принцип минимальных привилегий

Учётные данные, используемые Terraform, должны обладать только теми правами, которые необходимы для выполнения задач. Например, если конфигурация создаёт только EC2-инстансы и VPC, то IAM-роль не должна иметь разрешений на удаление S3-бакетов или изменение IAM-политик. Это снижает потенциальный ущерб при компрометации состояния или плана.

Провайдеры часто предоставляют возможность ограничить действия через политики. В AWS это делается через IAM; в Azure — через RBAC и управляемые идентификаторы. Terraform сам по себе не проверяет полномочия — он лишь передаёт запросы, поэтому ответственность за безопасность лежит на администраторе.

Защита состояния

Файл состояния может содержать чувствительные данные: IP-адреса, DNS-имена, пароли баз данных (если они не помечены как sensitive). Чтобы минимизировать риски:

  • Используется шифрование на уровне бэкенда (например, AWS KMS для S3).
  • Включается опция encrypt = true в конфигурации бэкенда.
  • Все чувствительные выходные значения помечаются как sensitive = true, что скрывает их из вывода apply и plan.

Пример:

output "db_password" {
value = aws_db_instance.main.password
sensitive = true
}

Это не предотвращает сохранение значения в состоянии, но защищает от случайного раскрытия в логах.

Расширенные возможности языка HCL

Локальные значения

Локальные значения (locals) позволяют вычислять производные данные внутри модуля без необходимости объявлять их как переменные или выходы. Это упрощает чтение и поддержку сложных выражений.

Пример:

locals {
common_tags = {
Environment = var.env
Owner = "dev-team"
ManagedBy = "terraform"
}
}

resource "aws_instance" "web" {
tags = merge(local.common_tags, { Name = "web-server" })
}

Такой подход централизует общую логику и избегает дублирования.

Условные выражения и циклы

HCL поддерживает условную логику через тернарный оператор и функции вроде can(), try(). Циклы реализуются через мета-аргумент for_each и count.

Пример с for_each:

variable "subnets" {
type = map(string)
default = {
public_a = "10.0.1.0/24"
public_b = "10.0.2.0/24"
}
}

resource "aws_subnet" "public" {
for_each = var.subnets

vpc_id = aws_vpc.main.id
cidr_block = each.value
availability_zone = each.key
}

Этот подход масштабируется: добавление нового элемента в subnets автоматически создаст новую подсеть без изменения структуры кода.

Функции и выражения

HCL включает богатую библиотеку встроенных функций: работа со строками (lower, replace), списками (concat, distinct), картами (lookup, merge), преобразование типов (jsonencode, yamldecode) и другие. Это позволяет строить динамические конфигурации без внешних скриптов.

Пример:

locals {
instance_names = [for i in range(var.instance_count) : "app-${i}"]
}

Такие конструкции делают код гибким и адаптивным к изменяющимся требованиям.

Интеграция с CI/CD и автоматизация

Рабочие процессы

Terraform хорошо встраивается в современные CI/CD-системы: GitHub Actions, GitLab CI, Jenkins, CircleCI. Типичный пайплайн включает этапы:

  1. Инициализация (terraform init) — загрузка провайдеров и модулей.
  2. Валидация (terraform validate) — проверка синтаксиса и структуры.
  3. Форматирование (terraform fmt -check) — соблюдение единого стиля.
  4. Планирование (terraform plan -out=tfplan) — генерация плана без применения.
  5. Утверждение — человек или система одобряет план.
  6. Применение (terraform apply tfplan) — выполнение изменений.

Такой подход обеспечивает контроль, аудит и воспроизводимость.

Блокировки и совместная работа

При использовании удалённого состояния Terraform автоматически блокирует файл состояния во время apply. Это предотвращает одновременные изменения от нескольких пользователей или пайплайнов. Если блокировка не поддерживается бэкендом (например, при использовании локального файла), возможны конфликты и повреждение состояния.

Некоторые бэкенды, такие как Consul или DynamoDB, обеспечивают надёжную блокировку через атомарные операции. Это критически важно в production-средах.

Мониторинг и уведомления

Результаты plan и apply можно направлять в системы мониторинга: Slack, Microsoft Teams, Email, Prometheus. Это позволяет команде отслеживать изменения в реальном времени. Например, после каждого apply в канал DevOps отправляется сообщение с перечнем изменённых ресурсов и ссылкой на коммит.

Версионирование и эволюция инфраструктуры

Управление версиями модулей

Модули могут быть опубликованы в реестрах (Terraform Registry, внутренние Git-репозитории) и использоваться с указанием конкретной версии. Это позволяет:

  • Изолировать изменения: обновление модуля в одном проекте не затрагивает другие.
  • Проводить тестирование: новая версия проверяется в staging перед переходом в prod.
  • Соблюдать семантическое версионирование: ~> 2.1 означает «любая версия >= 2.1.0, но < 3.0.0».

Пример:

module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "20.0.0"
# ...
}

Это гарантирует стабильность и предсказуемость.

Миграция и рефакторинг

Со временем инфраструктура меняется: появляются новые требования, устаревают старые компоненты. Terraform поддерживает миграцию через:

  • moved-блоки: позволяют переименовать или переместить ресурс без его уничтожения.
  • Импорт существующих ресурсов: команда terraform import связывает уже существующий ресурс с конфигурацией.
  • Ручное редактирование состояния: через terraform state mv или terraform state rm — с осторожностью.

Пример moved:

moved {
from = aws_instance.old_name
to = aws_instance.new_name
}

После этого plan покажет, что ресурс не будет удалён и создан заново, а просто переименован в состоянии.

Поддержка мультиоблачных и гибридных сред

Terraform одинаково эффективно управляет ресурсами в нескольких облаках одновременно. Один и тот же рабочий процесс применяется к AWS, Azure, GCP, Yandex Cloud, OpenStack и другим платформам. Это особенно ценно для организаций, использующих стратегию multi-cloud или hybrid-cloud.

Пример:

provider "aws" {
region = "us-west-2"
}

provider "azurerm" {
features {}
}

resource "aws_s3_bucket" "logs" {
bucket = "my-app-logs"
}

resource "azurerm_storage_account" "main" {
name = "myappstorage"
location = "West Europe"
resource_group_name = "rg-prod"
account_tier = "Standard"
account_replication_type = "LRS"
}

Такая конфигурация создаёт ресурсы в двух разных облаках, но управляется единым инструментом. Это упрощает обучение, стандартизацию и аудит.


Экосистема и сообщество

Официальные и сторонние модули

HashiCorp поддерживает набор официальных провайдеров и модулей, прошедших внутреннюю проверку. Они доступны в Terraform Registry — централизованном каталоге, где можно найти готовые решения для AWS, Azure, Google Cloud, Kubernetes и других платформ. Эти модули часто соответствуют лучшим практикам: используют версионирование, имеют документацию, примеры и тесты.

Сообщество также создаёт тысячи сторонних модулей. Они могут быть полезны для нишевых задач или новых технологий, но требуют осторожности: не все из них поддерживаются регулярно, некоторые содержат уязвимости или устаревшие подходы. Перед использованием рекомендуется проверить:

  • Дату последнего обновления
  • Количество звёзд и форков на GitHub
  • Наличие issue-трекера и активность автора
  • Соответствие текущей версии Terraform

Пример вызова популярного модуля:

module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"

name = "production-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
}

Такой модуль заменяет десятки строк ручной конфигурации и гарантирует соответствие рекомендациям AWS.

Инструменты расширения

Вокруг Terraform сложилась богатая экосистема инструментов:

  • Terragrunt — надстройка для управления повторяющимися конфигурациями через DRY-принцип (Don’t Repeat Yourself). Позволяет выносить общие настройки вверх по иерархии каталогов.
  • TFLint — линтер, проверяющий стиль, безопасность и соответствие политике.
  • TFSec — сканер уязвимостей в конфигурациях (например, открытые порты, отсутствие шифрования).
  • Checkov — инструмент от Bridgecrew для проверки соответствия стандартам безопасности и compliance (CIS, PCI-DSS, HIPAA).
  • Atlantis — система автоматизации plan и apply через pull request в Git.

Эти инструменты интегрируются в CI/CD и повышают надёжность развёртываний.

Ограничения и сценарии, неподходящие для Terraform

Императивные задачи

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

Terraform управляет состоянием, а не процессами. Он не следит за работоспособностью ресурсов после их создания. Для этого нужны дополнительные инструменты: Prometheus, Datadog, CloudWatch.

Частые изменения состояния

Если инфраструктура меняется каждые несколько минут (например, в средах с высокой динамикой, таких как serverless-функции с переменной нагрузкой), использование Terraform может привести к конфликтам состояний и задержкам. В таких случаях лучше применять облачные нативные решения: AWS SAM, Azure Bicep, Google Cloud Deployment Manager.

Отсутствие обратной связи

Terraform не получает информацию о том, что произошло с ресурсом после его создания. Если кто-то вручную изменит параметры EC2-инстанса в консоли AWS, Terraform не узнает об этом до следующего plan. При следующем применении он вернёт ресурс к описанному состоянию, что может нарушить работу системы. Это называется «дрейфом конфигурации» (configuration drift).

Для борьбы с дрейфом используются:

  • Запрет ручных изменений через IAM-политики
  • Регулярные plan в CI/CD
  • Инструменты вроде Driftctl, которые сравнивают состояние в облаке и в Terraform

Сравнение с другими инструментами управления инфраструктурой

Ansible

Ansible — императивный инструмент, ориентированный на конфигурацию серверов и приложений. Он выполняет команды по порядку: «установи пакет, запусти службу, скопируй файл». Terraform декларативен и управляет самими ресурсами: «должен существовать сервер с этим ПО».

Ansible хорошо работает внутри уже созданных машин, Terraform — на уровне их создания. Часто они используются вместе: Terraform поднимает инфраструктуру, Ansible настраивает софт.

CloudFormation / ARM / Bicep

Эти инструменты являются нативными для конкретных облаков: AWS CloudFormation, Azure Resource Manager (ARM), Google Deployment Manager. Они тесно интегрированы с API и поддерживают все новые функции сразу. Однако они не переносимы между облаками.

Terraform обеспечивает унификацию: один язык, один рабочий процесс для всех платформ. Это особенно ценно в multi-cloud-средах.

Pulumi

Pulumi использует настоящие языки программирования (Python, TypeScript, Go) вместо HCL. Это даёт больше гибкости: можно писать циклы, классы, использовать библиотеки. Однако это усложняет чтение и аудит: код становится менее декларативным и более процедурным.

Terraform остаётся проще для команд, где не все участники — разработчики. HCL легко читается даже системными администраторами без опыта программирования.

Поддержка и развитие

Terraform активно развивается компанией HashiCorp. Новые версии выходят регулярно и включают:

  • Поддержку новых API облачных провайдеров
  • Улучшения производительности графа зависимостей
  • Расширение возможностей языка HCL
  • Усиление безопасности (например, встроенный анализ чувствительных данных)

Проект имеет открытый исходный код (Mozilla Public License 2.0), что позволяет сообществу вносить вклад, проверять код и форкать при необходимости. В 2023 году HashiCorp изменила лицензию на Business Source License (BSL), но основной функционал остаётся свободным для большинства пользователей.

Развитие движется в сторону:

  • Более глубокой интеграции с CI/CD
  • Улучшения работы с большими конфигурациями (модульные зависимости, частичное применение)
  • Поддержки policy-as-code через Sentinel (в Enterprise-версии) или Open Policy Agent (в open-source)

Эти направления делают Terraform всё более зрелым инструментом для enterprise-сред.