Posted on :: 2646 Words :: Tags: , ,

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?

  1. Zero reusability (không thể deploy lên staging mà không duplicate mọi thứ)
  2. Environment-specific values nhét cứng vào code
  3. Đổi instance size phải đổi code
  4. Passwords trong Git (chúc bạn may mắn với security incidents)
  5. 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:

  1. Input Variables: Làm code linh hoạt
  2. Outputs: Lấy values quan trọng sau khi deploy
  3. 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)

  1. Default values trong variable blocks
  2. Environment variables (TF_VAR_name)
  3. terraform.tfvars file
  4. terraform.tfvars.json file
  5. *.auto.tfvars files (alphabetical order)
  6. -var-file=... command-line flag
  7. -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:

  1. Đọc desired state từ .tf files của bạn
  2. Đọc current state từ terraform.tfstate
  3. So sánh chúng để generate plan
  4. 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:

  1. Không collaboration (team members không share được state)
  2. Không locking (concurrent terraform apply gây corruption)
  3. Không backup (xóa file = mất infrastructure tracking)
  4. 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:

  1. Cả hai đọc current state: "2 instances tồn tại"
  2. Cả hai plan tạo thêm 1 instance
  3. Cả hai apply changes
  4. 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?

  1. Khác biệt giữa list(string)map(string) là gì?

    • Lists là ordered, access bằng index ([0], [1]). Maps là key-value pairs, access bằng key (["dev"], ["prod"]).
  2. Cái nào thắng: terraform.tfvars hay -var CLI flag?

    • CLI flags có precedence cao nhất. -var override mọi thứ.
  3. 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.
  4. Chuyện gì xảy ra nếu bạn mất state file?

    • Terraform quên infrastructure của bạn exists. apply tiếp theo sẽ cố recreate mọi thứ, gây conflicts.
  5. Production secrets nên lưu ở đâu?

    • External secret stores (AWS Secrets Manager, HashiCorp Vault). Reference chúng qua data sources lúc runtime.

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à modules
  • terraform plan: Preview changes trước khi apply
  • terraform apply: Deploy infrastructure safely
  • terraform destroy: Tear down resources cleanly
  • terraform refresh: Sync state với reality
  • terraform 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

Điều hướng series:


Bài viết này là phần của series "Terraform from Fundamentals to Production".