Terraform IaC: Quản lý hạ tầng Cloud Platform chuẩn production

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

  1. Cài Terraform và cấu hình AWS CLI với IAM user có quyền EC2/VPC
  2. Tạo project với cấu trúc thư mục chuẩn
  3. Viết code tạo VPC + subnet + security group + EC2 chạy Nginx
  4. Chạy init → plan → apply, truy cập web server qua public IP
  5. Thay đổi instance_type từ t3.micro lên t3.small → chạy plan, quan sát output
  6. Chạy terraform destroy để dọn dẹp

Lab 2: Remote State và Workspace

  1. Tạo S3 bucket + DynamoDB table cho remote state (theo hướng dẫn trên)
  2. Cấu hình backend S3 trong providers.tf
  3. Tạo 2 workspace: devproduction
  4. Deploy cùng code lên 2 workspace với instance type khác nhau
  5. Xác nhận 2 state file riêng biệt trên S3

Lab 3: Viết Module

  1. Extract code VPC thành module riêng trong modules/vpc/
  2. Gọi module từ thư mục environments/dev/
  3. Thêm output từ module VPC (vpc_id, subnet_ids)
  4. Tạo thêm module EC2 nhận vpc_id và subnet_id từ module VPC

Tài nguyên tham khảo

Tác giả: Mạnh Hoàng

Tôi là Hoàng Mạnh, người sáng lập blog SysadminSkills.com. Tôi viết về quản trị hệ thống, bảo mật máy chủ, DevOps và cách ứng dụng AI để tự động hóa công việc IT. Blog này là nơi tôi chia sẻ những gì đã học được từ thực tế – đơn giản, ngắn gọn và áp dụng được ngay.