lesson ── terraform-production ── ~16 мин ── 5 шагов
С Terraform 1.6 встроен test-runner. Файлы *.tftest.hcl описывают
сценарии через run блоки и проверки assert. На этом уроке сделаешь
модуль bucket, напишешь к нему тесты в command = plan режиме
(быстро, без apply; провайдер всё равно поднимется и сходит к
endpoint'у — на LocalStack это безопасно) и в command = apply.
интерактивный sandbox
Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
cd /home/student/tf-tests/modules/bucket
cat > main.tf <<'EOF'
resource "aws_s3_bucket" "this" {bucket = var.name
tags = merge(
{ ManagedBy = "terraform" },var.tags,
)
}
EOF
cat > variables.tf <<'EOF'
variable "name" {type = string
validation {condition = length(var.name) >= 3 && length(var.name) <= 63
error_message = "bucket name must be 3-63 chars"
}
}
variable "tags" {type = map(string)
default = {}}
EOF
cat > outputs.tf <<'EOF'
output "id" {value = aws_s3_bucket.this.id
}
EOF
Модуль готов. У него один input name (с validation), опциональные
tags, output id.
✓ Модуль bucket написан. Сейчас обмажем тестами.
Тесты идут рядом с модулем. Конвенция, каталог tests/:
mkdir -p /home/student/tf-tests/modules/bucket/tests
cat > /home/student/tf-tests/modules/bucket/tests/naming.tftest.hcl <<'EOF'
variables {name = "my-test-bucket"
}
run "bucket_name_propagates" {command = plan
assert {condition = aws_s3_bucket.this.bucket == "my-test-bucket"
error_message = "var.name does not reach the resource"
}
}
run "managedby_tag_set" {command = plan
assert {condition = aws_s3_bucket.this.tags["ManagedBy"] == "terraform"
error_message = "ManagedBy tag missing"
}
}
run "custom_tags_merged" {command = plan
variables { tags = { Owner = "ci" }}
assert {condition = aws_s3_bucket.this.tags["Owner"] == "ci"
error_message = "Custom tag not merged"
}
}
EOF
Запусти:
cd /home/student/tf-tests/modules/bucket
terraform init -backend=false
terraform test
Должно показать «3 passed, 0 failed». Все три теста в режиме plan облако не нужно.
✓ Три тестов pass'нулись. plan-only, быстро.
Validation должна срабатывать. Тест на это:
cat > /home/student/tf-tests/modules/bucket/tests/validation.tftest.hcl <<'EOF'
run "rejects_too_short_name" {command = plan
variables {name = "ab"
}
expect_failures = [
var.name,
]
}
run "rejects_too_long_name" {command = plan
variables {name = "this-name-is-way-too-long-and-should-fail-because-its-over-63-characters-easy"
}
expect_failures = [
var.name,
]
}
EOF
terraform test
Должно показать «5 passed». Эти два теста pass'нулись потому что validation действительно зафейлил, то, чего мы и ожидали.
✓ Validation проверена. Тесты ловят инверсные кейсы.
Plan-only видит только статику. Чтобы проверить реальное создание ресурса (например, что id совпадает с именем), нужен apply. Provider в тесте должен указывать на LocalStack.
cat > /home/student/tf-tests/modules/bucket/tests/apply.tftest.hcl <<'EOF'
provider "aws" {region = "us-east-1"
access_key = "test"
secret_key = "test"
s3_use_path_style = true
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints {s3 = "http://localstack:4566"
iam = "http://localstack:4566"
sts = "http://localstack:4566"
}
}
run "real_apply_creates_bucket" {command = apply
variables {name = "test-apply-bucket"
}
assert {condition = aws_s3_bucket.this.id == "test-apply-bucket"
error_message = "id should equal name for s3 bucket"
}
assert {condition = length(output.id) > 0
error_message = "output.id is empty"
}
}
EOF
terraform test
Должно показать «6 passed». Apply-test реально создал бакет в LocalStack и сразу убрал, runner делает destroy после.
✓ Apply-test прошёл. Бакет был создан и снесён за один тест.
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 для полной матрицы.
При разработке теста бесполезно гонять все остальные. Filter:
terraform test -filter=tests/validation.tftest.hcl
Гоняет только validation-тесты, остальные skip'ает. Time-save при длинных apply-сценариях.
Несколько фильтров:
terraform test \
-filter=tests/naming.tftest.hcl \
-filter=tests/validation.tftest.hcl
Эти два файла, apply-test не запустится.
✓ Filter работает. В CI-pipeline это даёт быстрые PR-checks отдельно от долгих integration-тестов.
Тесты, обязательство. Каждый нужно поддерживать. Антипаттерны:
output "id" { value = ... }
тестировать что output равен ARN'у нечем; if это сломалось,
plan не сработает вообще.Что тестировать:
merge, format, conditional).expect_failures).no diff after refactor, golden plan).См. iac-testing-theory.
.tftest.hcl файлы рядом с модулем. run, сценарий, assert
проверка. command = plan для быстрых unit-тестов, command = apply
для интеграционных. expect_failures для инверсных проверок
(что валидация сработала).
команды
terraform testпрогнать все *.tftest.hcl.terraform test -filter=tests/foo.tftest.hclтолько один файл, удобно при разработке теста.terraform test -verboseпоказывает plan-output каждого run'а, для debug.концепции