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 planshow diff sebelum applyterraform validatesyntax check- Unit test: terratest (Go) atau Open Policy Agent (Rego) — both butuh tooling tambahan
Pulumi:
pulumi previewshow diff- TS compiler catch type error pre-runtime
- Unit test: native Mocha/Jest dengan
@pulumi/pulumitest 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
.tfstatedi 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 detikterraform apply: 4 menit 22 detik (sequential apply by default)- Output verbosity: moderate
Pulumi:
pulumi preview: 35 detikpulumi 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:
-
Provider version pinning: saya tidak pin version, upgrade tiba-tiba break syntax.
cloudflare_record→cloudflare_recorddi v5 punya breaking change. Sekarang saya pin semua provider. -
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.
-
Sensitive value di state: secret kadang tersimpan plain di state. Saya pakai remote backend dengan encryption + access control.
Pulumi:
-
TS dependency drift: package.json drift antara dev/CI. Lock file disciplined wajib.
-
Runtime cost: Pulumi engine via Node.js. Apply pakai ~280MB RAM. Untuk CI runner kecil: marginal.
-
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:
- Tim TS-heavy, familiar paradigm
- Testing capability native (mocha/jest stack mereka)
- 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