Module 18 IaC 5 labs

Infrastructure as Code: Terraform, OpenTofu, Bicep & CloudFormation

Quản lý toàn bộ hạ tầng cloud bằng code — từ nguyên lý IaC, vòng đời state, modules tái sử dụng, đến remote backend và tích hợp policy check vào CI/CD pipeline.

Công cụ thực hành Terraform CLI, OpenTofu CLI, Azure CLI, Bicep CLI, VS Code + extensions
Nền tảng CLI / VS Code, Azure, Terraform Cloud / OpenTofu, Azure Portal
Thời điểm phát hành 23/05/2026
Ngày biên soạn 23/05/2026
Người biên soạn Trần Văn Hòa — Microsoft Certified Trainer (MCT)

Mục tiêu học tập

1. Lý thuyết cốt lõi

1.1. Infrastructure as Code là gì?

IaC là thực hành mô tả và quản lý hạ tầng bằng file code (thay vì thao tác thủ công), lưu trữ trong version control và áp dụng quy trình phần mềm (review, test, CI/CD). Theo Infrastructure as Code, 3rd Ed. (Kief Morris, O'Reilly 2025), IaC mang lại ba lợi ích nền tảng: repeatability (triển khai lại y hệt bất kỳ lúc nào), consistency (dev/staging/prod giống nhau), và speed (tự động hóa thay thế toàn bộ công việc thủ công).

Nguyên lý cốt lõi

  • Idempotency: chạy code nhiều lần cho kết quả giống nhau — không tạo thêm resource nếu đã tồn tại.
  • Declarative: mô tả trạng thái mong muốn (what), tool tự tìm ra cách đạt đến đó (how). Ví dụ: Terraform, Bicep, CloudFormation.
  • Imperative: mô tả từng bước thực hiện (how). Ví dụ: Bash script, Ansible tasks.
  • Immutable infrastructure: thay vì sửa server đang chạy, tạo mới và destroy cũ — giảm "configuration drift".
  • State: bản đồ ánh xạ giữa code và resource thực tế; phải được lưu an toàn và chia sẻ trong team.

1.2. Terraform & OpenTofu — kiến trúc

Terraform (HashiCorp, 2014) dùng ngôn ngữ HCL (HashiCorp Configuration Language). Năm 2023 HashiCorp chuyển sang license BSL, cộng đồng fork ra OpenTofu (CNCF, 2024) — API/HCL tương thích 100%, license Mozilla Public License 2.0. Kiến trúc gồm ba thành phần chính (theo Terraform: Up and Running, Yevgeniy Brikman):

Vòng đời Terraform:

terraform init      # tải providers, khởi tạo backend
terraform validate  # kiểm tra cú pháp HCL
terraform plan      # preview thay đổi (diff state vs config)
terraform apply     # áp dụng thay đổi (ghi state)
terraform show      # xem state hiện tại dạng human-readable
terraform state list # liệt kê resource trong state
terraform destroy   # xóa toàn bộ resource được quản lý

1.3. Modules — tái sử dụng và đóng gói

Module là thư mục chứa .tf files có variables.tf (input), outputs.tf (output) và main.tf (logic). Module che giấu complexity: caller chỉ cần truyền biến, không cần biết bên trong tạo resource như thế nào. Modules có thể publish lên Terraform Registry hoặc lưu trong private Git repo và version bằng Git tag. Theo Infrastructure as Code with OpenTofu (Tyran Vosk), pattern phổ biến: module/network, module/compute, module/database — chia theo concern, không theo môi trường.

1.4. Remote Backend & State Locking

Lưu state local phù hợp học tập nhưng không an toàn cho team: hai người chạy apply đồng thời sẽ corrupt state. Remote backend giải quyết bằng:

1.5. Bicep — IaC native Azure

Bicep là DSL của Microsoft (2021), transpile sang ARM JSON. Cú pháp ngắn gọn hơn ARM JSON ~4 lần, tích hợp sâu với Azure (first-class types, IDE support qua VS Code Bicep extension). Không cần quản lý state — Azure Resource Manager tự theo dõi. Phù hợp cho đội thuần Azure, không cần multi-cloud. Lệnh chính: az deployment group create --template-file main.bicep.

1.6. CloudFormation — IaC native AWS

AWS CloudFormation (YAML/JSON) là IaC native cho AWS — không cần cài CLI riêng, state quản lý bởi AWS Stacks. AWS CDK (Cloud Development Kit) cho phép viết IaC bằng TypeScript/Python/Java, compile sang CloudFormation. Phù hợp cho AWS-only environment với deep integration (drift detection, StackSets cho multi-account).

1.7. So sánh các tool IaC

Tiêu chí Terraform / OpenTofu Bicep CloudFormation
Multi-cloudCó (1,000+ providers)Azure onlyAWS only
State managementTự quản lý (tfstate)Azure tự động (ARM)AWS tự động (Stack)
Ngôn ngữHCL (declarative)Bicep DSLYAML / JSON
LicenseBSL (TF) / MPL (OpenTofu)MIT / OpenAWS service
Phù hợpMulti-cloud, hybridAzure-first teamAWS-only environment

2. Thực hành (Labs)

LAB-001

Terraform/OpenTofu tạo VNet + Subnet trên Azure

Terraform CLI · Azure CLI · VS Code

🎯 Mục tiêu: Dùng Terraform (hoặc OpenTofu) tạo Resource Group, Virtual Network và Subnet trên Azure bằng vòng đời đầy đủ: init → validate → plan → apply → inspect state → destroy.

🧰 Công cụ / nền tảng: Terraform CLI ≥ 1.7 hoặc OpenTofu CLI ≥ 1.7, Azure CLI, VS Code + HashiCorp Terraform extension.

📦 Chuẩn bị:

# Cài Terraform (Windows - winget)
winget install HashiCorp.Terraform

# Hoặc cài OpenTofu (drop-in replacement)
winget install OpenTofu.OpenTofu

# Cài Azure CLI
winget install Microsoft.AzureCLI

# Đăng nhập Azure
az login
az account show    # xác nhận subscription đúng
az account set --subscription "<subscription-id>"

▶️ Các bước:

# 1. Tạo thư mục project
mkdir iac-azure-vnet && cd iac-azure-vnet

# 2. Tạo providers.tf
cat > providers.tf << 'EOF'
terraform {
  required_version = ">= 1.7"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.100"
    }
  }
}

provider "azurerm" {
  features {}
  # Credentials từ az login (Azure CLI auth) - không cần hard-code
}
EOF
# 3. Tạo variables.tf
cat > variables.tf << 'EOF'
variable "location" {
  description = "Azure region"
  type        = string
  default     = "southeastasia"
}

variable "resource_group_name" {
  description = "Name of the resource group"
  type        = string
  default     = "rg-iac-lab"
}

variable "vnet_address_space" {
  description = "VNet CIDR block"
  type        = string
  default     = "10.10.0.0/16"
}

variable "subnet_prefixes" {
  description = "Map of subnet name to CIDR"
  type        = map(string)
  default = {
    "snet-app" = "10.10.1.0/24"
    "snet-db"  = "10.10.2.0/24"
  }
}
EOF
# 4. Tạo main.tf
cat > main.tf << 'EOF'
resource "azurerm_resource_group" "main" {
  name     = var.resource_group_name
  location = var.location

  tags = {
    Environment = "lab"
    ManagedBy   = "terraform"
    Module      = "m18-iac"
  }
}

resource "azurerm_virtual_network" "main" {
  name                = "vnet-iac-lab"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  address_space       = [var.vnet_address_space]

  tags = azurerm_resource_group.main.tags
}

resource "azurerm_subnet" "subnets" {
  for_each = var.subnet_prefixes

  name                 = each.key
  resource_group_name  = azurerm_resource_group.main.name
  virtual_network_name = azurerm_virtual_network.main.name
  address_prefixes     = [each.value]
}
EOF
# 5. Tạo outputs.tf
cat > outputs.tf << 'EOF'
output "resource_group_id" {
  value = azurerm_resource_group.main.id
}

output "vnet_id" {
  value = azurerm_virtual_network.main.id
}

output "subnet_ids" {
  value = { for k, v in azurerm_subnet.subnets : k => v.id }
}
EOF
# 6. Vòng đời đầy đủ
terraform init          # tải provider azurerm ~3.100

terraform validate      # kiểm tra syntax
# Output: Success! The configuration is valid.

terraform plan -out=tfplan   # preview changes
# Output: Plan: 4 to add, 0 to change, 0 to destroy.

terraform apply tfplan  # tạo resources (xác nhận "yes" hoặc thêm -auto-approve)
# Output: Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

# 7. Inspect state
terraform state list
# azurerm_resource_group.main
# azurerm_virtual_network.main
# azurerm_subnet.subnets["snet-app"]
# azurerm_subnet.subnets["snet-db"]

terraform show          # chi tiết từng resource
terraform output        # xem output values

# 8. Verify trên Azure CLI
az network vnet list --resource-group rg-iac-lab --output table
az network vnet subnet list --resource-group rg-iac-lab --vnet-name vnet-iac-lab --output table

🖥️ Đối chiếu GUI (Portal): Azure Portal → Resource Groups → rg-iac-lab → xem VNet và 2 Subnet; tag "ManagedBy: terraform" hiển thị rõ.

✅ Kết quả mong đợi: terraform state list liệt kê 4 resources; terraform output subnet_ids trả về map JSON chứa ID của snet-app và snet-db; Portal hiển thị VNet với address space 10.10.0.0/16.

🧹 Cleanup:

terraform destroy   # xóa toàn bộ resource (xác nhận "yes")
# Output: Destroy complete! Resources: 4 destroyed.
LAB-002

Remote state backend với Azure Blob Storage

Terraform CLI · Azure CLI · Azure Portal

🎯 Mục tiêu: Di chuyển state từ local lên Azure Blob Storage làm remote backend, kích hoạt state locking, và xác minh team collaboration flow (simulate 2 operations đồng thời).

🧰 Công cụ / nền tảng: Terraform CLI, Azure CLI, Azure Storage Account (tạo mới trong lab).

📦 Chuẩn bị: Tiếp tục từ LAB-001 hoặc dùng project mới. Azure subscription active.

▶️ Các bước:

# 1. Tạo Storage Account cho Terraform state (chạy một lần, ngoài Terraform)
RESOURCE_GROUP="rg-terraform-state"
STORAGE_ACCOUNT="stterraformstate$RANDOM"   # tên phải unique toàn cầu
CONTAINER="tfstate"
LOCATION="southeastasia"

az group create --name $RESOURCE_GROUP --location $LOCATION

az storage account create \
  --name $STORAGE_ACCOUNT \
  --resource-group $RESOURCE_GROUP \
  --location $LOCATION \
  --sku Standard_LRS \
  --encryption-services blob \
  --allow-blob-public-access false

az storage container create \
  --name $CONTAINER \
  --account-name $STORAGE_ACCOUNT

# Lấy Storage Account key
STORAGE_KEY=$(az storage account keys list \
  --resource-group $RESOURCE_GROUP \
  --account-name $STORAGE_ACCOUNT \
  --query '[0].value' -o tsv)

echo "Storage Account: $STORAGE_ACCOUNT"
echo "Storage Key: $STORAGE_KEY"
# 2. Cập nhật providers.tf để thêm backend block
# (Thay <STORAGE_ACCOUNT> bằng tên thực tế từ bước trên)
cat > providers.tf << 'EOF'
terraform {
  required_version = ">= 1.7"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.100"
    }
  }

  # Remote backend: state được lưu trên Azure Blob Storage
  backend "azurerm" {
    resource_group_name  = "rg-terraform-state"
    storage_account_name = "<STORAGE_ACCOUNT>"   # thay tên thực tế
    container_name       = "tfstate"
    key                  = "m18-lab.tfstate"       # tên file state trong blob
  }
}

provider "azurerm" {
  features {}
}
EOF
# 3. Re-initialize với backend mới (migrate state local → remote)
ARM_ACCESS_KEY=$STORAGE_KEY terraform init -migrate-state
# Prompt: "Do you want to copy existing state to the new backend?" → yes
# Output: Successfully configured the backend "azurerm"!

# 4. Verify state đã được migrate
az storage blob list \
  --account-name $STORAGE_ACCOUNT \
  --container-name tfstate \
  --output table
# Hiển thị: m18-lab.tfstate

# 5. Verify state locking (chạy plan để kích hoạt lock tạm thời)
terraform plan
# Trong quá trình plan, state bị lock
# Kiểm tra lease status trên blob (trong thời gian plan đang chạy):
az storage blob show \
  --account-name $STORAGE_ACCOUNT \
  --container-name tfstate \
  --name m18-lab.tfstate \
  --query "properties.lease" \
  --output json

# 6. Apply một thay đổi nhỏ để test round-trip
# Thêm tag mới vào resource group trong main.tf, rồi:
terraform apply -auto-approve

# 7. Xem state từ remote (không cần local tfstate file)
rm -f terraform.tfstate*   # xóa local state nếu còn
terraform state list        # vẫn liệt kê được resource từ remote!

🖥️ Đối chiếu Portal: Azure Portal → Storage Account → Containers → tfstate → blob m18-lab.tfstate → xem nội dung JSON; khi terraform apply đang chạy, blob có lease status "Leased" (state locking).

✅ Kết quả mong đợi: terraform state list hoạt động sau khi xóa file local; blob m18-lab.tfstate xuất hiện trong container; ARM_ACCESS_KEY không cần hard-code vào file (truyền qua env var).

🧹 Cleanup:

terraform destroy -auto-approve
az group delete --name rg-terraform-state --yes --no-wait
az group delete --name rg-iac-lab --yes --no-wait
LAB-003

Viết Terraform module tái sử dụng

Terraform CLI · Azure CLI · VS Code

🎯 Mục tiêu: Đóng gói hạ tầng network thành module tái sử dụng, gọi module từ root config với input variables khác nhau cho môi trường devprod.

🧰 Công cụ / nền tảng: Terraform CLI, Azure CLI, VS Code + Terraform extension.

📦 Chuẩn bị: Terraform CLI đã cài; Azure CLI đã đăng nhập.

▶️ Các bước:

# 1. Cấu trúc thư mục module
mkdir -p iac-modules/modules/network
mkdir -p iac-modules/environments/dev
mkdir -p iac-modules/environments/prod
cd iac-modules

# Cấu trúc đầy đủ:
# iac-modules/
# ├── modules/
# │   └── network/
# │       ├── main.tf
# │       ├── variables.tf
# │       └── outputs.tf
# └── environments/
#     ├── dev/
#     │   └── main.tf
#     └── prod/
#         └── main.tf
# 2. Viết module network (modules/network/variables.tf)
cat > modules/network/variables.tf << 'EOF'
variable "name_prefix" {
  description = "Prefix for resource names"
  type        = string
}

variable "location" {
  description = "Azure region"
  type        = string
}

variable "resource_group_name" {
  description = "Existing resource group name"
  type        = string
}

variable "vnet_cidr" {
  description = "VNet address space CIDR"
  type        = string
}

variable "subnets" {
  description = "Map of subnet name to CIDR prefix"
  type        = map(string)
}

variable "tags" {
  description = "Tags to apply to all resources"
  type        = map(string)
  default     = {}
}
EOF

# modules/network/main.tf
cat > modules/network/main.tf << 'EOF'
resource "azurerm_virtual_network" "this" {
  name                = "${var.name_prefix}-vnet"
  location            = var.location
  resource_group_name = var.resource_group_name
  address_space       = [var.vnet_cidr]
  tags                = var.tags
}

resource "azurerm_subnet" "this" {
  for_each = var.subnets

  name                 = each.key
  resource_group_name  = var.resource_group_name
  virtual_network_name = azurerm_virtual_network.this.name
  address_prefixes     = [each.value]
}

resource "azurerm_network_security_group" "this" {
  name                = "${var.name_prefix}-nsg"
  location            = var.location
  resource_group_name = var.resource_group_name
  tags                = var.tags
}
EOF

# modules/network/outputs.tf
cat > modules/network/outputs.tf << 'EOF'
output "vnet_id" {
  description = "Virtual Network resource ID"
  value       = azurerm_virtual_network.this.id
}

output "vnet_name" {
  description = "Virtual Network name"
  value       = azurerm_virtual_network.this.name
}

output "subnet_ids" {
  description = "Map of subnet name to resource ID"
  value       = { for k, v in azurerm_subnet.this : k => v.id }
}

output "nsg_id" {
  description = "NSG resource ID"
  value       = azurerm_network_security_group.this.id
}
EOF
# 3. Gọi module từ environment dev (environments/dev/main.tf)
cat > environments/dev/main.tf << 'EOF'
terraform {
  required_version = ">= 1.7"
  required_providers {
    azurerm = { source = "hashicorp/azurerm", version = "~> 3.100" }
  }
}

provider "azurerm" { features {} }

resource "azurerm_resource_group" "dev" {
  name     = "rg-iac-dev"
  location = "southeastasia"
  tags     = { Environment = "dev", ManagedBy = "terraform" }
}

module "dev_network" {
  source = "../../modules/network"   # đường dẫn tương đối tới module

  name_prefix         = "dev"
  location            = azurerm_resource_group.dev.location
  resource_group_name = azurerm_resource_group.dev.name
  vnet_cidr           = "10.10.0.0/16"
  subnets = {
    "snet-app" = "10.10.1.0/24"
    "snet-db"  = "10.10.2.0/24"
  }
  tags = azurerm_resource_group.dev.tags
}

output "dev_subnet_ids" {
  value = module.dev_network.subnet_ids
}
EOF
# 4. Init và apply cho môi trường dev
cd environments/dev
terraform init
terraform plan
terraform apply -auto-approve

# 5. Xem output của module
terraform output dev_subnet_ids
# Output:
# {
#   "snet-app" = "/subscriptions/.../subnets/snet-app"
#   "snet-db"  = "/subscriptions/.../subnets/snet-db"
# }

# 6. Kiểm tra graph dependency (visualize module call)
terraform graph | head -30
# Hoặc xuất ra dot format rồi mở bằng Graphviz:
terraform graph > graph.dot

🖥️ Đối chiếu VS Code: Extension HashiCorp Terraform cung cấp hover documentation cho resource type, auto-complete cho variable names, và validate HCL khi gõ.

✅ Kết quả mong đợi: Module được gọi từ environments/dev tạo 1 VNet + 2 Subnet + 1 NSG dưới rg-iac-dev; terraform state list hiển thị module.dev_network.azurerm_virtual_network.this.

🧹 Cleanup: terraform destroy -auto-approve trong thư mục environments/dev.

LAB-004

Bicep: tạo Azure App Service + Storage Account

Bicep CLI · Azure CLI · Azure Portal

🎯 Mục tiêu: Viết Bicep template triển khai App Service Plan + Web App + Storage Account với connection string được inject tự động; deploy bằng Azure CLI và xác minh qua Portal.

🧰 Công cụ / nền tảng: Bicep CLI (tích hợp trong Azure CLI ≥ 2.20), Azure CLI, VS Code + Bicep extension (ms-azuretools.vscode-bicep).

📦 Chuẩn bị:

# Kiểm tra Bicep CLI
az bicep version
# Nếu chưa có:
az bicep install

# Cài VS Code extension
code --install-extension ms-azuretools.vscode-bicep

▶️ Các bước:

# 1. Tạo thư mục
mkdir iac-bicep-lab && cd iac-bicep-lab
# 2. Tạo main.bicep
cat > main.bicep << 'EOF'
@description('Base name for all resources')
param baseName string = 'iaclab'

@description('Azure region for all resources')
param location string = resourceGroup().location

@description('App Service Plan SKU')
@allowed(['F1', 'B1', 'B2', 'S1'])
param appServiceSku string = 'B1'

// Unique suffix based on resource group ID to avoid name collision
var uniqueSuffix = uniqueString(resourceGroup().id)

// Storage Account
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: '${baseName}${uniqueSuffix}'
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    allowBlobPublicAccess: false
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
  }
}

// App Service Plan
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
  name: '${baseName}-asp'
  location: location
  sku: {
    name: appServiceSku
  }
  properties: {
    reserved: false  // Windows plan
  }
}

// Web App
resource webApp 'Microsoft.Web/sites@2023-01-01' = {
  name: '${baseName}-${uniqueSuffix}-app'
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      netFrameworkVersion: 'v8.0'
      appSettings: [
        {
          name: 'STORAGE_CONNECTION_STRING'
          // Inject connection string tự động - không hard-code secret
          value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
        }
        {
          name: 'WEBSITE_RUN_FROM_PACKAGE'
          value: '1'
        }
      ]
    }
    httpsOnly: true
  }
}

// Outputs
output webAppUrl string = 'https://${webApp.properties.defaultHostName}'
output webAppName string = webApp.name
output storageAccountName string = storageAccount.name
EOF
# 3. Build Bicep → ARM JSON (tùy chọn, để xem output)
az bicep build --file main.bicep
# Tạo ra main.json - file ARM JSON tương đương

# 4. Validate template trước khi deploy
az deployment group validate \
  --resource-group rg-bicep-lab \
  --template-file main.bicep \
  --parameters baseName=hoatranlab appServiceSku=B1

# Tạo resource group nếu chưa có
az group create --name rg-bicep-lab --location southeastasia

# 5. Deploy bằng Azure CLI
az deployment group create \
  --name deploy-$(date +%Y%m%d-%H%M%S) \
  --resource-group rg-bicep-lab \
  --template-file main.bicep \
  --parameters baseName=hoatranlab appServiceSku=B1 \
  --output table

# Output:
# Name                            State       Timestamp
# ------------------------------ ----------- --------
# deploy-20260523-140000          Succeeded   ...

# 6. Lấy outputs
az deployment group show \
  --name <deployment-name> \
  --resource-group rg-bicep-lab \
  --query properties.outputs \
  --output json

# 7. Verify Web App đang chạy
WEB_APP_URL=$(az deployment group show \
  --name <deployment-name> \
  --resource-group rg-bicep-lab \
  --query "properties.outputs.webAppUrl.value" -o tsv)
curl -I $WEB_APP_URL
# 8. Xem What-if (tương đương terraform plan)
az deployment group what-if \
  --resource-group rg-bicep-lab \
  --template-file main.bicep \
  --parameters baseName=hoatranlab appServiceSku=S1   # thay đổi SKU
# Output hiển thị màu: ~ (modify), + (add), - (delete)

🖥️ Đối chiếu Portal: Azure Portal → Resource Groups → rg-bicep-lab → Deployments → xem deployment history với status Succeeded/Failed; Web App → Configuration → Application settings để xác nhận STORAGE_CONNECTION_STRING đã được inject.

✅ Kết quả mong đợi: Deployment trạng thái Succeeded; curl -I <webAppUrl> trả về HTTP 200 hoặc 403 (app running, no code deployed); connection string App Setting có giá trị thực (không phải placeholder).

🧹 Cleanup: az group delete --name rg-bicep-lab --yes --no-wait

LAB-005

IaC policy check (tfsec / checkov) trong CI/CD pipeline

tfsec · checkov · GitHub Actions · CLI

🎯 Mục tiêu: Tích hợp static analysis bảo mật IaC (tfseccheckov) vào GitHub Actions pipeline để bắt lỗi cấu hình nguy hiểm trước khi terraform apply; cố tình tạo lỗi rồi fix để thấy workflow.

🧰 Công cụ / nền tảng: GitHub Actions, tfsec, checkov (Python), Terraform CLI, VS Code.

📦 Chuẩn bị: Repo GitHub có Terraform code (dùng từ LAB-001 hoặc LAB-003). Cài checkov local để test: pip install checkov hoặc dùng trong container.

▶️ Các bước:

# 1. Tạo file Terraform có lỗi bảo mật CỐ Ý (để demo)
cat > storage_insecure.tf << 'EOF'
# CẢnh báo: file này chứa cấu hình KHÔNG AN TOÀN để demo policy check
resource "azurerm_storage_account" "insecure_demo" {
  name                     = "stinsecuredemo"
  resource_group_name      = "rg-iac-lab"
  location                 = "southeastasia"
  account_tier             = "Standard"
  account_replication_type = "LRS"

  # LỖI 1: Cho phép public blob access
  allow_nested_items_to_be_public = true

  # LỖI 2: HTTPS không bắt buộc
  enable_https_traffic_only = false

  # LỖI 3: TLS version thấp
  min_tls_version = "TLS1_0"
}
EOF
# 2. Chạy tfsec local để thấy lỗi trước
# Cài tfsec (Windows)
winget install tfsec

# Scan thư mục hiện tại
tfsec . --format default
# Output sẽ báo:
# HIGH: Storage account has public access enabled
# HIGH: Storage account not using TLS 1.2
# MEDIUM: Storage account not using HTTPS-only

# Chạy checkov local
pip install checkov   # hoặc dùng Docker
checkov -d . --framework terraform
# Output:
# Check: CKV_AZURE_3: "Ensure that 'Storage Account' has no public blob access"
# FAILED for resource: azurerm_storage_account.insecure_demo
# 3. Tạo GitHub Actions workflow tích hợp policy check
mkdir -p .github/workflows
cat > .github/workflows/iac-security-check.yml << 'EOF'
name: IaC Security Policy Check

on:
  pull_request:
    paths:
      - '**.tf'
      - '**.bicep'
  push:
    branches: [main]

jobs:
  # ----- tfsec: Terraform security scanner -----
  tfsec:
    name: tfsec Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run tfsec
        uses: aquasecurity/[email protected]
        with:
          soft_fail: false     # fail pipeline nếu có HIGH issue
          working_directory: .

  # ----- checkov: Multi-framework IaC scanner -----
  checkov:
    name: Checkov Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: .
          framework: terraform
          soft_fail: false
          output_format: sarif
          output_file_path: checkov-results.sarif

      - name: Upload SARIF results
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: checkov-results.sarif
          category: checkov

  # ----- terraform validate & plan (chỉ chạy sau khi scan pass) -----
  terraform-plan:
    name: Terraform Plan
    needs: [tfsec, checkov]     # phụ thuộc vào scan job pass trước
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.7"

      - name: Terraform Init
        run: terraform init -backend=false   # skip backend cho plan-only

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan (dry run)
        run: terraform plan -input=false
        env:
          ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
          ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
          ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
        continue-on-error: true   # plan có thể fail nếu không có real Azure credentials
EOF
# 4. Push code có lỗi bảo mật → pipeline fail
git add storage_insecure.tf .github/workflows/iac-security-check.yml
git commit -m "test: add insecure storage to trigger policy check"
git push origin main

# GitHub Actions → workflow fail ở bước tfsec và checkov
# Security tab → Code scanning alerts hiển thị SARIF findings

# 5. Fix các lỗi bảo mật
cat > storage_insecure.tf << 'EOF'
# FIXED: cấu hình an toàn
resource "azurerm_storage_account" "insecure_demo" {
  name                     = "stinsecuredemo"
  resource_group_name      = "rg-iac-lab"
  location                 = "southeastasia"
  account_tier             = "Standard"
  account_replication_type = "LRS"

  allow_nested_items_to_be_public = false  # FIX 1
  enable_https_traffic_only        = true  # FIX 2
  min_tls_version                  = "TLS1_2"  # FIX 3
}
EOF

git add storage_insecure.tf
git commit -m "fix: harden storage account configuration per tfsec policy"
git push origin main
# GitHub Actions → tất cả scan jobs xanh

🖥️ Đối chiếu Portal (GitHub): GitHub → Actions → workflow run → job "tfsec Scan" hiển thị đỏ với lỗi cụ thể; sau khi fix, job xanh; GitHub → Security → Code scanning alerts hiển thị findings từ SARIF.

✅ Kết quả mong đợi (lỗi): Pipeline fail với message tfsec found 3 potential security issues. Sau khi fix: tất cả jobs xanh; Code scanning alerts được closed/resolved tự động.

🧹 Cleanup: git rm storage_insecure.tf && git commit -m "chore: remove demo insecure file" && git push. Disable workflow nếu không dùng tiếp: gh workflow disable iac-security-check.yml.

3. Tình huống doanh nghiệp thực tế

Bối cảnh: Công ty logistics mở rộng sang 3 cloud region

Đội infra 4 người, đang quản lý 200+ Azure resources bằng cách click Portal thủ công. Mỗi lần audit compliance phải screenshot từng resource. Khi tạo môi trường mới cho khách hàng mất 2 ngày. CTO yêu cầu "môi trường mới trong 30 phút, audit trail tự động, không có config drift".

Giải pháp IaC end-to-end

  • Tool selection: Terraform (đội cần multi-region Azure + một số AWS cho 1 khách hàng). OpenTofu cho môi trường air-gapped không cho phép HashiCorp BSL.
  • Module library: 4 core modules (network, compute, database, monitoring) publish lên private Terraform Registry (GitLab). Mỗi module có semantic versioning, CHANGELOG, và test bằng terratest.
  • Remote state: Azure Blob với RBAC — dev team chỉ có quyền read state; apply chỉ chạy qua CI/CD pipeline với Workload Identity (không có static credentials).
  • Pipeline IaC: PR → checkov + tfsec scan → terraform plan (post comment lên PR) → merge → auto-apply staging → manual approval → apply production.
  • Compliance: Terraform resource tags bắt buộc (policy check) tạo audit trail; Azure Policy "deny" resource tạo ngoài Terraform.
  • Kết quả: Môi trường mới từ 2 ngày giảm xuống 28 phút; compliance audit từ 3 ngày xuống 2 giờ (chạy script so sánh state vs policy); zero config drift sau 3 tháng.

📚 Nguồn tham khảo

Module 17: CI/CD Fundamentals Module 19: Configuration Management
Zalo