State: Terraform Lưu Gì, Vì Sao Cần, và Drift

K
Kai··7 min read

Bài 2 ta đã liếc qua file state và thấy nó ánh xạ tên local với resource thật. Bài này đào sâu, vì state là thứ gây nhầm lẫn và gây sự cố nhiều nhất với người mới. Ta sẽ trả lời ba câu: vì sao Terraform cần một file riêng thay vì hỏi thẳng AWS mỗi lần, nó lưu chính xác cái gì, và chuyện gì xảy ra khi thực tế lệch khỏi state. Câu cuối ta sẽ tự tay dựng ra để xem.

Mục tiêu

Hiểu vai trò của state đủ chắc để không sợ nó: biết nó lưu gì, vì sao cần, đọc được bằng state list / state show, và nắm cơ chế refresh dẫn tới việc phát hiện drift.

Vì sao Terraform cần state

Câu hỏi tự nhiên: Terraform đã biết cấu hình bạn muốn, AWS thì biết thực tế đang có gì, vậy cần file state ở giữa làm gì? Tài liệu HashiCorp đưa ra bốn lý do.

Ánh xạ với thực tế. Cấu hình của bạn viết aws_s3_bucket.demo, còn AWS chỉ biết một bucket tên tf-series-bai4-2026.... State là cầu nối giữa hai tên đó: nó ghi "resource local tên demo chính là bucket id này". Không có ánh xạ này, Terraform không biết khi bạn sửa block demo thì phải đụng vào bucket nào trên AWS. Bản mẫu đầu của Terraform từng thử không dùng state, dựa vào tag để nhận diện resource, và thất bại vì không phải resource nào cũng hỗ trợ tag.

Metadata về phụ thuộc. State lưu cả quan hệ phụ thuộc giữa các resource. Điều này quan trọng nhất lúc xóa: khi bạn gỡ một resource khỏi cấu hình, nó không còn trong file để Terraform suy ra thứ tự xóa, nên thứ tự đó phải được nhớ trong state từ trước. Không có nó, Terraform có thể xóa nhầm thứ tự (xóa subnet trước khi xóa instance trong subnet).

Hiệu năng. State cache lại thuộc tính của mọi resource. Với hạ tầng lớn (hàng trăm, hàng nghìn resource), hỏi lại từng cái qua API mỗi lần plan là quá chậm vì độ trễ mạng và giới hạn rate limit. State cho phép Terraform tính kế hoạch dựa trên bản cache, chỉ refresh khi cần.

Đồng bộ nhóm. Khi nhiều người cùng làm trên một hạ tầng, state đặt ở nơi chung (remote) đảm bảo ai cũng làm việc trên cùng một bản, và khóa lại để hai người không apply đè lên nhau. Đây là lý do Part II chuyển state lên S3.

Gộp lại, state tồn tại để Terraform không phải "khám phá" lại toàn bộ hạ tầng từ đầu mỗi lần chạy — một việc vừa chậm vừa không phải lúc nào cũng làm được.

State lưu gì

Tạo lại một bucket để soi (bài này dùng thêm tag Env = "dev"):

resource "aws_s3_bucket" "demo" {
  bucket_prefix = "tf-series-bai4-"
  force_destroy = true

  tags = {
    Project = "terraform-series"
    Env     = "dev"
  }
}

Sau apply, xem state bằng lệnh thay vì mở file JSON thô:

$ terraform state list
aws_s3_bucket.demo

$ terraform state show aws_s3_bucket.demo
    bucket                      = "tf-series-bai4-20260525025632034200000001"
    hosted_zone_id              = "Z3O0J2DXBE1FTB"
    id                          = "tf-series-bai4-20260525025632034200000001"
    tags                        = {
        "Env"     = "dev"
        "Project" = "terraform-series"
    }
    ...

state list liệt kê mọi resource Terraform đang quản lý theo địa chỉ local. state show <địa_chỉ> in toàn bộ thuộc tính của một resource đúng như đã ghi trong state. Đây không phải Terraform hỏi AWS — nó đọc từ file state đã cache. Với hạ tầng lớn, đó là khác biệt giữa tức thì và chờ vài phút.

Refresh: so ba chiều

Mỗi lần plan (và apply), trước khi tính diff, Terraform làm bước refresh: với mỗi resource trong state, nó gọi provider hỏi AWS xem resource đó hiện ra sao, rồi cập nhật bản đọc về trong bộ nhớ. Sau đó nó so ba thứ:

   main.tf                terraform.tfstate            AWS (thực tế)
   (bạn MUỐN gì)          (lần cuối ĐÃ BIẾT)           (đang CÓ gì)
   Env = "dev"            Env = "dev"                  Env = "dev"
        │                       │                            │
        └───────────┬───────────┴──────────────┬────────────┘
                    ▼                           ▼
              refresh: đọc AWS, cập nhật bản trong bộ nhớ
                    │
                    ▼
              so cấu hình ⟷ thực tế  →  diff

Khi cả ba khớp, diff rỗng, plan báo "No changes" (đã thấy ở bài 2). Cơ chế này có ích nhất khi chúng không khớp.

Drift: khi thực tế lệch khỏi cấu hình

Drift là khi ai đó sửa hạ tầng ngoài Terraform — bấm console lúc xử lý sự cố, chạy aws CLI tay, hoặc một công cụ khác đụng vào. Hãy tự tạo drift để xem Terraform phản ứng. Đổi tag Env từ dev sang production bằng AWS CLI, hoàn toàn sau lưng Terraform:

$ aws s3api put-bucket-tagging --bucket tf-series-bai4-2026... \
    --tagging 'TagSet=[{Key=Project,Value=terraform-series},{Key=Env,Value=production}]'

Giờ chạy plan:

$ terraform plan
aws_s3_bucket.demo: Refreshing state... [id=tf-series-bai4-20260525025632034200000001]

  ~ update in-place

  # aws_s3_bucket.demo will be updated in-place
  ~ resource "aws_s3_bucket" "demo" {
        id   = "tf-series-bai4-20260525025632034200000001"
      ~ tags = {
          ~ "Env"     = "production" -> "dev"
            "Project" = "terraform-series"
        }
    }

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

Ký hiệu ~ nghĩa là "sửa tại chỗ". Terraform thấy thực tế là production nhưng cấu hình nói dev, nên nó đề xuất kéo thực tế về dev: "production" -> "dev". Đây là hành vi mặc định và là điểm cốt lõi của mô hình khai báo — cấu hình là nguồn sự thật, mọi thay đổi tay sẽ bị apply tiếp theo san phẳng về đúng cái bạn đã viết. Nếu chạy terraform apply lúc này, tag quay lại dev.

Hai lựa chọn khi gặp drift

Không phải lúc nào bạn cũng muốn xóa thay đổi tay. Đôi khi thay đổi đó là đúng và bạn muốn chấp nhận nó. Terraform có chế độ -refresh-only: chỉ đồng bộ state cho khớp thực tế, không sửa hạ tầng.

$ terraform plan -refresh-only
Note: Objects have changed outside of Terraform

  # aws_s3_bucket.demo has changed
  ~ resource "aws_s3_bucket" "demo" {
      ~ tags = {
          ~ "Env"     = "dev" -> "production"
            "Project" = "terraform-series"
        }
    }

This is a refresh-only plan, so Terraform will not take any actions to undo
these. If you were expecting these changes then you can apply this plan to
record the updated values in the Terraform state without changing any remote
objects.

Để ý chiều mũi tên đảo lại: "dev" -> "production". Lần này Terraform không định sửa AWS; nó đề xuất ghi giá trị thực tế (production) vào state. Phân biệt hai chế độ:

Plan thường coi cấu hình là chân lý và muốn kéo thực tế về khớp cấu hình (sẽ sửa AWS). Plan -refresh-only coi thực tế là chân lý và chỉ cập nhật state cho khớp (không đụng AWS, mà cũng không đụng cấu hình — sau đó bạn nên tự sửa cấu hình cho đồng bộ). Lựa chọn đúng tùy tình huống: thay đổi tay là nhầm lẫn thì dùng plan thường để san phẳng; thay đổi tay là cố ý và bạn sẽ cập nhật cấu hình sau thì dùng -refresh-only để state không báo động sai.

🧹 Dọn dẹp

$ terraform destroy -auto-approve
aws_s3_bucket.demo: Destruction complete after 0s
Destroy complete! Resources: 1 destroyed.

State là plaintext

Nhắc lại một lần nữa vì nó nghiêm trọng: file state chứa mọi thuộc tính của resource ở dạng văn bản thường, kể cả những giá trị nhạy cảm như mật khẩu database hay private key. Bất kỳ ai đọc được file state đều đọc được chúng. Hệ quả thực tế: tuyệt đối không commit terraform.tfstate vào git, và khi làm việc nhóm phải cất state ở nơi mã hóa, kiểm soát truy cập. Đó chính là việc Part II sẽ làm với remote state trên S3, kèm khóa state để nhiều người không ghi đè nhau.

Tổng kết

State tồn tại vì bốn lý do: ánh xạ tên local với resource thật, nhớ phụ thuộc để xóa đúng thứ tự, cache thuộc tính cho nhanh, và đồng bộ khi làm nhóm. Mỗi plan bắt đầu bằng refresh — đọc thực tế rồi so ba chiều giữa cấu hình, state và AWS. Khi lệch, plan thường kéo thực tế về cấu hình (~ ... -> giá_trị_cấu_hình), còn -refresh-only ghi thực tế vào state mà không sửa hạ tầng. File state là plaintext nên phải giữ kín.

Ta đã nhắc nhiều lần rằng Terraform "tự suy ra thứ tự" và "dựng đồ thị phụ thuộc". Bài tới mổ chính cái đồ thị đó: phụ thuộc ngầm sinh ra từ đâu, khi nào cần depends_on, và xem trực tiếp đồ thị Terraform dựng bằng lệnh graph.