Penulisan Chart yang Sesungguhnya

Penulisan chart Helm untuk aplikasi multi-resource nyata dengan template dan fungsi templating.

Daftar isi

Topik ini membahas penulisan chart Helm untuk aplikasi catatan yang terdiri atas lebih dari satu objek, lengkap dengan values per lingkungan dan fungsi templating untuk membentuk manifest. Pembahasan dimulai dari anatomi chart dan berkas Chart.yaml, dilanjutkan lima fungsi templating yang paling sering muncul pada chart nyata, kemudian berkas values untuk staging dan produksi, dan ditutup perenderan chart dengan tiga kombinasi values. Struktur chart pada topik ini sudah lengkap, sehingga bidang version pada Chart.yaml naik menjadi 1.0.0.

Anatomi chart aplikasi nyata#

Chart nyata jarang berhenti pada satu template. Aplikasi catatan membutuhkan Deployment dan Service, dan keduanya berbagi nama serta label yang sama, sehingga nilai-nilai itu perlu dinyatakan satu kali lalu disisipkan ke setiap template. Helm menyediakan folder templates untuk menampung semua template, ditambah berkas khusus bernama _helpers.tpl untuk definisi yang digunakan ulang di seluruh chart.

text Struktur direktori
catatan-api/
├── Chart.yaml
├── values.yaml
└── templates/
    ├── _helpers.tpl
    ├── deployment.yaml
    └── service.yaml

Chart.yaml pada tahap ini menambahkan deskripsi yang lebih jelas dan menaikkan version karena struktur chart sudah lengkap. Bidang appVersion tetap mengikuti versi aplikasi yang dibungkus, sedangkan version menyatakan versi chart itu sendiri. Pada bundle topik ini, seluruh berkas chart ditampilkan datar di samping artikel sesuai nama yang disesuaikan, misalnya template-deployment.yaml yang dalam chart sesungguhnya tersimpan sebagai templates/deployment.yaml.

yaml Chart.yaml
apiVersion: v2
name: catatan-api
description: Chart Deployment dan Service untuk API aplikasi catatan
version: 1.0.0
appVersion: "1.2.0"

values.yaml pada chart ini lebih kaya daripada draf awal. Kunci containerPorts berbentuk daftar karena aplikasi membuka dua port, yaitu 8000 untuk API dan 9090 untuk metrik, sedangkan resources menyimpan permintaan dan batas sumber daya yang diteruskan ke container.

yaml values.yaml
replicaCount: 1
image:
  repository: registry.local/catatan-api
  tag: ""
containerPorts:
  - 8000
  - 9090
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 250m
    memory: 256Mi

Fungsi templating yang digunakan#

Lima fungsi berikut menutupi hampir seluruh kebutuhan penulisan chart sehari-hari, yaitu .Values untuk mengambil nilai, include untuk menyisipkan template bernama, toYaml untuk mengubah struktur menjadi teks YAML, range untuk mengulang butir daftar, dan default untuk mengganti nilai kosong.

FungsiPeranContoh pada chart
.Valuesmengambil nilai dari values.yaml{{ .Values.replicaCount }}
includemenyisipkan template bernama dari _helpers.tpl{{ include "catatan.nama" . }}
toYamlmengubah struktur menjadi teks YAML{{ toYaml .Values.resources }}
rangemengulang setiap butir daftar{{ range .Values.containerPorts }}
defaultmengganti nilai yang kosong`{{ .Values.image.tag

Definisi template bernama ditulis pada _helpers.tpl melalui kata kunci define, kemudian disisipkan dengan include. Tanda strip pada pembuka dan penutup aksi mengendalikan spasi di sekitar keluaran, sehingga baris kosong yang tidak diinginkan tidak muncul pada manifest hasil perenderan.

text Potongan: templates/_helpers.tpl
{{- define "catatan.nama" -}}
{{ .Chart.Name }}
{{- end -}}

{{- define "catatan.label" -}}
app: {{ .Chart.Name }}
komponen: api
{{- end -}}

Berkas deployment.yaml berikut menempatkan kelima fungsi tersebut pada posisi yang menyusun manifest utuh. Placeholder .Chart.Name dan .Chart.AppVersion mengambil nilai dari Chart.yaml, sehingga label dan tag image ikut berganti apabila versi berubah.

yaml template-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "catatan.nama" . }}
  labels:
    {{- include "catatan.label" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Chart.Name }}
  template:
    metadata:
      labels:
        {{- include "catatan.label" . | nindent 8 }}
    spec:
      containers:
        - name: api
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          ports:
            {{- range .Values.containerPorts }}
            - containerPort: {{ . }}
            {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

Tanda pipa meneruskan keluaran satu fungsi ke fungsi berikutnya. Contoh paling terlihat adalah {{ toYaml .Values.resources | nindent 12 }}. Struktur resources diubah menjadi teks YAML, lalu setiap barisnya diberi indentasi sepanjang dua belas spasi agar sesuai kedudukannya di dalam manifest.

values untuk tiap lingkungan#

Perbedaan antara lingkungan ditampung pada berkas values terpisah yang menimpa nilai default. Berkas staging menaikkan jumlah replika menjadi dua, menyematkan tag kandidat rilis, dan memperbesar batas sumber daya, sedangkan berkas produksi menetapkan tiga replika dengan tag rilis stabil serta permintaan sumber daya yang lebih besar.

yaml values-staging.yaml
replicaCount: 2
image:
  tag: "1.3.0-rc.2"
resources:
  limits:
    cpu: 500m
    memory: 512Mi
yaml values-produksi.yaml
replicaCount: 3
image:
  tag: "1.2.0"
resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: "1"
    memory: 512Mi

Berkas lingkungan hanya menuliskan kunci yang berbeda dari values.yaml. Saat instalasi atau peningkatan berjalan, Helm menggabungkan keduanya secara bertingkat. Kunci pada berkas lingkungan menimpa nilai default, sedangkan kunci yang tidak disebut mengikuti nilai default. Perintah pemanggilannya berbentuk helm upgrade -f values-produksi.yaml, dan apabila lebih dari satu berkas disebutkan, berkas yang dituliskan belakangan menimpa berkas sebelumnya.

Merender chart dengan tiga kombinasi values#

Perenderan chart pada sandbox artikel ini disimulasikan dengan Python. Skrip berikut membaca template-deployment.yaml dan Chart.yaml dari bundle, kemudian merendernya tiga kali, yaitu dengan nilai default, dengan gabungan values staging, dan dengan gabungan values produksi. Fungsi include, toYaml, nindent, range, dan default ditiru secara sederhana agar hasilnya setia pada perilaku Helm.

python render-chart.py
# Perender sederhana templat Helm: meniru penggantian .Values, include,
# toYaml, range, dan default pada tiga kombinasi values.
import copy
import re
import yaml

with open("Chart.yaml", encoding="utf-8") as berkas:
    chart = yaml.safe_load(berkas)
with open("template-deployment.yaml", encoding="utf-8") as berkas:
    templat = berkas.read()
with open("values.yaml", encoding="utf-8") as berkas:
    values_default = yaml.safe_load(berkas)
with open("values-staging.yaml", encoding="utf-8") as berkas:
    values_staging = yaml.safe_load(berkas)
with open("values-produksi.yaml", encoding="utf-8") as berkas:
    values_produksi = yaml.safe_load(berkas)

def gabung(asal, tambahan):
    # penggabungan bertingkat seperti helm upgrade -f
    hasil = copy.deepcopy(asal)
    for kunci, nilai in tambahan.items():
        if isinstance(nilai, dict) and isinstance(hasil.get(kunci), dict):
            hasil[kunci] = gabung(hasil[kunci], nilai)
        else:
            hasil[kunci] = copy.deepcopy(nilai)
    return hasil

def helper_nama(konteks):
    # meniru define "catatan.nama" pada _helpers.tpl
    return konteks["Chart"]["Name"]

def helper_label(konteks):
    # meniru define "catatan.label" pada _helpers.tpl
    return "app: " + konteks["Chart"]["Name"] + "\nkomponen: api"

HELPERS = {"catatan.nama": helper_nama, "catatan.label": helper_label}

def ambil_nilai(jalur, konteks):
    if jalur == ".":
        return konteks["."] if "." in konteks else konteks
    objek = konteks
    for kunci in jalur.lstrip(".").split("."):
        objek = objek[kunci]
    return objek

def ke_yaml(objek):
    return yaml.dump(objek, sort_keys=True, default_flow_style=False).rstrip("\n")

def terapkan(nama, argumen, nilai_masuk, konteks):
    if nama == "include":
        return HELPERS[argumen[0]](konteks)
    if nama == "toYaml":
        return ke_yaml(argumen[0] if argumen else nilai_masuk)
    if nama == "nindent":
        jumlah = int(argumen[0])
        return "\n" + "\n".join(" " * jumlah + b for b in str(nilai_masuk).split("\n"))
    if nama == "default":
        return nilai_masuk if nilai_masuk not in (None, "") else argumen[0]
    raise SystemExit("fungsi tidak dikenal: " + nama)

def evaluasi(ekspresi, konteks):
    tahap = [t.strip() for t in ekspresi.split("|")]
    nilai = None
    if tahap[0].startswith("."):
        nilai = ambil_nilai(tahap[0], konteks)
        tahap = tahap[1:]
    for teks_tahap in tahap:
        m = re.match(r"(\w+)\s*(.*)$", teks_tahap)
        nama, sisa = m.group(1), m.group(2).strip()
        argumen = [b.strip('"') for b in re.findall(r'"[^"]*"|[^\s]+', sisa)] if sisa else []
        # argumen yang berupa jalur objek dievaluasi terlebih dahulu
        argumen = [ambil_nilai(a, konteks) if a.startswith(".") else a for a in argumen]
        nilai = terapkan(nama, argumen, nilai, konteks)
    return nilai

POLA_AKSI = re.compile(r"(\{\{-?)\s*(.*?)\s*(-?\}\})", re.DOTALL)

def tokenisasi(templat):
    mentah, posisi = [], 0
    for m in POLA_AKSI.finditer(templat):
        teks = templat[posisi:m.start()]
        if m.group(1) == "{{-":
            teks = teks.rstrip()
        mentah.append(["teks", teks, False])
        mentah.append(["aksi", m.group(2).strip(), m.group(3) == "-}}"])
        posisi = m.end()
    mentah.append(["teks", templat[posisi:], False])
    token = []
    for i, t in enumerate(mentah):
        if t[0] == "teks" and i > 0 and mentah[i - 1][2]:
            t = ["teks", t[1].lstrip(), False]
        token.append(t)
    return token

def render(token, konteks):
    keluaran, i = [], 0
    while i < len(token):
        t = token[i]
        if t[0] == "teks":
            keluaran.append(t[1])
        elif t[1].startswith("range "):
            dalam, i = 0, i + 1
            isi = []
            while not (token[i][0] == "aksi" and token[i][1] == "end" and dalam == 0):
                if token[i][0] == "aksi" and token[i][1].startswith("range "):
                    dalam += 1
                if token[i][0] == "aksi" and token[i][1] == "end":
                    dalam -= 1
                isi.append(token[i])
                i += 1
            for butir in ambil_nilai(t[1].split(" ", 1)[1], konteks):
                keluaran.append(render(isi, {**konteks, ".": butir}))
        elif t[1] == "end":
            pass
        else:
            keluaran.append(str(evaluasi(t[1], konteks)))
        i += 1
    return "".join(keluaran)

for nama, nilai in (("default", values_default),
                    ("staging", gabung(values_default, values_staging)),
                    ("produksi", gabung(values_default, values_produksi))):
    # objek Chart pada Helm berbentuk kamus dengan kunci kapital
    konteks = {"Values": nilai,
               "Chart": {"Name": chart["name"], "AppVersion": chart["appVersion"]}}
    print(f"=== values {nama} ===")
    print(render(tokenisasi(templat), konteks))
text Keluaran
=== values default ===
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catatan-api
  labels:
    app: catatan-api
    komponen: api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: catatan-api
  template:
    metadata:
      labels:
        app: catatan-api
        komponen: api
    spec:
      containers:
        - name: api
          image: "registry.local/catatan-api:1.2.0"
          ports:
            - containerPort: 8000
            - containerPort: 9090
          resources:
            limits:
              cpu: 250m
              memory: 256Mi
            requests:
              cpu: 100m
              memory: 128Mi

=== values staging ===
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catatan-api
  labels:
    app: catatan-api
    komponen: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: catatan-api
  template:
    metadata:
      labels:
        app: catatan-api
        komponen: api
    spec:
      containers:
        - name: api
          image: "registry.local/catatan-api:1.3.0-rc.2"
          ports:
            - containerPort: 8000
            - containerPort: 9090
          resources:
            limits:
              cpu: 500m
              memory: 512Mi
            requests:
              cpu: 100m
              memory: 128Mi

=== values produksi ===
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catatan-api
  labels:
    app: catatan-api
    komponen: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: catatan-api
  template:
    metadata:
      labels:
        app: catatan-api
        komponen: api
    spec:
      containers:
        - name: api
          image: "registry.local/catatan-api:1.2.0"
          ports:
            - containerPort: 8000
            - containerPort: 9090
          resources:
            limits:
              cpu: '1'
              memory: 512Mi
            requests:
              cpu: 250m
              memory: 256Mi

Bagian default memperlihatkan cara kerja fungsi default. Tag image yang kosong pada values.yaml digantikan oleh appVersion 1.2.0. Bagian staging memperlihatkan penggabungan bertingkat. Permintaan sumber daya tetap dipertahankan dari nilai default, sedangkan batasnya mengikuti berkas staging. Bagian produksi memperlihatkan tiga replika dengan permintaan yang lebih besar, dan ketiga keluaran hanya berbeda pada nilai yang disebut oleh berkas lingkungannya.

Pada kluster sungguhan, hasil yang sama dapat dilihat tanpa instalasi melalui helm template.

bash
# tidak-dijalankan
# merender seluruh templat chart ke terminal menggunakan values staging
helm template catatan ./catatan-api -f values-staging.yaml
text Contoh keluaran
---
# Source: catatan-api/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: catatan-api
  labels:
    app: catatan-api
    komponen: api

Ringkasan#

  • Chart nyata menampung beberapa template sekaligus dan memanfaatkan _helpers.tpl untuk definisi nama serta label yang digunakan ulang.
  • Fungsi .Values, include, toYaml, range, dan default menutupi kebutuhan templating yang paling sering muncul pada chart sehari-hari.
  • values per lingkungan dituliskan secara parsial dan digabung secara bertingkat di atas nilai default saat instalasi atau peningkatan.
  • helm template merender chart tanpa instalasi, sehingga hasil setiap kombinasi values dapat diperiksa sebelum dikirim ke kluster.

Referensi#