Terraform модули: как переиспользовать инфраструктурный код
авг, 17 2026
Представьте, что вы строите дом. Вы не каждый раз изобретаете новый способ класть кирпичи или строить фундамент. Вы используете проверенные чертежи и стандартные блоки. В мире Terraform is инструмент для управления инфраструктурой как кодом (IaC), который позволяет описывать серверы, сети и базы данных в декларативном виде то же самое. Но когда проект растет, копипаста кода превращается в хаос. Здесь на сцену выходят Terraform модули.
Модуль - это просто папка с файлами .tf, которую можно вызывать снова и снова. Звучит просто? Да. Но правильная структура модулей экономит сотни часов работы команды. Мы разберем, как упаковать инфраструктуру так, чтобы она была понятна новичку, безопасна для продакшена и легка в поддержке.
Почему копипаста убивает ваши проекты
Когда у вас один VPC (Virtual Private Cloud) в AWS, все выглядит мило. Один файл main.tf, несколько ресурсов, и готово. Но добавьте еще два региона, три окружения (dev, staging, prod) и десять сервисов. Что происходит?
- Вы меняете версию AMI (Amazon Machine Image) в одном месте, забываете про другое.
- Настройки безопасности (Security Groups) расходятся между окружениями.
- Новый разработчик тратит неделю на то, чтобы понять, почему здесь поднят S3-бакет, а там - нет.
Проблема не в Terraform, а в отсутствии абстракции. Модули решают эту проблему, создавая «черные ящики» с предсказуемым входом и выходом. Вместо того чтобы писать 50 строк кода для создания EC2-инстанса, вы вызываете модуль с тремя параметрами.
Анатомия идеального модуля
Чтобы модуль был полезным, он должен быть самодостаточным, но гибким. Давайте посмотрим на структуру папки модуля, например, modules/vpc/.
- variables.tf: Здесь описываются входные параметры. Не пишите
variable "vpc_name" {}без типа. Укажитеtype = string,default = "my-vpc". Это делает код самодокументирующим. - main.tf: Логика создания ресурсов. Здесь мы связываем переменные с реальными ресурсами AWS, GCP или Azure.
- outputs.tf: Критически важный файл. Он определяет, что модуль «возвращает» наружу. Например, ID созданной VPC или публичный IP адреса.
- README.md: Обязательный файл. Без него модуль бесполезен для других команд. Опишите, какие переменные принимает модуль и что выводит.
Важное правило: модуль не должен знать о контексте приложения. Модуль vpc не должен заботиться о том, что внутри будет работать Kubernetes или Docker. Он просто создает сеть. Контекст добавляется на уровне вызова модуля.
Как правильно вызывать модули
Вызов модуля происходит в корневом файле проекта (обычно main.tf верхнего уровня). Вот пример того, как выглядит вызов нашего модуля VPC:
module "vpc" {
source = "./modules/vpc"
name = "production-vpc"
cidr_block = "10.0.0.0/16"
azs = ["eu-central-1a", "eu-central-1b"]
public_subnets = true
}
Обратите внимание на атрибут source. Он может указывать на локальную папку (./modules/...), Git-репозиторий или даже на официальный реестр Terraform Registry. Использование локальных путей удобно для начала, но для больших организаций лучше хранить общие модули в центральном Git-репозитории. Это гарантирует, что все команды используют одну и ту же версию проверенного кода.
Если вы используете Git, путь к модулю может выглядеть так: git::https://github.com/company/terraform-modules.git//modules/vpc?ref=v1.2.0. Фиксация версии через tag или commit hash спасает от сюрпризов при обновлении зависимостей.
Версионирование и управление изменениями
Здесь кроется главная боль. Что делать, если нужно изменить логику модуля, которая уже используется в 20 местах? Если вы измените код модуля напрямую, Terraform увидит изменения и попытается применить их ко всем окружениям сразу. Иногда это хорошо, иногда - катастрофа.
Есть два подхода:
- Совместимые изменения (Backwards Compatible): Добавление новых опциональных переменных с дефолтными значениями. Старый код продолжает работать, новый получает фичи.
- Несовместимые изменения (Breaking Changes): Переименование ресурсов, изменение типов, удаление обязательных параметров. Такое требует создания новой версии модуля (например, v2.0.0).
Рекомендация: всегда используйте семантическое версионирование (SemVer) для ваших модулей. Мажорная версия меняется при breaking changes, минорная - при добавлении фич, патч - при исправлении багов. Это позволяет командам выбирать, когда им переходить на новую версию.
Таблица сравнения подходов к организации модулей
Типичные ошибки и как их избежать
Даже опытные инженеры совершают одни и те же ошибки. Вот топ-3 проблемы, которые встречаются чаще всего:
- Чрезмерная абстракция. Создание модуля для одного ресурса (например, отдельного Security Group). Лучше объединять связанные ресурсы в один модуль (например, «networking»), чем плодить десятки мелких модулей. Правило: если модуль имеет менее 3 ресурсов и 2 переменных, возможно, его стоит инлайнить в основной код.
- Непредсказуемые имена ресурсов. Если в модуле используются хэши или случайные суффиксы без необходимости, это усложняет отладку. Используйте стабильные имена, основанные на переменных входа.
- Игнорирование state. Забывание о том, что модуль хранит состояние. При переносе модуля из одного окружения в другое обязательно используйте
terraform state mvили импорт, иначе Terraform попытается создать дубликаты ресурсов.
Практический пример: модуль для баз данных
Рассмотрим конкретный кейс. Нам нужен модуль для создания PostgreSQL базы данных в RDS (AWS Relational Database Service).
В файле variables.tf мы объявим:
db_instance_class(тип: string, дефолт: db.t3.micro)db_name(тип: string, обязательный)allocated_storage(тип: number, дефолт: 20 GB)
В main.tf мы создадим ресурс aws_db_instance, привязав к нему эти переменные. Также добавим ресурс aws_db_subnet_group и aws_db_security_group (или правила безопасности), чтобы база была доступна только из внутренней подсети.
В outputs.tf выведем:
endpoint- адрес подключения.port- порт.username- имя пользователя (если генерируем его).
Такой модуль можно использовать для любой базы данных в любом проекте. Разработчику не нужно знать детали настройки RDS. Ему достаточно передать название БД и размер дисков. Все остальное Terraform сделает по шаблону.
Интеграция с CI/CD
Модули становятся по-настоящему мощными, когда интегрируются в конвейер сборки. Представьте: разработчик пушит изменения в ветку feature/new-db-module. CI-система (GitHub Actions, GitLab CI) запускает terraform plan для этого модуля в изолированном тестовом окружении. Если план проходит успешно и тесты (Terratest, для примера) зеленые, изменения мержаются в main. После этого модуль автоматически публикуется в ваш внутренний реестр с новой версией.
Это устраняет человеческий фактор. Никто не забудет протестировать изменения перед тем, как они попадут в прод. И никто не сломает чужой код, потому что каждый модуль проверяется независимо.
Чек-лист перед публикацией модуля
Прежде чем отправлять модуль в общий доступ, пройдитесь по этому списку:
- Все переменные имеют типы и дефолтные значения (где возможно).
- README.md содержит примеры использования.
- Проверены права доступа (IAM policies) - модуль не запрашивает лишних прав.
- Тестировалось применение модуля с нуля (
terraform apply) и обновление существующего состояния. - Имена ресурсов уникальны и читаемы.
- Нет хардкода значений (регионы, аккаунты), которые должны передаваться как переменные.
Часто задаваемые вопросы
Можно ли использовать модуль внутри другого модуля?
Да, Terraform поддерживает вложенность модулей. Однако злоупотреблять этим не стоит. Глубина более двух уровней затрудняет отладку и понимание потока данных. Лучше создавать плоскую структуру или комбинировать модули на уровне корня.
Как передать весь объект в качестве переменной модуля?
Используйте тип object или map(string). Например, variable "tags" { type = map(string) }. Это позволяет передавать набор меток одним параметром, вместо десяти отдельных переменных.
Что делать, если модуль из Registry устарел?
Скопируйте исходный код модуля в свой репозиторий, внесите необходимые исправления и ведите его как собственный внутренний модуль. Это называется «форком». Вы теряете автоматические обновления от авторов, но получаете полный контроль.
Нужно ли разделять модули по облачным провайдерам?
Обычно да. Модуль для AWS VPC отличается от модуля для GCP VPC логикой и ресурсами. Создавайте отдельные папки aws_vpc и gcp_vpc. Универсальные модули, работающие в любом облаке, пишут редко и сложны в поддержке.
Как отладить ошибку в модуле?
Используйте команду terraform console. Она открывает интерактивную среду, где можно выводить значения переменных и ресурсов модуля во время выполнения плана. Также полезно временно добавлять ресурс null_resource с логиной в консоль для отслеживания значений.