Posted on :: 2423 Words :: Tags: , ,

Cơ Bản Về HCL

Bạn đã viết code Terraform được ba phần rồi. Nhưng bạn có thực sự hiểu mình đang viết cái gì không?

Nếu bạn đang copy examples mà không biết tại sao resource khác với data, hay khi nào dùng list(string) thay vì map(string), thì phần này sẽ giải đáp hết. Mình sẽ đào sâu vào HashiCorp Configuration Language (HCL) — ngôn ngữ chạy Terraform.

📦 Code Examples

Repository: terraform-hcl-tutorial-series Phần Này: Part 4 - HCL Syntax Examples

Lấy ví dụ hoạt động:

git clone https://github.com/khuongdo/terraform-hcl-tutorial-series.git
cd terraform-hcl-tutorial-series
git checkout part-04
cd examples/part-04-hcl-fundamentals/

# Thử các examples
terraform init
terraform plan

Tại Sao HCL Tồn Tại

HCL không phải JSON đâu nhé. Cũng không phải YAML. HashiCorp build nó riêng cho infrastructure code, và lựa chọn này quan trọng hơn bạn nghĩ đấy.

JSON thì dài dòng, không support comments, khiến người đọc khổ sở. YAML nhìn clean cho đến khi bạn gặp indentation hell và tự hỏi sao pipeline fail chỉ vì một khoảng trắng vô hình. HCL cho bạn thứ tốt hơn: một ngôn ngữ đọc như code nhưng define infrastructure.

Bạn được gì:

  • Comments thực sự tồn tại trong files
  • Expressions và functions thay vì static strings
  • Strong typing bắt lỗi trước khi terraform apply phá nát cuối tuần của bạn
  • Interpolation cho dynamic configurations
  • Conditionals và loops cho patterns không thể làm được trong JSON thuần

Đây là điều quan trọng: hiểu HCL thay đổi cách bạn viết Terraform. Bạn sẽ không còn copy-paste từ Stack Overflow nữa mà bắt đầu design infrastructure.

HCL Hoạt Động Thế Nào

Mọi file Terraform được build từ ba thứ: blocks, arguments, và expressions. Vậy thôi.

Blocks

Block là một container. Nó có type, có thể có labels, và body trong ngoặc nhọn:

<BLOCK_TYPE> "<LABEL>" "<LABEL>" {
  # Arguments ở đây
  <IDENTIFIER> = <EXPRESSION>
}

Ví dụ thực tế:

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
}

variable "region" {
  type    = string
  default = "us-west-2"
}

output "instance_ip" {
  value = aws_instance.web.public_ip
}

Các block types bạn sẽ dùng liên tục:

  • resource tạo infrastructure
  • data đọc stuff có sẵn
  • variable khai báo inputs
  • output expose values
  • locals define computed values
  • module gọi reusable modules
  • terraform config Terraform
  • provider setup cloud provider credentials

Arguments

Arguments gán values. Bên trái là tên, bên phải là expression:

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"  # String
  instance_type = var.instance_type        # Variable reference
  count         = 3                        # Number
}

Convention: một argument mỗi dòng. Làm diffs dễ đọc.

Expressions

Đây là chỗ HCL trở nên hữu ích. Expressions tính toán values:

# Literals
"hello"
42
true

# References
var.instance_type
aws_vpc.main.id

# Math
var.instance_count * 2

# String interpolation
"Instance: ${var.name}"

# Conditionals
var.env == "prod" ? "t3.large" : "t3.micro"

# Functions
file("${path.module}/userdata.sh")
jsonencode({ name = "app", port = 8080 })

# Loops
[for s in var.subnets : s.id]

Thay vì hardcode mọi thứ, bạn compute values dựa trên context. Đó là power move.

Type System

HCL có strong types. Điều này tốt vì nó bắt lỗi sớm, không phải lúc deploy lúc 3 giờ sáng.

Primitive Types

String — text values

variable "region" {
  type    = string
  default = "us-west-2"
}

# Multi-line strings dùng heredoc
variable "user_data" {
  type    = string
  default = <<-EOF
    #!/bin/bash
    echo "Hello, World!"
  EOF
}

Number — integers và floats (HCL không phân biệt)

variable "instance_count" {
  type    = number
  default = 3
}

variable "disk_size" {
  type    = number
  default = 100.5
}

Bool — true hoặc false

variable "enable_monitoring" {
  type    = bool
  default = true
}

# Pattern phổ biến: conditional resources
resource "aws_cloudwatch_alarm" "cpu" {
  count = var.enable_monitoring ? 1 : 0
  # ...
}

Complex Types

List — ordered collection, cùng type cho tất cả elements

variable "availability_zones" {
  type    = list(string)
  default = ["us-west-2a", "us-west-2b", "us-west-2c"]
}

# Access by index
locals {
  first_az = var.availability_zones[0]
}

# Loop với count
resource "aws_subnet" "public" {
  count             = length(var.availability_zones)
  availability_zone = var.availability_zones[count.index]
  cidr_block        = "10.0.${count.index}.0/24"
}

Map — key-value pairs (keys luôn là strings)

variable "tags" {
  type = map(string)
  default = {
    Environment = "production"
    Owner       = "platform-team"
    CostCenter  = "engineering"
  }
}

# Access by key
locals {
  env = var.tags["Environment"]
}

# Merge maps lại với nhau
locals {
  all_tags = merge(
    var.tags,
    { ManagedBy = "terraform" }
  )
}

Set — unordered, unique values

variable "allowed_ports" {
  type    = set(number)
  default = [80, 443, 8080]
}

# Dùng với for_each
resource "aws_security_group_rule" "ingress" {
  for_each = var.allowed_ports

  type        = "ingress"
  from_port   = each.value
  to_port     = each.value
  protocol    = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

Object — structured data với typed attributes

variable "server_config" {
  type = object({
    instance_type = string
    ami_id        = string
    root_volume   = object({
      size = number
      type = string
    })
    tags = map(string)
  })

  default = {
    instance_type = "t3.micro"
    ami_id        = "ami-0c55b159cbfafe1f0"
    root_volume = {
      size = 20
      type = "gp3"
    }
    tags = {
      Name = "web-server"
    }
  }
}

# Access nested attributes
locals {
  volume_size = var.server_config.root_volume.size
}

Tuple — fixed-length list với specific types mỗi position

variable "connection_info" {
  type = tuple([string, number, bool])
  # Phải chính xác: [string, number, bool]
  default = ["db.example.com", 5432, true]
}

locals {
  host    = var.connection_info[0]
  port    = var.connection_info[1]
  use_ssl = var.connection_info[2]
}

Any — chấp nhận bất cứ gì (dùng ít thôi)

variable "custom_config" {
  type    = any
  default = {}
}

Type Safety Tip

Tránh any trừ khi thực sự cần. Nó bypass type checking và khiến debugging đau đầu. Nếu bạn có structured data, dùng object({...}) thay thế.

String Interpolation và Directives

Embed expressions trong strings với ${}:

locals {
  region      = "us-west-2"
  environment = "production"

  bucket_name = "${var.project}-${local.environment}-logs"
  # Kết quả: "myapp-production-logs"
}

String directives (%{}) cho phép dùng control flow bên trong strings:

locals {
  user_data = <<-EOF
    #!/bin/bash
    %{ for ip in var.server_ips ~}
    echo "Server: ${ip}"
    %{ endfor ~}
  EOF
}

Conditionals trong strings:

locals {
  greeting = <<-EOT
    Hello, ${var.username}!
    %{ if var.is_admin }
    You have admin privileges.
    %{ else }
    You have standard access.
    %{ endif }
  EOT
}

Operators

Math và logic hoạt động như hầu hết ngôn ngữ:

# Arithmetic
var.count + 5
var.price * 1.2
var.total / var.quantity

# Comparison
var.count > 10
var.env == "prod"
var.enabled != false

# Logical
var.is_prod && var.enable_monitoring
var.is_dev || var.is_staging
!var.disabled

Built-In Functions

Terraform cho bạn 100+ functions. Bạn không thể viết custom ones — bạn làm việc với những gì được cung cấp. Đây là những cái bạn sẽ thực sự dùng.

File Functions

file() đọc file dưới dạng string:

locals {
  user_data = file("${path.module}/scripts/init.sh")
}

filebase64() đọc file dưới dạng base64:

resource "aws_s3_object" "config" {
  bucket  = aws_s3_bucket.app.id
  key     = "config.json"
  content = filebase64("${path.module}/config.json")
}

templatefile() renders templates với variables:

# templates/userdata.tpl
#!/bin/bash
echo "Environment: ${environment}"
echo "Region: ${region}"

# main.tf
locals {
  user_data = templatefile("${path.module}/templates/userdata.tpl", {
    environment = var.environment
    region      = var.region
  })
}

Path References

path.module   # Current module directory
path.root     # Root module directory
path.cwd      # Current working directory

Encoding Functions

jsonencode() chuyển HCL values thành JSON:

locals {
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "s3:GetObject"
      Resource = "arn:aws:s3:::my-bucket/*"
    }]
  })
}

jsondecode() parses JSON strings:

locals {
  config_data = jsondecode(file("config.json"))
  api_url     = local.config_data.api.url
}

yamlencode()yamldecode() làm tương tự cho YAML:

locals {
  deployment = yamldecode(file("k8s/deployment.yaml"))
  replicas   = local.deployment.spec.replicas
}

Collection Functions

length() đếm elements:

length(["a", "b", "c"])  # 3
length({ key = "value" })  # 1

concat() merges lists:

concat(["a", "b"], ["c", "d"])  # ["a", "b", "c", "d"]

merge() combines maps:

merge(
  { name = "app" },
  { env = "prod" }
)
# Kết quả: { name = "app", env = "prod" }

lookup() lấy map value với fallback:

lookup(var.tags, "Environment", "unknown")
# Returns var.tags["Environment"] nếu tồn tại, nếu không thì "unknown"

keys()values() extract từ maps:

keys({ a = 1, b = 2 })    # ["a", "b"]
values({ a = 1, b = 2 })  # [1, 2]

element() lấy item theo index (wraps around):

element(["a", "b", "c"], 0)  # "a"
element(["a", "b", "c"], 5)  # "c" (5 % 3 = 2)

contains() check nếu list có value:

contains(["prod", "staging"], var.env)  # true/false

distinct() removes duplicates:

distinct(["a", "b", "a", "c"])  # ["a", "b", "c"]

flatten() un-nests lists:

flatten([["a", "b"], ["c", "d"]])  # ["a", "b", "c", "d"]

sort() sorts lists:

sort(["c", "a", "b"])  # ["a", "b", "c"]

String Functions

split()join() chuyển đổi giữa strings và lists:

split(",", "a,b,c")      # ["a", "b", "c"]
join("-", ["a", "b"])    # "a-b"

upper()lower() đổi case:

upper("hello")  # "HELLO"
lower("WORLD")  # "world"

replace() swaps substrings:

replace("hello-world", "-", "_")  # "hello_world"

trimspace() removes whitespace:

trimspace("  hello  ")  # "hello"

format() làm printf-style formatting:

format("Instance %03d in %s", 42, "us-west-2")
# "Instance 042 in us-west-2"

Conditionals

Ternary operator là bạn của bạn:

condition ? true_value : false_value

Trong thực tế:

resource "aws_instance" "web" {
  instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"

  tags = {
    Environment = var.environment == "prod" ? "Production" : "Development"
  }
}

Bạn có thể chain chúng, nhưng nó sẽ lộn xộn nhanh lắm. Dùng locals thay thế:

locals {
  instance_type = (
    var.environment == "prod" ? "t3.large" :
    var.environment == "staging" ? "t3.medium" :
    "t3.micro"
  )
}

Conditional resources với count:

resource "aws_cloudwatch_alarm" "cpu" {
  count = var.enable_monitoring ? 1 : 0
  # Tạo 1 nếu true, 0 nếu false (không có resource)
  # ...
}

For Expressions và Loops

For expressions transform collections. Nghĩ như list comprehensions trong Python hoặc map/filter trong JavaScript.

Transform Lists

variable "names" {
  default = ["alice", "bob", "charlie"]
}

locals {
  # Chuyển thành uppercase
  upper_names = [for name in var.names : upper(name)]
  # ["ALICE", "BOB", "CHARLIE"]

  # Filter và transform
  short_names = [for name in var.names : name if length(name) < 6]
  # ["alice", "bob"]
}

Transform Maps

variable "instances" {
  default = {
    web1 = "t3.micro"
    web2 = "t3.small"
    db   = "t3.large"
  }
}

locals {
  # Transform values
  instance_sizes = {
    for name, type in var.instances :
    name => upper(type)
  }
  # { web1 = "T3.MICRO", web2 = "T3.SMALL", db = "T3.LARGE" }

  # Filter map
  web_instances = {
    for name, type in var.instances :
    name => type
    if substr(name, 0, 3) == "web"
  }
  # { web1 = "t3.micro", web2 = "t3.small" }
}

Extract từ Objects

variable "servers" {
  type = list(object({
    name = string
    size = string
  }))

  default = [
    { name = "web", size = "small" },
    { name = "db", size = "large" },
  ]
}

locals {
  # Extract attribute
  server_names = [for s in var.servers : s.name]
  # ["web", "db"]

  # Build map từ list
  server_map = {
    for s in var.servers :
    s.name => s.size
  }
  # { web = "small", db = "large" }
}

Resource Iteration với for_each

Tạo multiple resources từ set:

variable "buckets" {
  type = set(string)
  default = ["logs", "backups", "artifacts"]
}

resource "aws_s3_bucket" "app" {
  for_each = var.buckets

  bucket = "${var.project}-${each.key}"

  tags = {
    Name = each.value
  }
}

# Reference: aws_s3_bucket.app["logs"].id

Hoặc từ map:

variable "environments" {
  type = map(object({
    instance_type = string
    instance_count = number
  }))

  default = {
    dev = {
      instance_type  = "t3.micro"
      instance_count = 1
    }
    prod = {
      instance_type  = "t3.large"
      instance_count = 3
    }
  }
}

module "app" {
  for_each = var.environments
  source   = "./modules/app"

  environment    = each.key
  instance_type  = each.value.instance_type
  instance_count = each.value.instance_count
}

for_each vs count

Dùng for_each khi items có unique identifiers (map keys, set values). Resources được address theo key: aws_instance.web["server1"]

Dùng count cho simple repetition với numeric indexes. Resources được address theo index: aws_instance.web[0]

Ưu tiên for_each khi có thể. Nó an toàn hơn khi remove items khỏi giữa list vì resources không bị renumber.

Kết Hợp Tất Cả Lại

Cùng build thứ gì đó thực tế dùng mọi thứ mình đã cover:

# variables.tf
variable "environment" {
  type    = string
  default = "dev"
}

variable "allowed_ingress_rules" {
  type = list(object({
    port        = number
    protocol    = string
    cidr_blocks = list(string)
    description = string
  }))

  default = [
    {
      port        = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "HTTP from anywhere"
    },
    {
      port        = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "HTTPS from anywhere"
    },
    {
      port        = 22
      protocol    = "tcp"
      cidr_blocks = ["10.0.0.0/8"]
      description = "SSH from internal"
    }
  ]
}

# main.tf
locals {
  # Common tags
  common_tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
    Project     = "web-app"
    CostCenter  = var.environment == "prod" ? "production" : "development"
  }

  # Filter SSH trong production
  ingress_rules = [
    for rule in var.allowed_ingress_rules :
    rule
    if !(var.environment == "prod" && rule.port == 22)
  ]

  # Generate rule names cho for_each
  rule_names = {
    for idx, rule in local.ingress_rules :
    "${rule.protocol}-${rule.port}" => rule
  }
}

resource "aws_security_group" "web" {
  name        = "${var.environment}-web-sg"
  description = "Security group for ${var.environment} web servers"

  tags = merge(
    local.common_tags,
    { Name = "${var.environment}-web-sg" }
  )
}

resource "aws_security_group_rule" "ingress" {
  for_each = local.rule_names

  security_group_id = aws_security_group.web.id
  type              = "ingress"
  from_port         = each.value.port
  to_port           = each.value.port
  protocol          = each.value.protocol
  cidr_blocks       = each.value.cidr_blocks
  description       = each.value.description
}

# outputs.tf
output "security_group_id" {
  value       = aws_security_group.web.id
  description = "ID of the web security group"
}

output "allowed_ports" {
  value = [for rule in local.ingress_rules : rule.port]
  description = "List of allowed ingress ports"
}

Chuyện gì đang xảy ra:

  1. Conditional tags dựa trên environment
  2. Filtering rules (không có SSH trong production)
  3. Chuyển list thành map cho for_each
  4. Tạo dynamic resources từ data
  5. Computed outputs từ final rule set

Đây là infrastructure as real code. Không phải configuration files — programming thực sự.

Test Your Understanding

Trước khi tiếp tục, trả lời những câu này:

Checkpoint!

  1. Sự khác biệt giữa argument và expression là gì?
  2. Khi nào bạn dùng list(string) thay vì set(string)?
  3. Làm sao đọc file và parse nó thành JSON trong một dòng?
  4. Tại sao for_each an toàn hơn count khi quản lý multiple resources?
  5. Chuyện gì xảy ra nếu bạn access var.tags.Environment khi key không tồn tại?
  6. Làm sao filter list để chỉ bao gồm items dài hơn 5 ký tự?
  7. Syntax cho multi-line strings với conditionals là gì?
  8. Bạn có thể define custom functions trong HCL không?

Đáp án:

  1. Argument là key = value; expression là value (bên phải)
  2. list(string) giữ order và cho phép duplicates; set(string) không có order và enforce uniqueness
  3. jsondecode(file("config.json"))
  4. for_each dùng stable keys; count dùng indexes mà shift khi items bị removed
  5. Lỗi. Dùng lookup(var.tags, "Environment", "default") để an toàn
  6. [for s in var.items : s if length(s) > 5]
  7. Dùng %{ if condition }...%{ endif } bên trong heredoc strings
  8. Không. HCL chỉ cung cấp built-in functions. Dùng modules cho reusability

Tiếp Theo Là Gì

Bạn giờ đã hiểu HCL hoạt động thế nào rồi đó. Trong Part 5, mình sẽ dùng kiến thức này để build flexible, reusable configurations với variables và outputs.

Bạn sẽ học:

  • Input variable validation và constraints
  • Sensitive variable handling
  • Output dependencies và data flow
  • Variable precedence và override strategies
  • Complex variable structures cho real-world scenarios

Sự khác biệt? Bạn sẽ không chỉ copy variable examples nữa. Bạn sẽ design variable schemas mà enforce correctness ở type level.


Điều hướng series: