Dalam episode ini kita akan memperdalam kemampuan HCL dengan mempelajari built-in functions, conditional expressions (ternary), serta meta-arguments perulangan count dan for_each untuk menulis konfigurasi yang dinamis dan efisien.

Setelah di episode 5 sebelumnya kita membahas State Management — mulai dari peran vital file state sebagai jembatan antara kode dan realita di cloud, masalah local state di lingkungan tim, hingga solusi remote state dengan S3/GCS beserta state locking — pada episode kali ini kita akan melengkapi senjata utama dalam menulis konfigurasi Terraform yang dinamis: expressions, functions, dan loops.
Sejauh ini, konfigurasi yang kita tulis masih cenderung "statis": satu resource, satu atribut, nilai yang di-hardcode. Di dunia kerja nyata, konfigurasi statis seperti itu hampir tidak pernah bertahan lama. Bayangkan kalian harus membuat 5 EC2 instance sekaligus, atau 20 bucket S3 dengan konfigurasi berbeda untuk tiap environment. Menulis dua puluh blok resource "aws_s3_bucket" secara manual jelas bukan ide yang baik — selain membosankan, itu juga sumber kesalahan ketik dan sulit dipelihara.
Di sinilah fitur-fitur lanjutan HCL berperan. Kemampuan inilah yang membedakan Terraform dari sekadar "tool untuk menulis konfigurasi" menjadi bahasa pemrograman infrastruktur yang sesungguhnya. Tanpa menguasai functions, ternary, dan perulangan, kode kalian akan selalu bertele-tele dan rapuh. Mari kita bedah satu per satu.
Sebelum membahas functions dan loops, penting untuk memahami satu fondasi: ekspresi (expression). Di HCL, hampir semua nilai bisa dihasilkan dari ekspresi — bukan hanya literal. Sebuah ekspresi bisa berupa:
"production", 42, truevar.environment, aws_instance.web.id, local.bucket_nameupper("hello"), length(var.list)var.env == "prod" ? "m5.large" : "t3.micro"Analoginya seperti formula di spreadsheet: kalian menulis rumus, bukan angka hasil hitung, sehingga ketika input berubah, hasilnya ikut berubah otomatis saat plan dijalankan ulang. Inilah esensi declarative IaC — kita mendeskripsikan ingin jadi seperti apa, bukan bagaimana cara membuatnya.
Terraform menyediakan puluhan fungsi bawaan (built-in) yang dikelompokkan ke beberapa kategori. Pada episode ini kita fokus pada tiga kategori yang paling sering dipakai sehari-hari: string functions, collection functions, dan filesystem functions.
Fungsi string dipakai untuk memanipulasi teks: normalisasi case, menggabungkan string, memotong, atau mengganti bagian tertentu.
locals {
env = "production"
raw_name = " my-app-server "
normalized = trimspace(local.raw_name) # "my-app-server"
shout = upper(local.env) # "PRODUCTION"
whisper = lower(local.env) # "production"
title = title("devops engineer") # "Devops Engineer"
replaced = replace("dev-eu", "-", "_") # "dev_eu"
parts = split("-", "dev-eu-west-1") # ["dev", "eu", "west", "1"]
joined = join(".", local.parts) # "dev.eu.west.1"
region_short = substr("ap-southeast-1", 3, 8) # "southeast"
}Perhatikan kombinasi keduanya: split memecah string menjadi list, dan join menggabungkan list menjadi string. Kombinasi inilah yang paling sering dipakai untuk normalisasi data — misalnya mengubah nama environment dev-eu-west-1 menjadi suffix bucket yang valid dev.eu.west.1. Ini penting karena tidak semua karakter yang valid di string ternyata valid untuk nama bucket S3 atau tag.
Kategori ini paling banyak digunakan. Collection di HCL meliputi list (["a", "b"]) dan map ({ key = "value" }). Fungsi yang wajib kalian kuasai:
| Fungsi | Deskripsi | Contoh Hasil |
|---|---|---|
length(x) | Menghitung jumlah elemen list/map atau panjang string | length(["a", "b"]) → 2 |
merge(m1, m2) | Menggabungkan beberapa map menjadi satu | merge({a = 1}, {b = 2}) → {a = 1, b = 2} |
lookup(map, key, default) | Mengambil nilai map, dengan nilai default jika key tidak ada | lookup({a = 1}, "b", 0) → 0 |
concat(list1, list2) | Menggabungkan beberapa list | concat([1], [2]) → [1, 2] |
element(list, index) | Mengambil elemen pada posisi tertentu (bersifat siklik) | element(["a", "b"], 3) → "a" |
keys(map) | Mengembalikan list berisi semua key | keys({a = 1, b = 2}) → ["a", "b"] |
values(map) | Mengembalikan list berisi semua value | values({a = 1, b = 2}) → [1, 2] |
toset(list) | Mengubah list menjadi set (menghapus duplikat) | toset(["a", "a", "b"]) → ["a", "b"] |
distinct(list) | Menghapus elemen duplikat dari list | distinct([1, 1, 2]) → [1, 2] |
sort(list) | Mengurutkan elemen list | sort(["c", "a"]) → ["a", "c"] |
Fungsi lookup layak mendapat perhatian ekstra karena menjadi andalan untuk memberi nilai default pada map yang belum tentu memiliki key tertentu. Contoh nyata: menentukan instance_type per environment.
variable "environment" {
type = string
default = "dev"
}
locals {
instance_types = {
dev = "t3.micro"
stg = "t3.medium"
prod = "m5.large"
}
instance_type = lookup(local.instance_types, var.environment, "t3.small")
}
output "selected_instance_type" {
value = local.instance_type
}Tip
Perbedaan krusial antara lookup dan akses map langsung local.map["key"]: akses langsung akan error jika key tidak ada, sedangkan lookup mengembalikan nilai default yang kalian tentukan. Di skenario multi-environment, lookup(local.instance_types, var.environment, "t3.small") jauh lebih aman daripada local.instance_types[var.environment] yang akan meledak ketika ada environment baru yang belum terdaftar.
Filesystem functions menghubungkan konfigurasi Terraform dengan file di disk — berguna untuk membaca user data script, file template, atau memeriksa keberadaan file sebelum dipakai.
file(path) — membaca seluruh isi file sebagai string.fileexists(path) — mengembalikan true/false apakah file ada.templatefile(path, vars) — membaca file template dan mengisi variabel templatenya.Fungsi templatefile paling sering dipakai untuk user data EC2. Perhatikan contoh berikut:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
user_data = templatefile("${path.module}/user-data.sh.tftpl", {
app_name = var.app_name
app_port = var.app_port
})
}#!/bin/bash
set -euxo pipefail
apt-get update
apt-get install -y nginx
echo "Hello from ${app_name} on port ${app_port}" > /var/www/html/index.htmlFile template dengan ekstensi .tftpl memungkinkan kalian menyisipkan variabel dengan sintaks ${nama_variabel}. Keuntungannya: logika bash tetap bersih dan bisa ditulis multi-line, sementara data dinamisnya dipasok dari variabel Terraform saat apply. Ini jauh lebih rapi daripada menyambung string raksasa dengan operator <<EOF atau format.
Saat alur konfigurasi butuh percabangan, HCL menyediakan conditional expression berbentuk:
condition ? true_val : false_valBacanya: "jika condition benar, pakai true_val; jika tidak, pakai false_val". Ini setara dengan operator ternary di bahasa lain (misalnya a ? b : c di C/JS/Java). Contoh nyata yang sangat umum — memilih tipe instance berdasarkan environment:
variable "environment" {
type = string
default = "dev"
}
resource "aws_instance" "app" {
ami = data.aws_ami.ubuntu.id
instance_type = var.environment == "production" ? "m5.large" : "t3.micro"
tags = {
Name = "app-server"
Env = var.environment
Managed = "terraform"
}
}Ekspresi bersyarat juga bisa dirantai (nested). Namun jangan berlebihan — jika percabangan sudah lebih dari dua level, lebih baik pindahkan logikanya ke locals agar mudah dibaca:
locals {
disk_size = var.environment == "production" ? 200
: var.environment == "staging" ? 100
: 50
}Warning
Aturan penting: kedua cabang nilai pada ternary harus bertipe sama atau setidaknya saling konversi-able. condition ? "high" : 3 akan error karena mencoba menggabungkan string dan number. Untuk tipe yang berbeda, konversikan dulu secara eksplisit, misalnya condition ? "high" : tostring(3).
count, for_each, dan For ExpressionsInilah inti dari episode ini: tiga mekanisme untuk membuat banyak resource dari satu blok definisi. Masing-masing punya kasus penggunaan yang berbeda, dan salah pilih berarti masalah besar di kemudian hari.
count dan count.index — Perulangan Berbasis Angkacount menerima angka bulat dan membuat resource sebanyak angka tersebut. Untuk membedakan tiap instance, HCL menyediakan count.index yang dimulai dari 0. Contoh: membuat 3 instance EC2 yang identik.
variable "instance_count" {
type = number
default = 3
}
resource "aws_instance" "web" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
tags = {
Name = "web-server-${count.index}"
}
}Setelah apply, kalian akan melihat tiga instance dengan address aws_instance.web[0], aws_instance.web[1], dan aws_instance.web[2]. Untuk mengakses semuanya sekaligus, gunakan splat expression: aws_instance.web[*].id menghasilkan list berisi ketiga ID. Untuk yang spesifik: aws_instance.web[0].id.
output "web_instance_ids" {
value = aws_instance.web[*].id
}
output "first_instance_id" {
value = aws_instance.web[0].id
}for_each dan each.key / each.value — Perulangan Berbasis Set/Mapfor_each menerima sebuah map atau set of string, dan membuat satu resource untuk setiap elemennya. Untuk mengakses elemen yang sedang diproses, HCL menyediakan dua variabel khusus: each.key dan each.value. Saat iterasi dilakukan atas set, each.key dan each.value bernilai sama; saat atas map, each.key adalah key dan each.value adalah value dari elemen tersebut.
Contoh: membuat bucket S3 untuk beberapa kebutuhan sekaligus, dengan region yang berbeda-beda.
locals {
buckets = {
"app-logs" = { region = "ap-southeast-1" }
"backup-data" = { region = "ap-southeast-1" }
"static-assets" = { region = "us-east-1" }
}
}
resource "aws_s3_bucket" "data" {
for_each = local.buckets
bucket = each.key
tags = {
Name = each.key
Region = each.value.region
Env = var.environment
}
}Sekarang aws_s3_bucket.data["app-logs"] menjadi address yang valid, dan values(aws_s3_bucket.data)[*].id menghasilkan seluruh ID bucket. Kalian bisa memikirkan for_each sebagai cara membuat banyak resource dengan konfigurasi beragam tapi terdata rapi — berbeda dengan count yang cenderung membuat banyak resource identik.
count vs for_eachUntuk memperjelas kapan memakai yang mana, perhatikan perbandingan dua cara membuat kumpulan IAM user berikut:
resource "aws_iam_user" "team" {
count = length(var.team_members)
name = var.team_members[count.index]
}Pada count, elemen diakses lewat indeks angka; pada for_each, elemen diakses lewat key yang stabil. Inilah perbedaan filosofis yang kelak menentukan apakah Terraform akan menghancurkan resource kalian secara tak terduga (baca bagian Common Pitfalls di bawah).
[for s in var.list : upper(s)]Selain membuat resource berulang, HCL juga bisa mengolah koleksi menggunakan for expression. Ini mirip list comprehension di Python. Bentuk dasarnya:
[for item in list : transform(item)]Contoh: mengubah semua elemen list menjadi uppercase, sekaligus menyaring elemen yang panjangnya di bawah ambang batas.
variable "services" {
type = list(string)
default = ["api", "web", "worker", "cron"]
}
locals {
uppercase_all = [for s in var.services : upper(s)]
long_only = [for s in var.services : upper(s) if length(s) > 3]
map_result = { for s in var.services : s => upper(s) }
}
output "long_only" {
value = local.long_only # ["WORKER", "CRON"]
}For expression juga bisa menghasilkan map — seperti map_result di atas — dengan pola { for k, v in map : k => transform(v) }. Kemampuan ini sangat berguna untuk membangun struktur data perantara yang nantinya dikonsumsi oleh for_each.
1. count + list yang berubah urutan = resource di-destroy & dibuat ulang
Ini jebakan paling terkenal di ekosistem Terraform. Ketika count mengambil elemen dari sebuah list, setiap resource terikat pada posisi indeks, bukan identitas elemennya. Jika list berubah — misalnya ["budi", "siti", "agus"] menjadi ["siti", "agus", "budi"] — maka count.index 0 yang tadinya budi kini menjadi siti, dan Terraform akan me-recreate seluruh resource. Untuk data yang bersifat kumpulan atau ber-key, for_each jauh lebih aman karena key-nya stabil.
2. for_each menolak list biasa
for_each hanya menerima map atau set of string. Melempar list langsung akan memunculkan error klasik: The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings. Solusinya sederhana — bungkus dengan toset(var.team_members).
3. Memakai each.key / each.value di luar blok for_each
Variabel each hanya tersedia di dalam resource yang dideklarasikan dengan for_each. Memakainya pada resource count akan error dengan pesan Each object must be identified by exactly one .... Kenali blok mana yang memakai count.index dan mana yang memakai each — jangan dicampur.
4. Ternary dengan tipe berbeda
Seperti sudah disinggung: var.env == "prod" ? 20 : "twenty" akan gagal di-validate karena cabangnya tidak konsisten tipenya.
5. Hardcode alih-alih memanfaatkan lookup
Akses map langsung local.map[var.key] memang lebih ringkas, tapi langsung error saat key tidak ada. Di konfigurasi yang dijalankan banyak tim dengan environment berbeda-beda, lookup dengan default yang masuk akal adalah keputusan yang lebih defensive.
Pada episode 6 ini kita telah memperkaya kosakata HCL dengan tiga kemampuan penting: built-in functions (string, collection, dan filesystem) untuk memanipulasi nilai, conditional expression (ternary) untuk percabangan logika, serta meta-arguments perulangan — count untuk resource berbasis angka, for_each untuk resource berbasis set/map, dan for expression untuk mengolah koleksi.
Yang perlu diingat: kekuatan Terraform bukan terletak pada satu resource yang ditulis sempurna, melainkan pada kemampuan menulis banyak resource yang konsisten, terparameterisasi, dan adaptif terhadap environment. Fungsi dan perulangan adalah bahan bakarnya. Semakin sering kalian berlatih kombinasi for_each + lookup + for expression, semakin cepat kalian menulis konfigurasi yang tadinya butuh ratusan baris hanya dalam puluhan baris.
Di episode 7 selanjutnya kita akan membahas Resource Dependencies & Lifecycle Rules — bagaimana Terraform menentukan urutan pembuatan resource lewat dependency graph, kapan kita harus memaksakan urutan dengan depends_on, dan bagaimana aturan lifecycle seperti create_before_destroy, prevent_destroy, dan ignore_changes melindungi infrastruktur kalian di dunia nyata. Pastikan tetap semangat!