Membuka mesin di balik chart: sintaks Go Template dengan delimiter {{ }}, action dan pipeline, kontrol whitespace {{- -}}, struktur kontrol if/with/range, fungsi bawaan seperti default, quote, toYaml, dan indent, serta cara mengakses objek .Values, .Release, .Chart, dan .Files.

Setelah di episode 7 sebelumnya kita membuat chart pertama dan melihat template yang berisi {{ .Values.replicaCount }} bekerja seperti ajaib — pada episode kali ini kita membongkar ajaib itu: Go Template language, bahasa templating yang menjadi mesin setiap chart Helm.
Mengapa topik ini penting? Karena hampir semua kegagalan chart yang tidak Anda buat template-nya bukanlah kegagalan Kubernetes, melainkan kegagalan sintaks dan logika template. Pesan error seperti unexpected "}" in operand atau can't evaluate field X in type interface {} akan menghampiri setiap penulis chart, dan satu-satunya cara mengatasinya adalah memahami aturan bahasanya — bukan menghafal solusi per kasus. Lebih dari itu: penguasaan template menentukan seberapa ekspresif chart kalian. Chart yang "biasa" hanya mengganti nilai; chart yang bagus bisa menghasilkan manifest yang berbeda total tergantung environment — Ingress di prod, tanpa Ingress di dev — hanya dari beberapa baris logika template. Inilah perbedaan antara chart yang dipakai satu tim dan chart yang dipakai seratus tim.
Di episode ini kita akan membedah fondasi bahasa Go Template: delimiter dan pipeline, kontrol whitespace, struktur kontrol if/with/range, fungsi bawaan untuk manipulasi teks dan YAML, konsep variabel dan scope, serta objek-objek yang bisa diakses Helm seperti .Values, .Release, dan .Chart.
Semua template Helm ditulis dalam dua dunia: teks biasa yang langsung menjadi output, dan action di antara delimiter {{ dan }} yang dievaluasi Go Template. Teks biasa dilewatkan apa adanya; action diganti dengan hasil evaluasinya.
replicas: {{ .Values.replicaCount }}Jika .Values.replicaCount bernilai 3, hasil render menjadi replicas: 3. Di dalam action, ada beberapa bentuk:
{{ .Values.replicaCount }} atau .Values.image.tag dengan path bertitik.{{ upper "helm" }} menghasilkan HELM.|, seperti pipeline di shell:name: {{ .Values.name | upper }}Pipeline dibaca dari kiri: nilai .Values.name dikirim sebagai argumen terakhir ke fungsi upper. Karena itu {{ .Values.name | upper }} sama dengan {{ upper .Values.name }}. Pipeline bisa dirantai: {{ .Values.name | upper | quote }} — menjadi upper, lalu hasilnya di-quote. Pipeline adalah idiom utama penulisan template Helm; hampir semua template production memakainya.
Untuk menulis komentar di dalam template yang tidak ikut dirender, gunakan {{/* ... */}}:
{{/* Nama release dipakai sebagai nama resource */}}
name: {{ .Release.Name }}Note
Kesalahan pertama penulis template baru adalah melupakan bahwa hampir semua yang di luar {{ }} akan menjadi output apa adanya — termasuk spasi, baris baru, dan indentasi. Template yang tidak menjaga whitespace akan menghasilkan YAML yang "jelek tapi valid" atau bahkan tidak valid sama sekali. Untuk itu kita butuh kontrol whitespace, yang kita bahas berikut.
{{- dan -}}Perhatikan template berikut:
replicaCount: {{ .Values.replicaCount }}
service:
port: {{ .Values.service.port }}Karena setiap baris menghasilkan satu baris output, hasil render tampak rapi. Masalah muncul saat action berada di tengah baris yang ingin kita hilangkan — terutama pada if dan range, di mana baris {{- if ... }} sendiri seharusnya tidak menghasilkan baris kosong. Untuk memotong whitespace, Go Template menyediakan dua tanda hubung: {{- memotong whitespace sebelum action (termasuk newline), dan -}} memotong whitespace setelah action.
{{- if .Values.ingress.enabled }}
ingress_active: yes
{{- else }}
ingress_active: no
{{- end }}Tanpa {{-, setiap baris {{- if ... }}, {{- else }}, dan {{- end }} akan meninggalkan baris kosong di output, membuat YAML tidak valid atau berantakan. Dengan trim, output menjadi:
ingress_active: noAturan emasnya: semua struktur kontrol (if, with, range, end) pakai {{- untuk trim kiri, dan umumnya -}} di akhir baris yang menutup struktur. Untuk end sering ditulis {{- end }} — cukup trim kiri. Ini salah satu detail yang paling sering membedakan chart yang rapi dari chart yang menyakitkan mata.
if, with, rangeGo Template punya tiga struktur kontrol inti, semua diakhiri end.
if / else if / else mengevaluasi nilai — nilai kosong (nil, string kosong, angka 0, false, list kosong, map kosong) dianggap false:
{{- if .Values.ingress.enabled }}
ingress_active: true
{{- else if .Values.service.loadBalancer }}
loadbalancer_active: true
{{- else }}
clusterip_active: true
{{- end }}with mengubah scope: di dalam blok with, titik . menunjuk ke nilai yang dimasukkan. Ini menghilangkan pengulangan path yang panjang:
{{- with .Values.database }}
host: {{ .host }}
port: {{ .port }}
user: {{ .user }}
{{- end }}Jika .Values.database adalah {host: "db.example.com", port: 5432, user: "app"}, maka di dalam blok, .host merujuk database.host — bukan root. Di luar blok, . kembali ke root. Efek samping penting: di dalam with, .Values tidak lagi bisa diakses secara langsung, jadi simpan nilai root ke variabel $ jika masih dibutuhkan (kita bahas di bagian scope).
range mengiterasi list atau map, dan setiap iterasi mengubah scope . ke elemen saat itu:
hosts:
{{- range $index, $host := .Values.hosts }}
- {{ $index }}: {{ $host }}
{{- end }}Dengan .Values.hosts: [web.example.com, api.example.com], hasilnya:
hosts:
- 0: web.example.com
- 1: api.example.comUntuk map, sintaksnya sama tapi $key dan $value:
labels:
{{- range $key, $value := .Values.labels }}
{{ $key }}: "{{ $value }}"
{{- end }}Warning
Iterasi map di Go tidak menjamin urutan — hasil range atas map bisa berbeda setiap render. Untuk manifest yang urutannya penting (misalnya urutan list dalam satu array), gunakan list sebagai pengganti, atau fungsi pengurutan seperti sortAlpha yang akan kita bahas di episode 9.
Go Template membawa sejumlah fungsi bawaan yang menjadi "kata kerja" sehari-hari penulisan template:
default: {{ .Values.name | default "world" }}
quoted: {{ .Values.name | quote }}
upper: {{ "helm" | upper }}
lower: {{ "HELM" | lower }}
title: {{ "helm chart" | title }}
trimmed: {{ " helm " | trim }}
squoted: {{ .Values.name | squote }}Dengan .Values.name: "arman", hasilnya:
default: arman
quoted: "arman"
upper: HELM
lower: helm
title: Helm Chart
trimmed: helm
squoted: 'arman'Fungsi yang paling banyak dipakai adalah default — memberi nilai pengganti jika nilai kosong. Pola klasiknya: {{ .Values.image.tag | default .Chart.AppVersion }} — jika user tidak mengisi tag, fallback ke versi aplikasi dari Chart.yaml. Ini contoh sempurna "default yang masuk akal" yang kita bahas di episode 7. Untuk path prefix/suffix, ada trimPrefix dan trimSuffix:
stripped: {{ "charts/mychart" | trimPrefix "charts/" }}
version: {{ "v1.27.0" | trimSuffix ".0" }}indent, nindent, toYaml, toJsonEmpat fungsi ini adalah jembatan antara data values dan struktur YAML yang benar. toYaml mengubah nilai Go (biasanya map/list bersarang) menjadi blok YAML:
env:
{{- toYaml .Values.env | nindent 4 }}Dengan .Values.env: {LOG_LEVEL: info, NODE_ENV: production}, toYaml menghasilkan dua baris YAML, lalu nindent 4 menambahkan baris baru di awal dan indentasi 4 spasi pada setiap baris:
env:
LOG_LEVEL: info
NODE_ENV: productionPerbedaan indent dan nindent: indent N menambah N spasi di awal setiap baris yang sudah ada, sementara nindent N menambah baris baru lalu mengindent — persis yang kita butuhkan saat menyisipkan blok di tengah YAML. Tanpa nindent, env: dan isinya akan menempel di baris yang sama, menghasilkan YAML yang salah. Untuk output JSON, gunakan toJson:
json: {{ .Values.env | toJson | quote }}Variabel di Go Template dideklarasikan dengan := dan selalu diawali $:
{{- $fullName := printf "%s-%s" .Release.Name .Chart.Name }}
name: {{ $fullName }}Aturan scope: variabel yang dideklarasikan di sebuah blok (if, with, range) hanya hidup di dalam blok itu. Variabel yang dideklarasikan di level dokumen tersedia di seluruh template. Ada satu variabel istimewa: $ — yang selalu menunjuk ke root context, tidak peduli seberapa dalam scope berubah. Ini penyelamat saat . telah digeser oleh with atau range:
{{- with .Values.database }}
host: {{ .host }}
release: {{ $.Release.Name }}
{{- end }}Di dalam blok with, .host membaca database.host, sementara $.Release.Name tetap bisa mengakses release dari root. Pola $ ini sangat umum di template production, terutama dalam loop range di dalam _helpers.tpl.
Tip
Ingat dua scope pitfall yang paling sering membingungkan: (1) variabel $var tidak tersedia di luar blok tempat ia dideklarasikan; (2) with dan range menggeser ., sehingga {{ .Values }} di dalam blok tidak bekerja — gunakan $ jika ingin kembali ke root. Memahami dua aturan ini menghilangkan sebagian besar kebingungan saat membaca template orang lain.
Selain .Values, Helm menyuntikkan beberapa objek konteks yang bisa diakses dari template mana pun:
| Objek | Isi | Contoh Akses |
|---|---|---|
.Values | Semua nilai gabungan (default chart + file + --set) | .Values.replicaCount |
.Release | Info release: .Name, .Namespace, .Revision, .IsInstall, .IsUpgrade, .Service | .Release.Name |
.Chart | Metadata dari Chart.yaml: .Name, .Version, .AppVersion, .Type | .Chart.AppVersion |
.Capabilities | Kemampuan cluster: .KubeVersion, .APIVersions.Has | .Capabilities.KubeVersion.Version |
.Template | Info template yang sedang dirender: .Name, .BasePath | .Template.Name |
.Files | Isi file non-template di dalam chart | .Files.Get "config/app.conf" |
Contoh penggunaan nyata .Files — chart yang membawa file konfigurasi statis:
data:
app.conf: |
{{ .Files.Get "config/app.conf" | indent 4 }}.Capabilities sering dipakai untuk menyesuaikan manifest terhadap versi cluster, misalnya memilih apps/v1 Deployment secara kondisional untuk cluster lama — detail yang akan kita kombinasikan dengan struktur kontrol dan fungsi pada episode berikutnya.
Important
.Release dan .Chart hanya tersedia saat Helm merender — bukan saat helm template? Jawabannya: helm template tetap mengisinya dengan nilai asumsi (release name dari argumen, metadata dari Chart.yaml), jadi kalian bisa men-debug template dengan aman tanpa cluster. Ini salah satu keunggulan Helm: seluruh template bisa diuji secara lokal, tanpa menyentuh Kubernetes sama sekali.
Pada episode 8 ini kita telah membedah bahasa Go Template yang menjadi mesin setiap chart: delimiter {{ }} dengan action dan pipeline |, kontrol whitespace {{- dan -}} yang menjaga YAML tetap valid, struktur kontrol if/else, with yang menggeser scope, dan range untuk iterasi list serta map. Kita menguasai fungsi bawaan default, quote, squote, upper, lower, title, trim, trimPrefix, trimSuffix, serta jembatan ke YAML/JSON: toYaml, toJson, indent, dan nindent. Kita memahami variabel $var, aturan scope, dan keajaiban $ untuk kembali ke root, plus objek-objek Helm: .Values, .Release, .Chart, .Capabilities, .Template, dan .Files.
Inti yang harus kalian bawa:
| adalah idiom utama; rantaikan fungsi dari kiri ke kanan.{{- dan -}} menjaga template menghasilkan YAML bersih; pakai pada semua struktur kontrol.with dan range menggeser .; gunakan $ untuk kembali ke root.toYaml | nindent N adalah cara standar menyisipkan data bersarang ke YAML.helm template tanpa cluster.Sekarang kalian paham sintaksnya. Di episode 9 selanjutnya kita menambah senjata: template functions & helper functions. Kita akan mempelajari fungsi string (printf, replace, split, join), konversi tipe (int, toString, atoi), fungsi list (list, append, first, last, reverse, uniq), fungsi dict (dict, hasKey, merge), pustaka Sprig (date, crypto, encoding, math), serta fungsi khusus Helm yang membedakannya dari Go Template biasa: include, required, fail, dan lookup. Sampai jumpa di episode 9!