Remote State Trên S3 Với use_lockfile
Part I khép lại với một file terraform.tfstate nằm ngay trong thư mục làm việc trên máy bạn. Điều đó ổn khi học một mình, nhưng vỡ ngay khi có người thứ hai, hoặc khi máy bạn hỏng. Part II mở màn bằng việc đưa state ra khỏi máy cá nhân, lên một nơi chung và an toàn: S3, với khóa để hai người không ghi đè lên nhau.
Vấn đề với state local
State local có ba điểm chết. Nó không chia sẻ được: đồng nghiệp của bạn không có file đó nên không apply được cùng hạ tầng. Nó không an toàn: file chứa giá trị nhạy cảm ở dạng plaintext (bài 4), nằm chình ình trên laptop, và nếu lỡ commit vào git thì lộ. Và nó không chống được ghi đồng thời: hai người cùng apply một lúc, hai bản state ghi đè nhau, hạ tầng và state lệch nhau, sửa lại rất khổ.
Remote state giải cả ba. State đặt ở một backend chung; ai cũng đọc cùng một bản; backend mã hóa và kiểm soát truy cập; và một cơ chế khóa (lock) đảm bảo tại một thời điểm chỉ một người ghi được.
Backend là gì, và bài toán con gà quả trứng
Backend là nơi Terraform cất state. Mặc định là local (file trên đĩa). Ta sẽ đổi sang backend s3. Nhưng có một vướng: để dùng S3 làm backend thì bucket phải tồn tại trước, mà ta lại muốn dùng Terraform để tạo bucket — con gà và quả trứng.
Cách giải thông dụng là tách làm hai. Một cấu hình bootstrap nhỏ, dùng state local, chỉ để tạo bucket chứa state. Nó chạy một lần, hiếm khi đổi, và state local của riêng nó không quan trọng lắm vì nội dung dễ dựng lại. Sau đó mọi cấu hình khác mới trỏ backend vào bucket đó.
Bootstrap: bucket chứa state
resource "aws_s3_bucket" "state" {
bucket_prefix = "tf-series-state-"
force_destroy = true # chỉ để dọn lab; KHÔNG bật ở thật
}
resource "aws_s3_bucket_versioning" "state" {
bucket = aws_s3_bucket.state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "state" {
bucket = aws_s3_bucket.state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "state" {
bucket = aws_s3_bucket.state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
Mỗi resource phụ ở đây có lý do riêng, và đều dùng kỹ thuật "resource tách rời" mà bài 5 đã thấy (versioning là resource riêng tham chiếu bucket). Versioning giữ lại mọi bản state cũ — nếu một apply làm hỏng state, bạn khôi phục được bản trước. Server-side encryption mã hóa state lúc lưu, vì nó chứa secret. Public access block chặn mọi cấu hình lỡ tay mở bucket ra Internet. Ba thứ này là mức tối thiểu cho một bucket chứa state.
Apply bootstrap bằng state local:
$ terraform apply -auto-approve
aws_s3_bucket.state: Creation complete after 3s [id=tf-series-state-20260525030320917300000001]
...
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Cấu hình backend s3
Giờ ở một thư mục khác (cấu hình "app"), khai báo backend trỏ vào bucket vừa tạo:
terraform {
required_version = ">= 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
backend "s3" {
bucket = "tf-series-state-20260525030320917300000001"
key = "app/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
use_lockfile = true
}
}
Backend cần ba thứ bắt buộc: bucket, key (đường dẫn file state trong bucket), và region. encrypt = true yêu cầu mã hóa khi truyền và lưu. Một điểm cần nhớ: block backend không dùng được biến — nó được đọc rất sớm, trước khi Terraform xử lý biến, nên mọi giá trị phải tĩnh. Đây là lý do ta điền thẳng tên bucket (Part VII sẽ thấy cách tách giá trị này ra file qua -backend-config).
Dòng đáng chú ý nhất là use_lockfile = true. Đây là điểm mà nhiều tài liệu cũ trên mạng sẽ dẫn bạn đi sai. Trước đây, để khóa state người ta phải dựng thêm một bảng DynamoDB và khai dynamodb_table. Cách đó nay đã lỗi thời: tài liệu hiện hành ghi rõ "khóa dựa trên DynamoDB đã deprecated và sẽ bị bỏ ở một bản minor sau". Thay vào đó, use_lockfile bật khóa native ngay trên S3. Bạn không cần dựng DynamoDB nữa, và bài này cũng không dựng.
init: chuyển sang backend
$ terraform init
Initializing the backend...
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
...
Terraform has been successfully initialized!
init đọc block backend và cấu hình S3 làm nơi chứa state. (Nếu thư mục đã có state local từ trước, init sẽ hỏi có muốn di trú state local lên backend mới không — trả lời yes là nó copy lên.) Apply thử một resource:
$ terraform apply -auto-approve
aws_s3_bucket.app: Creation complete after 3s [id=tf-series-bai6-app-2026...]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Kiểm chứng state không còn ở local mà nằm trên S3:
$ ls terraform.tfstate
ls: terraform.tfstate: No such file or directory
$ aws s3 ls s3://tf-series-state-20260525030320917300000001/app/
2026-05-25 10:04:51 3218 terraform.tfstate
State giờ sống trên S3 tại đúng key đã khai. Ai có quyền vào bucket đều dùng được cùng bản này.
💰 Chi phí
State chỉ vài KB. Lưu nó trên S3 gần như miễn phí, và use_lockfile không tốn gì thêm (chỉ là một object nhỏ tạo-xóa quanh mỗi thao tác). Đây là khác biệt nữa so với DynamoDB: bỏ luôn một dịch vụ phải trả tiền và phải quản lý.
use_lockfile vận hành ra sao
Cơ chế đáng mổ vì nó giải thích vì sao khóa hoạt động mà không cần dịch vụ riêng. Khi bắt đầu một thao tác ghi (plan/apply/destroy), Terraform thử tạo một object khóa tên <key>.tflock (ở đây là app/terraform.tfstate.tflock) bằng một lời ghi có điều kiện "chỉ tạo nếu object chưa tồn tại". S3 hỗ trợ ghi có điều kiện ở tầng API. Nếu chưa ai giữ khóa, object được tạo, Terraform giữ nó suốt thao tác rồi xóa khi xong. Nếu đã có người giữ, lời ghi có điều kiện thất bại, và Terraform biết state đang bị khóa.
Tạo ra xung đột để xem tận mắt: cho một apply giữ lock (đang chờ xác nhận), trong lúc đó chạy một lệnh khác trên cùng state:
$ terraform plan
Error: Error acquiring the state lock
Error message: operation error S3: PutObject, https response error
StatusCode: 412, ... api error PreconditionFailed: At least one of the
pre-conditions you specified did not hold
Lock Info:
ID: ce1c8cbb-5211-21f5-7878-9b867e6246cb
Path: tf-series-state-2026.../app/terraform.tfstate
Operation: OperationTypeApply
Who: ops@workstation.local
Version: 1.15.4
Created: 2026-05-25 03:05:04 UTC
StatusCode: 412 PreconditionFailed chính là lời ghi có điều kiện bị từ chối: object .tflock đã tồn tại nên S3 không cho tạo đè. Đây không phải lỗi, mà là khóa đang làm đúng việc của nó. Khối Lock Info cho biết ai đang giữ khóa, thao tác gì, từ lúc nào — đủ để bạn đi hỏi đồng nghiệp thay vì đập state. Khi tiến trình kia xong, nó xóa .tflock, và lệnh của bạn chạy được.
Trường hợp khóa bị kẹt (tiến trình chết giữa chừng, không kịp xóa khóa), có terraform force-unlock <ID> để gỡ thủ công — nhưng chỉ dùng khi chắc chắn không còn ai đang chạy, vì gỡ nhầm sẽ mở đường cho ghi đè.
🧹 Dọn dẹp
Dọn theo thứ tự ngược với lúc tạo: destroy "app" trước (state của nó nằm trong bucket bootstrap), rồi mới dỡ bootstrap.
$ cd app && terraform destroy -auto-approve
Destroy complete! Resources: 1 destroyed.
$ cd ../0-bootstrap
$ aws s3 rm s3://tf-series-state-2026... --recursive # bucket có versioning, dọn object trước
$ terraform destroy -auto-approve
Destroy complete! Resources: 4 destroyed.
Tổng kết
State local không chia sẻ, không an toàn, không chống ghi đồng thời. Đưa nó lên S3 giải cả ba: bootstrap một bucket có versioning, mã hóa và chặn public bằng cấu hình local một lần, rồi trỏ backend s3 của các cấu hình khác vào đó. use_lockfile = true bật khóa native trên S3 qua lời ghi có điều kiện tạo object .tflock; xung đột hiện ra dưới dạng lỗi 412 PreconditionFailed kèm thông tin ai đang giữ khóa. Đây là cách hiện hành — DynamoDB cho việc khóa đã deprecated, không cần dựng nữa.
State đã an toàn, nhưng làm việc với state hằng ngày còn cần nhiều thao tác hơn: đổi tên resource trong state, gỡ một resource ra khỏi quản lý mà không xóa nó, và đưa hạ tầng có sẵn (dựng tay từ trước) vào Terraform. Bài 7 đi qua các thao tác state đó, với điểm nhấn là import block — cách khai báo để Terraform nhận quản lý resource có sẵn mà không cần gõ lệnh import thủ công.