اولین Helm Chart هر تیمی معمولاً همان خروجی helm create است که چند خطش عوض شده. این Chart در Develop کار میکند، اما وقتی به Staging و Production میرسد کمکم کپیهای مختلف از آن ساخته میشود و بعد از چند ماه هیچکس نمیداند تفاوت محیطها دقیقاً چیست.
در این مقاله الگویی را توضیح میدهم که در پروژههای بانکی و فینتک برای مدیریت دهها میکروسرویس در سه محیط استفاده کردهام.
ساختار پیشنهادی
charts/api/
├── Chart.yaml
├── values.yaml # safe defaults shared by all environments
├── values-dev.yaml
├── values-staging.yaml
├── values-production.yaml
└── templates/
├── _helpers.tpl
├── deployment.yaml
├── service.yaml
├── configmap.yaml
├── hpa.yaml
├── pdb.yaml
└── servicemonitor.yaml
قانون اصلی: تفاوت محیطها فقط در فایلهای Values است. اگر جایی در Template نوشتهاید if eq .Values.env "production"، احتمالاً باید آن را به یک Value تبدیل کنید.
values.yaml: پیشفرضهای امن
replicaCount: 2
image:
repository: harbor.example.ir/bank/api
tag: "" # set by CI, never "latest"
pullPolicy: IfNotPresent
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { memory: 512Mi } # no CPU limit to avoid throttling
probes:
readiness: { path: /health/ready, initialDelaySeconds: 5 }
liveness: { path: /health/live, initialDelaySeconds: 20 }
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 6
targetCPU: 70
pdb:
enabled: true
minAvailable: 1
config:
LOG_LEVEL: Information
serviceMonitor:
enabled: true
و تفاوتها برای Production:
# values-production.yaml
replicaCount: 3
resources:
requests: { cpu: 500m, memory: 512Mi }
limits: { memory: 1Gi }
autoscaling:
enabled: true
maxReplicas: 12
config:
LOG_LEVEL: Warning
Deployment: جزئیاتی که فرق ایجاد میکنند
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "api.fullname" . }}
labels: {{- include "api.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels: {{- include "api.selectorLabels" . | nindent 6 }}
strategy:
rollingUpdate: { maxSurge: 25%, maxUnavailable: 0 }
template:
metadata:
labels: {{- include "api.selectorLabels" . | nindent 8 }}
annotations:
# restart pods automatically when the ConfigMap changes
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
spec:
containers:
- name: api
image: "{{ .Values.image.repository }}:{{ required "image.tag is required" .Values.image.tag }}"
ports: [{ name: http, containerPort: 8080 }]
envFrom: [{ configMapRef: { name: {{ include "api.fullname" . }} } }]
readinessProbe:
httpGet: { path: {{ .Values.probes.readiness.path }}, port: http }
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
livenessProbe:
httpGet: { path: {{ .Values.probes.liveness.path }}, port: http }
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
resources: {{- toYaml .Values.resources | nindent 12 }}
سه نکتهی مهم در این Template:
checksum/config: وقتی ConfigMap تغییر کند، Hash عوض میشود و Podها خودکار Restart میشوند. بدون آن، تغییر Config تا Restart بعدی اعمال نمیشود.required: اگر CI فراموش کند Tag را بدهد، Helm با خطای واضح متوقف میشود، نه اینکه بیصدا Image اشتباه Deploy شود.maxUnavailable: 0: در Rolling Update هیچوقت ظرفیت سرویس کمتر از حالت عادی نمیشود.
تست قبل از Deploy
helm lint charts/api -f charts/api/values-production.yaml
helm template api charts/api -f charts/api/values-production.yaml --set image.tag=test > /tmp/out.yaml
# with the helm-diff plugin: see exactly what will change in the cluster
helm diff upgrade api charts/api -n production -f charts/api/values-production.yaml --set image.tag=1.9.0
helm diff را در Merge Requestهای Production اجرا کنید و خروجیاش را در کامنت MR بگذارید. بازبین دقیقاً میبیند چه چیزی در کلاستر عوض میشود.
انتشار Chart در Harbor
Harbor از Helm Chart بهصورت OCI Artifact پشتیبانی میکند، پس Chart و Image هر دو در یک جا نسخهگذاری میشوند:
helm package charts/api --version 1.9.0 --app-version 1.9.0
helm registry login harbor.example.ir -u "$HARBOR_USER" -p "$HARBOR_PASSWORD"
helm push api-1.9.0.tgz oci://harbor.example.ir/charts
# install from the registry
helm upgrade --install api oci://harbor.example.ir/charts/api --version 1.9.0 \
-n production -f values-production.yaml --atomic --wait
Library Chart: یک Template برای همهی میکروسرویسها
وقتی ۱۵ میکروسرویس دارید که همه Deployment و Service و HPA تقریباً یکسانی دارند، یک Library Chart بسازید:
# charts/common/Chart.yaml
apiVersion: v2
name: common
type: library
version: 2.3.0
و در هر سرویس:
# charts/api/Chart.yaml
dependencies:
- name: common
version: 2.3.x
repository: oci://harbor.example.ir/charts
# charts/api/templates/deployment.yaml
{{ include "common.deployment" . }}
حالا اضافه کردن یک قابلیت جدید (مثلاً topologySpreadConstraints برای پخش Podها روی Nodeهای مختلف) یعنی یک تغییر در common و بالا بردن نسخه، نه ۱۵ Merge Request جداگانه.
اشتباهات رایج
- Secret در
values.yaml: Secretها را با External Secrets یا Sealed Secrets مدیریت کنید، نه در Git. - نبودن Readiness Probe: بدون آن، Rolling Update ترافیک را به Podی میفرستد که هنوز آماده نیست.
- نسخهگذاری نکردن Chart: Chart هم مثل کد باید Semantic Version داشته باشد.
- Helm بدون
--atomic: استقرار نیمهکاره، کلاستر را در وضعیت نامشخص رها میکند.
جمعبندی
Helm Chart خوب مثل یک قرارداد است: تیم توسعه فقط Values را عوض میکند و استانداردهای Production (Probe، Resource، PDB و مانیتورینگ) پیشفرضاند. با یک Chart، چند فایل Values، انتشار در Harbor و یک Library Chart، مدیریت دهها سرویس در چند محیط ساده و قابل پیشبینی میشود.