Kiểm Thử: terraform test, mock_provider, và Terratest
Pipeline ở bài trước lo việc chạy Terraform an toàn, nhưng nó không trả lời câu: code này có đúng không? Một module ghép tên bucket sai, một validation hụt một trường hợp — những lỗi logic đó qua được validate và tflint, chỉ lộ ra khi apply, đôi khi ở prod. Terraform có khung kiểm thử riêng để bắt chúng sớm. Bài này dùng terraform test với mock provider để test mà không cần dựng hạ tầng, và giới thiệu Terratest cho mức sâu hơn.
Mục tiêu
Viết và chạy được test bằng terraform test với file .tftest.hcl, dùng mock_provider để test không tốn hạ tầng, kiểm cả trường hợp thành công lẫn trường hợp phải thất bại, và biết khi nào cần tới Terratest.
Vì sao cần test riêng
validate kiểm cú pháp, tflint/Trivy kiểm quy ước và bảo mật. Không cái nào kiểm logic: nếu module của bạn ghép tên bucket từ project và environment, làm sao chắc nó ghép đúng tf-series-dev-data chứ không phải tf-series--data? Nếu có validation, làm sao chắc nó thực sự chặn giá trị sai? Đây là việc của kiểm thử — khẳng định hành vi mong đợi bằng assertion chạy được.
terraform test: file .tftest.hcl
Khung terraform test GA từ Terraform 1.6. Test viết trong file .tftest.hcl (thường để trong thư mục tests/), gồm các block run — mỗi run chạy một lệnh (plan hoặc apply) với bộ biến, rồi kiểm bằng assert.
Lấy một cấu hình có logic đáng test: ghép tên bucket và validate môi trường.
variable "project" { type = string }
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment phải là dev, staging hoặc prod."
}
}
locals {
bucket_name = "${var.project}-${var.environment}-data"
}
resource "aws_s3_bucket" "data" {
bucket = local.bucket_name
tags = { Environment = var.environment }
}
File test tests/bucket.tftest.hcl:
mock_provider "aws" {}
run "bucket_name_is_composed" {
command = plan
variables {
project = "tf-series"
environment = "dev"
}
assert {
condition = aws_s3_bucket.data.bucket == "tf-series-dev-data"
error_message = "Tên bucket ghép sai: ${aws_s3_bucket.data.bucket}"
}
}
run "tag_matches_env" {
command = plan
variables {
project = "tf-series"
environment = "prod"
}
assert {
condition = aws_s3_bucket.data.tags["Environment"] == "prod"
error_message = "Tag Environment không khớp."
}
}
run "invalid_env_rejected" {
command = plan
variables {
project = "tf-series"
environment = "production" # sai -> phải fail validation
}
expect_failures = [var.environment]
}
mock_provider: test không cần hạ tầng thật
Dòng mock_provider "aws" {} là điểm mấu chốt. Từ Terraform 1.7, mock_provider giả lập dữ liệu provider trả về, nên terraform test chạy mà không gọi AWS, không tạo resource, không cần credential. Test trở thành unit test thuần logic, chạy trong mili-giây, an toàn để chạy ở mọi PR. Ba kiểu assertion trong ví dụ: hai run đầu kiểm giá trị tính ra (command = plan đủ để Terraform tính giá trị mà không apply), run thứ ba dùng expect_failures để khẳng định validation phải chặn environment = "production".
Chạy:
$ terraform test
tests/bucket.tftest.hcl... in progress
run "bucket_name_is_composed"... pass
run "tag_matches_env"... pass
run "invalid_env_rejected"... pass
tests/bucket.tftest.hcl... tearing down
tests/bucket.tftest.hcl... pass
Success! 3 passed, 0 failed.
Ba test qua. expect_failures đảo ngược kỳ vọng: test pass vì validation đã chặn đúng như mong đợi — đây là cách kiểm rằng hàng rào bài 9 thật sự chặn, không phải chỉ hy vọng nó chặn.
Một test thất bại trông ra sao
Để thấy khung test bắt lỗi thật, sửa một assertion cho kỳ vọng sai (ví dụ chờ tên tf-series-WRONG):
$ terraform test
run "bucket_name_is_composed"... fail
Error: Test assertion failed
Tên bucket ghép sai: tf-series-dev-data
Failure! 2 passed, 1 failed.
Test fail in ra chính error_message bạn viết, kèm giá trị thực tế (tf-series-dev-data) — đủ để biết sai ở đâu ngay. Trong CI, một test fail làm hỏng job, chặn merge. Đây là chốt cuối cho tính đúng mà bài 18 còn thiếu.
command = plan và command = apply
Mặc định nên dùng command = plan cho test logic — nhanh, không tạo gì, ghép với mock_provider thành unit test. Khi cần kiểm hành vi thật (resource dựng lên có đúng thuộc tính AWS gán không, endpoint có sống không), dùng command = apply: Terraform dựng thật trong lúc test rồi assert trên kết quả thật, và tự dọn (tearing down) khi xong. Loại này chậm và tốn tiền hơn, dành cho integration test chạy thưa hơn unit test.
nhanh, nhiều ┌─────────────────────────┐
▲ │ unit: plan + mock_provider│ logic, không hạ tầng
│ ├─────────────────────────┤
│ │ integration: apply thật │ hành vi thật, có dọn
│ ├─────────────────────────┤
chậm, ít │ Terratest (Go) │ end-to-end, kiểm sâu
└─────────────────────────┘
Terratest: kiểm thử tích hợp bằng Go
terraform test đủ cho phần lớn nhu cầu. Khi cần kiểm sâu hơn — dựng hạ tầng rồi gọi HTTP vào endpoint, SSH vào instance, kiểm trạng thái runtime — Terratest (thư viện Go của Gruntwork) là lựa chọn lâu đời. Bạn viết test bằng Go: code gọi terraform apply, chạy các kiểm tra tùy ý trên hạ tầng thật, rồi terraform destroy ở cuối. Đổi lại sức mạnh là phải viết Go và mỗi test dựng-phá hạ tầng thật nên chậm. Quy tắc: ưu tiên terraform test cho unit/integration trong-Terraform, dùng Terratest khi cần kiểm tra ngoài phạm vi Terraform thấy được.
Tổng kết
terraform test (GA 1.6) kiểm logic cấu hình bằng file .tftest.hcl: block run chạy plan/apply với biến, assert khẳng định kết quả, expect_failures khẳng định một validation phải chặn. mock_provider (1.7) cho test chạy không cần AWS thật — nhanh, an toàn cho mọi PR. Test fail in ra error_message kèm giá trị thực, chặn merge trong CI. command = apply dùng cho integration test dựng thật rồi assert; Terratest (Go) dành cho kiểm thử end-to-end sâu ngoài tầm Terraform.
Đến đây ta đã đi hết: nền tảng, state, biến, module, đa môi trường, lifecycle, CI/CD, kiểm thử. Bài cuối ráp tất cả thành một capstone — dựng một hạ tầng đa môi trường hoàn chỉnh (VPC, ALB, ASG, RDS, S3) bằng module qua pipeline, rồi teardown sạch, khép lại series bằng một lộ trình học tiếp.