Terraform: Проектирование и развертывание облачной инфраструктуры

Глубокое погружение в экосистему Terraform для создания масштабируемой и отказоустойчивой инфраструктуры. Курс фокусируется на декларативном подходе, управлении состоянием и создании переиспользуемых модулей для подготовки к работе в Production-средах.

1. Декларативная модель и жизненный цикл ресурсов в Terraform

Декларативная модель и жизненный цикл ресурсов в Terraform

Скрипт развертывания ста серверов падает на пятьдесят седьмом из-за сетевого таймаута. Ошибка исправлена, но запускать скрипт заново страшно: он попытается заново создать первые пятьдесят шесть машин, что приведет к конфликтам имен, ошибкам API и непредсказуемому состоянию системы. Инженеру приходится вручную комментировать строки кода или писать сложную логику проверок if exists. Эта ситуация — классический тупик императивного подхода к инфраструктуре, который неизбежно возникает при масштабировании.

Решение этой проблемы лежит в смене парадигмы: переходе от описания шагов к описанию финального результата.

Императивный и декларативный подходы

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

Императивный подход отвечает на вопрос «Как сделать?». Инженер пишет точные инструкции: вызови API, создай сеть, подожди 10 секунд, если успешно — создай виртуальную машину, если нет — выведи ошибку. Вся логика обработки состояний, зависимостей и ошибок ложится на плечи автора кода. Bash-скрипты, Ansible (в режиме ad-hoc команд) или скрипты на Python с использованием SDK облачного провайдера — это императивные инструменты.

Пример императивной логики (псевдокод):

Декларативный подход отвечает на вопрос «Что должно получиться?». Инженер описывает желаемое конечное состояние системы (Desired State). Инструмент сам вычисляет текущее состояние (Actual State), сравнивает их и генерирует минимально необходимый набор императивных команд для приведения реальности к желаемому виду.

Пример декларативной логики на HashiCorp Configuration Language (HCL):

В этом коде нет проверок на существование сети. Если сеть уже есть, Terraform ничего не сделает. Если её нет — создаст. Если она есть, но называется иначе — переименует (или пересоздаст, в зависимости от возможностей API провайдера). Сложность вычисления дельты между «как есть» и «как надо» делегируется ядру Terraform.

Идемпотентность как следствие декларативности

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

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

Математически логику работы декларативного движка можно выразить так:

Где — это набор конкретных действий (Create, Update, Delete), необходимых для устранения разницы.

Жизненный цикл Terraform-конфигурации

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

1. Инициализация (terraform init)

Код Terraform сам по себе не умеет создавать ресурсы. HCL — это просто язык разметки. Взаимодействие с реальными облаками осуществляют плагины — провайдеры (Providers).

На этапе инициализации Terraform анализирует код, находит блоки provider (например, Yandex Cloud, AWS, GitHub) и скачивает нужные бинарные файлы плагинов в локальную скрытую директорию .terraform. Также на этом этапе настраивается подключение к хранилищу состояния (State backend). Без успешной инициализации дальнейшая работа невозможна.

2. Планирование (terraform plan)

Самый важный этап с точки зрения безопасности. Terraform выполняет операцию Read: опрашивает API облачного провайдера, чтобы узнать реальное положение дел (Actual State), сравнивает его с кодом (Desired State) и выводит подробный отчет о том, что он собирается сделать.

В выводе команды plan используются специальные символы: * + (зеленый) — ресурс будет создан. * - (красный) — ресурс будет удален. * ~ (желтый) — ресурс будет изменен на месте (In-place update). * -/+ (красно-зеленый) — ресурс будет удален и создан заново (Replacement). Это происходит, когда изменяется параметр, который API облака не позволяет менять на лету (например, зона доступности виртуальной машины).

Чтение вывода terraform plan — обязательный навык. Пропуск этого шага и слепое применение изменений часто приводит к инцидентам, когда из-за опечатки в коде Terraform решает удалить и пересоздать базу данных.

3. Применение (terraform apply)

На этом этапе Terraform берет вычисленный план и начинает отправлять императивные запросы к API провайдера. Важно понимать, что apply по умолчанию включает в себя скрытый plan, требуя от пользователя подтверждения (ввода yes), если план не был сохранен в файл заранее.

В CI/CD пайплайнах обычно используется связка:

  • terraform plan -out=tfplan (генерация и сохранение плана).
  • Ручное подтверждение (Code Review).
  • terraform apply tfplan (применение строго сохраненного плана, чтобы исключить ситуацию, когда за время между планированием и применением кто-то изменил инфраструктуру вручную).
  • 4. Уничтожение (terraform destroy)

    Специальный случай применения, эквивалентный удалению всех описанных в коде ресурсов. Terraform корректно удаляет ресурсы в порядке, обратном их созданию, соблюдая все зависимости.

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

    В императивном скрипте порядок создания ресурсов определяется порядком строк в файле. В декларативном HCL порядок блоков кода не имеет значения. Terraform самостоятельно вычисляет, в какой последовательности нужно вызывать API.

    Для этого ядро Terraform строит направленный ациклический граф (Directed Acyclic Graph, DAG). Узлы графа — это ресурсы, а ребра — зависимости между ними.

    Зависимости бывают двух типов:

  • Неявные (Implicit dependencies). Возникают автоматически, когда один ресурс ссылается на атрибут другого.
  • В примере выше виртуальная машина yandex_compute_instance.web использует yandex_vpc_network.my_net.id. Terraform понимает: чтобы получить ID сети, сеть нужно сначала создать. Следовательно, ВМ зависит от сети.

  • Явные (Explicit dependencies). Задаются вручную с помощью мета-аргумента depends_on. Используются редко, когда зависимость есть логически, но не выражена через передачу атрибутов. Например, приложение на ВМ при старте скачивает файл из S3-бакета. ВМ не использует ID бакета в конфигурации, но если бакет не будет создан первым, приложение упадет.
  • !Обход графа зависимостей

    Построение графа дает мощнейшее преимущество — параллелизм. Если Terraform видит в графе ветви, не зависящие друг от друга (например, три разные виртуальные машины в одной подсети), он будет создавать их одновременно. По умолчанию Terraform запускает до 10 параллельных потоков (регулируется флагом -parallelism), что кардинально ускоряет развертывание объемных инфраструктур по сравнению с последовательными скриптами.

    Управление жизненным циклом: блок lifecycle

    Стандартный алгоритм Terraform (создать, обновить на месте, либо удалить-и-создать-заново) покрывает 90% задач. Однако для критичных production-систем этого бывает недостаточно. Для тонкой настройки поведения используется специальный блок lifecycle, который встраивается внутрь ресурса.

    create_before_destroy

    По умолчанию, если изменение требует пересоздания ресурса (Replacement), Terraform сначала удаляет старый ресурс, а затем создает новый. Для базы данных или балансировщика нагрузки это означает гарантированный даунтайм.

    Установка create_before_destroy = true инвертирует этот процесс. Terraform сначала создаст новый ресурс с новыми параметрами, дождется его готовности, и только потом удалит старый.

    !Механизм create_before_destroy

    Этот паттерн идеален для реализации Zero-Downtime Deployment (ZDD), но требует осторожности. Новый и старый ресурсы будут существовать одновременно в течение короткого времени. Если ресурс требует уникального имени (например, доменное имя или статический IP), одновременное существование двух объектов с одинаковыми уникальными параметрами вызовет ошибку API облака.

    prevent_destroy

    Защита от случайного удаления, известная как «защита от дурака». Если ресурс помечен этим флагом, любая операция terraform plan, которая подразумевает удаление ресурса (явное через destroy, или неявное из-за пересоздания), завершится ошибкой еще на этапе вычислений.

    Это обязательная практика для stateful-ресурсов: баз данных, хранилищ объектов (S3), дисков с критичными данными. Чтобы удалить такой ресурс, инженеру придется осознанно зайти в код, удалить блок prevent_destroy, закоммитить изменения и только потом выполнить удаление.

    ignore_changes

    Декларативная модель требует, чтобы код был единственным источником истины. Но в реальности инфраструктура может изменяться извне. Вспоминаем концепцию дрейфа конфигурации: если кто-то вручную добавит тег на сервер, Terraform при следующем запуске заметит расхождение и удалит этот тег, чтобы привести реальность в соответствие с кодом.

    Но иногда дрейф легитимен. Классический пример — автомасштабирование (Autoscaling). Допустим, Terraform создает группу виртуальных машин с начальным размером size = 3. Дальше в дело вступает облачный балансировщик, который из-за высокой нагрузки увеличивает размер группы до 5. Если запустить Terraform сейчас, он увидит: в коде 3, по факту 5. План покажет удаление двух машин, что приведет к падению production под нагрузкой.

    Мета-аргумент ignore_changes указывает Terraform игнорировать расхождения в определенных атрибутах.

    Теперь Terraform создаст группу из трех машин, но в будущем перестанет обращать внимание на изменение атрибута size, полностью делегировав управление размером внешнему автоскейлеру.

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

    2. Архитектура провайдеров и инициализация рабочего окружения

    Архитектура провайдеров и инициализация рабочего окружения

    Сам по себе Terraform абсолютно ничего не знает об облачных платформах. Если заглянуть в его исходный код, там нет ни одной строчки, описывающей, как создать виртуальную машину в Yandex Cloud, как настроить S3-бакет в AWS или как выдать права в Google Cloud. Ядро Terraform — это исключительно математический и логический движок. Вся магия взаимодействия с реальным миром вынесена за пределы ядра и передана специальным плагинам — провайдерами.

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

    Разделение труда: Terraform Core и Providers

    Архитектура Terraform строится на жестком разделении на два независимых компонента: Terraform Core (ядро) и Terraform Providers (провайдеры). Оба компонента представляют собой скомпилированные бинарные файлы, написанные на языке Go, но их задачи радикально отличаются.

    Terraform Core отвечает за чтение конфигурационных файлов (HCL), построение графа зависимостей, управление состоянием (State) и вычисление разницы между желаемым и фактическим состоянием. Ядро принимает решения о том, что и в каком порядке нужно сделать.

    Terraform Providers — это исполнители. Это отдельные программы, которые знают, как общаться с конкретным API облачной платформы, SaaS-сервиса или даже локальной базы данных. Провайдер содержит логику аутентификации, обработки ошибок API, повторных попыток (retries) и маппинга структур данных Terraform в JSON или XML, понятный конечному сервису.

    !Портрет Митчелла Хасимото

    Когда вы запускаете планирование или применение изменений, Terraform Core не вызывает облачные API напрямую. Вместо этого ядро запускает бинарный файл нужного провайдера как дочерний процесс. Взаимодействие между ядром и провайдером происходит по протоколу gRPC (Remote Procedure Call поверх HTTP/2).

    !Схема взаимодействия Terraform Core и Provider

    Ядро отправляет провайдеру gRPC-запрос: «Прочитай текущее состояние ресурса с идентификатором X». Провайдер формирует REST API запрос к облаку, получает ответ, переводит его в понятный для ядра формат и возвращает через gRPC. Если ядро решает, что ресурс нужно создать, оно отправляет gRPC-вызов с набором аргументов, а провайдер выполняет POST-запрос к API платформы.

    Такая микросервисная архитектура внутри одного инструмента дает три критических преимущества:

  • Независимые циклы релизов. Если Yandex Cloud выпускает новый сервис, разработчикам не нужно ждать обновления самого Terraform. Достаточно обновить только провайдер yandex-cloud/yandex.
  • Безопасность и стабильность. Ошибка (panic) в коде провайдера убьет только процесс провайдера, но не обрушит ядро Terraform, которое сможет корректно обработать сбой и сохранить текущее состояние.
  • Открытая экосистема. Любая компания может написать свой провайдер для внутренней системы (например, для самописной системы управления IP-адресами) и использовать его наравне с публичными облаками.
  • Анатомия адреса провайдера (FQN)

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

    Сегодня каждый провайдер имеет Fully Qualified Name (FQN) — полностью определенное имя, состоящее из трех частей: hostname/namespace/type.

  • Hostname: Адрес реестра, где хранится провайдер. По умолчанию это registry.terraform.io (публичный реестр HashiCorp). Вы можете использовать внутренние реестры, указав свой домен, например, terraform.company.internal/.
  • Namespace: Пространство имен, обычно соответствующее названию компании или GitHub-аккаунту разработчика (например, hashicorp, yandex-cloud, datadog).
  • Type: Имя самого провайдера, которое используется в конфигурации (например, aws, yandex, kubernetes).
  • Полный адрес провайдера Yandex Cloud выглядит так: registry.terraform.io/yandex-cloud/yandex. Именно этот адрес Terraform использует для поиска и загрузки нужного плагина.

    Инициализация рабочего окружения: что делает init

    Команда terraform init — это первый шаг в любом рабочем процессе IaC. Часто её воспринимают просто как команду, которую «нужно запустить перед началом работы», но под капотом происходит сложный процесс сборки рабочего окружения.

    Главная задача фазы инициализации — проанализировать конфигурационные файлы, найти все упоминания провайдеров (как явные в блоках terraform { required_providers { ... } }, так и неявные, выводимые из названий ресурсов), скачать их бинарные файлы и подготовить локальную директорию .terraform.

    !Процесс работы terraform init

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

  • Ядро обращается к реестру по FQN провайдера и запрашивает доступные версии.
  • Проверяются ограничения версий, указанные в коде (например, version = "~> 0.90.0").
  • Выбирается максимально доступная версия, удовлетворяющая условиям.
  • Скачивается ZIP-архив с бинарным файлом, скомпилированным под вашу операционную систему и архитектуру процессора (например, darwin_arm64 для Apple Silicon или linux_amd64 для серверов).
  • Архив распаковывается в скрытую директорию .terraform/providers/.
  • Если вы работаете в изолированной среде (Air-gapped environment) без доступа к интернету, механизм инициализации позволяет использовать локальные зеркала (Provider Network Mirror). В этом случае в конфигурационном файле CLI (~/.terraformrc) прописывается путь к локальной файловой системе или внутреннему HTTP-серверу, откуда ядро будет забирать заранее скачанные плагины, минуя публичный реестр.

    Фиксация зависимостей: .terraform.lock.hcl

    В разработке программного обеспечения существует классическая проблема: код работает на ноутбуке разработчика, но падает на сервере сборки (CI/CD). В контексте инфраструктуры это может привести к катастрофе. Если в понедельник вы развернули кластер баз данных с одной версией провайдера, а в пятницу ваш коллега запустил код с новой версией, в которой изменилась логика работы с API, инфраструктура может быть непреднамеренно разрушена.

    Для обеспечения детерминированности (воспроизводимости) среды при первом запуске terraform init создается файл .terraform.lock.hcl. Этот файл выполняет ту же роль, что package-lock.json в Node.js или poetry.lock в Python.

    Lock-файл жестко фиксирует точную версию каждого провайдера и криптографические хеши их бинарных файлов.

    Внутри файла вы увидите два типа хешей:

  • h1: — хеш-сумма распакованной директории с провайдером.
  • zh: — хеш-сумма оригинального ZIP-архива, скачанного из реестра.
  • Когда lock-файл уже существует, последующие запуски terraform init больше не будут искать новые версии провайдеров в реестре. Terraform прочитает файл, скачает строго зафиксированную версию и сверит хеш-суммы. Если злоумышленник подменит бинарный файл провайдера в реестре или в локальном кэше, хеши не совпадут, и Terraform откажется выполнять инициализацию, выдав ошибку безопасности.

    Файл .terraform.lock.hcl обязательно должен быть добавлен в систему контроля версий (Git). Это гарантирует, что вся команда и все CI/CD пайплайны будут работать с идентичными бинарными файлами провайдеров.

    Кроссплатформенная блокировка

    Здесь кроется важный нюанс, с которым сталкиваются команды при настройке CI/CD. Допустим, разработчик работает на MacBook с процессором M1 (архитектура darwin_arm64). Он пишет код, запускает init, Terraform скачивает провайдер для macOS, вычисляет его хеш и записывает в .terraform.lock.hcl. Разработчик коммитит код в Git.

    Затем этот код запускается в GitLab CI, который работает на Linux-серверах (linux_amd64). Terraform в CI читает lock-файл, видит зафиксированную версию, скачивает архив для Linux и... падает с ошибкой. Ошибка возникает потому, что хеш ZIP-архива для Linux отличается от хеша ZIP-архива для macOS, а в lock-файле сохранен только хеш от macOS.

    Чтобы решить эту проблему, существует специальная команда: terraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=windows_amd64

    Эта команда заставляет Terraform обратиться к реестру, скачать хеши для всех указанных платформ и добавить их в lock-файл. После этого конфигурация становится по-настоящему кроссплатформенной, и инициализация успешно пройдет как на локальных машинах разработчиков, так и на серверах автоматизации.

    Конфигурация провайдеров и множественные экземпляры (Aliases)

    Помимо загрузки, провайдер необходимо настроить. Настройка происходит в блоке provider. Именно здесь указываются параметры аутентификации (токены, ключи), регионы по умолчанию и другие глобальные настройки для взаимодействия с API.

    В большинстве случаев одного блока provider достаточно. Все ресурсы типа yandex_* автоматически будут использовать эту конфигурацию. Но возникают архитектурные задачи, требующие одновременной работы с несколькими независимыми средами в рамках одного проекта.

    Например, вам нужно развернуть основную инфраструктуру в одном каталоге (folder) Yandex Cloud, а резервные копии базы данных складывать в S3-бакет, находящийся в совершенно другом облаке или каталоге в целях изоляции доменов отказа. Или вы настраиваете VPC-пиринг между двумя разными аккаунтами AWS.

    Для этого используется механизм псевдонимов (aliases). Вы можете объявить несколько блоков одного и того же провайдера, присвоив дополнительным блокам уникальные имена через аргумент alias. Провайдер без алиаса считается провайдером по умолчанию (default provider).

    Теперь у нас в памяти запущено два логических экземпляра провайдера yandex с разными настройками. Чтобы указать ресурсу, какой именно экземпляр использовать, применяется мета-аргумент provider внутри блока ресурса.

    Мета-аргумент provider принимает ссылку на конкретный экземпляр в формате имя_провайдера.алиас. Если этот аргумент не указан, Terraform неявно подставит провайдер по умолчанию для данного типа ресурсов.

    Использование алиасов позволяет строить сложные мультирегиональные и мультитенантные архитектуры в рамках одного графа зависимостей. Terraform сможет корректно рассчитать порядок создания ресурсов, даже если они физически располагаются в разных независимых облачных аккаунтах, и передать данные из одного аккаунта в другой (например, получить IP-адрес ВМ из первого каталога и добавить его в правила балансировщика во втором каталоге).

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

    3. Язык HCL: типы данных, интерполяция и динамические выражения

    Язык HCL: типы данных, интерполяция и динамические выражения

    Конфигурация инфраструктуры редко бывает абсолютно статичной. Если вам нужно развернуть три виртуальные машины, можно трижды скопировать блок ресурса. Но если машин пятьдесят, они распределены по трем разным зонам доступности, а правила файрвола должны генерироваться на лету в зависимости от переданного списка IP-адресов, прямое копирование приведет к тысячам строк нечитаемого кода. Язык HCL (HashiCorp Configuration Language) решает эту проблему, предоставляя разработчику инструменты программирования — переменные, циклы, условия и функции, — сохраняя при этом общую декларативную природу описания инфраструктуры.

    Система типов данных: от примитивов к сложным структурам

    HCL строго типизирован. Хотя при написании кода Terraform часто сам догадывается о нужных типах (неявное приведение), понимание системы типов критически важно при проектировании модулей и написании сложных выражений.

    Все типы делятся на три категории: примитивные, коллекции и структурные.

    Примитивные типы интуитивно понятны:

  • string — строка текста (всегда в двойных кавычках).
  • number — число (целое или с плавающей точкой).
  • bool — логическое значение (true или false).
  • !Иерархия типов данных в HCL

    Коллекции: ловушка индексов (List vs Set)

    Коллекции объединяют несколько значений одного типа. В HCL их три: list (список), map (словарь) и set (множество). Разница между list и set — одна из самых частых причин аварий на production-инфраструктуре при масштабировании.

    list — это упорядоченная последовательность. Элементы идентифицируются по их числовому индексу (начиная с нуля).

    set — это неупорядоченная коллекция уникальных значений. У элементов нет индексов, они идентифицируются по самому значению.

    Представьте, что вы используете list для создания серверов. Индексы распределились так: 0 = a, 1 = b, 2 = c. Если вы решите удалить первую зону (a) из середины списка, массив сдвинется. Зона b получит индекс 0, а c — индекс 1. Terraform, сверяя фактическое состояние с желаемым, увидит, что параметры ресурсов под индексами 0 и 1 изменились. Вместо того чтобы просто удалить один сервер, он пересоздаст оставшиеся серверы, так как их привязка к индексам поменялась.

    Использование set исключает эту проблему. Поскольку индексов нет, удаление "ru-central1-a" просто убирает один элемент из множества, не затрагивая остальные. Правило хорошего тона в IaC: если порядок элементов не имеет значения, всегда используйте set вместо list.

    Структурные типы: Object и Tuple

    Если коллекции требуют, чтобы все элементы были одного типа (например, только строки), то структурные типы позволяют комбинировать разные типы данных.

    object похож на map, но с жестко заданной схемой. Это аналог структур (struct) в языках программирования.

    Если передать в эту переменную объект без поля ssd или передать строку вместо числа в ram, Terraform выдаст ошибку еще на этапе инициализации (до обращения к облачному провайдеру). Это мощный инструмент валидации входных данных.

    Интерполяция и ссылки на атрибуты

    Интерполяция — это механизм внедрения значений переменных или атрибутов ресурсов в строки. В ранних версиях Terraform (до 0.12) интерполяция требовалась повсеместно. Сегодня синтаксис {var.environment}-{var.default_zone}" hcl zone = var.default_zone hcl resource "yandex_compute_disk" "data" { name = "data-disk" type = "network-ssd" size = var.env == "prod" ? 100 : 20 } hcl variable "users" { default = ["alice", "bob", "sysadmin", "charlie"] }

    locals { clean_users = [ for u in var.users : upper(u) if !length(regexall("admin", u)) > 0 ] # Результат: ["ALICE", "BOB", "CHARLIE"] } hcl variable "subnets" { type = list(object({ name = string cidr = string })) default = [ { name = "web", cidr = "10.0.1.0/24" }, { name = "db", cidr = "10.0.2.0/24" } ] }

    locals { subnets_map = { for s in var.subnets : s.name => s.cidr } # Результат: { "web" = "10.0.1.0/24", "db" = "10.0.2.0/24" } } hcl variable "web_ports" { type = list(number) default = [80, 443, 8080] }

    resource "yandex_vpc_security_group" "web_sg" { name = "web-security-group" network_id = yandex_vpc_network.main.id

    dynamic "ingress" { for_each = var.web_ports

    content { protocol = "TCP" description = "Allow port ${ingress.value}" v4_cidr_blocks = ["0.0.0.0/0"] port = ingress.value } } } hcl output "web_ips" { value = yandex_compute_instance.web[*].network_interface.0.nat_ip_address } hcl locals { networks = [ { name = "vpc1", subnets = ["10.0.1.0/24", "10.0.2.0/24"] }, { name = "vpc2", subnets = ["192.168.1.0/24"] } ] } ` Если вы попытаетесь пройтись по этому списку, вы получите структуру вида [["10.0.1.0/24", "10.0.2.0/24"], ["192.168.1.0/24"]]. Terraform не умеет итерироваться по двумерным массивам для создания ресурсов. Функция flatten превратит это в плоский список ["10.0.1.0/24", "10.0.2.0/24", "192.168.1.0/24"]`, с которым легко работать дальше.

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

    4. Управление состоянием (State): хранение, блокировки и безопасность

    Управление состоянием (State): хранение, блокировки и безопасность

    Представьте, что вы строите небоскреб, имея на руках идеальный чертеж. Но как только заложен фундамент, чертеж магическим образом перестает отражать реальность: рабочие внесли изменения, материалы осели, а часть конструкций скрыта под землей. Чтобы продолжить стройку, вам нужен не просто чертеж, а точный слепок того, что уже возведено. В Terraform таким «слепком» является State-файл. Если вы его потеряете, Terraform «ослепнет»: он увидит ваш код, но не узнает в существующих облачных ресурсах свои собственные создания и попытается построить всё заново, что в условиях продакшена равносильно катастрофе.

    Анатомия State-файла: что скрыто внутри terraform.tfstate

    На начальных этапах работы Terraform создает локальный файл terraform.tfstate. Это обычный JSON, который выполняет роль связующего звена между вашим декларативным кодом (Desired State) и реальностью в облаке (Actual State).

    Многие начинающие инженеры ошибочно полагают, что Terraform каждый раз опрашивает API облачного провайдера, чтобы понять, что нужно изменить. На самом деле, Terraform доверяет состоянию, записанному в State-файле. Это критически важно для производительности: в крупных инфраструктурах с тысячами ресурсов прямой опрос API при каждом запуске plan занимал бы десятки минут и приводил к блокировкам из-за превышения лимитов (Rate Limiting).

    Внутри State-файла хранятся три ключевых типа данных:

  • Маппинг ресурсов: Terraform связывает логическое имя ресурса в коде (например, yandex_compute_instance.web_server) с его уникальным идентификатором в облаке (ID инстанса).
  • Метаданные и зависимости: Информация о том, в каком порядке создавались ресурсы и какие атрибуты были получены «на лету» (например, динамически назначенный IP-адрес).
  • Кэш атрибутов: Все параметры ресурсов, включая те, которые вы не описывали в коде явным образом.
  • Рассмотрим структуру записи ресурса в JSON:

    Здесь id — это тот самый «крючок», за который Terraform держится при обновлении инфраструктуры. Если вы вручную удалите сеть в консоли облака, Terraform при следующем запуске обнаружит расхождение (Drift), так как в State запись есть, а в API — нет.

    Проблема локального хранения и переход к Remote Backend

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

    Первая — синхронизация. Если инженер А применил изменения, его локальный State обновился. Если инженер Б запустит apply со своего компьютера, его State будет устаревшим. Он не увидит созданных ресурсов и попытается создать их дубликаты, что приведет к ошибкам именования или конфликтам в облаке.

    Вторая — безопасность. State-файл содержит конфиденциальную информацию в открытом виде. Пароли от баз данных, приватные ключи сертификатов, токены доступа — всё, что генерируется или передается в ресурсы, оседает в JSON. Хранение такого файла в Git-репозитории (даже приватном) — грубейшее нарушение безопасности.

    Третья — одновременный доступ. Если два инженера или два CI-пайплайна запустят apply одновременно, они могут начать изменять одни и те же ресурсы, что приведет к непредсказуемому состоянию инфраструктуры и повреждению самого State-файла.

    Решением является Remote Backend — механизм, позволяющий хранить состояние в удаленном надежном хранилище с поддержкой блокировок.

    S3-совместимые хранилища и блокировки через DynamoDB

    В экосистеме Yandex Cloud и AWS стандартом де-факто является связка Object Storage (S3) для хранения файла и NoSQL базы данных (например, DynamoDB или её аналоги) для управления блокировками.

    Механика блокировок (State Locking)

    Когда вы запускаете terraform plan или apply, Terraform пытается создать запись в таблице блокировок. Эта запись содержит ID процесса, имя пользователя и временную метку. Если другой участник команды попытается запустить Terraform в это же время, он получит ошибку:

    > Error: Error acquiring the state lock > Lock Info: > ID: a1b2c3d4-e5f6... > Operation: OperationTypeApply > Who: j.doe@company.com > Created: 2023-10-27 10:00:00 UTC

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

    Настройка Backend в Yandex Cloud

    Для настройки удаленного хранения в Yandex Cloud используется провайдер s3. Несмотря на название, он универсален для всех S3-совместимых хранилищ.

    Здесь key определяет путь к файлу внутри бакета. Хорошей практикой является разделение состояний разных окружений (dev, staging, prod) по разным ключам или даже разным бакетам.

    Безопасность State-файла: шифрование и секреты

    State-файл — это «золотая жила» для злоумышленника. Если в вашем коде есть ресурс yandex_iam_service_account_key, то приватный ключ этого аккаунта будет сохранен в State в открытом текстовом виде.

    Стратегии защиты

  • Шифрование при хранении (Encryption at Rest): При использовании S3-бэкенда необходимо включить серверное шифрование (SSE). В Yandex Object Storage это делается на уровне бакета с использованием ключей KMS (Key Management Service). Terraform будет прозрачно расшифровывать данные при чтении, но в самом хранилище они будут лежать в зашифрованном виде.
  • Ограничение доступа (IAM): Доступ к бакету со State-файлами должен иметь только ограниченный круг лиц (Lead DevOps) и сервисный аккаунт, под которым работает CI/CD пайплайн. Обычным разработчикам достаточно прав на чтение (и то не всегда), но не на удаление или изменение.
  • Минимизация секретов в коде: Используйте внешние хранилища секретов, такие как HashiCorp Vault или Yandex Lockbox. Однако помните: даже если вы передаете секрет из Vault в Terraform как переменную, он всё равно попадет в State-файл. На текущий момент в Terraform нет способа полностью исключить попадание чувствительных данных в State, если они используются в аргументах ресурсов.
  • Жизненный цикл State: миграция, импорт и ручные правки

    Иногда возникает необходимость вмешаться в работу Terraform и вручную подсказать ему, как соотносить код и реальность. Для этого существует семейство команд terraform state.

    Импорт существующей инфраструктуры

    Представьте, что до внедрения Terraform в компании уже были созданы десятки виртуальных машин вручную. Чтобы начать управлять ими через код, недостаточно просто описать их в .tf файлах. Terraform не «подхватит» их автоматически, так как в State-файле нет записей о них.

    Для этого используется команда import:

    terraform import yandex_compute_instance.vm-1 <instance_id>

    Эта команда не создает код, она только добавляет запись в State-файл. После импорта вам нужно вручную написать конфигурацию ресурса так, чтобы terraform plan показал отсутствие изменений (No changes).

    Переименование и перемещение ресурсов

    Если вы решили изменить имя ресурса в коде (например, с web_server на app_server), Terraform воспримет это как удаление старого ресурса и создание нового. Чтобы избежать простоя (downtime), нужно обновить State:

    terraform state mv yandex_compute_instance.web_server yandex_compute_instance.app_server

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

    Удаление из-под управления

    Иногда нужно перестать управлять ресурсом через Terraform, но при этом не удалять его физически. Команда terraform state rm <address> удаляет запись из State. Теперь ресурс «свободен», и Terraform о нем больше не знает.

    Изоляция состояний: Workspaces против структурного деления

    Как организовать хранение State для множества окружений? Существует два основных подхода.

    Terraform Workspaces

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

    terraform workspace new prod

    Terraform создаст отдельный файл состояния (в S3 это будет путь вроде env:/prod/path/to/state). Это удобно для быстрого развертывания идентичных копий инфраструктуры, например, для временных feature-стендов. Однако для долгоживущих окружений (Dev/Prod) воркспейсы считаются антипаттерном, так как они используют один и тот же код и один и тот же бэкенд, что повышает риск «выстрелить в ногу», случайно применив изменения не туда.

    Структурное деление (Directory-based)

    Более надежный подход — разделение конфигураций по директориям:

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

    Проблема чувствительных данных в выводе (Sensitive Outputs)

    При работе с модулями и сложными конфигурациями часто возникает необходимость передать данные из одного State в другой. Для этого используются outputs. Если выходное значение содержит секрет, его нужно пометить как sensitive = true.

    Terraform скроет это значение в консоли (заменит на <sensitive>), но — и это критически важно — в State-файле оно останется в открытом виде. Пометка sensitive защищает только глаза оператора и логи CI/CD, но не сам файл состояния.

    Восстановление после повреждения State

    Повреждение State-файла — редкое, но крайне неприятное событие. Обычно это происходит из-за прерывания процесса записи (например, падение сети при работе с S3 без блокировок) или некорректных ручных правок JSON.

    Поскольку State — это JSON, его можно править вручную в текстовом редакторе, но это крайняя мера. Перед любыми манипуляциями с terraform state или ручными правками всегда делайте бэкап. Большинство Remote Backend (включая S3) поддерживают версионирование объектов. Включите версионирование на бакете со State — это позволит вам откатиться к предыдущему исправному состоянию за считанные секунды.

    Если State безвозвратно утерян, а бэкапов нет, единственный путь — полная процедура terraform import для всех ресурсов. Для больших систем это может занять дни кропотливой работы. Именно поэтому надежность хранения State является приоритетом номер один в дизайне IaC-платформы.

    Взаимодействие между разными State-файлами (Remote State Data Source)

    В крупных компаниях инфраструктура часто разбивается на слои: сетевой слой (VPC, подсети), слой данных (DB, S3) и слой приложений (K8s, VM). Каждым слоем может управлять отдельная команда, и у каждого слоя свой State-файл.

    Как команде приложений узнать ID подсети, созданной сетевой командой? Для этого используется data source terraform_remote_state.

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

    Управление состоянием — это не просто техническая деталь Terraform, а фундамент его надежности. Понимание того, как State соотносится с реальностью, как работают блокировки и где таятся угрозы безопасности, отличает Middle DevOps инженера от новичка. Правильно настроенный Remote Backend с шифрованием и блокировками — это страховой полис вашей инфраструктуры, гарантирующий, что завтрашний apply не превратится в сеанс экстренного восстановления системы из пепла.

    5. Идемпотентность в действии: планирование изменений и разрешение конфликтов

    Идемпотентность в действии: планирование изменений и разрешение конфликтов

    Вы добавляете безобидный тег Environment = "Production" к конфигурации базы данных, запускаете планирование и видите леденящий душу вывод: Plan: 1 to add, 0 to change, 1 to destroy. Terraform собирается полностью удалить продуктивную базу данных и создать её заново. Причина кроется не в теге. За день до этого дежурный инженер вручную увеличил размер диска базы данных через веб-консоль облака. Поскольку размер диска в большинстве облачных платформ — это атрибут, изменение которого может требовать пересоздания инстанса (Replacement), Terraform, стремясь вернуть систему к задокументированному состоянию, генерирует план с удалением.

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

    Механика вычисления дельты: трехстороннее сравнение

    Чтобы понять, откуда берутся конфликты, необходимо разобрать, как именно Terraform вычисляет ту самую дельту (разницу между тем, что есть, и тем, что должно быть). Многие ошибочно полагают, что Terraform просто сравнивает код (HCL) с облаком. В действительности происходит трехстороннее сравнение, в котором участвуют три сущности:

  • Desired State (Целевое состояние) — то, что написано в ваших .tf файлах.
  • Known State (Известное состояние) — то, что сохранено в terraform.tfstate после последнего успешного запуска.
  • Actual State (Фактическое состояние) — то, что прямо сейчас отвечает API облачного провайдера (Yandex Cloud, AWS и т.д.).
  • !Трехстороннее сравнение состояний в Terraform

    Когда вы запускаете команду планирования, Terraform неявно инициирует фазу Refresh. На этом этапе он опрашивает API облака для каждого ресурса, записанного в State-файле, и обновляет Known State до Actual State.

    Только после того, как локальный слепок синхронизирован с реальностью, алгоритм начинает сравнивать обновленный State с кодом (HCL). Если обнаруживается расхождение, Terraform смотрит в схему провайдера, чтобы понять характер измененного атрибута. Если атрибут помечен провайдером как ForceNew (например, зона доступности виртуальной машины или движок базы данных), генерируется план на пересоздание (). Если атрибут допускает обновление «на лету» (например, теги или правила фаервола), генерируется план на изменение ().

    Идемпотентность при масштабировании: ловушка индексов

    Стремление к идемпотентности накладывает жесткие ограничения на то, как мы проектируем циклы и множественные ресурсы. Классический пример нарушения идемпотентного поведения кроется в выборе между мета-аргументами count и for_each.

    Предположим, необходимо развернуть три одинаковых веб-сервера. Использование count = 3 создает ресурсы, которые идентифицируются в State-файле по числовому индексу:

  • yandex_compute_instance.web[0]
  • yandex_compute_instance.web[1]
  • yandex_compute_instance.web[2]
  • Если возникает необходимость удалить второй сервер (индекс 1), разработчик может уменьшить count до 2 и попытаться внедрить логику пропуска. Но Terraform работает с count как с жестким массивом. Удаление элемента из середины приводит к сдвигу индексов.

    !Проблема сдвига индексов при использовании count

    Сервер, который ранее был под индексом 2, теперь должен стать 1. Terraform видит, что параметры ресурса web[1] (который остался в коде) не совпадают с параметрами старого web[1] (который был удален), и принимает решение модифицировать или пересоздать совершенно здоровый сервер, просто чтобы «подогнать» его под новый индекс. Это грубое нарушение принципа минимизации воздействия.

    Именно поэтому для любых ресурсов, которые могут удаляться или добавляться не с конца списка, необходимо использовать for_each. Этот мета-аргумент принимает на вход словарь (map) или множество (set), привязывая состояние ресурса к строковому ключу, а не к порядковому номеру:

  • yandex_compute_instance.web["frontend-a"]
  • yandex_compute_instance.web["frontend-b"]
  • yandex_compute_instance.web["frontend-c"]
  • Удаление "frontend-b" из входной переменной приведет к удалению ровно одного ресурса. Строковые ключи остальных серверов останутся неизменными, и Terraform отрапортует об идеальной идемпотентности для оставшейся инфраструктуры.

    Разрешение конфликтов: когда реальность сопротивляется коду

    Конфликты между кодом и облаком неизбежны. Они возникают в двух основных формах: появление неуправляемых ресурсов (Unmanaged Resources) и дрейф конфигурации (Configuration Drift).

    Столкновение с неуправляемыми ресурсами

    Вы пишете код для создания S3-бакета company-backups. Запускаете применение, но провайдер возвращает ошибку API: BucketAlreadyExists. Оказывается, бакет с таким именем уже был создан кем-то вручную год назад.

    Terraform не может просто «захватить» существующий ресурс при выполнении обычного создания. Для системы идемпотентность означает, что если ресурса нет в State-файле, его нужно создать. Если API отвечает, что имя занято, возникает тупик.

    Разрешение этого конфликта требует процедуры импорта. Команда импорта связывает существующий физический идентификатор облачного ресурса с логическим адресом в HCL-коде. Синтаксис выглядит так: terraform import yandex_storage_bucket.main company-backups. После выполнения этой команды Terraform скачивает текущее состояние бакета из облака и записывает его в State-файл под именем yandex_storage_bucket.main. При следующем планировании Terraform сравнит ваш код с импортированным состоянием. Если ваш код описывает бакет неточно (например, не указаны правила версионирования, которые по факту включены), Terraform предложит удалить эти правила, чтобы привести реальность в соответствие с кодом.

    Стратегии обработки дрейфа

    Когда Terraform обнаруживает дрейф (кто-то вручную изменил параметры), у инженера есть два пути разрешения конфликта, известных как Reconciliation (примирение).

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

    Путь 2: Приведение кода к реальности (Adopt). Если ручное изменение было критически важным (например, экстренное масштабирование кластера при DDoS-атаке), вы не хотите его отменять. В этом случае вы должны изменить HCL-код так, чтобы он в точности повторял текущее состояние облака. Вы обновляете код, запускаете планирование, и добиваетесь вывода: No changes. Your infrastructure matches the configuration.

    Фантомные изменения (Perpetual Diff)

    Один из самых раздражающих сценариев при работе с IaC — это «вечная дельта» или Perpetual Diff. Вы применяете конфигурацию, она успешно разворачивается. Вы тут же, без единого изменения в коде или облаке, запускаете планирование снова, и Terraform опять показывает изменения. Идемпотентность сломана.

    Чаще всего это происходит из-за нормализации данных на стороне облачного API. Рассмотрим пример: вы передаете MAC-адрес для сетевого интерфейса в формате AA:BB:CC:DD:EE:FF. Облачный провайдер принимает запрос, успешно создает интерфейс, но в своей внутренней базе сохраняет MAC-адрес в нижнем регистре: aa:bb:cc:dd:ee:ff.

    При следующем запуске Terraform считывает состояние из API (aa:bb:cc...) и сравнивает его с вашим кодом (AA:BB:CC...). Строки не совпадают. Terraform предлагает изменить регистр на верхний. API снова принимает запрос, снова сохраняет в нижнем регистре. Возникает бесконечный цикл.

    Для разрешения таких конфликтов качественные Terraform-провайдеры используют встроенные функции нормализации (например, DiffSuppressFunc в коде самого провайдера на Go), которые говорят ядру Terraform: «Считай AA и aa идентичными». Но если провайдер написан с недочетами, бремя решения ложится на инженера.

    Временным решением становится использование блока lifecycle с аргументом ignore_changes. Указав Terraform игнорировать конкретный атрибут, вы отключаете проверку идемпотентности для этого поля, заставляя систему закрыть глаза на фантомную дельту.

    Гарантия консистентности: фиксация плана

    Облачная среда высокодинамична. Между моментом, когда вы посмотрели вывод terraform plan, и моментом, когда вы набрали terraform apply, может пройти несколько минут. За это время состояние облака может измениться (например, автомасштабирование добавит новые узлы).

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

    Для обеспечения строгой предсказуемости в критичных средах применяется механизм фиксации плана: terraform plan -out=tfplan Эта команда создает бинарный файл tfplan, который содержит не только вычисленную дельту, но и точный снимок конфигурации и состояния на момент планирования.

    Последующий запуск terraform apply tfplan гарантирует выполнение исключительно тех действий, которые были зафиксированы в файле. Если за прошедшее время фактическое состояние облака изменилось так, что план больше не может быть применен безопасно, выполнение завершится ошибкой, предотвращая непредсказуемое поведение системы.

    Eventual Consistency и временные аномалии

    Последний нюанс, влияющий на планирование изменений — это согласованность в конечном счете (Eventual Consistency) самих облачных платформ. В распределенных системах создание ресурса может быть асинхронным. API Yandex Cloud или AWS может ответить HTTP 200 OK (ресурс создан) до того, как этот ресурс станет физически доступен во всех зонах доступности.

    Terraform, получив успешный ответ, записывает ресурс в State-файл и переходит к созданию зависимых ресурсов. Если следующий ресурс (например, маршрут в таблице маршрутизации) попытается сослаться на только что созданный шлюз, который еще не успел реплицироваться внутри облака, произойдет ошибка ResourceNotFound.

    Это не ошибка логики Terraform, это столкновение синхронного графа зависимостей с асинхронной природой облака. Опытные DevOps-инженеры учитывают это при планировании, иногда используя провайдеры вроде time для искусственного введения пауз (time_sleep) между созданием критически важных инфраструктурных узлов, давая облаку время на внутреннюю синхронизацию.

    Понимание механизмов планирования, вычисления дельты и разрешения конфликтов переводит инженера с уровня «написания скриптов» на уровень архитектурного управления инфраструктурой. Идемпотентность — это не свойство кода по умолчанию, это результат тщательного согласования структур данных в HCL с поведением реальных облачных API.