~/yazilar/helm-ile-paket-yonetimi

Helm ile chart kurmak ve kendi chart'ını yazmak

9 dk okuma

k3s'i kurduğun anda kümende Helm ile kurulmuş iki şey vardı, sen Helm'i hiç kurmamış olsan da. İlk yazıdaki helm-install-traefik pod'larını hatırlarsan Traefik böyle geliyordu. k3s içinde bir Helm denetleyicisi çalışıyor ve HelmChart nesnelerini okuyup kuruyor:

kubectl -n kube-system get helmcharts
NAME          JOB                        CHART                                                                      ...
traefik       helm-install-traefik       https://%{KUBERNETES_API}%/static/charts/traefik-....tgz                   ...
traefik-crd   helm-install-traefik-crd   https://%{KUBERNETES_API}%/static/charts/traefik-crd-....tgz               ...

Benim için asıl işe yarayan tarafı, bir önceki yazının sonunda kalan sorunu çözmesi: aynı uygulamayı farklı ayarlarla birden çok yere kurmak için YAML kopyalamak yerine tek bir şablon ve ortam başına küçük bir ayar dosyası tutuyorsun.

Chart, values ve release

Chart, şablonlaştırılmış Kubernetes YAML'larının paketi. İçinde Deployment, Service, Ingress gibi dosyalar var ama değerlerin yerinde {{ .Values.replicaCount }} gibi yer tutucular duruyor.

Values, bu yer tutucuları dolduran ayarlar. Her chart bir values.yaml ile varsayılanlarını getiriyor, sen sadece değiştirmek istediklerini kendi dosyanda veriyorsun.

Release, bir chart'ın belirli değerlerle kümeye kurulmuş hali. Aynı chart'ı aynı kümeye iki farklı isimle kurarsan iki ayrı release olur.

Helm 3 ile birlikte eski sürümlerdeki Tiller bileşeni tamamen kalktı. Helm artık kümede ayrı bir şey çalıştırmıyor, senin kubeconfig'inle ve senin yetkilerinle doğrudan API'ye konuşuyor. Release geçmişini de kurulduğu namespace'te Secret olarak tutuyor (sh.helm.release.v1.<isim>.v<sürüm> adında). Rollback buradan çalışıyor.

Kurulum

Helm'in resmi kurulum script'i var. k3s'te yaptığım gibi önce indirip okuyorum:

curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
less get_helm.sh
bash get_helm.sh
helm version
version.BuildInfo{Version:"v3.18.6", GitCommit:"...", GitTreeState:"clean", GoVersion:"go1.24.6"}

Script /usr/local/bin/helm dosyasını koyuyor ve indirdiği arşivin checksum'ını doğruluyor. İmza kontrolü varsayılan olarak kapalı, VERIFY_SIGNATURES=true ile açılıyor. Paket yöneticisiyle kurmayı tercih edersen Helm'in dokümantasyonunda apt deposu da var.

İlk komutta şu hatayı görürsen:

Error: Kubernetes cluster unreachable: Get "http://localhost:8080/version": dial tcp 127.0.0.1:8080: connect: connection refused

Helm kubeconfig bulamamış ve varsayılan adrese gitmiş demektir. k3s'in kubeconfig'i /etc/rancher/k3s/k3s.yaml altında, Helm ise KUBECONFIG değişkenine ya da ~/.kube/config dosyasına bakıyor. İlk yazıda kopyayı alıp KUBECONFIG'i ayarladıysan bu hatayı görmezsin.

Grafana'yı hazır chart ile kurmak

Örnek olarak Grafana'nın resmi chart'ını kuruyorum. Kurum içinde izleme paneli olarak sık karşılaşılan bir uygulama ve chart'ı kalıcı depolama, Ingress ve Secret kullanımını bir arada gösteriyor.

helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
helm search repo grafana/grafana --versions | head -5
NAME             CHART VERSION   APP VERSION   DESCRIPTION
grafana/grafana  x.y.z           a.b.c         The leading tool for querying and visualizing t...
...

İki ayrı sürüm var: chart sürümü ve uygulama sürümü. Ben kurarken her zaman chart sürümünü --version ile sabitliyorum. Vermezsen o gün en yeni chart geliyor ve aynı komut bir ay sonra başka bir şey kuruyor. Image etiketini sabitlemekle aynı mantık.

Chart'ın neleri ayarlamana izin verdiğini görmek için varsayılan değerleri dosyaya döküyorum:

helm show values grafana/grafana --version x.y.z > grafana-varsayilan.yaml
wc -l grafana-varsayilan.yaml

Bu dosya uzun. Hepsini kopyalayıp düzenleme hatasına düşme. Kendi dosyanda sadece değiştirdiğin değerler olsun. Böylece chart'ı yükselttiğinde yeni varsayılanlar otomatik geliyor ve dosyana bakan biri neyi bilerek değiştirdiğini hemen görüyor.

Yönetici parolası

Chart'a parolayı values içinde düz metin verebiliyorsun ama o zaman parola values dosyasında ve release geçmişinde duruyor. Onun yerine önce bir Secret açıp chart'a onun adını veriyorum. Parolayı da kendim yazmak yerine ürettiriyorum:

kubectl create namespace monitoring
kubectl -n monitoring create secret generic grafana-admin \
  --from-literal=admin-user=admin \
  --from-literal=admin-password="$(openssl rand -base64 24)"

values dosyası

# grafana-values.yaml
admin:
  existingSecret: grafana-admin
  userKey: admin-user
  passwordKey: admin-password

persistence:
  enabled: true
  size: 5Gi

ingress:
  enabled: true
  ingressClassName: traefik
  hosts:
    - grafana.k8s.example.com
  path: /
  pathType: Prefix

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    memory: 512Mi

persistence açık olmazsa Grafana pano ve kullanıcılarını pod içinde tutuyor, pod yeniden açılınca hepsi gidiyor. k3s'te varsayılan depolama sınıfı local-path olduğu için storageClassName vermeye gerek yok, veri sunucunun diskinde /var/lib/rancher/k3s/storage altında duruyor.

Kurmak

Kurmadan önce chart'ın ne üreteceğini görmek istiyorum. helm template kümeye dokunmadan YAML'ı ekrana basıyor:

helm template grafana grafana/grafana --version x.y.z \
  -n monitoring -f grafana-values.yaml | less

İçinde Ingress'in doğru host'la, PVC'nin istediğim boyutla oluştuğunu görünce kuruyorum:

helm install grafana grafana/grafana --version x.y.z \
  -n monitoring -f grafana-values.yaml --wait --timeout 5m
NAME: grafana
LAST DEPLOYED: ...
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
NOTES:
1. Get your 'admin' user password by running:
...

--wait, Helm'in kaynaklar hazır olana kadar beklemesini sağlıyor. Vermezsen Helm YAML'ı API'ye gönderip "deployed" diyor, pod'lar o sırada daha image çekiyor bile olabilir. Otomasyonda bu fark önemli.

Durumu görmek için:

helm list -n monitoring
helm get values grafana -n monitoring
kubectl -n monitoring get pods,pvc,ingress

helm get values sana sadece senin verdiğin değerleri gösteriyor. Kurulumdan çok sonra hangi ayarların verildiğini buradan görebilirsin. Yine de asıl kaynak grafana-values.yaml dosyası olmalı ve bir repoda durmalı.

Yükseltmek

Bir ayarı değiştirmek de chart sürümünü yükseltmek de aynı komutla yapılıyor. Mesela bellek limitini artırdım:

helm upgrade grafana grafana/grafana --version x.y.z \
  -n monitoring -f grafana-values.yaml --wait

Burada Helm'in değerleri nasıl taşıdığını bilmek gerekiyor. Hiç -f ya da --set vermezsen Helm 3 önceki release'in değerlerini kendisi kullanıyor. Ama tek bir --set ya da başka bir -f verdiğin anda önceki değerler bırakılıyor ve vermediğin her şey chart varsayılanına dönüyor. Bellek limitini --set ile değiştirip -f grafana-values.yaml'ı unutursan persistence kapanıyor, Ingress siliniyor ve sen sadece bir değeri değiştirdiğini sanıyorsun. --reuse-values diye bir bayrak var ama yeni chart sürümünde eklenen değerlerle birlikte kullanınca tuhaf sonuçlar verebiliyor. Bence her seferinde values dosyasını açıkça vermek en güvenlisi.

Chart sürümünü yükseltmeden önce iki şeye bakıyorum: chart'ın değişiklik notları (özellikle major sürüm atlıyorsa) ve iki sürüm arasındaki farkın ne olacağı. Fark için helm-diff eklentisi işe yarıyor:

helm plugin install https://github.com/databus23/helm-diff
helm diff upgrade grafana grafana/grafana --version x.y.w \
  -n monitoring -f grafana-values.yaml

Çıktı, kümedeki kaynaklarla yeni sürümün üreteceği kaynaklar arasındaki farkı diff biçiminde gösteriyor. Silinecek bir PVC görüyorsan orada dur.

Geri almak

Her install, upgrade ve rollback yeni bir revizyon oluşturuyor:

helm history grafana -n monitoring
REVISION  UPDATED  STATUS      CHART           APP VERSION  DESCRIPTION
1         ...      superseded  grafana-x.y.z   ...          Install complete
2         ...      deployed    grafana-x.y.w   ...          Upgrade complete

Yükseltmeden memnun değilsem birinci revizyona dönüyorum:

helm rollback grafana 1 -n monitoring --wait

Geri alma, eski revizyonu yeni bir numarayla tekrar uyguluyor. Geçmişe bakınca 3 numaralı, açıklaması Rollback to 1 olan bir satır görürsün. Geçmiş hiç silinmiyor, bu da neyin ne zaman olduğunu takip etmeyi kolaylaştırıyor.

Rollback'in yapmadığı şeyleri bilmek de gerekiyor. PVC içindeki veriye dokunmuyor: yeni Grafana sürümü açılırken veritabanı şemasını güncellediyse eski sürüm o veriyle açılmayabilir. CRD'leri de geri almıyor. Yani rollback uygulamanın tanımını geri alıyor, verisini değil. Veri taşıyan bir uygulamayı yükseltmeden önce yedek almak bu yüzden hâlâ gerekli.

Kendi chart'ın

Bir önceki yazıdaki podinfo YAML'larını bir chart'a çeviriyorum. helm create bir iskelet üretiyor:

helm create podinfo
tree podinfo
podinfo
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml

Bu iskelet her durumu düşünen genel bir şablon ve ilk okumada kafa karıştırıyor. Ben templates/ içini boşaltıp kendi dosyalarımı yazıyorum. Ne yazdığımı satır satır bildiğim bir chart, her şeyi yapan ama anlamadığım bir chart'tan daha kolay bakım istiyor.

rm -rf podinfo/templates/* podinfo/charts

Chart.yaml:

apiVersion: v2
name: podinfo
description: podinfo için basit bir chart
type: application
version: 0.1.0
appVersion: "6.7.1"

version chart'ın kendi sürümü, şablonu her değiştirdiğinde artırıyorsun. appVersion içindeki uygulamanın sürümü.

values.yaml:

replicaCount: 2

image:
  repository: ghcr.io/stefanprodan/podinfo
  tag: ""   # boş kalırsa Chart.yaml içindeki appVersion kullanılıyor

ui:
  message: "Merhaba"
  color: "#2f5d7c"

resources:
  requests:
    cpu: 50m
    memory: 64Mi
  limits:
    memory: 128Mi

ingress:
  enabled: true
  host: podinfo.k8s.example.com
  tlsSecret: ""

templates/configmap.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}
data:
  PODINFO_UI_MESSAGE: {{ .Values.ui.message | quote }}
  PODINFO_UI_COLOR: {{ .Values.ui.color | quote }}

templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}
  labels:
    app.kubernetes.io/name: {{ .Chart.Name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Chart.Name }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Chart.Name }}
        app.kubernetes.io/instance: {{ .Release.Name }}
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
    spec:
      containers:
        - name: podinfo
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          ports:
            - name: http
              containerPort: 9898
          envFrom:
            - configMapRef:
                name: {{ .Release.Name }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          readinessProbe:
            httpGet:
              path: /readyz
              port: http
          livenessProbe:
            httpGet:
              path: /healthz
              port: http
            initialDelaySeconds: 10
          securityContext:
            allowPrivilegeEscalation: false
            capabilities:
              drop: ["ALL"]

securityContext'i bir önceki yazıdaki Deployment'tan aynen taşıdım. YAML'ı şablona çevirirken bu tür satırları kaybetmek kolay ve chart ilk halinden daha gevşek çıkıyor.

İki satır özellikle işe yarıyor. checksum/config notu, ConfigMap'in içeriğinin özetini pod şablonuna yazıyor. ConfigMap değişince özet değişiyor, pod şablonu değişmiş sayılıyor ve Kubernetes rolling update başlatıyor. Bir önceki yazıda elle yaptığımız rollout restart artık gerekmiyor. toYaml ... | nindent 12 ise resources bloğunu values'tan olduğu gibi alıp doğru girintiyle yerleştiriyor. Girinti sayısı yanlış olursa YAML bozuluyor, chart yazarken en çok zaman kaybettiren şey bu.

templates/service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}
spec:
  selector:
    app.kubernetes.io/name: {{ .Chart.Name }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  ports:
    - name: http
      port: 80
      targetPort: http

templates/ingress.yaml:

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ .Release.Name }}
spec:
  ingressClassName: traefik
  {{- if .Values.ingress.tlsSecret }}
  tls:
    - hosts:
        - {{ .Values.ingress.host }}
      secretName: {{ .Values.ingress.tlsSecret }}
  {{- end }}
  rules:
    - host: {{ .Values.ingress.host }}
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: {{ .Release.Name }}
                port:
                  name: http
{{- end }}

Kontrol etmek için:

helm lint ./podinfo
helm template deneme ./podinfo | less
==> Linting ./podinfo
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed

icon uyarısı önemsiz. lint sözdizimini yakalıyor ama mantık hatalarını yakalamıyor, template çıktısını gözle okumak hâlâ gerekli.

Ortam başına values

Asıl kazanç burada. Test ortamı için küçük bir dosya:

# values-test.yaml
replicaCount: 1
ui:
  message: "Test ortamı"
ingress:
  host: podinfo-test.k8s.example.com
helm install podinfo-test ./podinfo -n test --create-namespace -f values-test.yaml

Aynı chart'ı bir önceki yazıdaki demo namespace'ine podinfo adıyla kurmaya kalkarsan şuna benzer bir hata alırsın:

Error: INSTALLATION FAILED: rendered manifests contain a resource that already exists. Unable to continue with install: ConfigMap "podinfo" in namespace "demo" exists and cannot be imported into the current release: invalid ownership metadata; ...

Helm, kendisinin oluşturmadığı kaynakları sahiplenmiyor. Doğru davranış bu, yoksa elle kurulmuş bir şeyi yanlışlıkla ezebilirdi. Geçiş için eski kaynakları silip chart'ı kurmak en temiz yol. Dizini toptan silme, 00-namespace.yaml da içinde olduğu için namespace'i ve içindeki Secret'ları da götürür. Dosyaları tek tek ver:

for f in ingress service deployment configmap; do
  kubectl delete -f "k8s/podinfo/$f.yaml"
done
helm install podinfo ./podinfo -n demo

Arada kısa bir kesinti oluyor, bunu bir bakım penceresine koy.

Chart'ı paylaşmak istersen helm package ./podinfo bir podinfo-0.1.0.tgz dosyası üretiyor. Kurumda OCI destekli bir registry varsa (Harbor gibi) chart'ı image'ların yanına itebilirsin:

helm package ./podinfo
helm push podinfo-0.1.0.tgz oci://registry.example.com/charts

Sıradaki yazı: Dokploy ile kendi sunucunda küçük bir PaaS