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-production-03-native-tests

lesson ── terraform-production ── ~16 мин ── 5 шагов

Нативные тесты, .tftest.hcl и assert

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

запустить sandbox →

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

Шаги

  1. 01

    Напиши минимальный модуль bucket

    bash
    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 написан. Сейчас обмажем тестами.

  2. 02

    Первый тест, command = plan

    Тесты идут рядом с модулем. Конвенция, каталог tests/:

    bash
    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

    Запусти:

    bash
    cd /home/student/tf-tests/modules/bucket
    terraform init -backend=false
    terraform test

    Должно показать «3 passed, 0 failed». Все три теста в режиме plan облако не нужно.

    ✓ Три тестов pass'нулись. plan-only, быстро.

  3. 03

    expect_failures, тест на валидацию

    Validation должна срабатывать. Тест на это:

    bash
    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 проверена. Тесты ловят инверсные кейсы.

  4. 04

    command = apply на LocalStack

    Plan-only видит только статику. Чтобы проверить реальное создание ресурса (например, что id совпадает с именем), нужен apply. Provider в тесте должен указывать на LocalStack.

    bash
    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

    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
  5. 05

    Прогнать только один тест-файл

    При разработке теста бесполезно гонять все остальные. Filter:

    bash
    terraform test -filter=tests/validation.tftest.hcl

    Гоняет только validation-тесты, остальные skip'ает. Time-save при длинных apply-сценариях.

    Несколько фильтров:

    bash
    terraform test \
      -filter=tests/naming.tftest.hcl \
      -filter=tests/validation.tftest.hcl

    Эти два файла, apply-test не запустится.

    ✓ Filter работает. В CI-pipeline это даёт быстрые PR-checks отдельно от долгих integration-тестов.

    Когда тест не нужен

    Тесты, обязательство. Каждый нужно поддерживать. Антипаттерны:

    1. Тест на pass-through. output "id" { value = ... } тестировать что output равен ARN'у нечем; if это сломалось, plan не сработает вообще.
    2. Тест на провайдер. «После apply бакет действительно создаётся», это работа AWS-provider'а, твой код не виноват.
    3. Тест на каждый атрибут. В модуле 20 переменных, тестировать что каждая «доходит», копи-паст. Тестируй один happy path и один-два кейса валидации.

    Что тестировать:

    • Сложные expressions (merge, format, conditional).
    • Validation (expect_failures).
    • Бизнес-инвариант («все имена начинаются с префикса team-»).
    • Рефакторинг (no diff after refactor, golden plan).

    См. iac-testing-theory.

    • → tftest.hcl целиком
    • → Что тестировать, что нет

Что ты узнал

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

концепции

  • · tests/, конвенция, не требование; файлы можно класть рядом с .tf
  • · command=plan не делает apply, но провайдер всё равно стартует (refresh, data sources, валидация креденшалов); полностью offline, только с mock_provider
  • · expect_failures = [var.x] делает test позитивным, если validation сработала

← предыдущий

for_each над модулем. N экземпляров одним блоком

следующий →

Large-scale state, разбиение монолита

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