linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Уроки
  • How it works
  • База знаний
  • Шпаргалка
  • Capstone
  • Собеседование
home/terraform/kb/Модули/tf-module-composition

kb/modules ── Модули ── intermediate

Композиция модулей: module of modules, передача провайдеров

Модуль может вызывать другие модули, внутри своего main.tf через тот же `module` блок. Адрес в state становится `module.A.module.B.<тип>.<имя>`. Провайдеры наследуются по умолчанию, но при нескольких alias'ах (multi-region, multi-account): передаются явно через `providers = { aws = aws.eu }`. `for_each` над модулем работает с TF 0.13+.

view as markdownaka: terraform-module-composition, terraform-nested-modules

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

Внутри child-модуля можно объявить ещё один module блок, точно так же, как в root'е:

hcl
# modules/app/main.tf
module "data_bucket" {
  source = "../s3-bucket"   # относительно файла, в котором написан блок
  name   = "${var.app_name}-data"
}
module "logs_bucket" {
  source = "../s3-bucket"
  name   = "${var.app_name}-logs"
}

Вызов из root:

hcl
# main.tf
module "billing" {
  source   = "./modules/app"
  app_name = "billing"
}

В state получается:

module.billing.module.data_bucket.aws_s3_bucket.this
module.billing.module.logs_bucket.aws_s3_bucket.this

Адрес отражает иерархию. Может быть три уровня, четыре, сколько угодно, но не делай больше двух без сильного основания. Каждый уровень = ещё один слой косвенности при дебаге.

Относительный путь в source

source = "../s3-bucket", путь относительно .tf файла модуля app, а не root'а. Это удобно при переноске папок: вся ./modules/ остаётся внутренне-consistent, неважно куда её положили.

Передача данных между модулями

Один модуль возвращает output, другой принимает его как input:

hcl
module "vpc" {
  source = "./modules/vpc"
  cidr   = "10.0.0.0/16"
}
module "alb" {
  source     = "./modules/alb"
  vpc_id     = module.vpc.id
  subnet_ids = module.vpc.public_subnet_ids
}

Terraform автоматически строит зависимость: module.alb создаётся после module.vpc. Никакого depends_on руками, interpolation делает это implicit. См. tf-depends-on.

Если нужен явный depends_on на модуль (модуль X должен ждать модуль Y, но output Y не используется в X): это есть с TF 0.13+:

hcl
module "alb" {
  source     = "./modules/alb"
  vpc_id     = module.vpc.id
  depends_on = [module.iam_roles]
}

Это редкий кейс. Если ты пишешь depends_on на модуль, подумай, нельзя ли вместо этого передать output. Обычно можно.

Передача провайдеров

По умолчанию модуль наследует провайдер от родителя:

hcl
# root
provider "aws" {
  region = "us-east-1"
}
module "logs" {
  source = "./modules/s3-bucket"
  # модуль использует provider "aws" автоматически, он один.
}

Сложности начинаются с alias-провайдерами. Например, ресурс multi-region:

hcl
provider "aws" {
  region = "us-east-1"
  alias  = "us"
}
provider "aws" {
  region = "eu-west-1"
  alias  = "eu"
}
module "logs_us" {
  source = "./modules/s3-bucket"
  providers = {
    aws = aws.us
  }
  name = "linuxlab-logs-us"
}
module "logs_eu" {
  source = "./modules/s3-bucket"
  providers = {
    aws = aws.eu
  }
  name = "linuxlab-logs-eu"
}

Если модуль ожидает несколько alias-ов, он сам должен это объявить:

hcl
# modules/multi-region-bucket/versions.tf
terraform {
  required_providers {
    aws = {
      source                = "hashicorp/aws"
      configuration_aliases = [aws.primary, aws.replica]
    }
  }
}

И в HCL модуля:

hcl
resource "aws_s3_bucket" "primary" {
  provider = aws.primary
  bucket   = var.name
}
resource "aws_s3_bucket" "replica" {
  provider = aws.replica
  bucket   = "${var.name}-replica"
}

Вызов:

hcl
module "geo" {
  source = "./modules/multi-region-bucket"
  providers = {
    aws.primary = aws.us
    aws.replica = aws.eu
  }
  name = "geo-data"
}

Это самый сложный случай. В простых модулях configuration_aliases не нужен, наследование провайдера достаточно.

for_each и count над модулем

С TF 0.13+ for_each и count работают над module блоком:

hcl
variable "buckets" {
  type = map(object({
    versioning = bool
  }))
  default = {
    logs = { versioning = false }
    data = { versioning = true }
  }
}
module "bucket" {
  source   = "./modules/s3-bucket"
  for_each = var.buckets
  name               = "linuxlab-${each.key}"
  versioning_enabled = each.value.versioning
}

Получаешь module.bucket["logs"] и module.bucket["data"] в state. Доступ из root:

hcl
output "all_arns" {
  value = { for k, m in module.bucket : k => m.arn }
}

Это базовая техника интермедиат-уровня: одна декларация модуля + map ⇒ N экземпляров со своими параметрами. См. tf-count-for-each.

Когда for_each, когда count

То же правило что для ресурсов: map для именованных (стабильные ключи при удалении), count для индексированных (когда порядок и количество важнее имён). Для модулей for_each практически всегда правильнее.

Передача переменных целиком

Если модулей много, и у них общие параметры, выноси их в locals:

hcl
locals {
  common_tags = {
    Owner   = "platform-team"
    Project = "billing"
    Env     = var.env
  }
}
module "app" {
  source = "./modules/app"
  tags   = local.common_tags
  # ...
}
module "billing" {
  source = "./modules/billing"
  tags   = local.common_tags
  # ...
}

Не пиши «передаю каждый тег отдельно» через 5 переменных. Один tags, один map(string), модули его расширяют через merge(var.tags, {Module = "..."}).

Подводные камни

  • Provider'ы внутри child-модуля, не пиши. Объявить provider "aws" { ... } внутри child-модуля можно, но не нужно. Это deprecated с TF 0.13. Провайдеры конфигурируются в root, передаются в child через providers = {}.

  • for_each на модуле + count внутри модуля, допустимо, но головоломно. State становится module.X["key"].aws_s3_bucket.this[0]. Дебажить тяжело. Старайся держать иерархию плоской.

  • depends_on на модуль не работает идеально до TF 1.4. В старых версиях зависимости через depends_on на модуле срабатывали неполно. Terraform мог начать destroy на ресурсе до destroy на «зависимом» модуле. С TF 1.4+ это починено, но если живёшь на 1.0-1.3, не полагайся.

  • module.X нельзя ссылаться целиком. output "all_arns" { value = module.bucket }, нельзя. Только конкретные outputs: module.bucket.arn, module.bucket.bucket. Если хочется «всё», собирай вручную через object.

  • Глубокая вложенность ломает state mv. Перенос ресурса из module.A.module.B.module.C.aws_s3_bucket.this в другое место, путь длинный, ошибиться легко. Дерево лучше держать плоским (root + 1 уровень модулей), а не цепочку из 3-4.

  • Передача providers ломается при удалении alias. Убрал provider "aws" { alias = "eu" } из root, все модули с providers = { aws = aws.eu } падают на plan. Сначала убери module блок, потом alias, не наоборот.

§ команды

bash
terraform state list

Видны все ресурсы со всех уровней модулей. По адресам читается иерархия.

bash
terraform plan -target=module.vpc

Спланировать только модуль vpc (и его зависимости). Полезно для дебага зависимостей.

bash
terraform graph | dot -Tpng > /tmp/g.png

Граф модулей и ресурсов. Видно почему apply строит в этом порядке.

bash
terraform providers

Видно какие провайдеры (и их alias) каждый модуль использует. Помогает при дебаге передачи.

§ см. также

  • tf-module-inputs-outputsКонтракт модуля: input variables и outputsСнаружи модуль виден только через input variables (что принимает) и output values (что отдаёт). Всё остальное, детали реализации. Хороший модуль скрывает ресурсы за этим контрактом так, чтобы можно было переписать тело без изменения вызовов в root-модуле.
  • tf-count-for-eachcount и for_each: несколько ресурсов из одного блокаcount создаёт N одинаковых ресурсов по индексам 0..N-1. for_each: ресурсы по ключам из set или map. Правило: count для одинаковых, for_each когда у каждого свои настройки. Если сомневаешься, бери for_each.
  • tf-provider-blockБлок provider: кому Terraform будет звонитьБлок provider настраивает плагин: в какой регион AWS обращаться, какие endpoints использовать, какие credentials брать. Один такой блок на провайдера в большинстве случаев хватит.
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки