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/lessons/tf-intermediate-01-first-module

lesson ── terraform-intermediate ── ~18 мин ── 7 шагов

Свой первый модуль: выноси S3 в reusable

Двенадцать 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 минут, без регистрации.

запустить sandbox →

stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя

Шаги

  1. 01

    Подготовь скелет модуля

    Создай файловую структуру:

    bash
    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

    • outputs`, стандарт, привыкай.
    подсказка

    Можно одной командой: `mkdir -p modules/audited-bucket && cd $_ && touch main.tf variables.tf outputs.tf`.

    ✓ Скелет готов. Теперь: контракт модуля.

  2. 02

    Опиши input variables

    Контракт модуля = что он принимает. В modules/audited-bucket/variables.tf:

    hcl
    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`.

    ✓ Контракт описан. Теперь ресурсы внутри модуля.

  3. 03

    Опиши ресурсы внутри модуля

    В modules/audited-bucket/main.tf:

    hcl
    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 тег с пользовательскими.
    • Versioning, это отдельный ресурс с TF 4.x+, не подблок.

    ✓ Ресурсы написаны. Осталось: что модуль отдаёт наружу.

  4. 04

    Опиши outputs

    В modules/audited-bucket/outputs.tf:

    hcl
    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

    • только то, что объявлено как output.

    ✓ Контракт замкнулся. Теперь: вызов из root.

  5. 05

    Вызови модуль из root

    Создай main.tf в /home/student/tf-module/:

    hcl
    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.
    • Root всё ещё может иметь собственные ресурсы, random_id живёт в root, не в модуле.

    ✓ Вызов готов. Теперь init и apply.

  6. 06

    init и apply: модуль попадает в state

    bash
    cd /home/student/tf-module
    terraform init

    В выводе:

    Initializing modules...
    - logs in modules/audited-bucket

    Это symlink в .terraform/modules/logs/ указывает на твой modules/audited-bucket/. См. tf-init-modules.

    bash
    terraform apply -auto-approve

    Должны создаться два ресурса (bucket + versioning) внутри модуля и один random_id в root.

    ✓ Apply прошёл. Plan чистый, значит state и HCL совпадают.

    То же самое на OpenTofu

    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 для полной матрицы.

    • → OpenTofu parity
  7. 07

    Проверь как модуль выглядит в state

    bash
    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. Контракт работает.

    Почему модуль, а не два resource блока в root

    Сейчас у тебя один вызов модуля. Кажется избыточно. Но:

    • Добавь ещё бакет, module "data" { source = "./modules/audited-bucket", ... }, одной строкой. Без модуля пришлось бы скопировать оба aws_s3_bucket + aws_s3_bucket_versioning.
    • Поменяй политику для всех (например, добавь aws_s3_bucket_public_access_block): правишь модуль, плюс ко всем вызовам. Без модуля, править N мест.
    • Тестируй модуль один раз, assertions на одном входе работают для всех.

    Это окупается на 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 с таблицей переменных

концепции

  • · Модуль: это директория; root vs child: это роль, а не разный тип артефакта
  • · Снаружи модуля видны только variables (input) и outputs
  • · В state ресурсы модуля: module.<имя>.<тип>.<имя>

← предыдущий

Troubleshooting Garden: распутай Cycle Error

следующий →

Линтеры, fmt, validate, tflint

Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки