# Композиция модулей: module of modules, передача провайдеров _Модули · TerraformLab Knowledge Base_ **TL;DR:** Модуль может вызывать другие модули, внутри своего main.tf через тот же `module` блок. Адрес в state становится `module.A.module.B.<тип>.<имя>`. Провайдеры наследуются по умолчанию, но при нескольких alias'ах (multi-region, multi-account): передаются явно через `providers = { aws = aws.eu }`. `for_each` над модулем работает с TF 0.13+. ## Модуль внутри модуля Внутри 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](/terraform/kb/tf-depends-on.md). Если нужен **явный** 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](/terraform/kb/tf-count-for-each.md). ### Когда 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) каждый модуль использует. Помогает при дебаге передачи. ## См. также - [Контракт модуля: input variables и outputs](/terraform/kb/tf-module-inputs-outputs.md) - [count и for_each: несколько ресурсов из одного блока](/terraform/kb/tf-count-for-each.md) - [Блок provider: кому Terraform будет звонить](/terraform/kb/tf-provider-block.md)