0

Destroy Worlds với Terraform #2: Values & Variables

Terraform có ba cách chính để quản lý các giá trị (values):

  1. Input Variables – Các biến đầu vào.
  2. Local Values (Locals) – Các giá trị nội bộ được tính toán trong configuration.
  3. Outputs – Các giá trị đầu ra. Mỗi loại có một mục đích sử dụng khác nhau. Việc hiểu rõ khi nào nên sử dụng từng loại sẽ giúp configuration của bạn trở nên rõ ràng, dễ bảo trì và dễ mở rộng hơn.

Bài hướng dẫn này sẽ giải thích cả ba loại thông qua các ví dụ thực tế trên AWS.

1. Input Variables

Input Variables là các tham số được người gọi (caller) cung cấp cho Module hoặc Configuration của bạn.

Chúng được khai báo bằng variable block và được tham chiếu thông qua cú pháp: var.name

1.1. Các kiểu Variable cơ bản

Terraform hỗ trợ nhiều kiểu dữ liệu khác nhau cho Variable.

String Variable:

Khai báo một biến kiểu String với giá trị mặc định (default).

variable "environment" {
  description = "Deployment environment (dev, staging, production)"
  type        = string
  default     = "dev"
}

Number Variable:

Khai báo biến kiểu Number để xác định số lượng EC2 instances cần tạo.

variable "instance_count" {
  description = "Number of EC2 instances to create"
  type        = number
  default     = 2
}

Boolean Variable:

Khai báo biến kiểu Boolean để xác định có bật detailed monitoring hay không.

variable "enable_monitoring" {
  description = "Whether to enable detailed monitoring"
  type        = bool
  default     = true
}

List Variable:

Khai báo biến kiểu List để xác định danh sách Availability Zones cần triển khai resource.

variable "availability_zones" {
  description = "AZs to deploy resources in"
  type        = list(string)
  default     = ["us-east-1a", "us-east-1b"]
}

Map Variable:

Khai báo biến kiểu Map để xác định các Instance Types tương ứng với từng environment.

variable "instance_types" {
  description = "Instance types per environment"
  type        = map(string)
  default = {
    dev        = "t3.micro"
    staging    = "t3.small"
    production = "t3.medium"
  }
}

1.2. Các kiểu Variable phức tạp

Đối với dữ liệu có cấu trúc (structured data), bạn nên sử dụng kiểu object.

Object Variable với cấu trúc cụ thể:

Ví dụ dưới đây khai báo một Database Configuration với các thuộc tính được xác định rõ ràng.

variable "database_config" {
  description = "Database configuration"
  type = object({
    engine         = string
    instance_class = string
    storage_gb     = number
    multi_az       = bool
    backup_days    = number
  })
  default = {
    engine         = "postgres"
    instance_class = "db.t3.medium"
    storage_gb     = 20
    multi_az       = false
    backup_days    = 7
  }
}

List of Objects:

Sử dụng List of Objects để định nghĩa danh sách các Security Group Rules.

variable "security_group_rules" {
  description = "Ingress rules for the security group"
  type = list(object({
    port        = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = []
}

Map of Objects:

Sử dụng Map of Objects để định nghĩa configuration cho nhiều services.

variable "services" {
  description = "Service configurations"
  type = map(object({
    cpu         = number
    memory      = number
    port        = number
    environment = string
  }))
  default = {}
}

1.3. Sensitive Variables

Đánh dấu các Variable chứa thông tin nhạy cảm bằng sensitive = true để ngăn chúng hiển thị trong output thông thường của Terraform CLI.

Lưu ý: Terraform vẫn lưu các giá trị nhạy cảm trong State File, trừ khi bạn sử dụng những tính năng được thiết kế để không lưu các giá trị đó.

Ví dụ:

variable "db_password" {
  description = "Database master password"
  type        = string
  sensitive   = true
}

variable "api_key" {
  description = "Third-party API key"
  type        = string
  sensitive   = true
}

Trong ví dụ này, cả Database Password và API Key đều được đánh dấu là Sensitive Variables.

1.4. Cách truyền giá trị cho Variables

Có một số cách để cung cấp giá trị cho Variables trong Terraform.

Cách 1: Sử dụng Command Line Flags

terraform plan -var="environment=production" -var="instance_count=4"

Bạn có thể truyền trực tiếp giá trị của Variable thông qua flag -var.

Cách 2: Sử dụng Variable File

terraform plan -var-file="production.tfvars"

Terraform sẽ lấy các giá trị được định nghĩa trong file production.tfvars.

Cách 3: Sử dụng Environment Variables

Các Environment Variables phải có tiền tố TF_VAR_.

export TF_VAR_environment="production"

export TF_VAR_instance_count=4

terraform plan

Cách 4: Sử dụng Auto-loaded Files

Các file có tên terraform.tfvars hoặc *.auto.tfvars sẽ được Terraform tự động load.

Ví dụ, file production.tfvars:

environment       = "production"
instance_count    = 4
enable_monitoring = true

database_config = {
  engine         = "postgres"
  instance_class = "db.r6g.large"
  storage_gb     = 100
  multi_az       = true
  backup_days    = 30
}

2. Local Values (Locals)

Local Values là các giá trị được tính toán bên trong Module.

Bạn có thể hiểu chúng giống như những biến nội bộ (private variables): chúng được tính toán dựa trên Input Variables, Data Sources hoặc các Local Values khác, nhưng không được expose ra bên ngoài dưới dạng Input.

Nói cách khác:

  • Variables: Nhận giá trị đầu vào từ bên ngoài.
  • Locals: Tính toán và lưu trữ các giá trị nội bộ dựa trên những dữ liệu đã có.

2.1. Local Values cơ bản

Ví dụ:

locals {
  # Derived from variables
  name_prefix = "${var.project}-${var.environment}"

  # Computed values
  is_production = var.environment == "production"

  # Common tags applied to all resources
  common_tags = {
    Project     = var.project
    Environment = var.environment
    ManagedBy   = "terraform"
    Team        = var.team
  }

  # Conditional values
  instance_type = local.is_production ? "t3.large" : "t3.micro"
  multi_az      = local.is_production ? true : false

  # Merged or transformed data
  all_tags = merge(local.common_tags, var.extra_tags)
}

Giải thích các thành phần:

Local Value Ý nghĩa
name_prefix Kết hợp project và environment để tạo naming convention thống nhất.
name_prefix Kiểm tra environment hiện tại có phải production hay không.
name_prefix Định nghĩa các Tags dùng chung cho tất cả Resources.
name_prefix Nếu là production thì sử dụng t3.large, ngược lại sử dụng t3.micro.
name_prefix Nếu là production thì bật Multi-AZ, ngược lại tắt.
name_prefix Kết hợp Common Tags với các Extra Tags thông qua hàm merge().

Một điểm quan trọng: Khi tham chiếu Local Values, bạn phải sử dụng tiền tố local. thay vì var..

Ví dụ:

  • local.name_prefix
  • local.common_tags
  • local.instance_type

2.2. Khi nào nên sử dụng Locals thay vì Variables?

Đây là phần rất quan trọng khi thiết kế Terraform Modules.

Sử dụng Variables khi:

  • Giá trị đến từ bên ngoài Module (được caller cung cấp).
  • Các Environment khác nhau cần những giá trị khác nhau.
  • Giá trị đó thực sự là một Parameter của Configuration.

Sử dụng Locals khi:

  • Giá trị được tính toán dựa trên những giá trị khác.
  • Bạn muốn tránh phải viết lặp lại cùng một Expression.
  • Bạn cần tính toán những giá trị phức tạp.
  • Bạn muốn đặt một cái tên có ý nghĩa cho một phép tính hoặc Expression.

Ví dụ thực tế kết hợp Variables và Locals

Giả sử bạn cần triển khai AWS Infrastructure cho nhiều Environment khác nhau như dev, staging và production.

Đầu tiên, khai báo Variables để nhận Input từ bên ngoài:

# Variables - inputs from the caller
variable "project" {
  type = string
}

variable "environment" {
  type = string
}

variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}


Tiếp theo, sử dụng Locals để tính toán dựa trên các Input đã nhận:
# Locals - computed from inputs
locals {
  # Naming convention
  name_prefix = "${var.project}-${var.environment}"

  common_tags = {
    Project     = var.project
    Environment = var.environment
    ManagedBy   = "terraform"
  }

  # Subnet CIDR calculation
  public_subnets = [
    cidrsubnet(var.vpc_cidr, 8, 1),
    cidrsubnet(var.vpc_cidr, 8, 2),
  ]

  private_subnets = [
    cidrsubnet(var.vpc_cidr, 8, 10),
    cidrsubnet(var.vpc_cidr, 8, 11),
  ]

  # Environment-specific settings
  settings = {
    dev = {
      instance_type = "t3.micro"
      min_size      = 1
      max_size      = 2
    }
    staging = {
      instance_type = "t3.small"
      min_size      = 2
      max_size      = 4
    }
    production = {
      instance_type = "t3.medium"
      min_size      = 3
      max_size      = 10
    }
  }

  # Look up settings for the current environment
  current_settings = local.settings[var.environment]
}

Giải thích:

  • name_prefix: Tự động tạo tên dựa trên Project và Environment.
  • common_tags: Định nghĩa Tags dùng chung.
  • public_subnets: Tính toán CIDR cho Public Subnets.
  • private_subnets: Tính toán CIDR cho Private Subnets.
  • settings: Lưu trữ cấu hình riêng cho từng Environment.
  • current_settings: Lấy cấu hình tương ứng với Environment hiện tại. Cuối cùng, sử dụng các Locals này trong Resource:
# Use locals in resources
resource "aws_instance" "app" {
  count         = local.current_settings.min_size
  ami           = data.aws_ami.app.id
  instance_type = local.current_settings.instance_type

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-app-${count.index}"
  })
}

Ở đây:

  • count: Số lượng EC2 Instances được tạo dựa trên min_size của Environment hiện tại.
  • ami: Lấy AMI ID từ Data Source.
  • instance_type: Lấy Instance Type tương ứng với Environment.
  • tags: Kết hợp Common Tags với Name Tag riêng cho từng Instance.

2.3. Các mẫu sử dụng Local Values nâng cao

Bạn có thể sử dụng Locals để thực hiện Data Transformation và xử lý các Logic phức tạp.

Ví dụ:

locals {
  # Flatten nested structures
  subnet_configs = flatten([
    for az_index, az in var.availability_zones : [
      {
        az         = az
        cidr       = cidrsubnet(var.vpc_cidr, 8, az_index + 1)
        type       = "public"
      },
      {
        az         = az
        cidr       = cidrsubnet(var.vpc_cidr, 8, az_index + 10)
        type       = "private"
      },
    ]
  ])

  # Create a map from a list for use with for_each
  subnet_map = {
    for s in local.subnet_configs :
    "${s.type}-${s.az}" => s
  }

  # Filter a collection
  production_services = {
    for name, config in var.services :
    name => config
    if config.environment == "production"
  }
}

Có ba kỹ thuật chính trong ví dụ trên:

1. Flatten nested structures

Sử dụng flatten() để làm phẳng các cấu trúc List lồng nhau.

Trong ví dụ này, Terraform duyệt qua từng Availability Zone và tạo ra hai cấu hình Subnet:

  • Một Public Subnet.
  • Một Private Subnet.

2. Create a Map from a List

Chuyển đổi một List thành Map để có thể sử dụng với for_each.

subnet_map = {
  for s in local.subnet_configs :
  "${s.type}-${s.az}" => s
}

Mỗi Subnet sẽ có một Key duy nhất được tạo từ loại Subnet và Availability Zone.

Ví dụ:

  • public-us-east-1a
  • private-us-east-1a
  • public-us-east-1b
  • private-us-east-1b

3. Filter a Collection

Lọc một Collection để chỉ lấy những Services thuộc Environment production.

production_services = {
  for name, config in var.services :
  name => config
  if config.environment == "production"
}

Như vậy, bạn có thể sử dụng Locals để chuẩn hóa dữ liệu, chuyển đổi cấu trúc và lọc các Resources trước khi sử dụng chúng trong Terraform.

3. Output Values

Outputs được sử dụng để expose các giá trị từ Module.

Các giá trị này sẽ hiển thị trong CLI sau khi chạy terraform apply và có thể được tham chiếu bởi những Modules hoặc Configurations khác.

Nói đơn giản:

  • Variables: Nhận dữ liệu đầu vào.
  • Locals: Xử lý và tính toán dữ liệu nội bộ.
  • Outputs: Xuất dữ liệu ra bên ngoài.

3.1. Các loại Output cơ bản

Basic Output:

Lấy ID của VPC vừa được tạo.

output "vpc_id" {
  description = "ID of the created VPC"
  value       = aws_vpc.main.id
}

Sensitive Output:

Output chứa thông tin nhạy cảm sẽ bị ẩn trong output mặc định của CLI.

output "db_password" {
  description = "Database master password"
  value       = random_password.db.result
  sensitive   = true
}

Computed Output:

Tạo một URL hoàn chỉnh dựa trên DNS Name của Application Load Balancer.

output "api_url" {
  description = "Full URL of the API"
  value       = "https://${aws_lb.api.dns_name}/api/v1"
}

Conditional Output:

Chỉ trả về Public IP của Bastion Host nếu Resource này được tạo.

output "bastion_ip" {
  description = "Public IP of the bastion host (if created)"
  value       = var.create_bastion ? aws_instance.bastion[0].public_ip : null
}

Nếu create_bastion = false, Output sẽ trả về null.

Map Output:

Xuất danh sách Subnet IDs theo từng loại Subnet.

output "subnet_ids" {
  description = "Map of subnet type to subnet IDs"
  value = {
    public  = aws_subnet.public[*].id
    private = aws_subnet.private[*].id
  }
}

3.2. Cách đọc Outputs

Sau khi chạy terraform apply, bạn có thể lấy các giá trị Output bằng những câu lệnh sau.

Hiển thị tất cả Outputs:

terraform output

Hiển thị một Output cụ thể:

terraform output vpc_id

Lấy giá trị Raw (hữu ích khi sử dụng trong Scripts):

terraform output -raw vpc_id

Lấy Output dưới định dạng JSON (hữu ích với các kiểu dữ liệu phức tạp):

terraform output -json subnet_ids

4. Kết hợp tất cả

Dưới đây là một ví dụ hoàn chỉnh kết hợp Variables, Locals và Outputs trong cùng một Terraform Configuration.

Giả sử bạn muốn tạo một VPC trên AWS. File 1: variables.tf Khai báo các Variables để nhận dữ liệu đầu vào.

# variables.tf
variable "project" {
  type        = string
  description = "Project name"
}

variable "environment" {
  type        = string
  description = "Environment name"
}

File 2: locals.tf Định nghĩa các Local Values để xử lý và chuẩn hóa dữ liệu.

# locals.tf
locals {
  name_prefix = "${var.project}-${var.environment}"

  common_tags = {
    Project     = var.project
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

File 3: main.tf Sử dụng Variables và Locals để tạo AWS Resource.

# main.tf
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-vpc"
  })
}

File 4: outputs.tf Expose VPC ID sau khi Resource được tạo thành công.

# outputs.tf
output "vpc_id" {
  value       = aws_vpc.main.id
  description = "VPC ID"
}

Với cách tổ chức này, mỗi file có một nhiệm vụ riêng:

File Nhiệm vụ
variables.tf Khai báo các Input Variables.
locals.tf Xử lý và tính toán các Local Values.
main.tf Khai báo và triển khai Resources.
outputs.tf Expose các giá trị đầu ra.

5. Tổng kết

Đây là bảng mình khuyên bạn nên ghi nhớ khi làm việc với Terraform Modules.

Tiêu chí Variable Local Output
Nhiệm vụ Nhận giá trị theo Environment Tính toán nội bộ Xuất dữ liệu
Khai báo variable locals output
Cách tham chiếu var.name local.name module.name.output
Ai cung cấp giá trị? Root Module hoặc Module (bên ngoài) gọi nó (caller) var, resource, data source, module output, hoặc các local khác trong cùng Module Terraform lấy giá trị từ provider như aws, azure, gcp

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.