lesson ── terraform-intermediate ── ~18 мин ── 7 шагов
Двенадцать beginner-уроков уместили один S3-бакет в main.tf. Это работает
пока бакетов мало и они разные. Как только нужно три «одинаковых» бакета с
versioning + tags + lifecycle, копи-паста начинает кусаться. Решение,
модуль.
В этом уроке ты вынесешь S3-бакет в ./modules/audited-bucket/, опишешь
контракт (variables + outputs), вызовешь модуль из root. Это первая
reusable-единица, фундамент intermediate-трека.
интерактивный sandbox
Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
Создай файловую структуру:
cd /home/student/tf-module
mkdir -p modules/audited-bucket
touch modules/audited-bucket/{main.tf,variables.tf,outputs.tf}ls -R modules/
Должно получиться:
modules/
└── audited-bucket/
├── main.tf
├── outputs.tf
└── variables.tf
Три файла, конвенция. Terraform читает все .tf в директории и
склеивает их, не важно куда что положено. Но разбивка `main + variables
Можно одной командой: `mkdir -p modules/audited-bucket && cd $_ && touch main.tf variables.tf outputs.tf`.
✓ Скелет готов. Теперь: контракт модуля.
Контракт модуля = что он принимает. В modules/audited-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 символа."
}
}
variable "versioning_enabled" {type = bool
description = "Включить versioning. Для prod-бакетов обычно true."
default = false
}
variable "tags" {type = map(string)
description = "Дополнительные теги. Module добавит свои сверху."
default = {}}
Заметь: у name нет default, значит обязательный. У остальных
есть default, необязательные. Это и есть «контракт»: что юзер должен
передать, а что может опустить. См. tf-module-inputs-outputs.
Если редактор внутри sandbox недоступен: `cat > FILE <<'EOF' ... EOF`.
✓ Контракт описан. Теперь ресурсы внутри модуля.
В modules/audited-bucket/main.tf:
resource "aws_s3_bucket" "this" {bucket = var.name
tags = merge(
{Module = "audited-bucket"
},
var.tags,
)
}
resource "aws_s3_bucket_versioning" "this" {bucket = aws_s3_bucket.this.id
versioning_configuration {status = var.versioning_enabled ? "Enabled" : "Suspended"
}
}
Обрати внимание:
this. Это idiom для модулей: «главный ресурс этого
модуля». Снаружи всё равно увидят module.<имя>.aws_s3_bucket.this.merge() объединяет обязательный Module тег с пользовательскими.✓ Ресурсы написаны. Осталось: что модуль отдаёт наружу.
В modules/audited-bucket/outputs.tf:
output "arn" {value = aws_s3_bucket.this.arn
description = "ARN бакета. Используется для IAM-политик."
}
output "bucket" {value = aws_s3_bucket.this.bucket
description = "Имя бакета (как создан)."
}
output "versioning_status" {value = aws_s3_bucket_versioning.this.versioning_configuration[0].status
}
Это всё, что root-модуль увидит. Никаких aws_s3_bucket_versioning.this.bucket
✓ Контракт замкнулся. Теперь: вызов из root.
Создай main.tf в /home/student/tf-module/:
resource "random_id" "suffix" {byte_length = 4
}
module "logs" {source = "./modules/audited-bucket"
name = "linuxlab-mod-logs-${random_id.suffix.hex}"versioning_enabled = true
tags = {Owner = "student"
}
}
output "logs_arn" {value = module.logs.arn
}
Ключевое:
source = "./modules/audited-bucket", относительно .tf файла,
в котором написан блок. См. tf-module-sources.module.logs.arn, снаружи модуль виден через свои outputs.random_id живёт
в root, не в модуле.✓ Вызов готов. Теперь init и apply.
cd /home/student/tf-module
terraform init
В выводе:
Initializing modules...
- logs in modules/audited-bucket
Это symlink в .terraform/modules/logs/ указывает на твой
modules/audited-bucket/. См. tf-init-modules.
terraform apply -auto-approve
Должны создаться два ресурса (bucket + versioning) внутри модуля и один random_id в root.
✓ Apply прошёл. Plan чистый, значит state и HCL совпадают.
OpenTofu держит CLI и state совместимыми с Terraform по командам
этого шага: миграция обычно проходит через mv .terraform .terraform.bak; tofu init -upgrade. Но при первом переходе
сделай backup state и прогон на feature-branch - расхождения
концентрируются в новых фичах (variables в backend,
state-encryption, OCI registry-backed модули). См.
tf-opentofu-parity для полной матрицы.
terraform state list
Должно вывести что-то вроде:
module.logs.aws_s3_bucket.this
module.logs.aws_s3_bucket_versioning.this
random_id.suffix
Префикс module.logs., адрес модуля. Внутри, aws_s3_bucket.this,
aws_s3_bucket_versioning.this. Имя «this» из модуля сохранилось.
Это базовая навигация по state с модулями. См. tf-module-basics.
✓ module.logs.aws_s3_bucket.this виден в state. Контракт работает.
Сейчас у тебя один вызов модуля. Кажется избыточно. Но:
module "data" { source = "./modules/audited-bucket", ... },
одной строкой. Без модуля пришлось бы скопировать оба
aws_s3_bucket + aws_s3_bucket_versioning.aws_s3_bucket_public_access_block): правишь модуль, плюс ко всем
вызовам. Без модуля, править N мест.Это окупается на 3-м применении. На 1-м, overhead, на 2-м, спорно, на 3-м, экономия времени. Не делай модуль преждевременно.
Модуль, это директория с .tf-файлами, на которую ссылаются через
module блок. Контракт = variable и output блоки. Внутри,
обычные ресурсы. В state ресурсы модуля живут под префиксом
module.<имя>..
команды
terraform initподтягивает source модулей в .terraform/modules/terraform state list | grep ^moduleчто в state из модулейterraform-docs markdown table modules/Xсгенерить README с таблицей переменныхконцепции