karawaci.kode

2026-06-23 · 7 min

Terraform vs Pulumi: Tim Jakarta Pakai Mana 2026

Klien SaaS Jakarta dengan tim 4 engineer minta migrate dari manual VPS provisioning ke IaC. Saya prototype Terraform dan Pulumi parallel selama 1 bulan untuk sample infrastructure (3 Hetzner VPS, Cloudflare DNS + R2, Postgres setup script). Comparison real berdasarkan tim TS-heavy.

Setup

Infrastructure scope yang di-IaC:

  • 3 Hetzner Cloud server (cx41, cx31, cx21)
  • Cloudflare zone + DNS record (15 record)
  • Cloudflare R2 bucket dengan lifecycle policy
  • Cloudflare Workers script binding
  • Postgres user + database via local-exec provisioner

Tim: 4 engineer, semua comfortable TypeScript, 1 ada exposure Terraform sebelumnya.

Code comparison

Terraform (HCL):

provider "hcloud" {
  token = var.hcloud_token
}

resource "hcloud_server" "web1" {
  name        = "web1.example.com"
  image       = "ubuntu-24.04"
  server_type = "cx41"
  location    = "fsn1"
  ssh_keys    = [hcloud_ssh_key.deployer.id]
  
  user_data = file("${path.module}/cloud-init/web.yaml")
  
  labels = {
    role = "web"
    env  = "production"
  }
}

resource "cloudflare_record" "api" {
  zone_id = var.cf_zone_id
  name    = "api"
  type    = "A"
  value   = hcloud_server.web1.ipv4_address
  ttl     = 300
}

Pulumi (TypeScript):

import * as hcloud from '@pulumi/hcloud';
import * as cloudflare from '@pulumi/cloudflare';
import * as fs from 'fs';

const sshKey = new hcloud.SshKey('deployer', {
  publicKey: fs.readFileSync('~/.ssh/id_ed25519.pub', 'utf-8'),
});

const web1 = new hcloud.Server('web1', {
  name: 'web1.example.com',
  image: 'ubuntu-24.04',
  serverType: 'cx41',
  location: 'fsn1',
  sshKeys: [sshKey.id],
  userData: fs.readFileSync('./cloud-init/web.yaml', 'utf-8'),
  labels: {
    role: 'web',
    env: 'production',
  },
});

const apiRecord = new cloudflare.Record('api', {
  zoneId: cfZoneId,
  name: 'api',
  type: 'A',
  value: web1.ipv4Address,
  ttl: 300,
});

Equivalent. Pulumi feel familiar untuk dev background. Terraform HCL declarative yang bersih untuk infrastructure-only context.

Learning curve di tim

Saya pair-program kedua approach dengan tiap engineer:

Terraform:

  • Engineer 1 (pernah Terraform): 2 hari ramp untuk current syntax + provider quirk
  • Engineer 2 (TS heavy, no IaC): 5 hari untuk paham state, plan/apply cycle, variable interpolation
  • Engineer 3 (Python background): 4 hari
  • Engineer 4 (junior): 7 hari

Pulumi:

  • Engineer 1: 3 hari (resistance karena HCL “lebih clean”)
  • Engineer 2: 2 hari (familiar TS pattern)
  • Engineer 3: 3 hari
  • Engineer 4: 5 hari

Untuk tim TS-heavy: Pulumi ramp-up lebih cepat 30-40%.

Testing & validation

Terraform:

  • terraform plan show diff sebelum apply
  • terraform validate syntax check
  • Unit test: terratest (Go) atau Open Policy Agent (Rego) — both butuh tooling tambahan

Pulumi:

  • pulumi preview show diff
  • TS compiler catch type error pre-runtime
  • Unit test: native Mocha/Jest dengan @pulumi/pulumi test mock
import * as pulumi from '@pulumi/pulumi';

pulumi.runtime.setMocks({...});

const infra = await import('./infrastructure');

describe('Server config', () => {
  it('uses cx41 for production', async () => {
    const serverType = await infra.web1.serverType.promise();
    expect(serverType).toBe('cx41');
  });
});

Test infrastructure dengan tools yang tim sudah familiar. Significant DX win.

State management

Terraform:

  • State file .tfstate di local atau remote backend (S3, GCS, Terraform Cloud)
  • State locking critical untuk concurrent apply
  • Setup remote state: 1-2 jam

Pulumi:

  • State di Pulumi Service (free tier untuk 1 user, $0.7 per resource bulanan untuk team)
  • Atau self-hosted backend (S3-compat)
  • Setup: 30 menit (Pulumi Service) atau 2 jam (self-host)

Cost Pulumi Service untuk klien saya (60+ resource × $0.7 = $42/mo team plan): kena. Saya pilih self-host backend di Cloudflare R2 sebagai gantinya. Saving + control.

Ekosistem provider

Terraform: ekosistem ter-besar. ~3000+ provider di Terraform Registry. Hetzner, Cloudflare, semua major cloud, plus niche provider (Vercel, Supabase, Auth0, dll).

Pulumi: support semua major. Provider untuk niche kadang lag 1-2 minor version dari Terraform.

Untuk klien saya yang pakai Cloudflare + Hetzner + Vercel + Stripe: kedua-nya cover. Saya tidak hit gap.

Apply experience

Test: apply infrastructure full (3 server + 15 DNS + R2 + Worker):

Terraform:

  • terraform plan: 28 detik
  • terraform apply: 4 menit 22 detik (sequential apply by default)
  • Output verbosity: moderate

Pulumi:

  • pulumi preview: 35 detik
  • pulumi up: 3 menit 50 detik (concurrent apply by default)
  • Output verbosity: rich (with progress UI di terminal)

Pulumi sedikit lebih cepat (concurrent), UI lebih nyaman untuk monitor progress.

Yang break

Terraform:

  1. Provider version pinning: saya tidak pin version, upgrade tiba-tiba break syntax. cloudflare_recordcloudflare_record di v5 punya breaking change. Sekarang saya pin semua provider.

  2. State drift: manual change di Cloudflare dashboard (engineer lain) bikin state drift. Detect di next plan. Painful. Saya lock down dengan IAM read-only token + IaC-only write.

  3. Sensitive value di state: secret kadang tersimpan plain di state. Saya pakai remote backend dengan encryption + access control.

Pulumi:

  1. TS dependency drift: package.json drift antara dev/CI. Lock file disciplined wajib.

  2. Runtime cost: Pulumi engine via Node.js. Apply pakai ~280MB RAM. Untuk CI runner kecil: marginal.

  3. Resource naming: Pulumi auto-generate suffix unique per resource. Kadang resource name di cloud ada hash random. Saya override dengan name: property explicit untuk readability.

OpenTofu

Saya juga prototype singkat dengan OpenTofu (fork Terraform setelah license change). 95% kompatibel dengan Terraform syntax. Provider Hetzner + Cloudflare resmi tersedia.

Untuk klien yang concern dengan HashiCorp license direction: OpenTofu viable. Untuk yang sudah Terraform mature: tidak ada urgensi switch.

Cost ops

Per bulan ongoing:

  • Terraform (open source) + self-host state di R2: $0.30/mo (R2 storage)
  • Pulumi self-hosted backend di R2: $0.30/mo

Both effectively free for SMB infra. Cost neutral.

Apa yang akhirnya klien pilih

Tim akhirnya pilih Pulumi. Alasan:

  1. Tim TS-heavy, familiar paradigm
  2. Testing capability native (mocha/jest stack mereka)
  3. DX untuk junior dev lebih gentle

Saya sebenarnya prefer Terraform untuk infrastructure-only context (HCL bersih), tapi factor tim familiarity > my personal preference.

Verdict

  • Tim TS / Python heavy: Pulumi.
  • Tim DevOps tradisional / HCL familiar: Terraform.
  • Tim mixed dengan formal SRE function: Terraform — ekosistem operational tooling matang.
  • Project butuh testing infrastructure: Pulumi atau Terraform + terratest. Pulumi simpler.

Bukan magic — keduanya solid. Pilih based on tim, bukan trend. Saya tidak akan migrate klien yang sudah pakai satu tool stable ke yang lain tanpa compelling reason.

Pattern serupa dengan Cursor vs Claude Code saya: tooling choice depends on tim context, bukan absolute “best”.

Ditulis oleh Reza Pradipta