lesson ── terraform-intermediate ── ~18 мин ── 7 шагов
The twelve beginner lessons kept a single S3 bucket in main.tf. That works
while you have few buckets and they are all different. The moment you need
three "identical" buckets with versioning, tags, and a lifecycle, copy and
paste starts to bite. The fix is a module.
In this lesson you move the S3 bucket into ./modules/audited-bucket/,
describe its contract (variables and outputs), and call the module from the
root. This is your first reusable unit, the foundation of the intermediate
track.
интерактивный sandbox
Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
Create the file structure:
cd /home/student/tf-module
mkdir -p modules/audited-bucket
touch modules/audited-bucket/{main.tf,variables.tf,outputs.tf}ls -R modules/
You should get:
modules/
└── audited-bucket/
├── main.tf
├── outputs.tf
└── variables.tf
Three files, by convention. Terraform reads every .tf in a directory
and joins them, so where you put what does not matter. But the split
into main + variables + outputs is the standard, so get used to it.
You can do it in one command: `mkdir -p modules/audited-bucket && cd $_ && touch main.tf variables.tf outputs.tf`.
✓ Skeleton ready. Now the module's contract.
A module's contract is what it accepts. In modules/audited-bucket/variables.tf:
variable "name" {type = string
description = "S3 bucket name. Must be globally unique."
validation {condition = length(var.name) >= 3 && length(var.name) <= 63
error_message = "Bucket name must be 3-63 characters."
}
}
variable "versioning_enabled" {type = bool
description = "Enable versioning. Usually true for prod buckets."
default = false
}
variable "tags" {type = map(string)
description = "Extra tags. The module adds its own on top."
default = {}}
Notice: name has no default, so it is required. The others have a
default, so they are optional. That is the "contract": what the user
must pass and what they may leave out. See tf-module-inputs-outputs.
If no editor is available inside the sandbox: `cat > FILE <<'EOF' ... EOF`.
✓ Contract described. Now the resources inside the module.
In 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"
}
}
Pay attention:
this. This is a module idiom: "the main
resource of this module." From outside you still see
module.<name>.aws_s3_bucket.this.merge() joins the mandatory Module tag with the user's tags.✓ Resources written. What is left: what the module exposes to the outside.
In modules/audited-bucket/outputs.tf:
output "arn" {value = aws_s3_bucket.this.arn
description = "Bucket ARN. Used for IAM policies."
}
output "bucket" {value = aws_s3_bucket.this.bucket
description = "Bucket name (as created)."
}
output "versioning_status" {value = aws_s3_bucket_versioning.this.versioning_configuration[0].status
}
This is everything the root module will see. No
aws_s3_bucket_versioning.this.bucket, only what is declared as an
output.
✓ The contract is closed. Now call it from the root.
Create main.tf in /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
}
The key points:
source = "./modules/audited-bucket" is relative to the .tf file
the block is written in. See tf-module-sources.module.logs.arn: from outside, the module is seen through its
outputs.random_id lives in the
root, not in the module.✓ The call is ready. Now init and apply.
cd /home/student/tf-module
terraform init
In the output:
Initializing modules...
- logs in modules/audited-bucket
This is a symlink in .terraform/modules/logs/ pointing at your
modules/audited-bucket/. See tf-init-modules.
terraform apply -auto-approve
Two resources should be created (bucket and versioning) inside the module, plus one random_id in the root.
✓ Apply went through. The plan is clean, so state and HCL agree.
OpenTofu keeps the CLI and state compatible with Terraform for the
commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. But on the first switch, back up
the state and run on a feature branch, since the differences cluster
in the newer features (variables in the backend, state encryption,
OCI registry-backed modules). See tf-opentofu-parity for the full
matrix.
terraform state list
It should print something like:
module.logs.aws_s3_bucket.this
module.logs.aws_s3_bucket_versioning.this
random_id.suffix
The module.logs. prefix is the module's address. Inside it,
aws_s3_bucket.this and aws_s3_bucket_versioning.this. The name
"this" from the module is preserved.
This is the basic way to navigate state with modules. See tf-module-basics.
✓ module.logs.aws_s3_bucket.this is visible in state. The contract works.
Right now you have a single module call. It looks like overkill. But:
module "data" { source = "./modules/audited-bucket", ... },
in one line. Without a module you would have to copy both
aws_s3_bucket and aws_s3_bucket_versioning.aws_s3_bucket_public_access_block): you edit the module, and the
change reaches every call. Without a module you edit N places.It pays off on the third use. On the first it is overhead, on the second it is debatable, on the third it saves time. Do not build a module prematurely.
A module is a directory of .tf files that you reference through a
module block. The contract is the variable and output blocks.
Inside it are ordinary resources. In state, a module's resources live
under the prefix module.<name>..
команды
terraform initpulls module sources into .terraform/modules/terraform state list | grep ^modulewhat in state comes from modulesterraform-docs markdown table modules/Xgenerate a README with a variables tableконцепции