Terraform: hạ tầng viết bằng code

4 câu hỏi~25 phútTừ khóa: Infrastructure as Code, provider, resource, plan, apply, state, remote backend, locking, module, drift, import

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 đề

3. Giải pháp: Infrastructure as Code với Terraform

Khái niệmÝ nghĩa
ProviderPlugin nói chuyện với API (aws, azurerm, kubernetes, helm)
ResourceMột thứ cần tạo: aws_db_instance, aws_vpc
Data sourceĐọc thứ đã có sẵn, không quản lý
Variable / outputTham số vào, giá trị ra
ModuleNhóm resource tái dùng, như một hàm
StateFile 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

TerraformCloudFormation / BicepPulumiAnsible
KiểuDeclarative, HCLDeclarative, chỉ AWS / chỉ AzureCode thật (Java, TS…)Thủ tục, cấu hình máy
StateTự quản (file state)Cloud quảnTự quản/SaaSKhông có
Multi-cloudCóKhôngCóCó
Hợp vớiDựng hạ tầngTeam chỉ dùng một cloudTeam muốn logic phức tạpCà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

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ý:

  1. Chạy terraform plan định kỳ (cron trong CI) để phát hiện drift sớm, báo vào Slack.
  2. 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).
  3. 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


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.