Mục lục
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 applyphá 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:
resourcetạo infrastructuredatađọc stuff có sẵnvariablekhai báo inputsoutputexpose valueslocalsdefine computed valuesmodulegọi reusable modulesterraformconfig Terraformprovidersetup 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 = {}
}
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() và 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() và 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() và join() chuyển đổi giữa strings và lists:
split(",", "a,b,c") # ["a", "b", "c"]
join("-", ["a", "b"]) # "a-b"
upper() và 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
}
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:
- Conditional tags dựa trên environment
- Filtering rules (không có SSH trong production)
- Chuyển list thành map cho
for_each - Tạo dynamic resources từ data
- 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:
- Sự khác biệt giữa argument và expression là gì?
- Khi nào bạn dùng
list(string)thay vìset(string)? - Làm sao đọc file và parse nó thành JSON trong một dòng?
- Tại sao
for_eachan toàn hơncountkhi quản lý multiple resources? - Chuyện gì xảy ra nếu bạn access
var.tags.Environmentkhi key không tồn tại? - Làm sao filter list để chỉ bao gồm items dài hơn 5 ký tự?
- Syntax cho multi-line strings với conditionals là gì?
- Bạn có thể define custom functions trong HCL không?
Đáp án:
- Argument là
key = value; expression là value (bên phải) list(string)giữ order và cho phép duplicates;set(string)không có order và enforce uniquenessjsondecode(file("config.json"))for_eachdùng stable keys;countdùng indexes mà shift khi items bị removed- Lỗi. Dùng
lookup(var.tags, "Environment", "default")để an toàn [for s in var.items : s if length(s) > 5]- Dùng
%{ if condition }...%{ endif }bên trong heredoc strings - 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:
- 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 (Bạn đang ở đây)
- Phần 5: Variables, Outputs & State
- 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)