Nếu bạn đang quản lý hạ tầng cloud bằng cách click tay trên console, đây là lúc thay đổi. Terraform IaC (Infrastructure as Code) giúp bạn định nghĩa, triển khai và quản lý toàn bộ hạ tầng cloud bằng code — tái sử dụng được, kiểm soát phiên bản được, và quan trọng nhất: không bao giờ quên bước nào. Bài viết này hướng dẫn bạn từ cài đặt đến vận hành Terraform thực chiến trong môi trường production.
Terraform IaC là gì và tại sao SysAdmin cần biết?
Terraform là công cụ IaC mã nguồn mở do HashiCorp phát triển. Thay vì vào console AWS/GCP/Azure để click tạo từng resource, bạn viết file cấu hình (`.tf`) mô tả hạ tầng mong muốn, rồi Terraform lo phần còn lại.
So sánh “click tay” vs Terraform
| Tiêu chí | Click tay trên Console | Terraform IaC |
|---|---|---|
| Tái tạo môi trường | Làm lại từ đầu, dễ sai | Chạy lại 1 lệnh, nhất quán |
| Kiểm soát phiên bản | Không có | Git commit từng thay đổi |
| Audit trail | CloudTrail chung chung | Diff rõ ràng, ai thay đổi gì |
| Môi trường nhiều cloud | Học lại từng console | 1 workflow cho AWS/GCP/Azure |
| Rollback | Khó, manual | Revert code + terraform apply |
Các khái niệm cốt lõi của Terraform
- Provider: Plugin kết nối Terraform với cloud/service (AWS, GCP, Azure, Cloudflare…)
- Resource: Đơn vị hạ tầng cụ thể — EC2 instance, S3 bucket, VPC subnet…
- State file: File
terraform.tfstate— Terraform dùng để biết hạ tầng hiện tại trông như thế nào - Plan: Bước xem trước thay đổi trước khi apply
- Module: Nhóm resources đóng gói, tái sử dụng được
- Variable: Tham số động để code linh hoạt, tái dùng cho nhiều môi trường
- Output: Giá trị xuất ra sau khi tạo resource (IP, ARN, endpoint…)
Cài đặt Terraform trên Linux
Các bước sau áp dụng cho Ubuntu 20.04/22.04/24.04 và CentOS/RHEL 8/9. Terraform không cần database hay daemon — chỉ là một binary duy nhất.
Cài trên Ubuntu/Debian
# Thêm HashiCorp GPG key và repository chính thức
wget -O- https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install -y terraform
# Xác nhận cài đặt thành công
terraform version
Output mẫu:
Terraform v1.9.8
on linux_amd64
Cài trên CentOS/RHEL
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum install -y terraform
Cài bằng tfenv (quản lý nhiều version)
Trong môi trường team, mỗi project có thể yêu cầu version Terraform khác nhau. tfenv giải quyết vấn đề này:
git clone --depth=1 https://github.com/tfutils/tfenv.git ~/.tfenv
echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
# Liệt kê các version có sẵn
tfenv list-remote | head -20
# Cài version cụ thể
tfenv install 1.9.8
tfenv use 1.9.8
terraform version
Cấu hình Provider AWS — Ví dụ thực chiến đầu tiên
Chúng ta sẽ dùng AWS làm ví dụ vì phổ biến nhất, nhưng workflow tương tự với GCP và Azure.
Cấu hình AWS Credentials
Không bao giờ hardcode Access Key vào file .tf! Dùng một trong các cách sau:
# Cách 1: AWS CLI (khuyến nghị cho local/dev)
aws configure
# Điền AWS Access Key ID, Secret Access Key, region, output format
# Cách 2: Environment variable (khuyến nghị cho CI/CD)
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_DEFAULT_REGION="ap-southeast-1"
# Cách 3: IAM Role (tốt nhất cho EC2/ECS/Lambda — zero credentials)
# Gắn IAM Role vào instance, Terraform tự lấy credentials
Cấu trúc project Terraform chuẩn
terraform-aws-project/
├── main.tf # Resources chính
├── variables.tf # Khai báo biến
├── outputs.tf # Giá trị output
├── providers.tf # Cấu hình provider
├── versions.tf # Ràng buộc version
└── terraform.tfvars # Giá trị biến (KHÔNG commit lên git nếu có secret)
File providers.tf
terraform {
required_version = ">= 1.8.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# Remote state — bắt buộc trong production (xem phần sau)
backend "s3" {
bucket = "my-terraform-state-prod"
key = "infra/terraform.tfstate"
region = "ap-southeast-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "Terraform"
}
}
}
File variables.tf
variable "aws_region" {
description = "AWS region để deploy resources"
type = string
default = "ap-southeast-1"
}
variable "environment" {
description = "Môi trường: dev, staging, production"
type = string
validation {
condition = contains(["dev", "staging", "production"], var.environment)
error_message = "environment phải là dev, staging hoặc production."
}
}
variable "project_name" {
description = "Tên project, dùng làm prefix cho resource names"
type = string
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.medium"
}
variable "vpc_cidr" {
description = "CIDR block cho VPC"
type = string
default = "10.0.0.0/16"
}
File main.tf — Tạo VPC + Subnet + EC2 instance
# VPC chính
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc-${var.environment}"
}
}
# Public subnet
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, 1) # 10.0.1.0/24
availability_zone = "${var.aws_region}a"
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-subnet-public-${var.environment}"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw-${var.environment}"
}
}
# Route table cho public subnet
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project_name}-rt-public-${var.environment}"
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
# Security Group cho web server
resource "aws_security_group" "web" {
name = "${var.project_name}-sg-web-${var.environment}"
description = "Security group cho web server"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
# QUAN TRỌNG: Chỉ cho phép SSH từ IP cụ thể trong production
cidr_blocks = ["203.0.113.0/32"] # Thay bằng IP văn phòng/VPN
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-sg-web-${var.environment}"
}
}
# Data source để lấy AMI Ubuntu 22.04 mới nhất
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical (Ubuntu official)
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# EC2 Instance
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
# User data script chạy khi instance khởi động
user_data = base64encode(<<-EOF
#!/bin/bash
apt update -y
apt install -y nginx
systemctl enable --now nginx
echo "Deployed by Terraform - ${var.environment}
" > /var/www/html/index.html
EOF
)
root_block_device {
volume_type = "gp3"
volume_size = 20
encrypted = true # Bắt buộc trong production
tags = {
Name = "${var.project_name}-root-${var.environment}"
}
}
tags = {
Name = "${var.project_name}-web-${var.environment}"
}
lifecycle {
# Không destroy instance cũ trước khi tạo instance mới
create_before_destroy = true
}
}
File outputs.tf
output "vpc_id" {
description = "ID của VPC vừa tạo"
value = aws_vpc.main.id
}
output "instance_public_ip" {
description = "Public IP của EC2 instance"
value = aws_instance.web.public_ip
}
output "instance_id" {
description = "Instance ID"
value = aws_instance.web.id
}
output "web_url" {
description = "URL truy cập web server"
value = "http://${aws_instance.web.public_ip}"
}
Workflow Terraform chuẩn: init → plan → apply
Bước 1: terraform init
terraform init
Output mẫu:
Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.82.2...
- Installed hashicorp/aws v5.82.2 (signed by HashiCorp)
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure.
Giải thích: init tải provider plugins, khởi tạo backend (S3 trong ví dụ), tạo thư mục .terraform/. Chạy lại khi thêm provider mới hoặc thay đổi backend.
Bước 2: terraform plan
# Luôn dùng -var-file hoặc tfvars
terraform plan -var-file="production.tfvars"
# Lưu plan ra file để apply sau (khuyến nghị trong CI/CD)
terraform plan -var-file="production.tfvars" -out=tfplan
Output mẫu (rút gọn):
Terraform will perform the following actions:
# aws_instance.web will be created
+ resource "aws_instance" "web" {
+ ami = "ami-0c02fb55956c7d316"
+ instance_type = "t3.medium"
+ public_ip = (known after apply)
...
}
# aws_vpc.main will be created
+ resource "aws_vpc" "main" {
+ cidr_block = "10.0.0.0/16"
...
}
Plan: 7 to add, 0 to change, 0 to destroy.
Đọc output plan: Dấu + = tạo mới, ~ = thay đổi in-place, -/+ = destroy và tạo lại, - = xóa. Luôn đọc kỹ phần “Plan: X to add, Y to change, Z to destroy” trước khi apply.
Bước 3: terraform apply
# Apply từ file plan đã lưu (không cần confirm)
terraform apply tfplan
# Hoặc apply trực tiếp (cần gõ "yes" để xác nhận)
terraform apply -var-file="production.tfvars"
Output mẫu sau apply thành công:
aws_vpc.main: Creating...
aws_vpc.main: Creation complete after 2s [id=vpc-0a1b2c3d4e5f67890]
aws_subnet.public: Creating...
...
aws_instance.web: Still creating... [30s elapsed]
aws_instance.web: Creation complete after 42s [id=i-0abcdef1234567890]
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.
Outputs:
instance_public_ip = "54.251.XXX.XXX"
web_url = "http://54.251.XXX.XXX"
Remote State — Bắt buộc trong môi trường team và production
State file là “bộ nhớ” của Terraform. Nếu lưu local, team không thể cùng làm việc và dễ bị mất. Trong production, bắt buộc dùng remote state.
Tạo S3 bucket và DynamoDB table cho remote state
# Tạo S3 bucket (thay your-account-id bằng AWS Account ID thực)
aws s3api create-bucket \
--bucket terraform-state-prod-your-account-id \
--region ap-southeast-1 \
--create-bucket-configuration LocationConstraint=ap-southeast-1
# Bật versioning để rollback state khi cần
aws s3api put-bucket-versioning \
--bucket terraform-state-prod-your-account-id \
--versioning-configuration Status=Enabled
# Bật encryption
aws s3api put-bucket-encryption \
--bucket terraform-state-prod-your-account-id \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}]
}'
# Block public access
aws s3api put-public-access-block \
--bucket terraform-state-prod-your-account-id \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
# Tạo DynamoDB table để lock state (tránh 2 người apply cùng lúc)
aws dynamodb create-table \
--table-name terraform-state-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region ap-southeast-1
Terraform Workspaces — Quản lý nhiều môi trường
# Tạo workspace cho từng môi trường
terraform workspace new dev
terraform workspace new staging
terraform workspace new production
# Chuyển workspace
terraform workspace select production
# Xem workspace hiện tại
terraform workspace show
# Output: production
# List tất cả workspace
terraform workspace list
# Output:
# default
# dev
# staging
# * production
Trong code, dùng terraform.workspace để phân biệt môi trường:
locals {
instance_type = terraform.workspace == "production" ? "t3.large" : "t3.micro"
min_capacity = terraform.workspace == "production" ? 3 : 1
}
Terraform Modules — Tái sử dụng code hạ tầng
Module giúp đóng gói logic hạ tầng, tái sử dụng cho nhiều project và môi trường. Đây là pattern quan trọng nhất khi Terraform codebase lớn dần.
Cấu trúc project với modules
terraform-modules/
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── ec2-web/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── rds/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ └── production/
│ ├── main.tf
│ └── terraform.tfvars
Gọi module trong environment
# environments/production/main.tf
module "vpc" {
source = "../../modules/vpc"
project_name = "myapp"
environment = "production"
vpc_cidr = "10.0.0.0/16"
}
module "web_server" {
source = "../../modules/ec2-web"
project_name = "myapp"
environment = "production"
vpc_id = module.vpc.vpc_id
subnet_id = module.vpc.public_subnet_id
instance_type = "t3.large"
}
# Dùng module từ Terraform Registry (HashiCorp verified)
module "alb" {
source = "terraform-aws-modules/alb/aws"
version = "~> 9.0"
name = "myapp-alb-production"
vpc_id = module.vpc.vpc_id
subnets = module.vpc.public_subnet_ids
}
Lỗi thường gặp và cách xử lý
Lỗi 1: Error acquiring the state lock
Error: Error acquiring the state lock
Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
ID: 12345678-...
Operation: OperationTypeApply
Who: john@server
Created: 2026-05-31 08:00:00
Nguyên nhân: Một người khác đang chạy apply, hoặc apply bị interrupt mà chưa giải phóng lock.
Cách xử lý:
# Chỉ dùng khi CHẮC CHẮN không ai đang apply
terraform force-unlock 12345678-...
Lỗi 2: Resource already exists
Error: creating EC2 Instance: InvalidGroup.Duplicate: The security group
'my-sg' already exists for VPC 'vpc-xxx'
Nguyên nhân: Resource đã tồn tại ngoài Terraform (tạo tay trước đó).
Cách xử lý: Import resource vào state Terraform:
terraform import aws_security_group.web sg-0a1b2c3d4e5f67890
Lỗi 3: terraform plan báo “destroy” không mong muốn
Nguyên nhân phổ biến: Thay đổi argument không hỗ trợ update in-place (ví dụ: availability_zone của subnet, engine version của RDS).
Cách xử lý:
# Dùng lifecycle rule để ngăn destroy vô tình
resource "aws_db_instance" "main" {
# ...
lifecycle {
prevent_destroy = true
}
}
Lỗi 4: Provider authentication failed
Error: No valid credential sources found for AWS Provider
Cách xử lý:
# Kiểm tra credentials hiện tại
aws sts get-caller-identity
# Nếu dùng profile
export AWS_PROFILE=my-profile
terraform plan
Lỗi 5: State file bị corrupted / drift
# Xem state hiện tại
terraform state list
# Xem chi tiết một resource trong state
terraform state show aws_instance.web
# Refresh state từ cloud (đồng bộ lại nếu ai thay đổi ngoài Terraform)
terraform refresh
# Xóa resource khỏi state mà không destroy (khi muốn "quên" resource)
terraform state rm aws_instance.web
Terraform trong CI/CD Pipeline
Tích hợp Terraform vào GitLab CI/CD hoặc GitHub Actions giúp tự động hóa việc kiểm tra và deploy hạ tầng.
GitHub Actions workflow mẫu
# .github/workflows/terraform.yml
name: Terraform CI/CD
on:
pull_request:
branches: [main]
push:
branches: [main]
env:
TF_VERSION: "1.9.8"
AWS_REGION: "ap-southeast-1"
jobs:
terraform-plan:
name: Terraform Plan
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Init
run: terraform init
working-directory: environments/production
- name: Terraform Validate
run: terraform validate
working-directory: environments/production
- name: Terraform Plan
run: terraform plan -out=tfplan -no-color
working-directory: environments/production
- name: Comment Plan on PR
uses: actions/github-script@v7
with:
script: |
const plan = require('fs').readFileSync('environments/production/tfplan.txt', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## Terraform Plan\n```\n' + plan + '\n```'
});
terraform-apply:
name: Terraform Apply
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production # Yêu cầu approval trong GitHub
steps:
- uses: actions/checkout@v4
# ... (tương tự trên)
- name: Terraform Apply
run: terraform apply -auto-approve
working-directory: environments/production
Checklist nghiệm thu trước khi dùng Terraform trong production
- ☑ Remote state đã cấu hình trên S3 (hoặc GCS/Azure Blob) với encryption và versioning bật
- ☑ State locking qua DynamoDB (AWS) hoặc tương đương để tránh concurrent apply
- ☑ Không hardcode credentials trong file .tf — dùng IAM Role hoặc environment variables
- ☑ Thêm .terraform/ và *.tfstate vào .gitignore — tuyệt đối không commit state lên git
- ☑ Tất cả resources có tags gồm Project, Environment, ManagedBy=Terraform
- ☑ Encryption bật cho EBS volumes, RDS, S3 buckets trong production
- ☑ Security Groups không mở 0.0.0.0/0 cho SSH/RDP trong production
- ☑ prevent_destroy = true cho RDS, S3 buckets chứa data quan trọng
- ☑ terraform validate pass trước khi plan
- ☑ terraform plan được review trước khi apply — đặc biệt với “to destroy”
- ☑ Tách code theo môi trường (dev/staging/production trong thư mục riêng hoặc workspace)
- ☑ Modules được versioned (dùng git tag hoặc registry version) không dùng
source = "../modules/xxx"trong production
Best Practices và lưu ý production
Quản lý secrets
Không lưu passwords/API keys trong tfvars. Dùng AWS Secrets Manager hoặc HashiCorp Vault:
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/myapp/db-password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
# ...
}
Terraform fmt và validate tự động
# Format code theo chuẩn Terraform
terraform fmt -recursive
# Kiểm tra syntax và logic cơ bản
terraform validate
# Dùng tfsec để scan security issues
docker run --rm -v "$(pwd):/src" aquasec/tfsec /src
Lệnh hữu ích khác
# Xem dependency graph (cần graphviz)
terraform graph | dot -Tpng > graph.png
# Destroy một resource cụ thể (không destroy toàn bộ)
terraform destroy -target=aws_instance.web
# Taint resource để force recreate lần apply tới
terraform taint aws_instance.web
# (Terraform >= 1.3: dùng -replace thay taint)
terraform apply -replace=aws_instance.web
Bài tập / Lab thực hành
Sau khi đọc xong, hãy tự làm lab này để củng cố kiến thức:
Lab 1: Tạo hạ tầng cơ bản
- Cài Terraform và cấu hình AWS CLI với IAM user có quyền EC2/VPC
- Tạo project với cấu trúc thư mục chuẩn
- Viết code tạo VPC + subnet + security group + EC2 chạy Nginx
- Chạy
init → plan → apply, truy cập web server qua public IP - Thay đổi
instance_typetừ t3.micro lên t3.small → chạy plan, quan sát output - Chạy
terraform destroyđể dọn dẹp
Lab 2: Remote State và Workspace
- Tạo S3 bucket + DynamoDB table cho remote state (theo hướng dẫn trên)
- Cấu hình backend S3 trong providers.tf
- Tạo 2 workspace:
devvàproduction - Deploy cùng code lên 2 workspace với instance type khác nhau
- Xác nhận 2 state file riêng biệt trên S3
Lab 3: Viết Module
- Extract code VPC thành module riêng trong
modules/vpc/ - Gọi module từ thư mục
environments/dev/ - Thêm output từ module VPC (vpc_id, subnet_ids)
- Tạo thêm module EC2 nhận vpc_id và subnet_id từ module VPC
Tài nguyên tham khảo
- Terraform Documentation chính thức — HashiCorp Developer
- Terraform Registry — Tìm providers và modules cộng đồng
- Terraform Best Practices — Anton Babenko
- AWS Terraform Best Practices — AWS Prescriptive Guidance
