Terraform — инфраструктура как код

Продолжительность: 18 уроков · Уровень: средний

Terraform — это инструмент Infrastructure as Code (IaC): он описывает облачную инфраструктуру (виртуальные машины, сети, базы данных) в виде кода и приводит реальное состояние к описанному. Одна команда apply — и вся инфраструктура создана.

1. Что такое IaC и зачем это нужно

2. Установка

# Linux (apt)
sudo apt-get install -y gnupg software-properties-common
wget -O- https://apt.releases.hashicorp.com/gpg | sudo 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" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform

terraform version
Для Windows скачай terraform.exe с официального сайта и добавь папку в PATH.

3. Основы языка HCL

Конфигурации пишутся на HCL (HashiCorp Configuration Language):

# main.tf
provider "aws" {
  region = "eu-west-1"
}

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

  tags = {
    Name = "web-server"
  }
}

Базовые конструкции

КонструкцияНазначение
providerоблако/сервис: aws, azurerm, google, docker
resourceсоздаваемый объект
dataчтение существующего ресурса
variableвходной параметр
outputвозвращаемое значение
moduleпереиспользуемый блок конфигурации

4. Провайдеры

Провайдер — плагин, который умеет общаться с конкретным облаком. Обязательно указывается версия:

terraform {
  required_version = ">= 1.0"

  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

5. Жизненный цикл: init / plan / apply / destroy

terraform init       # скачать провайдеров и модули
terraform plan       # показать план изменений
terraform apply      # применить (после подтверждения)
terraform destroy    # удалить всю инфраструктуру
terraform fmt        # отформатировать конфигурацию
terraform validate   # проверить синтаксис
Всегда смотри вывод terraform plan ДО ввода yes. Он показывает, что именно будет создано/изменено/удалено — и что из них разрушено безвозвратно.

6. Переменные и output

Переменные определены в variables.tf:

variable "env" {
  type        = string
  default     = "dev"
  description = "Окружение"
}

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

Значения задаются файлом terraform.tfvars или через -var:

env       = "prod"
instances = 4
terraform apply -var "env=prod" -var-file=prod.tfvars

Вывод результатов наружу:

output "public_ip" {
  value = aws_instance.web.public_ip
}

7. State — файл состояния

Terraform хранит реальное состояние инфраструктуры в файле terraform.tfstate. Он связывает описанные ресурсы с созданными.

8. Remote backend

terraform {
  backend "s3" {
    bucket = "my-tf-state"
    key    = "prod/terraform.tfstate"
    region = "eu-west-1"
  }
}

Перед переключением старого state на новый backend выполняется миграция:

terraform init -migrate-state
Remote backend позволяет блокировать конфигурацию: два разработчика не применят изменения одновременно.

9. Модули

Модуль — переиспользуемый набор ресурсов. Свои модули лежат в папках, чужие — в реестре:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"
  cidr    = "10.0.0.0/16"
}
terraform init    # подтянет модуль из реестра

10. Workspaces

Workspace — независимое окружение с собственным state в рамках одной конфигурации:

terraform workspace new dev
terraform workspace new prod
terraform workspace select prod
terraform apply

11. Практические советы

12. Terraform в CI/CD

Инфраструктуру меняют не руками, а через пайплайн: plan в pull request, apply после ревью. Это защищает от «два админа, одна кнопка» и оставляет историю изменений.

# .github/workflows/terraform.yml (фрагмент)
jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Plan
        run: |
          terraform init
          terraform plan -out=plan.out
      - name: Apply
        if: github.ref == 'refs/heads/main'
        run: terraform apply -auto-approve plan.out

Секреты провайдера передаются переменными окружения из секретов CI, а сам план сохраняется как артефакт для аудита.

В пайплайне важно не применять план вслепую: разрешайте apply только для ветки main после ревью, а для временных веток оставляйте один plan с выгрузкой артефакта. Так каждое изменение инфраструктуры становится записью в истории, а откат — понятным шагом назад.

Готовый инструмент для этой механики — Terraform Cloud: план виден по ссылке, apply запускается кнопкой, а state хранится в управляемом remote backend.

13. Решение конфликтов и проблем

Проблемы Terraform делятся на две группы: блокировка state и рассинхрон с реальной инфраструктурой (drift). Разберём типичные случаи.

terraform import aws_s3_bucket.logs my-log-bucket
terraform state list          # что под контролем
terraform state rm my-service # вывести ресурс из-под управления

Частый сценарий — ручная правка ресурса в облаке: следующий terraform plan покажет расхождение. Решений три: принять фактическое состояние, вернуть конфигурацию обратным apply или перенести правку в код. Любой выбор фиксируется в истории проекта.

Ручная правка .tfstate запрещена: потеряете связь конфигурации с ресурсами. Любые операции состояния — только через terraform state ....

14. Секреты и интеграция с Vault

Пароли и ключи в переменных окружения — стартовый, но слабый подход. HashiCorp Vault хранит секреты централизованно и выдаёт их с динамическим TTL.

# Провайдер vault в конфигурации
provider "vault" {
  address = "https://vault.example.com"
}

data "vault_kv_secret_v2" "db" {
  mount = "secret"
  name  = "myapp/db"
}

resource "aws_instance" "web" {
  user_data = <<-EOF
    export DB_PASSWORD="${data.vault_kv_secret_v2.db.data["password"]}"
  EOF
}

Сам Vault для Terraform использует токен CI с ограниченной политикой. Подробнее — в курсе Vault; управление секретов также показано в Ansible.

Никогда не записывай секрет в plan.out или лог пайплайна: экранируй вывод и не выводи переменные, содержащие пароли.
Вместо дёргания Vault на каждый apply используйте Terraform Cloud vars или CI-secrets для bootstrap и Vault — для долгоживущих секретов приложений.

15. Качество кода: tflint, tfsec, terraform-docs

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

tflint          # неверные типы, лишние аргументы провайдера
tfsec           # уязвимости: открытые порты, публичные бакеты
terraform-docs  # README из переменных и выходов
Начните с terraform fmt -recursive и tflint в pre-commit: мелочи, отвлекающие ревьюера, уйдут автоматически.
Анализаторы дают severity, но решение за вами: находку, закрытую на сетевом уровне, можно внести в исключения с явным комментарием.

16. Чтение окружения: data, remote state и random

Не вся инфраструктура управляется Terraform: что-то создано вручную или другим модулем. Для чтения существуют data-источники — они не создают ресурсы, а только читают.

data "terraform_remote_state" "base" {
  backend = "s3"
  config  = { bucket = "tf-state", key = "base/terraform.tfstate" }
}

resource "random_password" "db" { length = 20 }

resource "aws_db_instance" "app" {
  password = random_password.db.result
}
Кросспроектные связи держите явными: читайте чужой state через data, а не захардкоженные ID — иначе правка в «базе» ломает зависимые стеки.
Значение из data считается известным сразу (known after apply только для новых ресурсов), поэтому планы становятся предсказуемее.

17. Политики как код: OPA и Sentinel

«Все знают, что прод-бакеты должны быть закрытыми» — недостаточно. Политики как код превращают правило в проверяемый машинный тест на этапе plan.

# rego: план отклоняется при публичном бакете
deny[msg] {
    bucket := input.some_bucket
    bucket.acl == "public-read"
    msg := "S3 bucket must not be public-read"
}

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

Заводите папку policy/ сразу: даже без OPA это «конституция» проекта — что можно и что нельзя.
Отключённая без следа политика хуже отсутствующей: она создаёт ложную безопасность. Только явный changelog с причиной отключения.

18. Откаты и безопасные миграции

Инфраструктура иногда ломается после apply. Хорошая новость: история состояний позволяет откатиться — откат это тоже «план вперёд», а не магия.

  1. до изменений снимите точку: terraform state pull;
  2. при регрессии верните прежний рабочий код и примените — ресурсы вернутся в требуемое состояние;
  3. переименования — через terraform state mv и блоки moved, а не удаление с пересозданием;
  4. для невосполнимых ресурсов — prevent_destroy = true.
terraform state pull > backup-$(date +%F).json
terraform state mv aws_instance.web aws_instance.web_new
terraform state rm  aws_instance.legacy
Пересоздание базы через «удалить и создать» почти всегда катастрофа: ходите через mv/moved, а разрушающие изменения — отдельным PR с ревью.
Снимок state не заменяет бэкап данных: он восстанавливает управление конфигурацией, а данные баз возвращаются своими инструментами.

19. Публикация и версионирование модулей

Модуль, написанный однажды и качественный, команда использует в десятках проектов. Чтобы делиться им честно, ему нужны версии и механика релиза — а не копирование папок по репозиториям.

# вызов модуля по git-тегу
module "vpc" {
  source = "git::https://gitlab.com/team/terraform-module-vpc.git?ref=v1.2.0"
  cidr   = "10.0.0.0/16"
}

Registry Terraform или частный приватный реестр (если команда большая) — кэш модулей с безопасностью и верс. Для старта достаточно git-тегов.

Запомните: модуль должен быть «одной ответственности»: у VPC-модуля нет задачи — ставить nginx. Это предельно упрощает ревью и версию.
«Последний коммит» в source без ?ref= — автобус без колес: любой push ломает инфраструктуру. Всегда привязывайте версию.
Каждый тег модуля — релиз: задача в трекере, changelog, апгрейд потребителей в отдельных PR.

20. Provisioners: локальные и remote

Provisioners выполняют команды при создании/уничтожении ресурса: установить пакет, прогнать скрипт, добавить в Ansible. Это «последний шанс» декларативности — почти всегда лучше вынести подготовку в образ, но provisioners упрощают жизнь в мелких случаях.

resource "aws_instance" "web" {
  provisioner "remote-exec" {
    inline = [
      "apt-get update -y",
      "systemctl start nginx",
    ]
  }
}
ТипКогда
local-execзапуск локально после/до ресурса (генерация конфига)
remote-execкоманда в самом ресурсе через SSH/WinRM
fileкопирование файла на удалённую машину

Опасность: provisioners выполняются только при создании (или с -force), полная идемпотентность не гарантирована. Отсюда правило — чем меньше, тем лучше.

Если задача «каждый раз по-новому» — это уже скрипт после apply: храните её в Ansible-плейбуках или отдельном вызове, не в Terraform.
Provisioner на destroy («удалить конфиг при удалении VM») выполняется даже при «лёгком» destroy — не пишите туда данные, которых больше нет.
Для выполнения кода на готовом ресурсе в проде правильные инструменты — Ansible и Packer, Terraform там слабое звено.

21. count и for_each: повторяющиеся ресурсы

Десять одинаковых ноутбуков или список пользователей в одну группу — копировать блоки руками нельзя. Два способа «массовости»: count (цифровая индексация) и for_each (обход коллекции по ключам/значениям).

variable "instances" {
  type    = map(string)
  default = { web = "t3.micro", api = "t3.small" }
}

resource "aws_instance" "app" {
  # for_each даёт читаемые ключи и устойчивый state
  for_each = var.instances
  instance_type = each.value
  tags = { Name = each.key }
}

Выбор: если элементы — самостоятельные сущности с именами — for_each; если «ряд» без идентичности — count. Оба использовать на одном ресурсе запрещено, но можно сгруппировать.

Используйте toset() для обхода списков: for_each требует коллекцию с уникальными ключами.
Изменение ключа/индекса переменной «пересоздаёт» ресурс Terraform (destroy+create). Продумывайте ключи так, чтобы объём менялся редко.
Сгенерированные ресурсы обращаются через индексацию module.x[each.key] — заучите синтаксис доступа заранее.

22. OpenTofu и будущее экосистемы

HashiCorp изменила лицензию Terraform, и сообщество ответило форком — OpenTofu: свободный аналог с тем же синтаксисом и совместимостью state. Это важная веха экосистемы.

# Совместимая команда: замените бинарь terraform
# на tofu — синтаксис и state остаются
tofu init
tofu plan
tofu apply

# Переход между тулами одного проекта не требует
# пересоздания инфраструктуры

Стратегия на будущее: держите инфраструктуру в «не-привязанном» к вендору виде — модули и стандартные команды. Тогда смена тула не меняет планы работ.

Следите за двумя репозиториями и выбирайте по сценарию: если нужна SaaS-панель — Terraform Cloud, если свобода и открытость — OpenTofu.
Не гонитесь за новыми фичами тула для «красивых планов»: совместимость и предсказуемость важнее, чем свежий блеск языка.
Экосистема живёт и вне «двух братьев»: Pulumi и CDK (TypeScript) рядом, и стоит знать их аргументы.

Что дальше

Инфраструктура поднята — теперь настрой её с помощью Ansible, упакуй приложения в Docker и оркестрируй через Kubernetes. Каталог курсов — на главной странице.

← Вернуться к курсам