Kiểm Thử: terraform test, mock_provider, và Terratest

K
Kai··5 min read·1 views

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 validatetflint, 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ừ projectenvironment, 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 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.