Kubernetes سطح: متوسط

Helm Chart حرفه‌ای برای Develop، Staging و Production؛ یک Chart برای همه‌ی محیط‌ها

ساختار درست Helm Chart، Valueهای جدا برای هر محیط، Probe و Resource، ری‌استارت خودکار با تغییر ConfigMap، انتشار Chart در Harbor و Library Chart برای ده‌ها میکروسرویس.

۵ دقیقه مطالعه

اولین 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 جداگانه.

اشتباهات رایج

  1. Secret در values.yaml: Secretها را با External Secrets یا Sealed Secrets مدیریت کنید، نه در Git.
  2. نبودن Readiness Probe: بدون آن، Rolling Update ترافیک را به Podی می‌فرستد که هنوز آماده نیست.
  3. نسخه‌گذاری نکردن Chart: Chart هم مثل کد باید Semantic Version داشته باشد.
  4. Helm بدون --atomic: استقرار نیمه‌کاره، کلاستر را در وضعیت نامشخص رها می‌کند.

جمع‌بندی

Helm Chart خوب مثل یک قرارداد است: تیم توسعه فقط Values را عوض می‌کند و استانداردهای Production (Probe، Resource، PDB و مانیتورینگ) پیش‌فرض‌اند. با یک Chart، چند فایل Values، انتشار در Harbor و یک Library Chart، مدیریت ده‌ها سرویس در چند محیط ساده و قابل پیش‌بینی می‌شود.