lesson ── terraform-production ── ~14 мин ── 5 шагов
Apply tests are expensive, and even LocalStack costs seconds on every run.
Mock providers swap the real AWS for synthesized answers; command = apply
becomes fast and offline. There are three tools: mock_provider,
override_resource, override_data. Available from Terraform 1.7+.
интерактивный sandbox
Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
To show off a mock with defaults you need a module where one resource references another through its id.
cd /home/student/tf-mocks/modules/audited-bucket
cat > main.tf <<'EOF'
resource "aws_s3_bucket" "this" {bucket = var.name
}
resource "aws_s3_bucket_versioning" "this" {bucket = aws_s3_bucket.this.id
versioning_configuration {status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "this" {bucket = aws_s3_bucket.this.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
EOF
cat > variables.tf <<'EOF'
variable "name" {type = string
}
EOF
cat > outputs.tf <<'EOF'
output "id" {value = aws_s3_bucket.this.id
}
EOF
Three resources: the bucket, versioning, and a public-access-block.
Versioning and the ACL both reference aws_s3_bucket.this.id.
✓ The module with cross-references is ready.
cat > /home/student/tf-mocks/modules/audited-bucket/tests/mock_bare.tftest.hcl <<'EOF'
mock_provider "aws" {}run "with_bare_mock" {command = apply
variables {name = "test-mocked"
}
assert {condition = aws_s3_bucket.this.bucket == "test-mocked"
error_message = "bucket name not propagated"
}
}
EOF
cd /home/student/tf-mocks/modules/audited-bucket
terraform init -backend=false
terraform test -filter=tests/mock_bare.tftest.hcl
You should see a pass. mock_provider "aws" {} generates defaults from
the schema, with no cloud at all. The apply is instant.
✓ The bare mock works. One test without LocalStack.
A bare mock returns "foo" for every string attribute. If two resources
reference aws_s3_bucket.this.id, both get "foo". That is fine, but
opaque. Let's pin an explicit id:
cat > /home/student/tf-mocks/modules/audited-bucket/tests/mock_with_defaults.tftest.hcl <<'EOF'
mock_provider "aws" { mock_resource "aws_s3_bucket" { defaults = {id = "mocked-id-001"
arn = "arn:aws:s3:::mocked-id-001"
}
}
}
run "versioning_references_correct_bucket" {command = apply
variables {name = "test-with-defaults"
}
assert {condition = aws_s3_bucket_versioning.this.bucket == "mocked-id-001"
error_message = "versioning should reference bucket id mocked-id-001"
}
assert {condition = aws_s3_bucket_public_access_block.this.bucket == "mocked-id-001"
error_message = "PAB should reference bucket id mocked-id-001"
}
}
EOF
terraform test -filter=tests/mock_with_defaults.tftest.hcl
You should see a pass. Versioning and the PAB reference mocked-id-001
correctly, because the mock pinned that for the bucket.
✓ Cross-resource references are verified without the cloud.
Sometimes the base mock defaults fit most runs, but one run needs a
different value. An override_resource inside a run takes priority over
the file-level mock.
cat > /home/student/tf-mocks/modules/audited-bucket/tests/override.tftest.hcl <<'EOF'
mock_provider "aws" { mock_resource "aws_s3_bucket" { defaults = {id = "default-id"
}
}
}
run "default_mock_used" {command = apply
variables { name = "x" } assert {condition = aws_s3_bucket_versioning.this.bucket == "default-id"
error_message = "expected default-id"
}
}
run "override_for_this_run" {command = apply
variables { name = "y" } override_resource {target = aws_s3_bucket.this
values = {id = "specific-override-id"
}
}
assert {condition = aws_s3_bucket_versioning.this.bucket == "specific-override-id"
error_message = "expected override id"
}
}
EOF
terraform test -filter=tests/override.tftest.hcl
You should see 2 passed. The first run uses the file-level default, the second uses its own override. This is handy for "one shared test plus special cases" without duplicating the mock config.
✓ Mock and override work together.
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. On a first switch, though, back
up the state and do a run on a feature branch. The differences
cluster in the newer features (variables in backend,
state encryption, OCI registry-backed modules). See
tf-opentofu-parity for the full matrix.
Let's show the difference briefly. Run all the mock tests under time:
time terraform test \
-filter=tests/mock_bare.tftest.hcl \
-filter=tests/mock_with_defaults.tftest.hcl \
-filter=tests/override.tftest.hcl
Note down the real time (1.2s, say).
Create an equivalent non-mock test:
cat > tests/realstack.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" {command = apply
variables { name = "real-test-bucket" } assert {condition = aws_s3_bucket.this.id == "real-test-bucket"
error_message = "id mismatch"
}
}
EOF
time terraform test -filter=tests/realstack.tftest.hcl
Compare them. LocalStack takes longer because of the real provider and the HTTP calls. Mock is 5 to 20 times faster. On a large test suite the difference is minutes.
This is what mocks exist for. In CI: the bulk of your unit tests on mocks; a couple of integration tests on LocalStack or AWS.
✓ Mock vs real: the difference is noticeable. Mock for the bulk of CI, LocalStack selectively.
Mock vs real is not dogma. Cases where mock loses:
A test on HCL correctness that hits a real API. Take
aws_iam_policy_document, a data source that renders JSON locally.
A mock returns a placeholder, the real data source returns the
actual policy. If your assert is length(jsondecode(...)) == 5,
you need the real one.
Cross-resource through a computed attribute. If resource A
returns a computed arn in a non-standard shape, and B parses it,
the mock returns "foo", B fails to parse, and the test fails by
mistake. An override saves you, but it is boilerplate.
You are testing the provider itself. A mock checks that "the HCL describes what you want correctly". If you want to be sure AWS accepts your config, you need the real one.
Rule of thumb: mock for "a test on your own code", real for "a test on the integration with the cloud".
mock_provider "aws" {} in a *.tftest.hcl file replaces the provider
with a fake. mock_resource "X" { defaults = {...} } sets values for one
specific type. override_resource / override_data swap a single value
inside one run block.
команды
terraform test -filter=tests/mocked.tftest.hclmock tests are faster, every apply is offline.terraform test -verboseyou see which overrides were applied.концепции