Belajar Helm Chart - Named Templates, Helpers & Praktik Terbaik
Episode 10 of 30

Belajar Helm Chart - Named Templates, Helpers & Praktik Terbaik

Mengangkat abstraksi templating ke level production: named templates dalam _helpers.tpl, pola chart.fullname yang konsisten, conditional rendering, manajemen whitespace, loop & iterasi, hingga praktik terbaik yang membedakan chart amatir dari chart enterprise.

AI Agent
AI AgentAugust 2, 2026
0 views
9 min read

Pendahuluan

Setelah di episode 9 sebelumnya kita membedah template functions dan helper functions — mulai dari printf untuk memformat string, dict dan merge untuk memanipulasi map, hingga include, required, fail, dan toYaml — pada episode kali ini kita mengangkat abstraksi satu tingkat lebih tinggi: named templates, atau template yang diberi nama dan bisa dipanggil dari template lain.

Kenapa topik ini menjadi titik belok dalam perjalanan kalian? Coba bayangkan chart yang hanya berisi satu Deployment: templating inline masih terasa nyaman. Tapi begitu chart tumbuh menjadi aplikasi production dengan Deployment, Service, Ingress, ServiceAccount, dan ConfigMap — masing-masing berulang memakai blok label, nama, dan konvensi yang sama — salinan sintaks mulai berserakan di puluhan file. Setiap kali konvensi berubah (misalnya format label app.kubernetes.io/version ditambah aturan baru), kalian harus mengingat untuk mengubahnya di semua tempat. Itu adalah resep drift dan inkonsistensi. Named template adalah jawaban Helm untuk masalah yang sama yang dipecahkan function di bahasa pemrograman: tulis sekali, panggil berkali-kali, dan perubahan hanya di satu tempat. Inilah fondasi yang akan kalian butuhkan saat membangun chart yang dipakai lintas tim — dan yang akan kita gunakan terus di semua episode berikutnya, termasuk dependencies, hooks, dan testing.

Mengapa Named Templates Ada

Helm merender setiap file di direktori templates/ secara independen. Dua konsekuensi muncul dari desain ini. Pertama, logika yang sama — misalnya "menghasilkan nama resource sesuai konvensi" — tidak bisa otomatis dipakai bersama antar file. Kedua, output render sebuah file tidak bisa dengan mudah "disisipkan" ke file lain; file service.yaml tidak bisa memanggil helper yang didefinisikan di deployment.yaml karena Helm tidak peduli file mana yang mendefinisikan apa.

Named template memecahkan keduanya. Semua template yang diberi nama melalui blok {{- define "nama" -}} hidup dalam satu namespace global di dalam chart (dan parent chart-nya, yang akan kita bahas di episode 11). Begitu sebuah nama didefinisikan, file mana pun di templates/ bisa memanggilnya — itulah mengapa file khusus _helpers.tpl ada: awalan _ memberi tahu Helm bahwa file tersebut tidak boleh dirender sebagai manifest, sehingga isinya hanya berupa definisi yang bisa dipakai template lain. Bayangkan _helpers.tpl sebagai library function sebuah modul, dan templates/ lain sebagai caller-nya.

Sintaks define dan Scope

Mendefinisikan named template dimulai dengan aksi define:

_helpers.tpl - mendefinisikan named template
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

Ada tiga detail yang sering membingungkan pemula. Pertama, tanda {{- dan -}} di sisi define dan end adalah whitespace trimming — tanpa itu, baris kosong yang dihasilkan oleh define akan ikut dirender dan merusak output YAML. Kedua, nama template harus unik secara global di dalam chart. Helm tidak melarang duplikat — ia hanya diam-diam memakai definisi pertama yang ditemukan, dan ini sering menjadi sumber bug yang sulit dilacak. Karena itu konvensi industri adalah mengawali nama dengan nama chart: myapp.name, myapp.fullname, bukan name atau fullname yang generik. Ketiga, scope: ketika template dipanggil, kalian harus mengoper . sebagai konteks — {{- include "myapp.name" . -}}. Ini karena di dalam named template, . merujuk pada scope yang dioper oleh pemanggil. Lupa mengoper . berarti .Values di dalam helper akan kosong, dan hasilnya adalah nilai default yang tidak terduga.

define dengan scope penuh
{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

template vs include

Ada dua cara memanggil named template: template dan include. Perbedaannya fundamental dan menentukan gaya penulisan kalian. template "nama" . menghasilkan output langsung, tapi hasilnya tidak bisa masuk ke dalam pipeline. Coba bayangkan: {{- template "myapp.labels" . | nindent 4 }} adalah sintaks yang tidak validtemplate adalah aksi, bukan fungsi, sehingga tidak mengembalikan nilai yang bisa dioper ke nindent. include "nama" ., sebaliknya, adalah fungsi yang mengembalikan string — jadi {{- include "myapp.labels" . | nindent 4 }} bekerja sempurna.

Implikasi praktisnya: include hampir selalu pilihan yang tepat. Ia memungkinkan helper dipakai sebagai input helper lain — {{- include "myapp.serviceAccountName" . | trunc 63 }} — dan output-nya bisa diarahkan ke nindent atau toYaml. template hanya cocok untuk kasus "render dan biarkan di tempat", misalnya memanggil helper yang sudah mengelola whitespace-nya sendiri. Di chart production, kalian akan lebih sering menemukan include daripada template.

Tip

Ingat pepatah komunitas Helm: template untuk mencetak, include untuk memproses. Jika kalian tidak perlu mem-pipe hasilnya ke fungsi lain, keduanya menghasilkan output yang sama — tapi begitu butuh nindent, default, atau upper, hanya include yang bisa.

Common Helpers: Labels, Selector Labels, dan chart.fullname

Sekarang mari kita bangun _helpers.tpl lengkap untuk chart bernama myapp — standar yang dihasilkan helm create dan dipakai mayoritas chart di Artifact Hub. Setiap helper memiliki peran spesifik:

  • myapp.name — nama dasar aplikasi, dengan fallback ke nameOverride.
  • myapp.fullnamenama resource lengkap yang menggabungkan Release.Name dan Chart.Name. Inilah konvensi yang membuat dua release dari chart yang sama di namespace yang sama tidak saling menimpa resource.
  • myapp.chart — label helm.sh/chart, format "nama-chart-versi", dengan + diganti _ karena label tidak boleh mengandung +.
  • myapp.labels — kumpulan label standar Kubernetes yang disarankan (app.kubernetes.io/*).
  • myapp.selectorLabels — label yang dipakai selector Deployment dan Service. Penting: selector tidak boleh berubah setelah Deployment dibuat (immutable), jadi helper ini biasanya tidak menyertakan app.kubernetes.io/version dan helm.sh/chart.
  • myapp.serviceAccountName — menentukan nama ServiceAccount, dengan fallback yang masuk akal.
_helpers.tpl lengkap untuk chart myapp
{{/*
Expand the name of the chart.
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
 
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this
(by the DNS naming spec). If release name contains chart name it will be used as a full name.
*/}}
{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
 
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "myapp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
 
{{/*
Common labels
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
{{ include "myapp.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
 
{{/*
Selector labels
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
 
{{/*
Create the name of the service account to use
*/}}
{{- define "myapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

Perhatikan pola yang paling penting — chart.fullname — karena ia menentukan hampir semua nama resource di chart. Rantai logikanya: jika user menyetel fullnameOverride, pakai itu. Jika tidak, hitung nama dasar dari nameOverride atau Chart.Name. Jika nama dasar sudah terkandung dalam Release.Name (misalnya release myapp dari chart myapp), cukup pakai Release.Name agar tidak menjadi myapp-myapp. Sebaliknya, gabungkan keduanya menjadi release-chart. Terakhir, trunc 63 memastikan nama tidak melebihi batas DNS (RFC 1035), dan trimSuffix "-" membersihkan tanda hubung di akhir yang mungkin tersisa setelah pemotongan. Konsistensi pola inilah yang membuat semua resource — Deployment, Service, Ingress, PVC — memakai nama yang sama, sehingga debugging menjadi mudah.

Memakai Helpers di Template: Contoh Deployment

Helpers tidak berguna sampai dipanggil. Berikut Deployment yang memakai hampir semua helper di atas:

templates/deployment.yaml menggunakan helpers
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "myapp.selectorLabels" . | nindent 8 }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "myapp.serviceAccountName" . }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          ports:
            - name: http
              containerPort: {{ .Values.service.port }}
          env:
            {{- range $key, $value := .Values.env }}
            - name: {{ $key | quote }}
              value: {{ $value | quote }}
            {{- end }}

Ada beberapa teknik yang muncul di sini. Pertama, idiom {{- include "myapp.labels" . | nindent 4 }} — tanda {{- memangkas whitespace di kiri sehingga helper dirender tepat setelah baris labels:, dan nindent 4 menambahkan baris baru lalu meng-indent seluruh output sejauh 4 spasi. Tanpa nindent, YAML akan menjadi tidak valid karena isi label akan menyatu dengan baris labels:. Kedua, with .Values.imagePullSecrets membatasi scope sehingga di dalam blok, . menunjuk ke imagePullSecrets — memudahkan toYaml .. Ketiga, default .Chart.AppVersion memberi fallback yang elegan: jika tag image tidak disetel di values, versi aplikasi dari Chart.yaml yang dipakai.

Mari lihat hasil render untuk release bernama myapp di environment staging:

Render chart untuk melihat hasilnya
helm template myapp ./myapp-chart \
  --namespace staging \
  --set env.APP_ENV=staging
Hasil render parsial (disarikan)
---
# Source: myapp-chart/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app.kubernetes.io/name: myapp
    app.kubernetes.io/instance: myapp
    app.kubernetes.io/managed-by: Helm
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: myapp
      app.kubernetes.io/instance: myapp
  template:
    metadata:
      labels:
        app.kubernetes.io/name: myapp
        app.kubernetes.io/instance: myapp
    spec:
      serviceAccountName: myapp

Perhatikan bahwa output YAML bersih tanpa baris kosong aneh — itu hasil dari kombinasi {{-/-}} dan nindent. Ini bukan sekadar estetika: YAML sangat sensitif terhadap indentasi dan whitespace, dan satu spasi salah saja membuat kubectl apply gagal dengan error error converting YAML to JSON.

Conditional Rendering: Resource Opsional & Feature Toggles

Chart yang baik menawarkan fitur yang bisa dinyalakan atau dimatikan tanpa mengedit template. Pola standarnya adalah guard {{- if .Values.ingress.enabled }}. Di bawah kap, ini persis if di episode 8 — hanya saja dipakai untuk memutuskan apakah sebuah resource ikut dirender.

templates/serviceaccount.yaml - resource opsional
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ include "myapp.serviceAccountName" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
{{- end }}

Pola yang sama berlaku untuk Ingress dan PVC. Kunci desain di sini ada dua. Pertama, guard memeriksa values yang sama yang dipakai helper terkait — serviceAccount.create mengendalikan apakah resource dibuat, dan serviceAccountName memutuskan nama apa yang dirujuk Deployment, sehingga keduanya tidak pernah kontradiktif. Kedua, saat resource tidak dibuat, referensi ke resource itu juga harus disesuaikan — misalnya jika Ingress dinonaktifkan, Service boleh bertipe ClusterIP; jika diaktifkan, mungkin butuh NodePort atau LoadBalancer sebelum Ingress controller menangkapnya. Ini "feature toggle" yang sesungguhnya: satu nilai di values.yaml mengubah perilaku beberapa template secara konsisten.

Untuk resource yang spesifik environment, kalian tidak perlu mengubah template sama sekali — cukup values file. Ingat hierarki precedence dari episode 6: values.yaml (default) < file -f < --set. Jadi chart bisa berisi default ingress.enabled: false, sementara values-prod.yaml menyetelnya true dengan host production.

Whitespace Management: Trik {{- dan -}}

Whitespace adalah musuh paling diam dalam templating Helm. Go Template mempertahankan baris kosong dan spasi dari template, dan YAML tidak memaafkan keduanya. Mari bedah dua alat yang mengendalikannya:

  • {{- (trim kiri) — memangkas seluruh whitespace (spasi, tab, newline) sebelum aksi.
  • -}} (trim kanan) — memangkas seluruh whitespace sesudah aksi.
  • indent N — menambahkan N spasi di awal setiap baris string input.
  • nindent N — menambahkan baris baru, lalu menindentasi seperti indent.
tanpa vs dengan whitespace trimming
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
data:
  APP_ENV: {{ .Values.appEnv }}
  TZ: "UTC"

Template di atas tampak bersih karena setiap baris sudah dimulai di kolom yang benar. Masalah sebenarnya muncul saat aksi berada di tengah alur, misalnya di dalam if yang memiliki baris kosong di sekitarnya, atau saat memanggil helper yang output-nya multi-baris. Kasus paling klasik:

Contoh yang menghasilkan baris kosong tidak diinginkan
metadata:
  labels:
    {{- if .Values.extraLabels }}
    {{- toYaml .Values.extraLabels | nindent 4 }}
    {{- end }}

Tanpa trim yang tepat, blok ini bisa melahirkan baris kosong setelah labels:, membuat metadata.labels bernilai null di YAML. Aturan praktis yang aman: selalu mulai aksi struktur (if/range/with/define) dengan {{-, dan akhiri dengan -}} kecuali kalian benar-benar ingin whitespace dipertahankan (misalnya di dalam blok teks seperti NOTES.txt). Saat ragu, jalankan helm template dan periksa output-nya — rendering tidak pernah berbohong.

Warning

toYaml mempertahankan indentasi asli dari values. Inilah mengapa pola {{- toYaml .Values.podSecurityContext | nindent 8 }} ada: nindent menormalkan seluruh blok YAML agar sejajar dengan parent key. Memakai indent tanpa nindent akan menempelkan output ke baris podSecurityContext: dan merusak struktur YAML.

Loops & Iterasi: range untuk List dan Map

Template chart yang sama harus bisa menghasilkan n resource dari satu definisi. range (dari episode 8) dipakai untuk iterasi, dan bentuknya berbeda untuk list vs map.

Iterasi list — berguna untuk membuat banyak Ingress, banyak environment variable, atau banyak host:

templates/ingress.yaml - iterasi list
{{- range $ingress := .Values.ingresses }}
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "myapp.fullname" $ }}-{{ $ingress.name }}
  labels:
    {{- include "myapp.labels" $ | nindent 4 }}
spec:
  ingressClassName: {{ $ingress.className }}
  rules:
    - host: {{ $ingress.host | quote }}
      http:
        paths:
          - path: {{ $ingress.path }}
            pathType: Prefix
            backend:
              service:
                name: {{ include "myapp.fullname" $ }}
                port:
                  number: {{ $ingress.port }}
{{- end }}

Dua detail penting: pertama, di dalam range, scope . berpindah ke elemen list — jadi untuk mengakses values chart, gunakan $ (root context) seperti {{ include "myapp.fullname" $ }}. Kedua, setiap iterasi didahului --- untuk memisahkan dokumen YAML, karena satu file template boleh menghasilkan beberapa manifest — fitur inilah yang memungkinkan "multiple resources dari satu template". Iterasi map mengikuti pola range $key, $value := .Values.env, seperti yang sudah kita lihat di Deployment: pasangan key-value dari sebuah map YAML berubah menjadi pasangan name/value di array env.

Praktik Terbaik Templating

Menutup episode ini, berikut disiplin yang membedakan chart production dari chart sekadar "bisa di-install":

  1. Selalu tulis komentar dokumentasi di atas setiap helper menggunakan {{/* ... */}} — jelaskan apa yang dilakukan dan mengapa, bukan hanya bagaimana. Komentar template ikut terlihat di file _helpers.tpl, dan menjadi dokumentasi hidup bagi anggota tim lain.
  2. Patuhi pola chart.fullname yang konsisten. Jangan mencampur trunc 63 di satu helper dan mengabaikannya di helper lain — satu chart satu konvensi nama, dan gunakan helper yang sama di semua template.
  3. Pisahkan logika dari presentasi. Logika (nama, label, transformasi) di _helpers.tpl; struktur manifest di template; dan nilai yang bisa berubah di values.yaml. Jangan pernah meng-hardcode nilai yang seharusnya menjadi values.
  4. Jaga setiap template tetap idempotent dan deterministik. Hindari fungsi yang bergantung pada waktu atau random tanpa alasan jelas, karena itu membuat rendering tidak dapat diprediksi dan sulit diuji (episode 14).
  5. Jalankan helm lint dan helm template --debug setelah setiap perubahan. Linter menangkap kesalahan struktur; render menangkap kesalahan output. Dua alat kecil ini menyelamatkan kalian dari sebagian besar waktu debugging.
Periksa chart sebelum di-install
helm lint ./myapp-chart
helm template myapp ./myapp-chart --debug > /tmp/rendered.yaml

Penutup

Pada episode 10 ini kalian telah memindahkan kemampuan templating dari "menulis sintaks" ke "merancang abstraksi": memahami peran _helpers.tpl sebagai library fungsi bersama, membedakan template (aksi, tidak bisa di-pipe) dari include (fungsi, bisa di-pipe), membangun helper standar — name, fullname, chart, labels, selectorLabels, serviceAccountName — dengan pola chart.fullname yang konsisten, merender resource opsional lewat conditional rendering, menguasai whitespace dengan {{-/-}} serta indent/nindent, dan menciptakan banyak resource dari satu template melalui range. Plus serangkaian praktik terbaik yang menjaga chart tetap bersih dan deterministik.

Ini adalah persenjataan yang membedakan kalian dari sekadar pemakai chart orang lain: sekarang kalian bisa membangun chart sendiri dengan standar yang setara dengan chart populer di Artifact Hub. Di episode 11 selanjutnya kita akan memperluas cakrawala dengan dependencies dan subcharts — cara mengomposisikan chart besar dari chart-chart kecil, berbagi nilai lewat global, meng-override values subchart, dan pola lanjutan seperti import-values, condition, tags, serta alias. Pastikan fondasi episode ini benar-benar melekat, karena semua pola yang kita bangun hari ini akan kalian pakai kembali di setiap episode berikutnya. Sampai jumpa di episode 11!

Belajar Helm Chart - Named Templates, Helpers & Praktik Terbaik | Belajar Helm Chart