Kiểm thử Infrastructure as Code với Terratest: Hướng dẫn thực tế cho môi trường Production

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

Vượt xa khỏi phân tích tĩnh cho hạ tầng

Trong nhiều năm, quy trình làm việc với Infrastructure as Code (IaC) của tôi rất dễ đoán: viết HCL, chạy terraform plan, và nhấn apply nếu kết quả trông có vẻ ổn. Cách tiếp cận này hoạt động tốt với các thiết lập cơ bản. Tuy nhiên, khi kiến trúc mở rộng, một bản ‘plan’ chỉ mang lại cảm giác an toàn giả tạo. Nó xác nhận ý định của Terraform, nhưng không thể đảm bảo rằng kết nối VPC peering thực sự điều hướng được lưu lượng hoặc cơ sở dữ liệu có thể truy cập qua cổng 5432.

Bước ngoặt đến vào sáu tháng trước. Một thay đổi nhỏ về dải CIDR trong một module VPC dùng chung đã vượt qua mọi bước kiểm tra thủ công nhưng lại gây ra xung đột định tuyến ngầm. Chúng tôi không hề hay biết cho đến khi môi trường staging bị sập. Thất bại đó đã thúc đẩy chúng tôi tích hợp Terratest. Thay vì đoán mò, giờ đây chúng tôi đối xử với hạ tầng như phần mềm. Chúng tôi khởi tạo tài nguyên thực, chạy các kiểm thử chức năng (functional tests) trên đó và hủy bỏ chúng ngay sau khi hoàn tất.

Kể từ khi áp dụng quy trình này, tỷ lệ ‘hotfix’ sau triển khai của chúng tôi đã giảm khoảng 40%. Chúng tôi không còn phải “cầu nguyện” mỗi khi triển khai vào chiều thứ Sáu nữa. Thay vào đó, chúng tôi dựa vào một bộ kiểm thử bằng Go để xác minh mọi thứ, từ phản hồi HTTP 200 đến việc gán các IAM policy cụ thể, trước khi mã nguồn được merge vào nhánh chính.

Thiết lập môi trường kiểm thử

Terratest là một thư viện Go, vì vậy bạn sẽ cần môi trường Go cùng với Terraform. Nếu bạn là một kỹ sư DevOps chưa từng chạm tới Go, đừng quá lo lắng. Hầu hết các bài kiểm thử hạ tầng đều tuân theo một khuôn mẫu lặp đi lặp lại. Một khi bạn nắm vững cấu trúc ban đầu, bạn có thể sao chép và điều chỉnh nó cho hầu hết mọi module.

Điều kiện tiên quyết

Đảm bảo máy cục bộ hoặc CI runner của bạn đã sẵn sàng các công cụ sau:

  • Terraform: Sử dụng phiên bản khớp với môi trường production của bạn (ví dụ: v1.5.0+).
  • Go: Phiên bản 1.18 trở lên là lựa chọn tốt nhất để hỗ trợ các thư viện hiện đại.
  • Cloud Credentials: Thiết lập các biến môi trường (như AWS_ACCESS_KEY_ID) trỏ đến một tài khoản kiểm thử riêng biệt.

Để bắt đầu, hãy khởi tạo một Go module mới trong thư mục gốc của dự án. Thao tác này giúp quản lý các dependency kiểm thử của bạn:

# Tạo thư mục kiểm thử riêng
mkdir test && cd test

# Khởi tạo Go module
go mod init github.com/your-org/infra-tests

# Tải module terraform của Terratest
go get github.com/gruntwork-io/terratest/modules/terraform

Cấu hình bộ kiểm thử Terratest đầu tiên

Terratest tuân theo một vòng đời nghiêm ngặt: Deploy (Triển khai), Validate (Xác thực), Undeploy (Hủy bỏ). Hãy thử kiểm thử một module tạo S3 bucket có bật tính năng versioning. Chúng ta muốn chứng minh rằng bucket đó thực sự tồn tại và trạng thái versioning là ‘Enabled’ — chứ không chỉ mặc định là nó hoạt động vì lệnh chạy thành công.

The Terraform Module (main.tf)

Xét module tiêu chuẩn này trong modules/s3:

resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration {
    status = "Enabled"
  }
}

output "bucket_id" {
  value = aws_s3_bucket.this.id
}

The Go Test Script (s3_test.go)

Tạo file s3_test.go trong thư mục test/. Tôi khuyên bạn nên sử dụng quy ước đặt tên duy nhất cho tài nguyên để tránh xung đột khi nhiều lập trình viên chạy kiểm thử cùng lúc.

package test

import (
	"testing"
	"github.com/gruntwork-io/terratest/modules/terraform"
	"github.com/stretchr/testify/assert"
)

func TestS3BucketVersioning(t *testing.T) {
	t.Parallel()

	terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
		TerraformDir: "../modules/s3",
		Vars: map[string]interface{}{
			"bucket_name": "terratest-audit-log-bucket-unique-id",
		},
	})

	// Đảm bảo 'terraform destroy' được chạy khi kết thúc
	defer terraform.Destroy(t, terraformOptions)

	// Kích hoạt 'terraform init' và 'terraform apply'
	terraform.InitAndApply(t, terraformOptions)

	// Lấy giá trị biến output
	bucketId := terraform.Output(t, terraformOptions, "bucket_id")

	// Kiểm tra xác nhận rằng bucket thực sự đã được tạo
	assert.NotEmpty(t, bucketId)
}

Hãy đặc biệt chú ý đến dòng defer terraform.Destroy. Đó là “bảo hiểm” của bạn. Nếu bài kiểm thử thất bại giữa chừng, lệnh này đảm bảo các tài nguyên sẽ được xóa sạch. Nếu không có nó, bạn có thể thấy một NAT Gateway tốn 500 USD/tháng hoặc một cụm EKS nhàn rỗi vẫn tồn tại trong tài khoản rất lâu sau khi bài kiểm thử kết thúc.

Xác thực và tích hợp CI/CD

Viết mã nguồn mới chỉ là bước đầu tiên; tự động hóa nó mới là nơi mang lại giá trị thực sự. Kiểm thử cục bộ rất tốt để gỡ lỗi, nhưng CI/CD pipeline mới là nơi các bài kiểm thử này đóng vai trò như người gác cổng cuối cùng.

Thực thi bộ kiểm thử

Chạy các bài kiểm thử từ thư mục test/ bằng lệnh sau:

go test -v -timeout 30m

Tôi đặt thời gian chờ (timeout) là 30 phút. Trong khi các bài kiểm thử Go thông thường kết thúc trong vài giây, các bài kiểm thử hạ tầng phụ thuộc rất nhiều vào nhà cung cấp cloud. Việc khởi tạo một instance RDS hoặc một phân phối CloudFront có thể dễ dàng mất từ 15 đến 20 phút.

GitHub Actions Workflow

Đây là cách tôi cấu hình một job xác thực điển hình bằng GitHub Actions. Nó đảm bảo mọi pull request đều được kiểm thử trong một môi trường sandbox thực tế trước khi được phép merge.

name: Xác thực IaC
on: [pull_request]
jobs:
  terratest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
        with:
          go-version: '1.20'
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
      - name: Execute Tests
        run: |
          cd test
          go mod tidy
          go test -v -timeout 60m
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_TEST_ACCOUNT_KEY }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_TEST_ACCOUNT_SECRET }}
          AWS_REGION: "us-west-2"

Quản lý tài nguyên bị “rò rỉ”

Ngay cả khi có defer, đôi khi các bài kiểm thử thất bại nghiêm trọng đến mức để lại tài nguyên dư thừa. Để tránh các hóa đơn bất ngờ, tôi sử dụng chiến lược “nuke”. Chúng tôi dùng một công cụ tên là aws-nuke trong tài khoản kiểm thử để xóa bất kỳ tài nguyên nào cũ hơn 24 giờ có gắn tag testing: true. Việc sử dụng một tài khoản AWS hoàn toàn tách biệt để kiểm thử cũng là bắt buộc. Nó tạo ra một ranh giới cứng giúp bảo vệ dữ liệu production của bạn khỏi việc vô tình bị xóa.

Suy ngẫm sau sáu tháng

Chuyển sang cách tiếp cận hướng kiểm thử (test-driven) cho hạ tầng đòi hỏi sự đầu tư ban đầu về thời gian và học tập. Nó sẽ làm chậm tốc độ phát triển ban đầu của bạn. Tuy nhiên, sự đánh đổi là sự tự tin cực lớn khi triển khai. Nhóm của tôi không còn lo sợ khi tái cấu trúc (refactor) các module mạng cốt lõi vì “lưới an toàn” của chúng tôi sẽ bắt được lỗi trước khi chúng tiếp cận dù chỉ một người dùng.

Nếu bạn đang quản lý các môi trường quan trọng, hãy bắt đầu từ quy mô nhỏ. Chọn một module quan trọng — có thể là logic security group hoặc cấu hình load balancer — và viết một bài kiểm thử duy nhất. Một khi bạn thấy nó phát hiện ra một lỗi cấu hình thực tế, bạn sẽ không bao giờ muốn quay lại cách xác minh thủ công nữa.

Share: