Declarative #

Kata “deklaratif” sering disebut saat membahas Terraform, tapi kebanyakan penjelasan berhenti di “kamu mendefinisikan apa yang diinginkan, bukan bagaimana cara membuatnya.” Penjelasan itu benar, tapi tidak cukup untuk mengubah cara kamu berpikir saat menulis konfigurasi. Pendekatan deklaratif bukan sekadar filosofi — ia memiliki implikasi teknis yang sangat konkret terhadap bagaimana Terraform menghitung perubahan, menyusun dependency, mengeksekusi resource, dan menangani error. Memahami mekanisme di balik pendekatan deklaratif akan membuat kamu menulis HCL yang lebih bersih, lebih efisien, dan lebih sedikit bug.

Deklaratif vs Imperatif: Perbedaan Fundamental #

Sebelum masuk ke mekanisme Terraform, penting untuk memahami perbedaan mendasar antara pendekatan deklaratif dan imperatif. Perbedaan ini bukan hanya soal sintaks — ia mengubah cara kamu memodelkan masalah.

Dalam pendekatan imperatif, kamu menulis urutan langkah untuk mencapai kondisi yang diinginkan. Kamu harus tahu state awal, state akhir, dan semua langkah di antaranya. Kalau state awal berbeda dari yang kamu asumsikan, script bisa gagal atau menghasilkan hasil yang tidak konsisten.

Dalam pendekatan deklaratif, kamu hanya mendefinisikan kondisi akhir. Kamu tidak peduli state awal — kamu hanya peduli kondisi akhir seperti apa yang kamu inginkan. Tool yang bertanggung jawab mencari cara untuk mencapai kondisi itu dari state manapun saat ini.

// ANTI-PATTERN: Berpikir imperatif di Terraform
// (mencoba mengontrol urutan yang sebenarnya tidak perlu)

resource "null_resource" "wait_for_vpc" {
  depends_on = [aws_vpc.main]

  provisioner "local-exec" {
    command = "sleep 10"  // Menunggu VPC "siap"  tidak reliable
  }
}

resource "aws_subnet" "public" {
  depends_on = [null_resource.wait_for_vpc]  // Dependency tidak perlu
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

// BENAR: Deklaratif  biarkan Terraform menentukan urutan

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id  // Referensi ini sudah cukup
  cidr_block = "10.0.1.0/24"    // untuk menyatakan dependency
}

Tabel berikut merangkum perbedaan utama:

AspekImperatifDeklaratif
InputUrutan langkahKondisi akhir
State awarenessHarus tahu state awalTidak peduli state awal
IdempotencyHarus diimplementasi manualBuilt-in secara natural
Error handlingTry-catch di setiap langkahRollback otomatis ke state konsisten
ParallelisasiManual (harus diatur)Otomatis (dari dependency graph)
Contoh toolAnsible, Shell script, PulumiTerraform, CloudFormation, Kustomize

Bagaimana Terraform Menghitung Perubahan #

Setiap kali kamu menjalankan terraform plan, Terraform melakukan tiga langkah yang sangat spesifik. Proses inilah yang membuat pendekatan deklaratif bisa bekerja secara teknis.

Langkah 1: Baca konfigurasi. Terraform membaca semua file .tf di direktori kerja dan membangun representation dari desired state — kondisi akhir yang kamu inginkan.

Langkah 2: Baca state. Terraform membaca file state (terraform.tfstate) yang berisi representation dari current state — kondisi infrastruktur saat ini berdasarkan operasi terakhir yang Terraform lakukan.

Langkah 3: Hitung delta. Terraform membandingkan desired state dan current state, lalu menghasilkan execution plan yang berisi daftar operasi (create, update, delete) yang perlu dilakukan untuk mencapai kondisi akhir.

flowchart TD
    A["terraform plan"] --> B["Baca konfigurasi\n(.tf files)"]
    A --> C["Baca state\n(.tfstate)"]
    B --> D["Desired State"]
    C --> E["Current State"]
    D --> F["Hitung Delta"]
    E --> F
    F --> G{"Ada\nperbedaan?"}
    G -->|"Ya"| H["Hasilkan Execution Plan"]
    G -->|"Tidak"| I["No changes.\nInfrastruktur up to date."]
    H --> J["Create\n(resource baru)"]
    H --> K["Update\n(resource berubah)"]
    H --> L["Delete\n(resource dihapus)"]

    style A fill:#e3f2fd,stroke:#1565c0
    style I fill:#e8f5e9,stroke:#2e7d32
    style J fill:#e8f5e9,stroke:#2e7d32
    style K fill:#fff3e0,stroke:#e65100
    style L fill:#ffebee,stroke:#c62828

Proses ini menghasilkan tiga jenis operasi:

  • Create — resource ada di konfigurasi tapi tidak ada di state. Artinya resource belum pernah dibuat atau baru saja ditambahkan ke konfigurasi.
  • Update in-place — resource ada di konfigurasi dan di state, tapi atributnya berubah. Terraform mengubah resource yang sudah ada tanpa menghapusnya.
  • Destroy — resource ada di state tapi tidak ada di konfigurasi. Artinya resource dihapus dari konfigurasi dan perlu dihapus dari infrastruktur.
# Contoh: konfigurasi yang sudah di-apply sebelumnya
resource "aws_instance" "web" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"  # ← ubah dari t3.small ke t3.micro
  tags = {
    Name = "web-server"
  }
}

# terraform plan akan menunjukkan:
# aws_instance.web will be updated in-place
# ~ {
#   ~ instance_type = "t3.small" -> "t3.micro"
# }

# Terraform tahu cukup ubah instance_type,
# tidak perlu hapus dan buat ulang instance

Idempotency: Konsekuensi Natural Pendekatan Deklaratif #

Idempotency berarti menjalankan operasi yang sama berkali-kali menghasilkan hasil yang sama. Dalam konteks infrastruktur, ini berarti menjalankan terraform apply sepuluh kali dengan konfigurasi yang sama hanya akan melakukan perubahan pada apply pertama — sembilan apply berikutnya tidak melakukan apapun.

Ini bukan fitur yang diimplementasikan secara manual — ia adalah konsekuensi natural dari pendekatan deklaratif. Karena Terraform selalu menghitung delta antara desired state dan current state, dan delta-nya nol ketika keduanya sama, maka tidak ada operasi yang perlu dilakukan.

# Apply pertama — membuat resource
$ terraform apply
aws_vpc.main: Creating...
aws_subnet.public: Creating...
aws_instance.web: Creating...
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

# Apply kedua — tidak ada perubahan
$ terraform apply
No changes. Your infrastructure matches the configuration.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

# Apply kesepuluh — tetap tidak ada perubahan
$ terraform apply
No changes. Your infrastructure matches the configuration.
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Idempotency ini sangat penting untuk CI/CD pipeline. Kamu bisa menjalankan terraform apply di setiap deploy tanpa khawatir akan mengganggu resource yang sudah ada. Kalau tidak ada perubahan, tidak ada yang terjadi.

stateDiagram-v2
    [*] --> DesiredState: Tulis konfigurasi
    DesiredState --> DeltaCalculation: terraform plan
    DeltaCalculation --> HasChanges: Ada delta
    DeltaCalculation --> NoChanges: Tidak ada delta
    HasChanges --> Apply: terraform apply
    Apply --> CurrentState: Resource diubah
    CurrentState --> DeltaCalculation: terraform plan (lagi)
    NoChanges --> [*]: Infrastruktur up to date
    CurrentState --> [*]: Infrastruktur up to date

Dependency Graph Otomatis #

Keuntungan besar pendekatan deklatif adalah Terraform bisa menghitung dependency graph secara otomatis. Kamu tidak perlu menentukan “buat VPC dulu, baru subnet, baru instance” — Terraform menyimpulkannya dari referensi antar resource.

Saat kamu menulis aws_vpc.main.id di dalam subnet, kamu secara tidak langsung memberitahu Terraform bahwa subnet bergantung pada VPC. Terraform mengumpulkan semua referensi ini dan membangun Directed Acyclic Graph (DAG) yang menentukan urutan eksekusi.

# Kamu menulis resource dalam urutan apapun — Terraform yang menentukan urutan eksekusi

resource "aws_instance" "web" {
  ami       = "ami-0abcdef1234567890"
  subnet_id = aws_subnet.public.id  # ← bergantung pada subnet
  vpc_security_group_ids = [aws_security_group.web.id]  # ← bergantung pada SG
}

resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id  # ← bergantung pada VPC
  cidr_block = "10.0.1.0/24"
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_security_group" "web" {
  vpc_id = aws_vpc.main.id  # ← bergantung pada VPC
  name   = "web-sg"
}

# Terraform menghitung dependency graph:
# Level 0: aws_vpc.main (tidak bergantung apapun)
# Level 1: aws_subnet.public, aws_security_group.web (bergantung VPC)
# Level 2: aws_instance.web (bergantung subnet + SG)
#
# Level 0 dieksekusi dulu, lalu level 1 (paralel), lalu level 2
flowchart TD
    VPC["aws_vpc.main"] --> SUBNET["aws_subnet.public"]
    VPC --> SG["aws_security_group.web"]
    SUBNET --> EC2["aws_instance.web"]
    SG --> EC2

    style VPC fill:#e3f2fd,stroke:#1565c0
    style SUBNET fill:#e8f5e9,stroke:#2e7d32
    style SG fill:#e8f5e9,stroke:#2e7d32
    style EC2 fill:#fff3e0,stroke:#e65100

Kamu bisa melihat dependency graph yang Terraform hitung dengan perintah terraform graph. Output-nya dalam format DOT yang bisa divisualisasikan dengan Graphviz.

# Lihat dependency graph dalam format DOT
$ terraform graph

# Visualisasikan sebagai SVG (butuh Graphviz installed)
$ terraform graph | dot -Tsvg > graph.svg

Parallelisasi Otomatis #

Resource yang tidak saling bergantung dieksekusi secara paralel. Dalam contoh di atas, aws_subnet.public dan aws_security_group.web bisa dibuat bersamaan karena keduanya hanya bergantung pada VPC — tidak saling bergantung.

# Contoh: 5 resource yang bisa dieksekusi paralel

resource "aws_s3_bucket" "logs" {
  bucket = "app-logs-2024"
}

resource "aws_s3_bucket" "assets" {
  bucket = "app-assets-2024"
}

resource "aws_s3_bucket" "backups" {
  bucket = "app-backups-2024"
}

# Ketiga bucket ini tidak saling bergantung
# → Terraform membuatnya secara paralel
# → Lebih cepat dari sequential creation

# Kamu bisa mengontrol jumlah parallelisme:
# $ terraform apply -parallelism=5
# (default: 10 operasi paralel)

Parallelisasi ini sangat terasa di infrastruktur besar. Membuat 50 security group yang tidak saling bergantung akan jauh lebih cepat dibanding script sequential yang membuat satu per satu.

depends_on untuk Dependency Eksplisit #

Ada kalanya dependency tidak bisa disimpulkan dari referensi langsung — misalnya ketika resource bergantung pada side effect dari resource lain. Untuk kasus ini, Terraform menyediakan depends_on sebagai mekanisme dependency eksplisit.

# Kasus: EKS node group butuh IAM role yang sudah ter-attach ke policy
# Dependency ini tidak terlihat dari referensi resource biasa

resource "aws_iam_role" "worker" {
  name = "eks-worker-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = { Service = "ec2.amazonaws.com" }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "worker_cni" {
  role       = aws_iam_role.worker.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
}

resource "aws_iam_role_policy_attachment" "worker_node" {
  role       = aws_iam_role.worker.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
}

resource "aws_eks_node_group" "workers" {
  cluster_name    = aws_eks_cluster.main.name
  node_role_arn   = aws_iam_role.worker.arn
  subnet_ids      = aws_subnet.private[*].id

  # depends_on diperlukan karena node group butuh role
  # yang SUDAH ter-attach ke policy — bukan hanya role-nya saja
  depends_on = [
    aws_iam_role_policy_attachment.worker_cni,
    aws_iam_role_policy_attachment.worker_node,
  ]
}
Gunakan depends_on hanya sebagai last resort. Jika dependency bisa dinyatakan melalui referensi langsung (misalnya aws_vpc.main.id), gunakan referensi itu. depends_on membuat dependency lebih kasar — semua resource di dalam depends_on harus selesai dulu, bahkan jika kamu hanya butuh satu atribut tertentu.

Mengapa Deklaratif Lebih Cocok untuk Infrastruktur #

Pendekatan deklaratif bukan cocok untuk semua domain — ada area di mana imperatif lebih natural (misalnya deployment pipeline, konfigurasi server). Tapi untuk infrastruktur cloud, deklaratif punya beberapa keunggulan yang sangat signifikan.

Konsistensi terjamin. Karena kamu mendefinisikan kondisi akhir, infrastruktur yang dihasilkan selalu konsisten dengan konfigurasi. Tidak ada “state tersembunyi” yang tercipta dari langkah-langkah yang salah urutan.

Recovery yang mudah. Ketika resource dihapus secara manual di cloud console, cukup jalankan terraform apply dan resource akan dibuat ulang. Kamu tidak perlu tahu apa yang dihapus atau bagaimana membuatnya kembali — cukup jalankan apply dan Terraform menghitung apa yang perlu dilakukan.

Variasi state awal ditangani otomatis. Script imperatif bisa gagal kalau state awal tidak sesuai asumsi. Terraform tidak punya masalah ini — ia selalu menghitung dari state saat ini ke kondisi akhir yang diinginkan, apapun state awalnya.

# Contoh: Resource yang dihapus manual di AWS Console

# Sebelumnya: 3 EC2 instance dikelola Terraform
# Seseorang menghapus 1 instance dari Console

# Script imperatif: ERROR
# "Instance i-abc123 not found" — script tidak tahu cara recover

# Terraform: "1 instance hilang, saya buat ulang"
# $ terraform plan
# - aws_instance.web[2] (resource deposed) will be created
# Plan: 1 to add, 0 to change, 0 to destroy.

Deklaratif vs Imperatif dalam Konteks Nyata #

Untuk memperjelas perbedaannya, berikut contoh yang sama — membuat VPC dengan subnet — dalam kedua pendekatan.

# IMPERATIF (AWS CLI):
# Kamu harus menentukan urutan, menangani error, dan cek kondisi

# Langkah 1: Buat VPC
VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 \
  --query 'Vpc.VpcId' --output text)

# Langkah 2: Tunggu VPC available
aws ec2 wait vpc-available --vpc-ids $VPC_ID

# Langkah 3: Buat subnet
SUBNET_ID=$(aws ec2 create-subnet --vpc-id $VPC_ID \
  --cidr-block 10.0.1.0/24 \
  --query 'Subnet.SubnetId' --output text)

# Langkah 4: Buat security group
SG_ID=$(aws ec2 create-security-group \
  --group-name web-sg --description "Web SG" \
  --vpc-id $VPC_ID \
  --query 'GroupId' --output text)

# Langkah 5: Tambah rule ke SG
aws ec2 authorize-security-group-ingress \
  --group-id $SG_ID --protocol tcp --port 443 --cidr 0.0.0.0/0

# Masalah:
# - Kalau step 3 gagal, step 4 dan 5 tidak jalan
# - Kalau dijalankan lagi, VPC baru dibuat (duplikat)
# - Tidak ada cleanup otomatis kalau gagal di tengah
# - Kalau VPC sudah ada dari run sebelumnya, script error
# DEKLARATIF (Terraform):
# Kamu hanya mendefinisikan kondisi akhir

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

resource "aws_security_group" "web" {
  vpc_id = aws_vpc.main.id
  name   = "web-sg"

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Keunggulan:
# - Urutan ditentukan otomatis dari dependency graph
# - Idempotent: apply berkali-kali hasilnya sama
# - Rollback otomatis kalau ada error
# - Kalau resource sudah ada, tidak dibuat ulang
# - Bisa di-destroy dengan terraform destroy

Tantangan Pendekatan Deklaratif #

Pendekatan deklaratif bukan tanpa tantangan. Ada beberapa situasi di mana kamu perlu bekerja lebih keras untuk mencapai hasil yang diinginkan.

Operasi sequential yang ketat. Jika kamu butuh operasi yang benar-benar harus sequential (misalnya: buat database, tunggu siap, jalankan migration, baru buat app server), deklaratif bisa terasa kaku. Terraform punya depends_on dan lifecycle hooks, tapi tidak sefleksif script sequential.

Provisioning yang butuh interaksi. Menjalankan perintah di server setelah dibuat (misalnya install software, jalankan setup script) bukan kekuatan Terraform. Provisioner seperti remote-exec ada, tapi dianggap anti-pattern untuk produksi.

Conditional logic kompleks. HCL punya count dan for_each untuk conditional resource, tapi tidak se-ekspresif bahasa pemrograman umum. Logika kompleks kadang lebih mudah diimplementasikan di Pulumi (imperative) daripada di Terraform (declarative).

# Tantangan: conditional resource di Terraform

# Kamu ingin NAT Gateway hanya di production
# Gunakan count dengan conditional expression

resource "aws_nat_gateway" "main" {
  count = var.environment == "production" ? 1 : 0
  # count = 0 → resource tidak dibuat
  # count = 1 → resource dibuat

  allocation_id = aws_eip.nat[0].id
  subnet_id     = aws_subnet.public[0].id
}

# Ini bekerja, tapi bisa membingungkan
# saat reference perlu menangani count = 0
# Misalnya: aws_nat_gateway.main[0].id bisa error
# kalau count = 0
flowchart TD
    A["Situasi"] --> B{"Perlu operasi\nsequential ketat?"}
    B -->|"Ya"| C["Pertimbangkan\nprovisioner atau\nnull_resource"]
    B -->|"Tidak"| D{"Butuh conditional\nkompleks?"}
    D -->|"Ya"| E["Gunakan count,\nfor_each,\natau conditional"]
    D -->|"Tidak"| F{"Butuh interaksi\ndengan server?"}
    F -->|"Ya"| G["Gunakan Ansible/\nuser_data/\nAMI baking"]
    F -->|"Tidak"| H["Terraform deklaratif\ncukup untuk kasus ini"]

    style C fill:#fff3e0,stroke:#e65100
    style E fill:#e3f2fd,stroke:#1565c0
    style G fill:#fff3e0,stroke:#e65100
    style H fill:#e8f5e9,stroke:#2e7d32

Ringkasan #

  • Deklaratif = definisikan kondisi akhir — kamu menyatakan apa yang diinginkan, Terraform menentukan bagaimana mencapainya dari state apapun saat ini.
  • Terraform menghitung delta antara konfigurasi (desired state) dan state (current state) — tiga langkah: baca config, baca state, bandingkan keduanya.
  • Idempotency adalah konsekuensi natural — menjalankan terraform apply berkali-kali dengan konfigurasi sama hanya melakukan perubahan pada kali pertama.
  • Dependency graph dihitung otomatis dari referensi antar resource — resource di level yang sama dieksekusi paralel untuk efisiensi.
  • depends_on untuk edge case — gunakan hanya ketika dependency tidak bisa disimpulkan dari referensi langsung, karena membuat dependency lebih kasar.
  • Recovery otomatis — jika resource dihapus manual, cukup jalankan terraform apply untuk membuatnya kembali.
  • Tantangan deklaratif — operasi sequential ketat, provisioning yang butuh interaksi, dan conditional logic kompleks memerlukan pendekatan khusus.

← Sebelumnya: Kapan Digunakan?   Berikutnya: Provider →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact