Terraform: hạ tầng viết bằng code
Tiếp đi: Terraform. Hạ tầng shop-app toàn bấm tay trên console, giờ cần dựng thêm môi trường staging giống hệt prod.
App đã có CI/CD. Nhưng cái nền bên dưới (VPC, cluster Kubernetes, database, Redis) vẫn là "bấm chuột". Bài này biến nó thành code.
1. Bài toán
Prod của shop-app được dựng 2 năm trước bằng console: một VPC, một EKS cluster, một RDS PostgreSQL, một ElastiCache Redis, vài security group. Người dựng đã nghỉ. Giờ cần staging giống prod.
2. Vấn đề
- Không ai biết đầy đủ đã cấu hình gì: port nào mở, parameter group nào, backup bao lâu.
- Dựng lại bằng tay thì staging chắc chắn lệch prod, bug "chỉ có trên prod" xuất hiện.
- Không review được thay đổi; ai đó mở port 5432 ra Internet cũng không ai biết.
- Mất region, không dựng lại kịp (DR, xem bài 15.16).
3. Giải pháp: Infrastructure as Code với Terraform
| Khái niệm | Ý nghĩa |
|---|---|
| Provider | Plugin nói chuyện với API (aws, azurerm, kubernetes, helm) |
| Resource | Một thứ cần tạo: aws_db_instance, aws_vpc |
| Data source | Đọc thứ đã có sẵn, không quản lý |
| Variable / output | Tham số vào, giá trị ra |
| Module | Nhóm resource tái dùng, như một hàm |
| State | File ghi "resource trong code ↔ id thật trên cloud" |
Terraform là declarative: mô tả trạng thái mong muốn, Terraform tự tính cần tạo/sửa/xóa gì.
4. Code từng bước
Bước 1: remote backend có locking
# envs/prod/backend.tf
terraform {
required_version = ">= 1.6"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
backend "s3" {
bucket = "shop-tfstate-prod"
key = "platform/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
dynamodb_table = "shop-tf-lock" # lock; bản mới có thể dùng use_lockfile = true
}
}
Bước 2: module dùng chung cho mọi môi trường
# modules/postgres/main.tf
variable "env" { type = string }
variable "instance_class" { type = string }
variable "subnet_ids" { type = list(string) }
variable "app_sg_id" { type = string }
resource "aws_db_subnet_group" "this" {
name = "shop-${var.env}"
subnet_ids = var.subnet_ids
}
resource "aws_db_instance" "this" {
identifier = "shop-${var.env}"
engine = "postgres"
engine_version = "16"
instance_class = var.instance_class
allocated_storage = 50
db_subnet_group_name = aws_db_subnet_group.this.name
vpc_security_group_ids = [aws_security_group.db.id]
username = "shop_admin"
manage_master_user_password = true # password do Secrets Manager sinh, không nằm trong code/state
multi_az = var.env == "prod"
backup_retention_period = 7
deletion_protection = var.env == "prod"
}
resource "aws_security_group" "db" {
name = "shop-${var.env}-db"
vpc_id = data.aws_subnet.first.vpc_id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [var.app_sg_id] # chỉ node app vào được
}
}
data "aws_subnet" "first" { id = var.subnet_ids[0] }
output "endpoint" { value = aws_db_instance.this.address }
Bước 3: gọi module cho từng môi trường
# envs/staging/main.tf
module "network" {
source = "../../modules/network"
env = "staging"
cidr = "10.20.0.0/16"
}
module "postgres" {
source = "../../modules/postgres"
env = "staging"
instance_class = "db.t4g.medium" # prod: db.r6g.large
subnet_ids = module.network.private_subnet_ids
app_sg_id = module.eks.node_sg_id
}
Bước 4: chạy
cd envs/staging
terraform init
terraform fmt -check && terraform validate
terraform plan -out=tfplan
# + aws_db_instance.this will be created
# ~ aws_security_group.db will be updated in-place
# -/+ aws_db_instance.x must be REPLACED ← đọc kỹ dòng này!
# Plan: 12 to add, 1 to change, 0 to destroy.
terraform apply tfplan
5. Cách hoạt động
code (.tf) state (S3) cloud thật (AWS API)
mong muốn ↔ Terraform nhớ gì ↔ đang tồn tại gì
│ │ │
└────── terraform plan: refresh state từ API, so 3 bên ──┐
▼
diff: + tạo, ~ sửa, -/+ thay thế, - xóa
│
terraform apply: lock state (DynamoDB) ─► gọi API ─► ghi state ─► unlock
Trong CI: PR chạy plan và comment kết quả vào PR; merge vào main mới chạy apply (có bước approve cho prod). Pipeline nhận quyền cloud qua OIDC như bài 18.2.
6. So sánh
| Terraform | CloudFormation / Bicep | Pulumi | Ansible | |
|---|---|---|---|---|
| Kiểu | Declarative, HCL | Declarative, chỉ AWS / chỉ Azure | Code thật (Java, TS…) | Thủ tục, cấu hình máy |
| State | Tự quản (file state) | Cloud quản | Tự quản/SaaS | Không có |
| Multi-cloud | Có | Không | Có | Có |
| Hợp với | Dựng hạ tầng | Team chỉ dùng một cloud | Team muốn logic phức tạp | Cài đặt bên trong VM |
Phân tầng hay dùng: Terraform dựng VPC, EKS, RDS; Helm + Argo CD (bài 18.3) deploy app lên cluster. Đừng dùng Terraform để deploy từng phiên bản app.
7. Khi nào nên / không nên
- Nên: mọi hạ tầng lâu dài, nhiều môi trường, cần review và audit.
- Nên: tách state theo môi trường và theo tầng (network, data, cluster) để giảm blast radius và plan nhanh.
- Không nên: một state khổng lồ cho tất cả; commit file state vào git (chứa thông tin nhạy cảm); sửa tay trên console sau khi đã quản bằng Terraform.
Có người vào console tăng instance RDS lên vì đang chậm, không sửa code Terraform. Lần apply sau chuyện gì xảy ra?
Đây là drift: cloud thật lệch so với code. Lần plan sau Terraform refresh, thấy instance_class thật là db.r6g.xlarge còn code là db.r6g.large, nên nó đề xuất hạ cấp về như code. Apply mù là DB bị hạ cấp đúng lúc đang tải cao.
Cách xử lý:
- Chạy
terraform planđịnh kỳ (cron trong CI) để phát hiện drift sớm, báo vào Slack. - Gặp drift: hoặc cập nhật code cho khớp thực tế (thay đổi đúng), hoặc apply để đưa về như code (thay đổi sai).
- Resource tạo tay hoàn toàn thì đưa vào quản lý bằng khối
import(Terraform 1.5+):
import {
to = aws_s3_bucket.invoices
id = "shop-prod-invoices"
}
Phòng hơn chữa: quyền console prod chỉ read-only, thay đổi đi qua PR. Khẩn cấp phải sửa tay thì mở PR đồng bộ code ngay sau đó.
State bị lock mãi không nhả vì pipeline chết giữa chừng, mình xóa lock đi được không?
Được, nhưng phải chắc chắn không còn ai đang apply. Lock tồn tại để ngăn hai lần apply cùng ghi state, ghi chồng sẽ làm state hỏng, Terraform "quên" resource đã tạo.
# 1. kiểm tra pipeline/ người nào đang chạy (xem lock info: Who, Created)
# 2. chắc chắn không ai chạy:
terraform force-unlock <LOCK_ID>
Đồng thời bật versioning cho bucket state. State hỏng thì khôi phục được bản trước từ S3 version.
Phỏng vấn hỏi về Terraform thì trả lời sao?
Q1. Terraform state là gì? Vì sao cần remote backend?
🇻🇳 Tiếng Việt:
State ánh xạ resource trong code với resource thật trên cloud và lưu thuộc tính của chúng, để Terraform tính diff. Remote backend như S3 hay Azure Storage cho cả team dùng chung một state, có lock tránh hai người apply cùng lúc, có mã hóa và versioning. State có thể chứa dữ liệu nhạy cảm nên không commit vào git.
🇬🇧 English:
State maps resources in code to real cloud resources and stores their attributes so Terraform can compute a diff. A remote backend such as S3 or Azure Storage gives the team one shared state with locking to prevent concurrent applies, plus encryption and versioning. State may contain sensitive data, so it never goes into git.
Q2. Drift là gì, xử lý thế nào?
🇻🇳 Tiếng Việt:
Drift là khi hạ tầng thật bị đổi ngoài Terraform. Em chạy plan định kỳ để phát hiện, rồi quyết định cập nhật code theo thực tế hoặc apply để đưa về code. Phòng ngừa bằng cách giới hạn quyền ghi trên console và bắt mọi thay đổi đi qua PR.
🇬🇧 English:
Drift is when real infrastructure is changed outside Terraform. We run scheduled plans to detect it, then either update the code to match reality or apply to restore the declared state. We prevent it by limiting write access in the console and routing every change through pull requests.
Q3. Bạn tổ chức code Terraform cho nhiều môi trường thế nào?
🇻🇳 Tiếng Việt:
Module tái dùng cho network, cluster, database; mỗi môi trường một thư mục gọi module với tham số riêng và một state riêng. PR chạy plan và hiển thị diff, merge mới apply, prod cần approve. CI xác thực với cloud bằng OIDC.
🇬🇧 English:
Reusable modules for network, cluster and database; each environment has its own folder calling those modules with its own variables and its own state. Pull requests run plan and show the diff, merge triggers apply, and production requires approval. CI authenticates to the cloud with OIDC.
Cách nhớ ngắn gọn
- Code = mong muốn, state = trí nhớ, cloud = thực tế.
- plan đọc kỹ dòng
-/+(replace) trước khi apply. - State remote + lock + versioning + mã hóa; không vào git.
- Module tái dùng, state tách theo môi trường và tầng.
- Drift: plan định kỳ, import resource tạo tay.
Tự kiểm tra
Ký hiệu -/+ trong plan nghĩa là gì, vì sao nguy hiểm?
Resource sẽ bị xóa rồi tạo lại (thay đổi field không sửa tại chỗ được). Với database là mất dữ liệu nếu không có bảo vệ.
Lock state dùng để làm gì?
Ngăn hai lần apply chạy đồng thời ghi chồng state.
Làm sao để password RDS không nằm trong code hay state?
Dùng manage_master_user_password để AWS sinh và lưu trong Secrets Manager; app đọc từ đó.
Đưa một bucket tạo tay vào Terraform thế nào?
Viết resource tương ứng và dùng khối import (hoặc lệnh terraform import), rồi plan cho tới khi không còn diff.
Có nên dùng Terraform để deploy mỗi phiên bản app không?
Thường không. Terraform cho hạ tầng; app deploy qua Helm/Argo CD.