Chương 07 · Infrastructure

Infrastructure as Code

Khai báo hạ tầng (VPC, subnet, EC2, RDS, K8s cluster, IAM, DNS) bằng code, version control, review qua PR. Terraform là tiêu chuẩn de-facto. Học sâu HCL, providers, state management, modules, drift, import. So sánh Pulumi, CloudFormation, ARM/Bicep.

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

Reproducible
100%
Cùng code = cùng infra mọi lúc, mọi nơi
Versioned
Git
Lịch sử thay đổi, blame, rollback
Reviewable
PR
Đồng đội review trước khi apply
Disaster Recovery
Code
Region die → rebuild bằng code

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ạnhImperativeDeclarative
Mô tả"Làm A, rồi làm B""Trạng thái cuối là X"
IdempotentKhông (mặc định)Có (engine handle)
Drift detectionPhải tự codeBuilt-in (compare desired vs actual)
Code volumeNhiều (handle edge case)Ít (engine handle)
Learning curveThấp (như script thường)Cao (cần hiểu DSL + engine)
ExamplesBash, Python với boto3Terraform, CloudFormation, K8s YAML

IaC modern là declarative. Pulumi pha trộn (imperative-looking syntax, declarative engine).

3. So sánh tools IaC

ToolTypeLanguageCloud
TerraformDeclarativeHCL (DSL)Multi-cloud
OpenTofuDeclarativeHCL (Terraform fork)Multi-cloud
PulumiHybridTS, Py, Go, C#Multi-cloud
CloudFormationDeclarativeJSON/YAMLAWS only
AWS CDKImperative → CFNTS, Py, Go, JavaAWS only
Azure ARM / BicepDeclarativeJSON / Bicep DSLAzure only
GCP Deployment ManagerDeclarativeYAML + JinjaGCP only (legacy)
CrossplaneDeclarativeK8s YAMLMulti-cloud
AnsibleImperative (mostly)YAMLServer 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áoresource 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)
DANGER state operations State là source of truth của Terraform. Sai = orphan resources hoặc phá production. Luôn:
  • Backup state trước (terraform state pull).
  • Test trên môi trường dev/staging trước.
  • Hiểu rõ terraform state rm KHÔ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:

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:

  1. Apply — đẩy về state mong muốn (revert thay đổi manual).
  2. Update code — cập nhật code khớp với drift (chấp nhận thay đổi manual).
  3. Refresh stateterraform refresh sync 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 caseRecommend
Multi-cloudTerraform/OpenTofu hoặc Pulumi
AWS only, prefer YAMLCloudFormation
AWS only, prefer codeAWS CDK
Azure onlyBicep
Team strong typingPulumi (TS)
K8s-heavyCrossplane (K8s YAML)

12. Best Practices

  1. Remote backend + lock — never local state for team.
  2. Pin provider version~> 5.0 chứ không latest.
  3. Use modules — refactor sớm, reuse.
  4. Don't commit secrets — dùng env var TF_VAR_* hoặc Vault data source.
  5. Tag everything — common tags qua locals + provider default_tags.
  6. Plan trước Apply — luôn review plan, đặc biệt prod.
  7. Apply trong CI — không apply từ laptop dev.
  8. Multi-env separate state — không dùng workspace cho prod/staging/dev.
  9. terraform fmt + validate trong pre-commit.
  10. Use data sources thay hardcode IDs.
  11. Output sensitive = true cho secrets.
  12. 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

  1. Hello Terraform: deploy 1 EC2 t3.micro chạy nginx (user_data). Verify curl public IP.
  2. VPC + Subnet: viết HCL tạo VPC 10.0.0.0/16, 3 public subnet ở 3 AZ, IGW, route table. Outputs subnet IDs.
  3. Module: refactor bài 2 thành module ./modules/vpc. Sử dụng từ root config.
  4. Terraform Registry: deploy production-grade VPC bằng module terraform-aws-modules/vpc/aws. Compare với code tự viết.
  5. Remote state: setup S3 + DynamoDB backend. Migrate state từ local sang remote.
  6. Multi-environment: tạo cấu trúc envs/dev, envs/prod với state riêng. Apply cả 2.
  7. Variable + tfvars: extract config (region, instance_type, env) thành variables. Tạo dev.tfvars + prod.tfvars.
  8. Drift: tạo EC2 bằng Terraform. Manually đổi instance_type qua AWS console. Chạy terraform plan, observe drift. Apply để revert.
  9. Import: tạo S3 bucket bằng AWS CLI. Import vào Terraform state. Plan = no changes.
  10. EKS cluster: dùng module terraform-aws-modules/eks/aws deploy EKS cluster với 2 worker node. Chi phí ~$0.10/hour — destroy ngay sau test.
  11. Static analysis: chạy tflint + tfsec + checkov. Sửa các findings HIGH.
  12. terraform test: viết test cho module VPC: assert CIDR đúng, public subnet có IGW, etc.
  13. CI pipeline: setup GitHub Actions: PR chạy fmt+validate+plan+comment; merge main chạy apply (require approval).
  14. 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 ở:

  • Declarative chậm hơn
  • Declarative khai báo trạng thái cuối ("muốn 3 server"), engine handle idempotency; Imperative liệt kê step ("create 1, create 2")
  • Imperative tốt hơn
  • Cả 2 giống hệt
Imperative: chạy 2 lần → tạo 2 lần (không idempotent). Declarative: chạy nhiều lần → cùng kết quả. Engine compare desired (code) với actual (state) → take action. Drift detection built-in. Terraform, K8s, CloudFormation đều declarative. Bash, AWS CLI thuần là imperative (phải tự handle idempotency).

Terraform state phải được lưu remote (S3, GCS, Terraform Cloud) khi:

  • Team có ≥ 2 dev — local state gây conflict, mất state = mất ký ức Terraform; remote backend cho phép locking + sharing
  • Project lớn hơn 100 file
  • Always (kể cả 1 dev cá nhân)
  • Không bao giờ cần
Local state: file 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?

  • count luôn tốt hơn
  • for_each chậm hơn
  • for_each gần như luôn tốt hơn — index-based count gây resource recreation khi xóa item giữa list (mọi index sau bị shift, Terraform thấy như "rename")
  • Cả 2 tương đương
count: list ["alice", "bob", "carol"]. Xóa "bob" → "carol" shift từ index 2 thành 1. Terraform: "destroy carol[2], rename alice[1] → bob's slot, rename carol từ 2 thành 1" — nguy hiểm cho stateful resource (DB, EBS). for_each (set/map): mỗi item key cố định, xóa "bob" chỉ destroy resource["bob"]. Dùng for_each cho 99% trường hợp.

Drift detection trong Terraform là:

  • Tự động sửa drift
  • Tool riêng
  • Plugin third-party
  • Built-in: terraform plan compare desired (code) với actual (refresh từ cloud) — diff = drift; có thể apply để revert hoặc update code
Mỗi 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:

  • Import file YAML
  • Resource đã tồn tại trên cloud (tạo manual hoặc tool khác), muốn quản lý qua Terraform — import vào state mà không recreate
  • Import module
  • Migration giữa clouds
Workflow: viết resource block trong code → 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à:

  • Chỉ version 5.0
  • Bất kỳ >= 5.0
  • >= 5.0, < 6.0 (allow patch và minor, block major)
  • Latest
~> 5.0 = pessimistic constraint: allow >= 5.0 but < 6.0 (allow 5.x.x). ~> 5.10 = >= 5.10 but < 5.11 (chỉ patch). Cách pin balance giữa security update (allow patch+minor) và stability (block major với breaking change). Terraform .terraform.lock.hcl lock thêm digest cho reproducible.

Module trong Terraform là:

  • Thư mục có .tf files với inputs (variables) + outputs + resources, có thể tái sử dụng từ nhiều caller
  • Plugin Terraform
  • Provider
  • State file
Module = "function" trong Terraform — input/output/body. Source: local path, Git, Terraform Registry, S3. Lợi: DRY, abstraction, version control. Best practice: refactor 5 resource trở lên thành module. Terraform Registry có module công như 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ị:

  • Workspace cho mỗi env
  • If/else trong code
  • Cùng state, khác variables
  • Thư mục riêng cho mỗi env (envs/dev/, envs/prod/) với state riêng — isolation tối đa, blast radius nhỏ, có thể khác provider config
Workspace cùng code, khác state — đơn giản nhưng chia sẻ provider config (cùng region, cùng account). Production thường ở account AWS riêng để isolate IAM, billing → workspace không phù hợp. Directory pattern + Terragrunt loại bỏ duplication. Best practice enterprise.

OpenTofu là:

  • Phiên bản đắt hơn của Terraform
  • Fork của Terraform 2023 sau HashiCorp đổi license sang BUSL — open source forever (CNCF), drop-in replacement
  • Tool khác hoàn toàn
  • Không liên quan Terraform
Aug 2023: HashiCorp đổi license Terraform từ MPL (truly open source) sang BUSL (Business Source — restrict commercial competitor). Cộng đồng fork ngay → OpenTofu (Linux Foundation, sau CNCF 2024). Cú pháp HCL giống hệt, provider compatible. Dự án mới + tránh vendor lock-in: OpenTofu. Có Terraform Enterprise: vẫn Terraform.

Terraform CI/CD pipeline best practice:

  • Apply từ laptop dev
  • Skip plan, apply ngay
  • PR: fmt+validate+plan + comment; main: apply (có approval gate); credential qua OIDC; remote state với lock
  • Manual deploy
Pipeline pattern: PR opened → tự chạy fmt + validate + plan → comment plan vào PR (review). Merge main → apply tự động hoặc với approval. Credential AWS qua OIDC (không long-lived secret). Tools: Atlantis (GitOps), Spacelift, Terraform Cloud — managed CI/CD chuyên Terraform.

Hoàn thành Chương 7. Tiếp theo: Chương 8 — Cloud Platforms →