Lifecycle và Provider Nâng Cao

K
Kai··5 min read

Rải rác qua series ta đã chạm tới block lifecycle (prevent_destroy ở bài 9, ignore_changes nhắc qua), provider, và terraform_data. Part VI gom chúng lại thành một bài cho gọn, vì đây là những công cụ tinh chỉnh hành vi mà bạn sẽ cần khi hạ tầng phức tạp lên. Mỗi tính năng đều có demo thật để thấy nó làm gì.

Mục tiêu

Nắm bốn tùy chọn lifecycle, cách chạy đa vùng bằng provider alias, terraform_data thay cho null_resource, vai trò cuối cùng của provisioner, và check block kiểm tra mềm.

Bốn tùy chọn lifecycle

Block lifecycle đặt trong resource, điều khiển cách Terraform tạo/sửa/xóa nó.

prevent_destroy chặn mọi plan định xóa resource. Hữu ích cho thứ không bao giờ được lỡ tay xóa (database prod, bucket state):

resource "aws_s3_bucket" "important" {
  lifecycle {
    prevent_destroy = true
  }
}

Thử destroy:

$ terraform destroy
Error: Instance cannot be destroyed

Resource aws_s3_bucket.important has lifecycle.prevent_destroy set, but the
plan calls for this resource to be destroyed.

Terraform từ chối thẳng. Muốn xóa thật thì phải sửa code tắt cờ đi trước — đúng ý đồ, biến việc xóa thành hành động có chủ đích chứ không phải tai nạn.

ignore_changes bảo Terraform bỏ qua thay đổi ở một số thuộc tính khi lập plan. Dùng khi có tiến trình ngoài (autoscaling, công cụ khác) sửa thuộc tính đó và bạn không muốn Terraform kéo về:

lifecycle {
  ignore_changes = [tags]
}

Sau dòng này, tag bị ai đó sửa tay sẽ không còn bị Terraform báo drift (đối lập với hành vi mặc định ở bài 4). Có thể dùng ignore_changes = all để bỏ qua tất cả.

create_before_destroy đảo thứ tự khi phải thay thế resource: tạo cái mới trước, rồi mới xóa cái cũ. Mặc định Terraform xóa trước tạo sau, gây gián đoạn; cờ này tránh downtime cho những resource thay thế được song song:

lifecycle {
  create_before_destroy = true
}

replace_triggered_by buộc thay thế resource khi một resource hay thuộc tính được tham chiếu thay đổi:

lifecycle {
  replace_triggered_by = [aws_ecs_service.svc.id]
}

Provider alias: đa vùng trong một cấu hình

Mặc định một cấu hình có một provider AWS cho một region. Muốn dựng resource ở nhiều region cùng lúc, khai nhiều provider phân biệt bằng alias:

provider "aws" {
  region = "ap-southeast-1"   # mặc định
}

provider "aws" {
  alias  = "us"
  region = "us-east-1"
}

resource "aws_s3_bucket" "sg" {
  bucket_prefix = "tf-series-bai17-sg-"
}

resource "aws_s3_bucket" "us" {
  provider      = aws.us   # dùng provider alias -> tạo ở us-east-1
  bucket_prefix = "tf-series-bai17-us-"
}

Resource không ghi provider dùng provider mặc định; resource ghi provider = aws.us dùng provider alias. Apply:

$ terraform output
sg_region = "ap-southeast-1"
us_region = "us-east-1"

Hai bucket sinh ở hai region khác nhau từ một lần apply. Đây là cách dựng hạ tầng đa vùng (sao lưu chéo vùng, CloudFront cần certificate ở us-east-1) mà không tách thành nhiều cấu hình. AWS provider v6 còn hỗ trợ multi-region ở mức tinh hơn, nhưng provider alias là cách nền tảng và đủ cho hầu hết nhu cầu.

terraform_data thay null_resource

Khi cần một resource "giả" để gắn triggers hay chạy provisioner, trước đây người ta dùng null_resource (từ provider null). Từ Terraform 1.4 có terraform_data tích hợp sẵn, không cần provider ngoài:

resource "terraform_data" "deploy_marker" {
  triggers_replace = [var.app_version]
  input            = "deployed ${var.app_version}"
}

triggers_replace nhận một danh sách; khi giá trị trong đó đổi, resource bị tạo lại. Đổi app_version từ v1 sang v2:

$ terraform plan -var app_version=v2
  # terraform_data.deploy_marker must be replaced
-/+ resource "terraform_data" "deploy_marker" {

Plan: 1 to add, 0 to change, 1 to destroy.

terraform_data cũng giữ được dữ liệu qua trường input/output, tiện hơn null_resource. Quy tắc: cần loại resource này thì dùng terraform_data, đừng kéo provider null về nữa.

Provisioner: giải pháp cuối

Provisioner (local-exec, remote-exec) chạy script lúc tạo/xóa resource. Chúng tồn tại, nhưng tài liệu lẫn cộng đồng đều khuyên coi đây là giải pháp cuối cùng. Lý do: provisioner nằm ngoài mô hình khai báo — Terraform không biết script làm gì, không thể lập plan cho nó, không idempotent, và lỗi giữa chừng để lại resource ở trạng thái nửa vời. Ưu tiên các cách khác: dùng user_data cho EC2, cloud-init, AMI dựng sẵn (Packer), hay công cụ cấu hình (Ansible) sau khi Terraform dựng hạ tầng. Chỉ rơi xuống provisioner khi thật sự không còn cách nào.

check block: kiểm tra mềm

Bài 9 nhắc check block (1.5) và hẹn ở đây. Khác với validation/precondition (chặn apply), check chỉ cảnh báo — nó kiểm điều kiện ở cuối thao tác và không làm hỏng deploy:

check "bucket_naming" {
  assert {
    condition     = startswith(aws_s3_bucket.important.id, "tf-series")
    error_message = "Tên bucket nên bắt đầu bằng 'tf-series'."
  }
}

Khi điều kiện sai:

$ terraform plan
Warning: Check block assertion failed

  on main.tf line 33, in check "bucket_naming":
  33:     condition = startswith(aws_s3_bucket.important.id, "prod-")

Tên bucket nên bắt đầu bằng 'tf-series'.

Warning, không phải Error, nên apply vẫn chạy. Precondition là "không đạt thì dừng", còn check là "không đạt thì báo cho biết mà vẫn tiếp tục". Dùng check cho những điều kiện nên giám sát nhưng không đáng chặn deploy (một endpoint trả 200, một quy ước đặt tên), để cảnh báo nổi lên mà không làm kẹt pipeline.

🧹 Dọn dẹp

prevent_destroy chặn destroy, nên phải sửa code tắt cờ rồi mới dọn được:

# đổi prevent_destroy = true -> false, apply, rồi:
$ terraform destroy -auto-approve
Destroy complete! Resources: 2 destroyed.

Đây cũng là minh chứng prevent_destroy làm đúng việc: muốn xóa phải qua một bước có chủ đích.

Tổng kết

Block lifecycle tinh chỉnh vòng đời: prevent_destroy chặn xóa nhầm, ignore_changes bỏ qua thay đổi ngoài, create_before_destroy tránh downtime khi thay thế, replace_triggered_by buộc thay thế theo resource khác. Provider alias cho phép đa vùng trong một cấu hình (provider = aws.us). terraform_data thay null_resource không cần provider ngoài. Provisioner là giải pháp cuối, ưu tiên user_data/cloud-init/Ansible. check block cảnh báo thay vì chặn, hợp cho giám sát mềm.

Đến đây ta đã có đủ ngôn ngữ và công cụ. Part VII chuyển sang vận hành: làm sao chạy Terraform trong CI/CD an toàn. Bài 18 dựng pipeline GitHub Actions — plan tự động trên pull request, apply khi merge, xác thực bằng OIDC không cần lưu khóa, và gắn các công cụ chất lượng fmt/validate/tflint/Trivy/Checkov vào pipeline.