Đọc State Khác và Refactor: remote_state, moved, removed
Bài trước ta tách mỗi môi trường ra một thư mục với state riêng. Việc đó đẻ ra một nhu cầu: tầng compute cần id VPC mà tầng network tạo ở một state khác. Bài này giải bằng terraform_remote_state, rồi xử lý hai tình huống refactor luôn gặp khi tổ chức lại code — đổi tên resource và gỡ resource khỏi quản lý — bằng moved và removed block, cách khai báo trong cấu hình thay cho lệnh tay đã thấy ở bài 7.
Mục tiêu
Đọc được output của một cấu hình từ cấu hình khác qua terraform_remote_state, và refactor an toàn bằng moved (đổi tên/di chuyển) và removed (gỡ khỏi state) ngay trong code.
terraform_remote_state: đọc output của state khác
Khi tách thành nhiều state, mỗi state công bố một số output (bài 9). terraform_remote_state là data source đọc output của một state khác. Một cấu hình "network" tạo bucket dùng chung và công bố tên:
# network/main.tf
resource "aws_s3_bucket" "shared" {
bucket_prefix = "tf-series-bai16-net-"
force_destroy = true
}
output "bucket_name" {
value = aws_s3_bucket.shared.id
}
Cấu hình "consumer" đọc output đó:
# consumer/main.tf
data "terraform_remote_state" "network" {
backend = "local"
config = {
path = "../network/terraform.tfstate"
}
}
output "consumed_bucket" {
value = data.terraform_remote_state.network.outputs.bucket_name
}
Truy cập qua data.terraform_remote_state.<tên>.outputs.<tên_output>. Apply consumer:
$ terraform apply -auto-approve
$ terraform output
consumed_bucket = "tf-series-bai16-net-2026..."
Consumer lấy được tên bucket do network tạo, dùng nó như thể biến của mình. Ở đây dùng backend local trỏ thẳng tới file state cho gọn; thực tế bạn đặt backend = "s3" với cùng bucket/key của tầng network (đúng remote state bài 6).
Một điểm về ranh giới: terraform_remote_state chỉ đọc được những gì state kia công bố qua output, không phải toàn bộ resource bên trong. Cách giới hạn này có chủ đích: output trở thành hợp đồng công khai giữa các tầng, còn chi tiết bên trong mỗi tầng vẫn riêng tư. Khi đổi cấu trúc nội bộ tầng network mà giữ nguyên output, consumer không bị ảnh hưởng.
moved block: đổi tên không xóa-tạo lại
Bài 7 đổi tên resource bằng lệnh terraform state mv gõ tay. Cách đó có nhược: nó là thao tác một lần trên máy người chạy, không lưu trong code, đồng nghiệp và CI không biết đã có sự đổi tên. moved block (Terraform 1.1) khai báo việc đổi tên trong cấu hình.
Có một bucket tên old_name, muốn đổi thành new_name. Nếu chỉ sửa tên, Terraform tưởng xóa cái cũ tạo cái mới. Thêm moved block:
moved {
from = aws_s3_bucket.old_name
to = aws_s3_bucket.new_name
}
resource "aws_s3_bucket" "new_name" {
bucket_prefix = "tf-series-bai16-"
force_destroy = true
}
$ terraform plan
# aws_s3_bucket.old_name has moved to aws_s3_bucket.new_name
Plan: 0 to add, 0 to change, 0 to destroy.
has moved to, và 0 thêm, 0 sửa, 0 xóa — không đụng vào hạ tầng thật, chỉ đổi địa chỉ trong state. Trước khi lập plan cho resource ở to, Terraform kiểm state xem có resource ở địa chỉ from không; có thì coi như cùng một object, chỉ đổi tên.
Khác biệt với state mv: moved block nằm trong code nên được review qua pull request, chạy nhất quán ở mọi nơi, và áp dụng được cho cả người khác lẫn CI mà không ai phải gõ lệnh tay. Sau khi mọi state đã áp dụng việc đổi tên, có thể xóa moved block đi.
removed block: gỡ khỏi state không destroy
Tương tự, bài 7 dùng state rm tay để gỡ resource khỏi quản lý mà giữ hạ tầng. removed block (Terraform 1.7) khai báo việc đó. Bỏ block resource đi và thay bằng removed:
removed {
from = aws_s3_bucket.new_name
lifecycle {
destroy = false
}
}
$ terraform apply -auto-approve
# aws_s3_bucket.new_name will no longer be managed by Terraform,
# but will not be destroyed
# (destroy = false is set in the configuration)
Plan: 0 to add, 0 to change, 0 to destroy.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
lifecycle { destroy = false } là chỗ quyết định: gỡ khỏi state nhưng không xóa object thật. Kiểm chứng bucket vẫn còn:
$ aws s3api head-bucket --bucket tf-series-bai16-2026...
{
"BucketArn": "arn:aws:s3:::tf-series-bai16-2026...",
"BucketRegion": "ap-southeast-1"
}
Bucket còn nguyên, chỉ là Terraform không quản lý nữa — đúng như state rm, nhưng khai báo trong code. (Nếu đặt destroy = true, hoặc đơn giản xóa block resource mà không có removed block, Terraform sẽ destroy object — đó là khác biệt cần cẩn thận.)
moved removed (destroy = false)
───── ─────────────────────────
from = old_name from = new_name
to = new_name lifecycle { destroy = false }
│ │
state: đổi địa chỉ state: gỡ khỏi quản lý
hạ tầng: KHÔNG đụng hạ tầng: GIỮ NGUYÊN (không destroy)
Vì sao nên dùng block thay vì lệnh tay
moved/removed block và state mv/state rm làm cùng việc, nhưng block thắng ở chỗ chúng là cấu hình: được version trong git, review qua PR, áp dụng giống nhau ở mọi máy và trong CI. Lệnh state tay là thao tác tức thời, dễ quên ghi lại, dễ chạy khác nhau giữa người này người kia. Với refactor có ảnh hưởng tới state — thứ nhạy cảm — khai báo trong code an toàn hơn hẳn. Lệnh tay vẫn hữu ích cho thao tác khẩn cấp một lần, nhưng refactor có kế hoạch thì dùng block.
🧹 Dọn dẹp
Bucket gỡ bằng removed (mồ côi) phải xóa tay; phần network/consumer thì destroy bình thường:
$ aws s3 rb s3://tf-series-bai16-... --force # bucket mồ côi sau removed
$ cd network && terraform destroy -auto-approve
Destroy complete! Resources: 1 destroyed.
Tổng kết
terraform_remote_state cho một cấu hình đọc output (chỉ output) của cấu hình khác qua data.terraform_remote_state.<tên>.outputs.<x>, biến output thành hợp đồng giữa các tầng hạ tầng. moved block (1.1) đổi tên/di chuyển resource trong state mà không xóa-tạo lại (has moved to, 0 destroy). removed block (1.7) với lifecycle { destroy = false } gỡ resource khỏi quản lý nhưng giữ object thật. Cả hai khai báo trong code nên review và tái lập được, an toàn hơn state mv/state rm tay cho refactor có kế hoạch.
Part V khép lại. Part VI gom các tính năng lifecycle và provider nâng cao mà ta đã chạm tới rải rác: create_before_destroy, prevent_destroy, ignore_changes, replace_triggered_by, provider alias đa vùng, terraform_data thay null_resource, provisioner như giải pháp cuối, và check block. Tất cả trong bài 17.