Đồ Thị Phụ Thuộc: Implicit, depends_on, và -target

K
Kai··6 min read

Từ bài 1 ta đã nghe câu "Terraform thực hiện các thao tác theo đúng thứ tự, tôn trọng mọi quan hệ phụ thuộc". Bài 3 nói thứ tự dòng trong file không quyết định gì. Bài 4 nhắc state lưu cả metadata phụ thuộc để xóa đúng thứ tự. Giờ ta mổ cái cơ chế chung phía sau tất cả: đồ thị phụ thuộc. Hiểu nó thì việc Terraform tạo cái gì trước, xóa cái gì sau, hết bí ẩn.

Mục tiêu

Thấy được đồ thị phụ thuộc Terraform dựng từ cấu hình của bạn, hiểu phụ thuộc ngầm đến từ đâu, biết khi nào phải khai báo phụ thuộc thủ công bằng depends_on, và vì sao -target chỉ nên dùng trong tình huống ngặt.

Phụ thuộc ngầm: tham chiếu là tất cả

Dựng hai resource có quan hệ: một bucket, và versioning bật trên bucket đó.

resource "aws_s3_bucket" "data" {
  bucket_prefix = "tf-series-bai5-"
  force_destroy = true
}

resource "aws_s3_bucket_versioning" "data" {
  bucket = aws_s3_bucket.data.id

  versioning_configuration {
    status = "Enabled"
  }
}

Mấu chốt nằm ở dòng bucket = aws_s3_bucket.data.id. Resource versioning cần biết tên bucket, và nó lấy bằng cách tham chiếu thuộc tính id của resource bucket. Chính tham chiếu này tạo ra một phụ thuộc ngầm: Terraform thấy versioning đọc giá trị từ bucket, nên hiểu bucket phải tồn tại trước. Bạn không khai báo thứ tự ở đâu cả — nó suy ra từ việc ai tham chiếu ai.

Apply và để ý thứ tự:

$ terraform apply -auto-approve
aws_s3_bucket.data: Creating...
aws_s3_bucket.data: Creation complete after 3s [id=tf-series-bai5-20260525025940839300000001]
aws_s3_bucket_versioning.data: Creating...
aws_s3_bucket_versioning.data: Creation complete after 2s [id=...]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Bucket xong hẳn rồi versioning mới bắt đầu, dù trong file ta viết versioning sau cũng không phải lý do — nếu đảo hai block, kết quả vẫn vậy.

Xem đồ thị tận mắt

Lệnh terraform graph xuất đồ thị ở định dạng DOT:

$ terraform graph
digraph G {
  rankdir = "RL";
  node [shape = rect, fontname = "sans-serif"];
  "aws_s3_bucket.data" [label="aws_s3_bucket.data"];
  "aws_s3_bucket_versioning.data" [label="aws_s3_bucket_versioning.data"];
  "aws_s3_bucket_versioning.data" -> "aws_s3_bucket.data";
}

Dòng quan trọng là cạnh "aws_s3_bucket_versioning.data" -> "aws_s3_bucket.data": versioning trỏ tới bucket, nghĩa là "versioning phụ thuộc bucket". Đây là cách Terraform mô hình hóa toàn bộ cấu hình của bạn — một đồ thị có hướng, mỗi resource là một đỉnh, mỗi tham chiếu là một cạnh. (DOT có thể vẽ ra ảnh bằng Graphviz: terraform graph | dot -Tpng > graph.png, hữu ích khi hạ tầng lớn.)

Có đồ thị rồi, Terraform sắp xếp topo (topological sort) để ra thứ tự thao tác: đỉnh nào không phụ thuộc ai thì làm trước, đỉnh phụ thuộc thì chờ thứ nó cần xong đã. Một hệ quả ít người để ý: những resource không có cạnh nối nhau là độc lập, nên Terraform tạo chúng song song (mặc định tối đa 10 thao tác cùng lúc). Đây là lý do apply một hạ tầng lớn nhanh hơn ta tưởng — nó không làm tuần tự từng cái, chỉ tuần tự theo các cạnh phụ thuộc.

   apply: thuận theo cạnh           destroy: ngược cạnh
   ───────────────────────          ─────────────────────────
   1. aws_s3_bucket.data            1. aws_s3_bucket_versioning.data
            │ (xong trước)                   │ (gỡ trước)
            ▼                                ▼
   2. aws_s3_bucket_versioning.data  2. aws_s3_bucket.data

Vì sao destroy đảo ngược thứ tự

Cùng đồ thị đó, lúc xóa Terraform đi ngược các cạnh. Lý do tự nhiên: nếu B phụ thuộc A, thì lúc tạo phải có A trước, còn lúc xóa phải gỡ B trước rồi mới gỡ A — không thể xóa cái đang được cái khác dựa vào. Quan sát destroy:

$ terraform destroy -auto-approve
aws_s3_bucket_versioning.data: Destroying...
aws_s3_bucket_versioning.data: Destruction complete after 1s
aws_s3_bucket.data: Destroying...
aws_s3_bucket.data: Destruction complete after 0s
Destroy complete! Resources: 2 destroyed.

Versioning bị gỡ trước, bucket sau, đúng ngược với lúc tạo. Đây cũng là lý do bài 4 nói state phải nhớ phụ thuộc: khi bạn xóa resource khỏi cấu hình, nó không còn trong file để suy ra thứ tự, nên thứ tự đó lấy từ metadata trong state.

Khi tham chiếu không đủ: depends_on

Phụ thuộc ngầm bắt được hầu hết trường hợp, vì thường resource này cần giá trị từ resource kia. Nhưng có những phụ thuộc ẩn mà cấu hình không lộ qua tham chiếu nào. Tài liệu mô tả depends_on dùng để "xử lý phụ thuộc ẩn giữa resource hoặc module mà Terraform không tự suy ra được".

Ví dụ thường gặp trong tài liệu: một EC2 instance cần một IAM role policy đã sẵn sàng lúc nó boot để chạy phần mềm bên trong, nhưng instance không tham chiếu policy đó trong bất kỳ argument nào. Quan hệ này nằm ở tầng ứng dụng, Terraform không nhìn thấy. Khai báo thủ công:

resource "aws_instance" "app" {
  # ... không có dòng nào tham chiếu policy ...
  depends_on = [aws_iam_role_policy.app]
}

Tài liệu nói rõ depends_on "chỉ nên dùng như lối cuối cùng, vì nó khiến Terraform lập kế hoạch dè dặt hơn, thay thế nhiều resource hơn mức cần". Lý do: vì không biết giá trị nào liên quan, Terraform phải giả định kịch bản xấu nhất. Nguyên tắc thực hành: ưu tiên tham chiếu trực tiếp (aws_iam_role_policy.app.arn) bất cứ khi nào được, vì nó vừa tạo phụ thuộc vừa cho Terraform biết chính xác giá trị nào phụ thuộc; chỉ rơi xuống depends_on khi quan hệ thật sự không thể hiện qua giá trị nào.

-target: lối thoát hiểm, không phải công cụ hằng ngày

Đôi khi bạn muốn apply chỉ một phần đồ thị, ví dụ lúc gỡ rối một resource cứng đầu. Cờ -target cho phép giới hạn thao tác vào một (hoặc vài) resource:

terraform apply -target=aws_s3_bucket.data

Terraform vẫn tôn trọng phụ thuộc của resource được nhắm (tạo luôn những thứ nó cần), nhưng bỏ qua phần còn lại của đồ thị. Vấn đề là làm vậy khiến state và cấu hình lệch nhau một cách có chủ đích, và bản thân Terraform sẽ in cảnh báo rằng đây là tính năng cho trường hợp đặc biệt, không nên dùng thường xuyên. Nếu bạn thấy mình gõ -target liên tục để apply "cho nhanh", đó là dấu hiệu nên tách cấu hình thành các state nhỏ hơn (Part V) chứ không phải lạm dụng cờ này. Coi nó như lối thoát hiểm: có để dùng khi kẹt, không phải để đi lại hằng ngày.

Tổng kết

Terraform mô hình hóa cấu hình thành một đồ thị có hướng: resource là đỉnh, tham chiếu giữa chúng là cạnh, tạo nên phụ thuộc ngầm. Lệnh graph cho xem đồ thị đó; từ nó Terraform sắp xếp topo để ra thứ tự, làm song song những nhánh độc lập, và đảo ngược thứ tự khi destroy. Khi quan hệ không lộ qua tham chiếu (phụ thuộc ẩn ở tầng ứng dụng), depends_on khai báo thủ công, nhưng chỉ dùng khi buộc phải. -target giới hạn thao tác vào một phần đồ thị, là lối thoát hiểm chứ không phải thói quen.

Đến đây Part I khép lại: ta đã nắm IaC và kiến trúc Terraform, vòng đời lệnh, HCL, state, và đồ thị phụ thuộc — đủ nền để đi vào phần thực chiến. Part II mở màn bằng vấn đề cấp bách nhất khi rời khỏi máy cá nhân: file state đang nằm local, không an toàn, không chia sẻ được. Bài 6 chuyển nó lên S3 với khóa state bằng use_lockfile, cách làm hiện hành thay cho DynamoDB đã lỗi thời.