Mục lục
- Variables, Outputs & State
- 📦 Code Examples
- Vấn Đề: Mọi Thứ Đều Hardcoded
- Giải Pháp: Variables, Outputs, State
- Input Variables: Đừng Hardcode Nữa
- Validation: Bắt Lỗi Trước Khi Deploy
- Variable Precedence: Values Đến Từ Đâu
- The Precedence Chain (Thấp đến Cao)
- Multi-Environment Setup
- Environment Variables
- Command-Line Variables
- Outputs: Đừng Lục Tìm Information
- State Management: Terraform's Memory
- Sensitive Data: Đừng Leak Passwords
- Vấn Đề
- Giải Pháp 1: Đánh Dấu Variables Là Sensitive
- Giải Pháp 2: Encrypted Remote State
- Giải Pháp 3: External Secrets Management (Best Practice)
- Real-World Pattern: Multi-Environment Setup
- Quick Check: Bạn Hiểu Chưa?
- Tiếp Theo Là Gì?
- Resources
Variables, Outputs & State
Bạn đã viết Terraform resources đầu tiên rồi. Code chạy ngon. Nhưng mọi thứ đều hardcoded.
Muốn deploy cùng một infrastructure lên staging và production? Copy-paste tất cả rồi đổi values bằng tay. Cần database endpoint cho app? SSH vào instance và mò tìm. Muốn biết cái gì đang tồn tại trên cloud account? Cầu trời là state file vẫn còn đó.
Cái này không scale được đâu.
Ở phần này, mình sẽ hướng dẫn bạn build infrastructure linh hoạt, reusable với variables, outputs và state management. Cùng một codebase, nhiều environments khác nhau. Không phải lục tìm IP addresses nữa. Không còn "ủa, mình đã deploy cái này chưa nhỉ?"
📦 Code Examples
Repository: terraform-hcl-tutorial-series Phần Này: Part 5 - Variables and State
Lấy working example:
git clone https://github.com/khuongdo/terraform-hcl-tutorial-series.git
cd terraform-hcl-tutorial-series
git checkout part-05
cd examples/part-05-variables-state/
# Thử các variable patterns
terraform init
terraform plan -var-file="dev.tfvars"
Vấn Đề: Mọi Thứ Đều Hardcoded
Đây là Terraform của người mới bắt đầu:
# main.tf
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0" # Nếu muốn AMI khác ở staging thì sao?
instance_type = "t3.medium" # Production size cho dev environment?
tags = {
Name = "prod-web-server" # Hardcoded "prod" trong dev code?
Environment = "production"
Owner = "alice@company.com" # Nếu Bob deploy thì sao?
}
}
resource "aws_db_instance" "main" {
engine = "postgres"
instance_class = "db.t3.large" # Cùng size cho dev và prod?
username = "admin"
password = "supersecret123" # Đừng nói là bạn commit cái này vào Git
}
Sai chỗ nào?
- Zero reusability (không thể deploy lên staging mà không duplicate mọi thứ)
- Environment-specific values nhét cứng vào code
- Đổi instance size phải đổi code
- Passwords trong Git (chúc bạn may mắn với security incidents)
- Không có cách nào lấy database endpoint sau khi deploy
Các team thực tế quản lý hàng chục environments. Cách này sập ngay lập tức.
Giải Pháp: Variables, Outputs, State
Ba concepts fix được mess này:
- Input Variables: Làm code linh hoạt
- Outputs: Lấy values quan trọng sau khi deploy
- State: Track những gì thực sự tồn tại
Cùng fix cái hardcoded disaster này nào.
Input Variables: Đừng Hardcode Nữa
Variables biến infrastructure của bạn thành template. Define một lần, deploy mọi nơi.
Basic Variables
# variables.tf
variable "instance_type" {
type = string
description = "EC2 instance type for web servers"
default = "t3.small"
}
variable "environment" {
type = string
description = "Deployment environment (dev, staging, prod)"
}
variable "enable_monitoring" {
type = bool
description = "Enable CloudWatch detailed monitoring"
default = false
}
Giờ dùng chúng:
# main.tf
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = var.instance_type
monitoring = var.enable_monitoring
tags = {
Name = "${var.environment}-web-server"
Environment = var.environment
}
}
Cùng code. Khác values mỗi environment. Đó là điểm mấu chốt.
Variable Types Ngoài Strings
Terraform support nhiều types cho complex configurations:
# String
variable "region" {
type = string
default = "us-east-1"
}
# Number
variable "instance_count" {
type = number
default = 2
}
# Boolean
variable "enable_backups" {
type = bool
default = true
}
# List (ordered collection)
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}
# Map (key-value pairs)
variable "instance_types" {
type = map(string)
default = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.large"
}
}
# Object (structured data)
variable "database_config" {
type = object({
engine = string
engine_version = string
instance_class = string
storage_gb = number
multi_az = bool
})
default = {
engine = "postgres"
engine_version = "15.4"
instance_class = "db.t3.small"
storage_gb = 20
multi_az = false
}
}
Dùng đúng type cho đúng việc. Lists cho ordered stuff, maps cho lookups, objects cho structured config.
Sử Dụng Complex Variables
# Instance types khác nhau mỗi environment
resource "aws_instance" "web" {
instance_type = var.instance_types[var.environment]
# dev -> t3.micro, prod -> t3.large
}
# Spread instances across availability zones
resource "aws_subnet" "public" {
count = length(var.availability_zones)
availability_zone = var.availability_zones[count.index]
}
# Dùng structured database configuration
resource "aws_db_instance" "main" {
engine = var.database_config.engine
engine_version = var.database_config.engine_version
instance_class = var.database_config.instance_class
allocated_storage = var.database_config.storage_gb
multi_az = var.database_config.multi_az
}
Validation: Bắt Lỗi Trước Khi Deploy
Variables không có validation rất nguy hiểm. Ai đó sẽ set environment = "production123" nhầm. Validation bắt được cái đó trước khi nó vào cloud account.
Basic Validation
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
Chạy terraform plan với environment = "production":
Error: Invalid value for variable
on variables.tf line 2:
2: variable "environment" {
Environment must be dev, staging, or prod.
Bắt được typo trước khi deploy. Đó là mục đích.
Advanced Validation
# Validate region format
variable "region" {
type = string
validation {
condition = can(regex("^[a-z]{2}-[a-z]+-[0-9]$", var.region))
error_message = "Region must match format: us-east-1, eu-west-2, etc."
}
}
# Validate instance count range
variable "instance_count" {
type = number
validation {
condition = var.instance_count >= 1 && var.instance_count <= 10
error_message = "Instance count must be between 1 and 10."
}
}
# Validate CIDR block format
variable "vpc_cidr" {
type = string
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "Must be a valid IPv4 CIDR block."
}
}
# Cross-field validation
variable "database_config" {
type = object({
instance_class = string
multi_az = bool
storage_gb = number
})
validation {
condition = (
var.database_config.multi_az == false ||
var.database_config.storage_gb >= 100
)
error_message = "Multi-AZ databases require at least 100GB storage."
}
}
Validation là documentation. Nó cho users biết chính xác values nào được chấp nhận và tại sao.
Variable Precedence: Values Đến Từ Đâu
Terraform load variable values từ nhiều sources. Hiểu precedence để tránh confused.
The Precedence Chain (Thấp đến Cao)
- Default values trong
variableblocks - Environment variables (
TF_VAR_name) terraform.tfvarsfileterraform.tfvars.jsonfile*.auto.tfvarsfiles (alphabetical order)-var-file=...command-line flag-var=...command-line flag
Precedence cao hơn ghi đè thấp hơn. CLI flags luôn thắng.
Multi-Environment Setup
# Directory structure
terraform/
├── main.tf
├── variables.tf
├── terraform.tfvars # Default values
├── environments/
│ ├── dev.tfvars # Development overrides
│ ├── staging.tfvars # Staging overrides
│ └── prod.tfvars # Production overrides
terraform.tfvars (defaults):
region = "us-east-1"
enable_monitoring = false
backup_retention_days = 7
environments/prod.tfvars (production overrides):
instance_type = "t3.large"
enable_monitoring = true
backup_retention_days = 30
multi_az = true
Deploy lên production:
terraform apply -var-file="environments/prod.tfvars"
Cùng codebase, khác configurations. Netflix dùng pattern này để deploy lên 190+ quốc gia.
Environment Variables
# Set variables qua environment (hữu ích trong CI/CD)
export TF_VAR_region="eu-west-1"
export TF_VAR_instance_count=5
terraform apply
# Không cần pass -var flags
Command-Line Variables
# Override từng values riêng lẻ
terraform apply \
-var="environment=staging" \
-var="instance_count=3"
Dùng environment variables trong CI/CD pipelines. Dùng .tfvars files cho persistent configuration.
Outputs: Đừng Lục Tìm Information
Sau khi deploy infrastructure, bạn cần thông tin: database endpoints, load balancer URLs, IP addresses. Outputs giải quyết việc này.
Basic Outputs
# outputs.tf
output "instance_public_ip" {
description = "Public IP address of web server"
value = aws_instance.web.public_ip
}
output "database_endpoint" {
description = "RDS database connection endpoint"
value = aws_db_instance.main.endpoint
}
output "load_balancer_url" {
description = "Application load balancer URL"
value = "https://${aws_lb.main.dns_name}"
}
Sau terraform apply:
Outputs:
instance_public_ip = "54.123.45.67"
database_endpoint = "mydb.abc123.us-east-1.rds.amazonaws.com:5432"
load_balancer_url = "https://my-alb-123456.us-east-1.elb.amazonaws.com"
Copy-paste ready. Không phải lục trong AWS Console.
Sensitive Outputs
output "database_password" {
description = "Master password for RDS instance"
value = aws_db_instance.main.password
sensitive = true
}
Với sensitive = true:
terraform apply
Outputs:
database_password = <sensitive>
Password không bị leak trong logs. Xem nó explicitly:
terraform output database_password
# Shows actual password
Luôn đánh dấu secrets là sensitive. Ngăn accidental exposure trong CI/CD logs.
Structured Outputs
# Group related outputs
output "database_connection" {
description = "Complete database connection information"
value = {
endpoint = aws_db_instance.main.endpoint
port = aws_db_instance.main.port
database = aws_db_instance.main.db_name
username = aws_db_instance.main.username
}
sensitive = true
}
Access trong scripts:
terraform output -json database_connection | jq '.endpoint'
Outputs Cho Module Composition
Khi build modules (Part 7), outputs trở thành module interfaces:
# networking module outputs
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_ids" {
value = aws_subnet.public[*].id
}
Dùng bởi application module:
module "networking" {
source = "./modules/networking"
}
module "application" {
source = "./modules/application"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.public_subnet_ids
}
Outputs kết nối modules lại với nhau. Chúng là API giữa các infrastructure layers.
State Management: Terraform's Memory
Terraform track infrastructure của bạn trong state file. Đây là cách nó biết cái gì exists, cái gì changed, và cái gì cần update.
State Là Gì?
Khi bạn chạy terraform apply, Terraform:
- Đọc desired state từ
.tffiles của bạn - Đọc current state từ
terraform.tfstate - So sánh chúng để generate plan
- Execute changes để làm current = desired
State file là memory của Terraform. Mất nó, Terraform quên mất infrastructure của bạn tồn tại.
State File Anatomy
Sau khi deploy EC2 instance, terraform.tfstate chứa:
{
"version": 4,
"terraform_version": "1.7.0",
"serial": 12,
"lineage": "a1b2c3d4-e5f6-7890-abcd-1234567890ab",
"outputs": {
"instance_ip": {
"value": "54.123.45.67",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "web",
"provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"id": "i-0abcd1234efgh5678",
"ami": "ami-0c55b159cbfafe1f0",
"instance_type": "t3.medium",
"public_ip": "54.123.45.67",
"private_ip": "10.0.1.42"
}
}
]
}
]
}
Các elements chính:
- version: State file format version
- serial: Tăng lên với mỗi change (phát hiện concurrent modifications)
- lineage: Unique ID ngăn state file mixups
- outputs: Cached output values
- resources: Complete resource attributes (IDs, IPs, ARNs)
Local vs Remote State
Local state (mặc định):
# State lưu trong terraform.tfstate ở current directory
terraform apply
# Creates/updates ./terraform.tfstate
Problems với local state:
- Không collaboration (team members không share được state)
- Không locking (concurrent
terraform applygây corruption) - Không backup (xóa file = mất infrastructure tracking)
- Secrets ở plaintext (state chứa passwords, keys)
Remote state (cách làm production):
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks" # State locking
}
}
Benefits:
- Shared access (team dùng cùng state)
- Locking (DynamoDB ngăn concurrent modifications)
- Encryption (state encrypted at rest)
- Versioning (S3 versioning cho phép rollback)
Mình sẽ cover remote state backends ở Part 9. Giờ thì: đừng bao giờ dùng local state trong production.
State Locking
Tưởng tượng hai engineers chạy terraform apply cùng lúc:
- Cả hai đọc current state: "2 instances tồn tại"
- Cả hai plan tạo thêm 1 instance
- Cả hai apply changes
- Kết quả: 4 instances thay vì 3 (race condition)
State locking ngăn điều này:
# Engineer 1
terraform apply
Acquiring state lock. This may take a few moments...
Lock acquired (ID: abc-123)
# Engineer 2 (cùng lúc)
terraform apply
Acquiring state lock. This may take a few moments...
Error: Error locking state: ConditionalCheckFailedException
Lock Info:
ID: abc-123
Operation: OperationTypeApply
Who: engineer1@laptop
Created: 2026-03-12 14:32:15 UTC
Apply của Engineer 2 bị block cho đến khi Engineer 1 xong. Không có race conditions.
Sensitive Data: Đừng Leak Passwords
Terraform state chứa mọi thứ về infrastructure của bạn, kể cả secrets. Handle cẩn thận.
Vấn Đề
resource "aws_db_instance" "main" {
password = "supersecret123" # ĐỪNG LÀM VẬY
}
Sau terraform apply, terraform.tfstate chứa:
{
"resources": [{
"instances": [{
"attributes": {
"password": "supersecret123" # Plaintext trong state
}
}]
}]
}
Bất kỳ ai có quyền truy cập state file đều có database password của bạn.
Giải Pháp 1: Đánh Dấu Variables Là Sensitive
variable "db_password" {
type = string
sensitive = true
}
Terraform ẩn sensitive values trong logs:
terraform plan
# aws_db_instance.main will be created
+ resource "aws_db_instance" "main" {
+ password = (sensitive value)
}
Nhưng state vẫn chứa plaintext. Cái này chỉ ngăn log leakage.
Giải Pháp 2: Encrypted Remote State
terraform {
backend "s3" {
bucket = "my-state-bucket"
key = "terraform.tfstate"
encrypt = true # Encrypts state at rest
# KMS encryption cho extra security
kms_key_id = "arn:aws:kms:us-east-1:123456789:key/abc-123"
}
}
State encrypted trong S3. Cần AWS KMS permissions để đọc.
Giải Pháp 3: External Secrets Management (Best Practice)
Đừng bao giờ lưu secrets trong Terraform. Fetch chúng lúc runtime:
# Fetch password từ AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/database/master-password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
Secret lưu trong Secrets Manager, không phải Git. State vẫn chứa nó, nhưng encrypted at rest.
Mình sẽ cover HashiCorp Vault integration ở Part 11. Giờ thì: secrets thuộc về secret stores, không phải code.
Real-World Pattern: Multi-Environment Setup
Ghép tất cả lại với nhau:
terraform/
├── main.tf # Core infrastructure
├── variables.tf # Variable declarations
├── outputs.tf # Output definitions
├── backend.tf # Remote state configuration
├── terraform.tfvars # Default values
└── environments/
├── dev.tfvars # Development overrides
├── staging.tfvars # Staging overrides
└── prod.tfvars # Production overrides
variables.tf:
variable "environment" {
type = string
description = "Deployment environment"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Must be dev, staging, or prod."
}
}
variable "instance_config" {
type = object({
type = string
count = number
})
description = "EC2 instance configuration"
validation {
condition = var.instance_config.count >= 1 && var.instance_config.count <= 20
error_message = "Instance count must be 1-20."
}
}
variable "enable_multi_az" {
type = bool
description = "Enable multi-AZ deployment for high availability"
default = false
}
main.tf:
resource "aws_instance" "web" {
count = var.instance_config.count
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_config.type
tags = {
Name = "${var.environment}-web-${count.index + 1}"
Environment = var.environment
}
}
resource "aws_db_instance" "main" {
engine = "postgres"
instance_class = var.environment == "prod" ? "db.t3.large" : "db.t3.small"
multi_az = var.enable_multi_az
# Password từ Secrets Manager
password = data.aws_secretsmanager_secret_version.db_password.secret_string
}
outputs.tf:
output "web_server_ips" {
description = "Public IPs of web servers"
value = aws_instance.web[*].public_ip
}
output "database_endpoint" {
description = "Database connection endpoint"
value = aws_db_instance.main.endpoint
sensitive = true
}
environments/prod.tfvars:
environment = "prod"
instance_config = {
type = "t3.large"
count = 5
}
enable_multi_az = true
Deploy lên production:
terraform apply -var-file="environments/prod.tfvars"
Plan: 6 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
Outputs:
web_server_ips = [
"54.123.45.67",
"54.123.45.68",
"54.123.45.69",
"54.123.45.70",
"54.123.45.71"
]
database_endpoint = <sensitive>
Cùng code cho dev, staging, prod. Chỉ variables khác nhau thôi.
Quick Check: Bạn Hiểu Chưa?
Trước khi chuyển sang Part 6, bạn trả lời được những câu này không?
Khác biệt giữa
list(string)vàmap(string)là gì?- Lists là ordered, access bằng index (
[0],[1]). Maps là key-value pairs, access bằng key (["dev"],["prod"]).
- Lists là ordered, access bằng index (
Cái nào thắng:
terraform.tfvarshay-varCLI flag?- CLI flags có precedence cao nhất.
-varoverride mọi thứ.
- CLI flags có precedence cao nhất.
Tại sao đánh dấu outputs là
sensitive = true?- Ngăn secrets xuất hiện trong console output và CI/CD logs. Bắt buộc phải dùng
terraform output <name>explicit để xem.
- Ngăn secrets xuất hiện trong console output và CI/CD logs. Bắt buộc phải dùng
Chuyện gì xảy ra nếu bạn mất state file?
- Terraform quên infrastructure của bạn exists.
applytiếp theo sẽ cố recreate mọi thứ, gây conflicts.
- Terraform quên infrastructure của bạn exists.
Production secrets nên lưu ở đâu?
- External secret stores (AWS Secrets Manager, HashiCorp Vault). Reference chúng qua
datasources lúc runtime.
- External secret stores (AWS Secrets Manager, HashiCorp Vault). Reference chúng qua
Nếu bạn tự tin với những câu trả lời này, bạn sẵn sàng cho Part 6 rồi đó.
Tiếp Theo Là Gì?
Trong Part 6: Core Terraform Workflow, bạn sẽ master development cycle:
terraform init: Initialize providers và modulesterraform plan: Preview changes trước khi applyterraform apply: Deploy infrastructure safelyterraform destroy: Tear down resources cleanlyterraform refresh: Sync state với realityterraform taint: Force resource recreation
Thêm workflows để handle drift, recover từ failures, và debug plans.
Sẵn sàng học Terraform development loop chưa? Tiếp tục Part 6 → (coming soon)
Resources
- Terraform Variables Documentation
- Terraform Outputs Documentation
- Terraform State Documentation
- AWS Secrets Manager Terraform Provider
Điều hướng series:
- Phần 1: Tại sao cần Infrastructure as Code?
- Phần 2: Cài đặt Terraform
- Phần 3: Tài nguyên Cloud đầu tiên
- Phần 4: Cơ bản về HCL
- Phần 5: Variables, Outputs & State (Bạn đang ở đây)
- Phần 6: Quy trình làm việc với Terraform
- Phần 7: Modules để tổ chức code (Sắp ra mắt)
- Phần 8: Mô hình Multi-Cloud (Sắp ra mắt)
- Phần 9: Quản lý State & Team Workflows (Sắp ra mắt)
- Phần 10: Testing & Validation (Sắp ra mắt)
- Phần 11: Bảo mật & Quản lý Secrets (Sắp ra mắt)
- Phần 12: Production Patterns & DevSecOps (Sắp ra mắt)
Bài viết này là phần của series "Terraform from Fundamentals to Production".