Модуль внутри модуля
Внутри child-модуля можно объявить ещё один module блок, точно так же,
как в root'е:
# 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:
# 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:
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+:
module "alb" {source = "./modules/alb"
vpc_id = module.vpc.id
depends_on = [module.iam_roles]
}
Это редкий кейс. Если ты пишешь depends_on на модуль, подумай, нельзя
ли вместо этого передать output. Обычно можно.
Передача провайдеров
По умолчанию модуль наследует провайдер от родителя:
# root
provider "aws" {region = "us-east-1"
}
module "logs" {source = "./modules/s3-bucket"
# модуль использует provider "aws" автоматически, он один.
}
Сложности начинаются с alias-провайдерами. Например, ресурс multi-region:
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-ов, он сам должен это объявить:
# modules/multi-region-bucket/versions.tf
terraform { required_providers { aws = {source = "hashicorp/aws"
configuration_aliases = [aws.primary, aws.replica]
}
}
}
И в HCL модуля:
resource "aws_s3_bucket" "primary" {provider = aws.primary
bucket = var.name
}
resource "aws_s3_bucket" "replica" {provider = aws.replica
bucket = "${var.name}-replica"}
Вызов:
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 блоком:
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:
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:
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, не наоборот.