# Контракт модуля: input variables и outputs _Модули · TerraformLab Knowledge Base_ **TL;DR:** Снаружи модуль виден только через input variables (что принимает) и output values (что отдаёт). Всё остальное, детали реализации. Хороший модуль скрывает ресурсы за этим контрактом так, чтобы можно было переписать тело без изменения вызовов в root-модуле. ## Зачем модулю контракт Модуль, это **чёрный ящик** для root'а. Root знает: «передаю эти input'ы, получаю эти output'ы». Что внутри, нерелевантно. Это позволяет: - Переписать ресурсы внутри модуля без правки root'а. - Использовать один модуль десятью разными root'ами. - Менять провайдер (`aws_s3_bucket_v2` вместо `aws_s3_bucket`) при обновлении без каскадных правок наружу. Контракт = `variable` блоки и `output` блоки. Всё остальное в модуле, деталь реализации. ## Input variables Каждый `variable` блок, параметр модуля. У него есть тип, описание, опционально default, опционально validation: ```hcl # modules/s3-bucket/variables.tf variable "name" { type = string description = "Имя S3-бакета. Должно быть глобально уникально." validation { condition = length(var.name) >= 3 && length(var.name) <= 63 error_message = "Bucket name должен быть 3-63 символа (правило AWS)." } validation { condition = can(regex("^[a-z0-9.-]+$", var.name)) error_message = "Bucket name: только lowercase, цифры, точка, дефис." } } variable "versioning_enabled" { type = bool description = "Включить versioning. Для prod-бакетов обычно true." default = false } variable "lifecycle_rules" { type = list(object({ id = string enabled = bool transition = optional(object({ days = number target = string })) })) description = "Lifecycle rules. См. examples/." default = [] } ``` ### Без default, обязательный аргумент Если у variable нет `default`, вызывающий **обязан** передать значение. Иначе terraform ругается на `plan`. Это **хорошо**: обязательные имена должны быть обязательными. ### Тип имеет значение `type = string`, пользователь может передать только строку. Не `42` (число), не `["a", "b"]` (список). Опечатка типа ловится на `plan`, не на `apply`. См. [hcl-types](/terraform/kb/hcl-types.md). ### Validation, защита границ модуля Validation ловит то, что тип не ловит: - «Длина 3-63», типу `string` всё равно. - «Только lowercase», типу `string` всё равно. - «days > 0», типу `number` всё равно. Validation запускается на `plan`, до любых API-вызовов. Это **дешёвый** способ отсечь невалидные конфигурации. Без него ошибка вылезет в AWS как «InvalidBucketName», и пользователь модуля не сразу поймёт что не так. ## Output values Outputs, то что модуль возвращает родителю: ```hcl # modules/s3-bucket/outputs.tf output "arn" { value = aws_s3_bucket.this.arn description = "ARN бакета. Используется для IAM-политик." } output "bucket" { value = aws_s3_bucket.this.bucket description = "Имя бакета (как передано в input)." } output "regional_domain_name" { value = aws_s3_bucket.this.bucket_regional_domain_name description = "DNS-имя для прямых S3-запросов." } ``` В root-модуле читаются как `module.<имя>.`: ```hcl output "logs_arn" { value = module.logs.arn } resource "aws_iam_policy" "logs_writer" { policy = jsonencode({ Statement = [{ Effect = "Allow" Action = "s3:PutObject" Resource = "${module.logs.arn}/*" }] }) } ``` ### sensitive output Если в output попадает секрет, пометь: ```hcl output "access_key_secret" { value = aws_iam_access_key.user.secret sensitive = true } ``` Это **не шифрует** значение. State по-прежнему содержит секрет в открытом виде (плохо, см. [tf-state](/terraform/kb/tf-state.md)). `sensitive = true` лишь скрывает значение из CLI-вывода `terraform output` и `plan`. Реальная защита секретов, через secret manager (Vault, SSM Parameter Store), не через `sensitive = true`. ### Output не выполняет `depends_on` неявно Если output зависит от ресурса, который существует только при определённых условиях, добавь `depends_on` явно: ```hcl output "lifecycle_rule_id" { value = length(aws_s3_bucket_lifecycle_configuration.this) > 0 ? aws_s3_bucket_lifecycle_configuration.this[0].id : null depends_on = [aws_s3_bucket_lifecycle_configuration.this] } ``` ## Как проектировать контракт ### Параметризуй то, что меняется Не делай переменную «на всякий случай». Variable должна отражать **реальное различие** между use-case'ами модуля. ```hcl # ПЛОХО: на всякий случай variable "force_destroy" { type = bool default = false } variable "object_ownership" { type = string default = "BucketOwnerEnforced" } # ещё 15 variable которые никто не меняет # ХОРОШО: только то, что реально разное между use-case'ами variable "name" { type = string } variable "tags" { type = map(string), default = {} } variable "lifecycle_rules" { type = list(object({...})), default = [] } ``` Не-параметризованные значения держи в `locals` внутри модуля. ### Имена, это контракт Переименуешь `variable "name"` → `variable "bucket_name"`, это **breaking change** для всех root'ов, которые используют модуль. Те же правила, что с API: ломать имена нельзя, или это новая major-версия. См. [tf-module-versioning](/terraform/kb/tf-module-versioning.md). ### Группируй сложные параметры в object Десять плоских `variable "rule_X_..."` хуже, чем один `variable "rules" типа list(object(...))`. С object-типом юзер видит сразу всю схему, не гадает какие из переменных идут вместе. ## Подводные камни - **Validation запускается на каждый plan.** Если в validation тяжёлая регулярка или большой `contains([...100 элементов...], var.x)`, это время. Обычно неважно, но в модулях которые вызываются 50 раз через `for_each`, может быть заметно. - **`description` нужен.** Без `description` `terraform-docs` не сгенерит нормальный README, и пользователь модуля будет читать сам HCL чтобы понять что значит `bucket_acl`. Заодно, это бесплатная документация которую IDE показывает на hover. - **`default = null` ≠ нет default.** `default = null` означает «по умолчанию значение null, и это валидно». Если хочешь сделать аргумент обязательным, убирай default целиком, а не ставь null. - **`sensitive = true` на input не шифрует, только маскирует.** Те же оговорки что для output: значение всё равно в state. См. [tf-state](/terraform/kb/tf-state.md). - **Output, ссылающийся на ресурс через `count`, ломается когда count = 0.** `aws_s3_bucket.this[0].arn` упадёт с «index 0 out of range» если ресурса нет. Используй `try(aws_s3_bucket.this[0].arn, null)` или splat: `aws_s3_bucket.this[*].arn` (вернёт список, возможно пустой). - **Нельзя сделать validation, ссылающуюся на другой variable.** TF 1.9+ добавил cross-variable validation через `precondition` в lifecycle, но в самом variable блоке всё ещё только self-validation. ## Команды ```bash terraform plan -var-file=examples/dev.tfvars ``` Тестируем контракт модуля с конкретным набором значений. ```bash terraform-docs markdown table modules/s3-bucket > modules/s3-bucket/README.md ``` Генерация README с таблицей variables и outputs. Установи отдельно: github.com/terraform-docs. ```bash terraform console -chdir=modules/s3-bucket ``` REPL внутри модуля. Полезно для проверки выражений в outputs. ## См. также - [Блок variable: вход в конфигурацию](/terraform/kb/tf-variable.md) - [Блок output: что Terraform возвращает наружу](/terraform/kb/tf-output.md) - [Композиция модулей: module of modules, передача провайдеров](/terraform/kb/tf-module-composition.md) - [sensitive в Terraform: про логи, не про шифрование](/terraform/kb/tf-sensitive.md)