1. Vì sao Infrastructure as Code?
Trước IaC, ta tạo infra qua console/UI: AWS Console click "Create EC2", set OS, security group, key pair, ... Vấn đề:
- Không reproducible — copy infra qua môi trường (dev/staging/prod) bằng tay → drift, sai sót.
- Không version control — không biết ai đổi gì, khi nào, vì sao.
- Không reviewable — admin click giờ mở port 22 cho 0.0.0.0/0 — ai biết?
- Không scalable — 100 VM, 50 service → không thể click thủ công.
- Disaster recovery khó — region die, rebuild thế nào? Không có blueprint.
Infrastructure as Code (IaC): hạ tầng được mô tả bằng code (text file), commit vào Git, áp dụng tự động.
1.1. Lợi ích IaC
1.2. Phenomenon: Cattle vs Pets
Server cũ (pre-IaC): "pet" — mỗi server có tên, lịch sử, cá tính riêng. Khi server bệnh, ta tốn công cứu.
Server với IaC: "cattle" — số 1, số 2, ... đàn lớn. Khi 1 con bệnh, ta thay con mới (recreate from code), không cứu.
Đây là gốc của immutable infrastructure: không sửa server đang chạy — luôn replace bằng instance mới từ image/template.
2. Declarative vs Imperative
2.1. Imperative — kể từng bước
# Bash script — imperative
#!/bin/bash
aws ec2 run-instances \
--image-id ami-0c55b159cbfafe1f0 \
--instance-type t3.micro \
--key-name my-key \
--security-group-ids sg-12345
# Chạy lại: tạo instance THỨ HAI (không idempotent)
2.2. Declarative — khai báo cuối
# Terraform — declarative
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
key_name = "my-key"
vpc_security_group_ids = ["sg-12345"]
tags = { Name = "web-server" }
}
# Chạy terraform apply: tạo instance NẾU chưa có; nothing nếu có rồi (idempotent)
2.3. So sánh
| Khía cạnh | Imperative | Declarative |
|---|---|---|
| Mô tả | "Làm A, rồi làm B" | "Trạng thái cuối là X" |
| Idempotent | Không (mặc định) | Có (engine handle) |
| Drift detection | Phải tự code | Built-in (compare desired vs actual) |
| Code volume | Nhiều (handle edge case) | Ít (engine handle) |
| Learning curve | Thấp (như script thường) | Cao (cần hiểu DSL + engine) |
| Examples | Bash, Python với boto3 | Terraform, CloudFormation, K8s YAML |
IaC modern là declarative. Pulumi pha trộn (imperative-looking syntax, declarative engine).
3. So sánh tools IaC
| Tool | Type | Language | Cloud |
|---|---|---|---|
| Terraform | Declarative | HCL (DSL) | Multi-cloud |
| OpenTofu | Declarative | HCL (Terraform fork) | Multi-cloud |
| Pulumi | Hybrid | TS, Py, Go, C# | Multi-cloud |
| CloudFormation | Declarative | JSON/YAML | AWS only |
| AWS CDK | Imperative → CFN | TS, Py, Go, Java | AWS only |
| Azure ARM / Bicep | Declarative | JSON / Bicep DSL | Azure only |
| GCP Deployment Manager | Declarative | YAML + Jinja | GCP only (legacy) |
| Crossplane | Declarative | K8s YAML | Multi-cloud |
| Ansible | Imperative (mostly) | YAML | Server config (xem ch9) |
3.1. Tại sao Terraform thắng?
- Multi-cloud — code 1 lần, support 3000+ providers (AWS, GCP, Azure, K8s, GitHub, Datadog, Cloudflare, ...).
- Mature ecosystem — Terraform Registry với 15,000+ modules public.
- HCL đơn giản hơn JSON, hỗ trợ comment, expression, function.
- Plan/Apply pattern — preview trước khi thực thi.
3.2. Drama HashiCorp 2023 + OpenTofu
08/2023: HashiCorp đổi license Terraform từ MPL (open source) sang BUSL (Business Source License). Cộng đồng fork ngay → OpenTofu (CNCF, 2024) — drop-in replacement free forever, cú pháp giống hệt.
Dùng cái nào? Cá nhân/team nhỏ: cả 2 OK. Enterprise có Terraform Enterprise: vẫn Terraform. Dự án mới prefer open source: OpenTofu.
4. Terraform basics
4.1. Cài đặt
# macOS
brew install terraform # hoặc opentofu
# Linux
wget https://releases.hashicorp.com/terraform/1.7.0/terraform_1.7.0_linux_amd64.zip
unzip terraform_1.7.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/
terraform version
# Tab completion
terraform -install-autocomplete
4.2. Workflow chuẩn
# 1. Init — tải provider, setup backend
terraform init
# 2. Format + validate
terraform fmt
terraform validate
# 3. Plan — preview thay đổi
terraform plan -out=plan.tfplan
# 4. Apply — thực thi
terraform apply plan.tfplan
# 5. Destroy — gỡ infra
terraform destroy
4.3. Project đầu tiên — VPC + EC2 trên AWS
# providers.tf
terraform {
required_version = ">= 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# main.tf
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = { Name = "main-vpc" }
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
tags = { Name = "public-subnet" }
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
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
}
}
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux_2023.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
user_data = <<-EOF
#!/bin/bash
yum install -y nginx
systemctl enable --now nginx
EOF
tags = { Name = "web-server" }
}
data "aws_ami" "amazon_linux_2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
# outputs.tf
output "web_ip" {
value = aws_instance.web.public_ip
}
terraform init
terraform plan
terraform apply -auto-approve
curl http://$(terraform output -raw web_ip)
terraform destroy
5. HCL — HashiCorp Configuration Language
5.1. Block syntax
block_type "label1" "label2" {
argument = value
nested_block {
...
}
}
5.2. Variables
# variables.tf
variable "region" {
type = string
default = "us-east-1"
description = "AWS region"
}
variable "instance_count" {
type = number
default = 3
}
variable "tags" {
type = map(string)
default = {
Owner = "platform"
Env = "dev"
}
}
variable "subnet_cidrs" {
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
variable "db_password" {
type = string
sensitive = true # không print ra log
}
# Validation
variable "instance_type" {
type = string
validation {
condition = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
error_message = "Must be t3.micro/small/medium."
}
}
# Set value
terraform apply -var "region=us-west-2"
# File terraform.tfvars hoặc *.auto.tfvars
cat > prod.tfvars <<EOF
region = "us-west-2"
instance_count = 10
EOF
terraform apply -var-file=prod.tfvars
# Env var TF_VAR_xxx
export TF_VAR_db_password=secret123
terraform apply
5.3. Outputs
output "vpc_id" {
value = aws_vpc.main.id
description = "ID of the VPC"
}
output "db_endpoint" {
value = aws_db_instance.main.endpoint
sensitive = true # không hiển thị
}
5.4. Locals
locals {
common_tags = {
Project = "myapp"
ManagedBy = "Terraform"
Env = var.environment
}
full_name = "${var.project}-${var.environment}"
}
resource "aws_instance" "web" {
# ...
tags = merge(local.common_tags, { Name = "${local.full_name}-web" })
}
5.5. Expressions & Functions
locals {
# String
name = upper(var.project) # "MYAPP"
prefix = "${var.env}-${var.project}" # interpolation
# List
azs = ["us-east-1a", "us-east-1b"]
azs_count = length(local.azs)
joined = join(",", local.azs)
# Map
tags = lookup(var.tags, "Name", "default")
merged = merge(var.tags, { New = "value" })
# Conditional
size = var.env == "prod" ? "large" : "small"
# for expression
upper_list = [for s in var.subnets : upper(s)]
filtered = [for s in var.subnets : s if length(s) > 5]
# for_map
name_to_id = { for k, v in var.servers : v.name => v.id }
}
5.6. count vs for_each
# count — index-based, dùng list
resource "aws_instance" "web" {
count = 3
ami = "ami-..."
instance_type = "t3.micro"
tags = { Name = "web-${count.index}" }
}
# aws_instance.web[0], [1], [2]
# for_each — key-based, dùng set/map
resource "aws_instance" "web" {
for_each = toset(["alpha", "beta", "gamma"])
ami = "ami-..."
tags = { Name = each.key }
}
# aws_instance.web["alpha"], ["beta"], ["gamma"]
# for_each với map
resource "aws_iam_user" "team" {
for_each = {
alice = { admin = true }
bob = { admin = false }
}
name = each.key
tags = { Admin = each.value.admin }
}
for_each preferred — index-based (count) dễ gây resource recreation khi xóa item giữa list (mọi index sau bị shift).
5.7. Data sources
# Đọc info hiện có (không tạo mới)
data "aws_vpc" "default" {
default = true
}
data "aws_subnets" "private" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default.id]
}
tags = { Tier = "private" }
}
resource "aws_instance" "web" {
subnet_id = data.aws_subnets.private.ids[0]
# ...
}
6. State Management — phần quan trọng nhất
6.1. State là gì?
Terraform state (terraform.tfstate): JSON file lưu mapping resource khai báo ↔ resource thật trên cloud.
{
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "web",
"instances": [{
"attributes": {
"id": "i-0abc123def456",
"ami": "ami-0c55b159...",
"instance_type": "t3.micro",
"public_ip": "54.xx.xx.xx"
}
}]
}
]
}
Mỗi terraform plan: state vs cloud thực tế vs config → diff → action.
6.2. Local state — KHÔNG OK cho team
Default: state lưu local terraform.tfstate. Vấn đề:
- Mỗi dev có state riêng → conflict.
- State chứa secret (DB password) plaintext.
- Nếu mất file → terraform mất "ký ức", thấy mọi resource là "must create".
6.3. Remote backend — must-have
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true # SSE-S3
dynamodb_table = "terraform-state-lock" # locking
}
}
S3 backend pattern:
- S3 bucket lưu state, encryption-at-rest, versioning.
- DynamoDB table lock state khi apply (tránh 2 dev apply đồng thời).
Alternatives:
- Terraform Cloud / Enterprise — managed, audit, policy.
- GCS (GCP), Azure Storage (Azure).
- Consul — HashiCorp ecosystem.
- Postgres, etcd — niche.
6.4. Setup S3 backend bootstrap
# Chicken-and-egg: cần S3 trước backend
# Cách: tạo bằng Terraform (trong project riêng "bootstrap"), chạy 1 lần
# bootstrap/main.tf
resource "aws_s3_bucket" "tfstate" {
bucket = "my-terraform-state"
}
resource "aws_s3_bucket_versioning" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_dynamodb_table" "tflock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
6.5. State commands
terraform state list # liệt kê resources
terraform state show aws_instance.web # detail
terraform state mv aws_instance.web aws_instance.web_old # rename trong state
terraform state rm aws_instance.web # remove khỏi state (không destroy)
terraform state pull > backup.tfstate # export
terraform state push backup.tfstate # import (DANGER)
- Backup state trước (
terraform state pull). - Test trên môi trường dev/staging trước.
- Hiểu rõ
terraform state rmKHÔNG xóa resource trên cloud.
7. Modules — Reusable infrastructure
7.1. Module là gì?
Module = thư mục có file .tf tái sử dụng. Mỗi module có inputs (variables), outputs, body (resources).
# modules/vpc/variables.tf
variable "name" { type = string }
variable "cidr" { type = string }
variable "azs" { type = list(string) }
# modules/vpc/main.tf
resource "aws_vpc" "this" {
cidr_block = var.cidr
tags = { Name = var.name }
}
resource "aws_subnet" "private" {
count = length(var.azs)
vpc_id = aws_vpc.this.id
cidr_block = cidrsubnet(var.cidr, 8, count.index)
availability_zone = var.azs[count.index]
tags = { Name = "${var.name}-private-${count.index}" }
}
# modules/vpc/outputs.tf
output "vpc_id" { value = aws_vpc.this.id }
output "subnet_ids" { value = aws_subnet.private[*].id }
# root main.tf — sử dụng module
module "prod_vpc" {
source = "./modules/vpc"
name = "prod"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
resource "aws_instance" "web" {
subnet_id = module.prod_vpc.subnet_ids[0]
# ...
}
7.2. Module sources
# Local
module "vpc" { source = "./modules/vpc" }
# Git
module "vpc" { source = "git::https://github.com/me/tf-modules.git//vpc?ref=v1.0" }
module "vpc" { source = "git::ssh://git@github.com/me/tf-modules.git//vpc?ref=v1.0" }
# Terraform Registry
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "main-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
}
# S3
module "vpc" { source = "s3::https://s3.amazonaws.com/bucket/vpc.zip" }
7.3. Terraform Registry — best modules
Sử dụng module có sẵn thay vì viết lại:
- terraform-aws-modules — VPC, EKS, RDS, EC2 — chuẩn de-facto cho AWS.
- terraform-google-modules — GCP equivalent.
- Azure modules.
7.4. Module composition
# envs/prod/main.tf
module "vpc" {
source = "../../modules/vpc"
name = "prod"
cidr = "10.0.0.0/16"
}
module "eks" {
source = "../../modules/eks"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
cluster_name = "prod-eks"
}
module "rds" {
source = "../../modules/rds"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.database_subnet_ids
vpc_security_group_id = module.eks.worker_sg_id
}
Module dependency tự động: Terraform infer order từ depends_on implicit hoặc tham chiếu output.
8. Workspaces & Multi-environment
8.1. Workspace mặc định
terraform workspace list
terraform workspace new staging
terraform workspace select staging
terraform workspace show
# Mỗi workspace có state riêng:
# .terraform/terraform.tfstate.d/staging/terraform.tfstate
8.2. Pattern: Directory per environment (recommended)
Workspace có hạn chế (cùng code, chỉ khác variables). Phổ biến hơn: thư mục riêng cho mỗi env:
infra/
├── modules/
│ ├── vpc/
│ ├── eks/
│ └── rds/
├── envs/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf # state riêng
│ ├── staging/
│ │ └── ...
│ └── prod/
│ └── ...
Mỗi env có:
- State riêng (S3 bucket key khác).
- Variables riêng.
- Có thể khác provider config (region, account).
# Apply staging
cd envs/staging
terraform init
terraform plan
terraform apply
# Apply prod (separate)
cd ../prod
terraform init # init lại cho backend riêng
terraform plan
8.3. Terragrunt — DRY for Terraform
Terragrunt (gruntwork-io) loại bỏ duplicate giữa env folders. Setup:
# envs/prod/terragrunt.hcl
include {
path = find_in_parent_folders()
}
terraform {
source = "../../modules//eks"
}
inputs = {
cluster_name = "prod-eks"
node_count = 5
}
# envs/staging/terragrunt.hcl
include {
path = find_in_parent_folders()
}
terraform {
source = "../../modules//eks"
}
inputs = {
cluster_name = "staging-eks"
node_count = 2
}
Bonus: Terragrunt support remote state config trên parent terragrunt.hcl (avoid duplicate backend block).
9. Drift Detection & Import
9.1. Drift là gì?
Drift = trạng thái thực của infra lệch với state Terraform (ai đó đổi qua console).
# Detect drift
terraform plan
# Plan: 0 to add, 2 to change, 0 to destroy.
# ~ aws_instance.web
# instance_type: "t3.micro" -> "t3.small" ← drift!
Khi drift, có 3 cách:
- Apply — đẩy về state mong muốn (revert thay đổi manual).
- Update code — cập nhật code khớp với drift (chấp nhận thay đổi manual).
- Refresh state —
terraform refreshsync state với reality (deprecated trong 1.0+).
9.2. Phòng ngừa drift
- Tắt console write access cho mọi người. Chỉ Terraform thay đổi.
- CI/CD apply Terraform tự động (GitOps pattern).
- Drift detection scheduled (daily plan trong CI, alert nếu khác).
9.3. terraform import — bring under management
Khi resource đã tạo manual và muốn quản lý qua Terraform:
# Step 1: viết resource block (chưa có ID)
cat > main.tf <<EOF
resource "aws_instance" "legacy" {
# placeholder, sẽ override bằng import
}
EOF
# Step 2: import — point Terraform tới resource thật
terraform import aws_instance.legacy i-0abc123def456
# Step 3: terraform show để lấy attributes
terraform state show aws_instance.legacy
# Step 4: copy attributes vào main.tf, resolve diff
terraform plan # nên là "no changes" sau khi xong
Terraform 1.5+ có import block declarative:
import {
to = aws_instance.legacy
id = "i-0abc123def456"
}
resource "aws_instance" "legacy" {
ami = "ami-..."
instance_type = "t3.micro"
# ...
}
Tools tự gen code: terraformer, aws2tf.
10. Testing IaC
10.1. Static analysis
terraform fmt -check # format
terraform validate # syntax + provider config
# tflint — lint rules
tflint --init
tflint .
# Security scan
tfsec . # CIS-style
checkov -d . # Bridgecrew (acquired by Palo Alto)
trivy config . # Aqua
# Cost estimation
infracost diff --path .
10.2. terraform test (1.6+)
# tests/main.tftest.hcl
run "vpc_creation" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR mismatch"
}
}
run "no_public_subnets_for_db" {
command = plan
assert {
condition = alltrue([
for s in aws_subnet.db : !s.map_public_ip_on_launch
])
error_message = "DB subnets must not have public IP"
}
}
terraform test
10.3. Terratest (Go integration test)
// test/vpc_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestVPC(t *testing.T) {
opts := &terraform.Options{
TerraformDir: "../examples/vpc",
Vars: map[string]interface{}{
"name": "test-vpc",
"cidr": "10.99.0.0/16",
},
}
defer terraform.Destroy(t, opts)
terraform.InitAndApply(t, opts)
vpcID := terraform.Output(t, opts, "vpc_id")
assert.NotEmpty(t, vpcID)
}
Terratest thật sự deploy → assert → destroy. Đắt nhưng chính xác. Run trong CI cho module công dùng.
11. Alternatives — Pulumi, CFN, Bicep
11.1. Pulumi
// index.ts
import * as aws from "@pulumi/aws";
const vpc = new aws.ec2.Vpc("main", {
cidrBlock: "10.0.0.0/16",
tags: { Name: "main-vpc" },
});
const subnet = new aws.ec2.Subnet("public", {
vpcId: vpc.id,
cidrBlock: "10.0.1.0/24",
});
export const vpcId = vpc.id;
Pros: dùng ngôn ngữ thật (TS, Py, Go) — full IDE, type checking, loop/condition tự nhiên. Test bằng unit test thông thường.
Cons: có thể abuse imperative pattern (loop trên data dynamic gây drift). State managed trên Pulumi Cloud (free cá nhân).
11.2. AWS CloudFormation
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Resources:
MyVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
Tags:
- Key: Name
Value: main-vpc
PublicSubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref MyVPC
CidrBlock: 10.0.1.0/24
Outputs:
VpcId:
Value: !Ref MyVPC
aws cloudformation deploy \
--template-file template.yaml \
--stack-name my-stack
Pros: native AWS, không cần state riêng (AWS lưu), drift detection built-in. Cons: AWS only, YAML/JSON dài, slow rollback.
11.3. AWS CDK
// CDK = TypeScript -> CloudFormation
import * as ec2 from 'aws-cdk-lib/aws-ec2';
const vpc = new ec2.Vpc(this, 'MyVPC', {
cidr: '10.0.0.0/16',
maxAzs: 3,
natGateways: 1,
});
CDK render ra CloudFormation template, deploy. High-level construct (vpc.addPublicSubnet) tiện hơn raw CFN. Vẫn lock-in AWS.
11.4. Azure Bicep
// main.bicep
resource vnet 'Microsoft.Network/virtualNetworks@2023-04-01' = {
name: 'my-vnet'
location: 'eastus'
properties: {
addressSpace: {
addressPrefixes: ['10.0.0.0/16']
}
}
}
Bicep = ARM template DSL — gọn hơn JSON 60%. Azure equivalent của CDK với ARM.
11.5. Khi nào chọn cái nào?
| Use case | Recommend |
|---|---|
| Multi-cloud | Terraform/OpenTofu hoặc Pulumi |
| AWS only, prefer YAML | CloudFormation |
| AWS only, prefer code | AWS CDK |
| Azure only | Bicep |
| Team strong typing | Pulumi (TS) |
| K8s-heavy | Crossplane (K8s YAML) |
12. Best Practices
- Remote backend + lock — never local state for team.
- Pin provider version —
~> 5.0chứ khônglatest. - Use modules — refactor sớm, reuse.
- Don't commit secrets — dùng env var
TF_VAR_*hoặc Vault data source. - Tag everything — common tags qua locals + provider default_tags.
- Plan trước Apply — luôn review plan, đặc biệt prod.
- Apply trong CI — không apply từ laptop dev.
- Multi-env separate state — không dùng workspace cho prod/staging/dev.
- terraform fmt + validate trong pre-commit.
- Use data sources thay hardcode IDs.
- Output sensitive = true cho secrets.
- Document module — README + examples/.
12.1. CI/CD pipeline cho Terraform
# .github/workflows/terraform.yml
name: Terraform
on:
pull_request:
paths: ['infra/**']
push:
branches: [main]
paths: ['infra/**']
jobs:
terraform:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # OIDC
pull-requests: write # comment plan
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0
- name: Configure AWS via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123:role/tf-deploy
aws-region: us-east-1
- run: terraform fmt -check
- run: terraform init
- run: terraform validate
- name: Plan
if: github.event_name == 'pull_request'
run: terraform plan -no-color -out=tfplan
- name: Comment plan
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const { stdout } = require('child_process');
const plan = require('child_process').execSync('terraform show tfplan').toString();
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '```\n' + plan + '\n```'
});
- name: Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve
12.2. Atlantis — GitOps cho Terraform
Atlantis: post comment atlantis plan trên PR → bot chạy plan, comment kết quả. atlantis apply sau approve. Pattern phổ biến enterprise.
13. Bài tập
- Hello Terraform: deploy 1 EC2 t3.micro chạy nginx (user_data). Verify curl public IP.
- VPC + Subnet: viết HCL tạo VPC 10.0.0.0/16, 3 public subnet ở 3 AZ, IGW, route table. Outputs subnet IDs.
- Module: refactor bài 2 thành module
./modules/vpc. Sử dụng từ root config. - Terraform Registry: deploy production-grade VPC bằng module
terraform-aws-modules/vpc/aws. Compare với code tự viết. - Remote state: setup S3 + DynamoDB backend. Migrate state từ local sang remote.
- Multi-environment: tạo cấu trúc envs/dev, envs/prod với state riêng. Apply cả 2.
- Variable + tfvars: extract config (region, instance_type, env) thành variables. Tạo dev.tfvars + prod.tfvars.
- Drift: tạo EC2 bằng Terraform. Manually đổi instance_type qua AWS console. Chạy
terraform plan, observe drift. Apply để revert. - Import: tạo S3 bucket bằng AWS CLI. Import vào Terraform state. Plan = no changes.
- EKS cluster: dùng module
terraform-aws-modules/eks/awsdeploy EKS cluster với 2 worker node. Chi phí ~$0.10/hour — destroy ngay sau test. - Static analysis: chạy tflint + tfsec + checkov. Sửa các findings HIGH.
- terraform test: viết test cho module VPC: assert CIDR đúng, public subnet có IGW, etc.
- CI pipeline: setup GitHub Actions: PR chạy fmt+validate+plan+comment; merge main chạy apply (require approval).
- Pulumi compare: same VPC viết bằng Pulumi TypeScript. So sánh ergonomics, pros/cons.
14. Quiz
Quiz cuối Chương 7
Declarative IaC khác Imperative ở:
Terraform state phải được lưu remote (S3, GCS, Terraform Cloud) khi:
terraform.tfstate trên laptop dev. 2 dev mỗi người state riêng → drift, conflict. State chứa secret plaintext. Mất file → Terraform thấy mọi resource là "must create" (gây catastrophe). Remote backend (S3 + DynamoDB lock): single source of truth, locked when apply, encrypted-at-rest, versioned.count vs for_each — khi nào prefer for_each?
Drift detection trong Terraform là:
terraform plan tự động refresh state từ cloud (đọc resource thật) rồi compare với code. Nếu ai đổi qua console (vd resize EC2 t3.micro → t3.small), plan hiện diff. Phòng ngừa: tắt console write access, CI/CD apply Terraform tự động, scheduled drift check (daily plan).terraform import dùng khi:
terraform import aws_instance.web i-0abc123 → state có entry → terraform state show để lấy attributes thật → copy vào code → plan = no changes. Terraform 1.5+ có import {} block declarative tiện hơn. Tools tự gen: terraformer, aws2tf.Pin provider version "~> 5.0" nghĩa là:
.terraform.lock.hcl lock thêm digest cho reproducible.Module trong Terraform là:
terraform-aws-modules/vpc/aws deploy production-grade VPC chỉ vài dòng.Multi-environment (dev/staging/prod) trong Terraform — pattern khuyến nghị:
OpenTofu là:
Terraform CI/CD pipeline best practice:
Hoàn thành Chương 7. Tiếp theo: Chương 8 — Cloud Platforms →