Helm وتغليف Kubernetes

مشروع: بناء Chart لتطبيق إنتاجي متكامل

18 دقيقة الدرس 10 من 28

مشروع: بناء Chart لتطبيق إنتاجي متكامل

كل ما تعلّمته في هذه الوحدة — القوالب، والمساعدات المُسمّاة، والتبعيات، والهوكات، والإصدارات — يتقاطع هنا في لحظة التطبيق الفعلي. ستبني Helm chart بجودة إنتاجية حقيقية لخدمة ويب من العالم الواقعي: واجهة API خلفية مدعومة بـ Redis، مع ملفات قيم لكل بيئة، وهوك لتهجير قاعدة البيانات، ومسبار جاهزية، وPodDisruptionBudget، وRBAC. بنهاية هذا الدرس ستملك chart قادراً على الانضمام مباشرة إلى أي pipeline في GitHub Actions وشحنه إلى أي كلاستر Kubernetes دون تعديل.

التطبيق هو taskflow-api: خدمة REST عديمة الحالة مبنية بـ Node.js تقرأ من Redis (مُدار خارجياً — AWS ElastiCache في staging/prod، وsubchart في بيئة التطوير). يحتاج إلى: Deployment، وService، وIngress، وConfigMap، وSecret، وServiceAccount، وPodDisruptionBudget، وHPA. كل حقل يختلف بين البيئات مكشوف كقيمة في الـ chart.

الخطوة الأولى — السقالة وChart.yaml

ابدأ من السقالة الرسمية، ثم حرّر Chart.yaml فوراً لتُعلن البيانات الوصفية الحقيقية وقيد إصدار Kubernetes وتبعية Redis كـ subchart:

helm create taskflow-api cd taskflow-api # حذف ملفات النموذج التي سنستبدلها بالكامل rm -rf templates/* values.yaml charts/ mkdir charts

حرّر Chart.yaml:

apiVersion: v2 name: taskflow-api description: TaskFlow REST API — stateless Node.js service type: application version: 0.1.0 # إصدار الـ chart — ارفعه مع كل تغيير في الـ chart appVersion: "1.0.0" # وسم صورة التطبيق — يُحدَّث بواسطة CI kubeVersion: ">=1.28.0" maintainers: - name: platform-team email: platform@example.com dependencies: - name: redis version: "19.x.x" repository: "oci://registry-1.docker.io/bitnamicharts" condition: redis.enabled # معطَّل في staging/prod (نستخدم ElastiCache)
إصدار الـ Chart مقابل appVersion: يتتبّع version الـ chart نفسه — تغييرات القوالب، قيم جديدة، كائنات مضافة. يتتبّع appVersion إصدار صورة Docker للتطبيق. في CI تُحدّث appVersion مع كل push للصورة؛ أما version فقط حين تتغيّر بنية الـ chart. افصل بينهما تماماً. تُطبّق فرق المنصات في Google وSpotify هذا الفصل بجعل pipeline البناء يستبدل appVersion فقط، بينما يُرفع version عبر PR منفصل لمستودع الـ chart.

الخطوة الثانية — ملف values.yaml الرئيسي

كل معامل يتغيّر بين البيئات يُودَع هنا مع افتراضيات آمنة وبسيطة (نسخة واحدة، موارد صغيرة — مناسبة للتطوير، تُلغى في staging/prod). وثّق كل مفتاح بتعليق؛ هذا الملف هو الواجهة العامة لـ chart الخاص بك.

# values.yaml — افتراضيات آمنة لبيئات التطوير المحلي replicaCount: 1 image: repository: ghcr.io/example/taskflow-api tag: "" # يُلغى بواسطة CI عبر --set image.tag=$SHA pullPolicy: IfNotPresent imagePullSecrets: [] nameOverride: "" fullnameOverride: "" serviceAccount: create: true annotations: {} # prod: {"eks.amazonaws.com/role-arn": "arn:aws:iam::..."} name: "" podAnnotations: prometheus.io/scrape: "true" prometheus.io/port: "3000" prometheus.io/path: "/metrics" podSecurityContext: runAsNonRoot: true runAsUser: 1001 fsGroup: 1001 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: [ALL] service: type: ClusterIP port: 80 targetPort: 3000 ingress: enabled: false className: nginx annotations: {} hosts: - host: taskflow.local paths: - path: / pathType: Prefix tls: [] resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi autoscaling: enabled: false minReplicas: 1 maxReplicas: 5 targetCPUUtilizationPercentage: 70 pdb: enabled: false # يجب أن يكون false مع replicaCount: 1 minAvailable: 1 config: logLevel: info nodeEnv: development # DSN لـ Redis الخارجي (يُستخدم في staging/prod) externalRedis: host: "" port: 6379 # Bitnami Redis subchart — مفعَّل في بيئة التطوير فقط redis: enabled: true architecture: standalone auth: enabled: false master: persistence: enabled: false resources: requests: cpu: 50m memory: 64Mi

الخطوة الثالثة — القوالب

أنشئ templates/_helpers.tpl أولاً — مساعدات التسمية التي ستستدعيها كل قالب آخر:

{{/* توسيع اسم الـ chart. */}} {{- define "taskflow-api.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{- define "taskflow-api.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- define "taskflow-api.labels" -}} helm.sh/chart: {{ include "taskflow-api.name" . }}-{{ .Chart.Version }} app.kubernetes.io/name: {{ include "taskflow-api.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{- define "taskflow-api.selectorLabels" -}} app.kubernetes.io/name: {{ include "taskflow-api.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* رابط Redis — subchart داخلي أو مضيف خارجي */}} {{- define "taskflow-api.redisUrl" -}} {{- if .Values.redis.enabled -}} redis://{{ .Release.Name }}-redis-master:6379 {{- else -}} redis://{{ required "externalRedis.host required when redis.enabled=false" .Values.externalRedis.host }}:{{ .Values.externalRedis.port }} {{- end }} {{- end }}

أنشئ الآن Manifests الأساسية. قالب Deployment هو الأكثر تعقيداً — لاحظ سياق الأمان، ومسابر الجاهزية/الحياة، وكيفية حقن رابط Redis عبر متغير بيئة مصدره ConfigMap:

# templates/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "taskflow-api.fullname" . }} labels: {{- include "taskflow-api.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: {{- include "taskflow-api.selectorLabels" . | nindent 6 }} template: metadata: annotations: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "taskflow-api.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "taskflow-api.fullname" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: api securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http containerPort: {{ .Values.service.targetPort }} protocol: TCP envFrom: - configMapRef: name: {{ include "taskflow-api.fullname" . }} readinessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 5 periodSeconds: 10 failureThreshold: 3 livenessProbe: httpGet: path: /healthz port: http initialDelaySeconds: 15 periodSeconds: 20 resources: {{- toYaml .Values.resources | nindent 12 }}
حاسبة الـ ConfigMap: يُجبر تعليق checksum/config على إعادة تشغيل متدرجة كلما تغيّر الـ ConfigMap. بدونه، سيتجاهل helm upgrade الذي يُحدّث قيمة إعداد فقط إعادة تشغيل الـ Pods — وتستمر في العمل بإعداد قديم. هذا السطر الواحد، الذي يستخدمه كل Helm chart رئيسي في المنظومة (cert-manager، Prometheus، ingress-nginx)، يمنع فئة كاملة من حوادث "لماذا لم يسرِ مفعول تغيير الإعداد؟".

أنشئ القوالب المتبقية — ConfigMap وService وIngress وServiceAccount وPDB وHPA:

# templates/configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: {{ include "taskflow-api.fullname" . }} labels: {{- include "taskflow-api.labels" . | nindent 4 }} data: NODE_ENV: {{ .Values.config.nodeEnv | quote }} LOG_LEVEL: {{ .Values.config.logLevel | quote }} REDIS_URL: {{ include "taskflow-api.redisUrl" . | quote }} PORT: {{ .Values.service.targetPort | quote }} --- # templates/pdb.yaml {{- if .Values.pdb.enabled }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: {{ include "taskflow-api.fullname" . }} labels: {{- include "taskflow-api.labels" . | nindent 4 }} spec: minAvailable: {{ .Values.pdb.minAvailable }} selector: matchLabels: {{- include "taskflow-api.selectorLabels" . | nindent 6 }} {{- end }} --- # templates/hpa.yaml {{- if .Values.autoscaling.enabled }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: {{ include "taskflow-api.fullname" . }} labels: {{- include "taskflow-api.labels" . | nindent 4 }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: {{ include "taskflow-api.fullname" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} {{- end }}

الخطوة الرابعة — ملفات قيم كل بيئة

لا تستخدم --set لأكثر من قيمة أو قيمتين عدديتين في الإنتاج. بدلاً من ذلك، احتفظ بملف قيم لكل بيئة في مجلد deploy/ منفصل. يحمل values.yaml الأساسي الافتراضيات الآمنة للتطوير؛ تُعيد ملفات البيئة تعريف ما يختلف فقط:

# deploy/values-staging.yaml replicaCount: 2 image: pullPolicy: Always ingress: enabled: true className: nginx annotations: cert-manager.io/cluster-issuer: letsencrypt-staging hosts: - host: api-staging.example.com paths: - path: / pathType: Prefix tls: - secretName: taskflow-api-staging-tls hosts: [api-staging.example.com] resources: requests: cpu: 200m memory: 256Mi limits: cpu: 1000m memory: 512Mi config: logLevel: debug nodeEnv: staging # استخدام ElastiCache — تعطيل الـ subchart redis: enabled: false externalRedis: host: staging-redis.abc123.0001.use1.cache.amazonaws.com port: 6379
# deploy/values-prod.yaml replicaCount: 3 image: pullPolicy: IfNotPresent ingress: enabled: true className: nginx annotations: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/rate-limit: "100" hosts: - host: api.example.com paths: - path: / pathType: Prefix tls: - secretName: taskflow-api-prod-tls hosts: [api.example.com] resources: requests: cpu: 500m memory: 512Mi limits: cpu: 2000m memory: 1Gi autoscaling: enabled: true minReplicas: 3 maxReplicas: 20 targetCPUUtilizationPercentage: 65 pdb: enabled: true minAvailable: 2 config: logLevel: warn nodeEnv: production serviceAccount: annotations: eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/taskflow-api-prod" redis: enabled: false externalRedis: host: prod-redis.abc123.0001.use1.cache.amazonaws.com port: 6379
Per-environment values override layering values.yaml replicas: 1 redis.enabled: true resources: tiny ingress: off values-staging.yaml replicas: 2 redis.enabled: false externalRedis.host: staging-… values-prod.yaml replicas: 3 HPA: enabled PDB: minAvailable 2 Helm Merge base + override Rendered Manifests Deployment (3 replicas) HPA (3 → 20) PDB (minAvailable 2) Ingress (api.example.com) → إصدار prod
يوفّر values.yaml الأساسي افتراضيات آمنة لبيئة التطوير؛ تُعيد ملفات البيئة تعريف ما يختلف فقط — يدمج محرّك Helm كليهما بعمق لإنتاج الـ Manifests المُصيَّرة النهائية.

الخطوة الخامسة — هوك تهجير ما قبل الترقية

يجب أن تُنفَّذ تهجيرات قاعدة البيانات قبل أن تُشغَّل الـ Pods الجديدة. يُعدّ هوك Job من نوع pre-upgrade في Helm النمط المعياري لذلك:

# templates/hooks/migrate.yaml apiVersion: batch/v1 kind: Job metadata: name: {{ include "taskflow-api.fullname" . }}-migrate-{{ .Release.Revision }} labels: {{- include "taskflow-api.labels" . | nindent 4 }} annotations: "helm.sh/hook": pre-upgrade,pre-install "helm.sh/hook-weight": "-5" "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded spec: backoffLimit: 2 activeDeadlineSeconds: 300 template: spec: restartPolicy: Never serviceAccountName: {{ include "taskflow-api.fullname" . }} securityContext: runAsNonRoot: true runAsUser: 1001 containers: - name: migrate image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" command: ["node", "dist/migrate.js"] envFrom: - configMapRef: name: {{ include "taskflow-api.fullname" . }}
مزلق إنتاجي — hook-delete-policy: أضف دائماً hook-delete-policy: before-hook-creation,hook-succeeded. بدون hook-succeeded، تتراكم Jobs التهجير القديمة في الـ namespace بعد كل نشر. بدون before-hook-creation، يُعيق Job فاشل الترقية التالية لأن Kubernetes يرفض إنشاء Job بنفس الاسم. يُضيف لاحقة Release.Revision اسماً فريداً لكل Job مع كل مراجعة، مما يمنحك كائناً جديداً في كل مرة بينما تُنظّف سياسة الحذف تلقائياً ما نجح.

الخطوة السادسة — التثبيت والتحقق في جميع البيئات

استخدم helm template محلياً أولاً — دون الحاجة لأي وصول إلى الكلاستر — للتحقق من أن كل ملف بيئة يُصيّر بالضبط ما تتوقعه. هذه خطوة إلزامية قبل أن يشحن أي pipeline CI أي chart:

# حلّ تبعية Redis كـ subchart helm dependency update . # تصيير جاف لكل بيئة (بلا حاجة للكلاستر) helm template taskflow-dev . \ --debug 2>&1 | head -100 helm template taskflow-staging . \ --values deploy/values-staging.yaml \ --debug 2>&1 | grep "replicas:\|redis.enabled\|externalRedis" helm template taskflow-prod . \ --values deploy/values-prod.yaml \ --debug 2>&1 | grep -E "minAvailable|maxReplicas|cpu:|memory:" # فحص الـ chart (إلزامي في CI) helm lint . --values deploy/values-staging.yaml helm lint . --values deploy/values-prod.yaml # تثبيت بيئة التطوير (تستخدم Redis subchart المدمجة) helm upgrade --install taskflow-dev . \ --namespace taskflow-dev \ --create-namespace \ --wait --atomic --timeout 5m0s # نشر staging (Redis خارجي، TLS، تسجيل debug) helm upgrade --install taskflow-staging . \ --namespace taskflow-staging \ --create-namespace \ --values deploy/values-staging.yaml \ --set image.tag=${IMAGE_TAG} \ --wait --atomic --timeout 5m0s # نشر prod (HPA، PDB، Redis إنتاجي، IRSA annotation) helm upgrade --install taskflow-prod . \ --namespace taskflow-prod \ --create-namespace \ --values deploy/values-prod.yaml \ --set image.tag=${IMAGE_TAG} \ --wait --atomic --timeout 10m0s
قارن قبل أن تنشر: ثبّت إضافة helm-diff (helm plugin install https://github.com/databus23/helm-diff) ونفّذ helm diff upgrade taskflow-prod . --values deploy/values-prod.yaml قبل كل ترقية إنتاجية. تطبع فروقاً مُلوّنة لما سيتغيّر في الكلاستر — فحص أمان لا غنى عنه، وهو ممارسة معيارية في كل نشر إنتاجي في شركات كـ Datadog وStripe وGitHub.

ما الذي بنيته

الهيكل النهائي للـ chart:

taskflow-api/ ├── Chart.yaml # بيانات وصفية + تبعية Redis ├── values.yaml # افتراضيات رئيسية (آمنة للتطوير) ├── deploy/ │ ├── values-staging.yaml # تعديلات staging │ └── values-prod.yaml # تعديلات prod (HPA، PDB، IRSA) ├── templates/ │ ├── _helpers.tpl # مساعدات التسمية + redisUrl │ ├── deployment.yaml # مسابر الجاهزية، تعليق checksum │ ├── service.yaml # ClusterIP │ ├── ingress.yaml # مُمكَّن شرطياً │ ├── configmap.yaml # NODE_ENV، LOG_LEVEL، REDIS_URL │ ├── serviceaccount.yaml # قابل للتوسيم بـ IRSA │ ├── pdb.yaml # شرطي، minAvailable │ ├── hpa.yaml # توسيع تلقائي شرطي autoscaling/v2 │ └── hooks/ │ └── migrate.yaml # Job من نوع pre-install/pre-upgrade └── charts/ └── redis-19.x.x.tgz # subchart محلي (للتطوير فقط)

يُرسّخ هذا الـ chart ستة أشهر من الخبرة الإنتاجية المكتسبة بشقّ الأنفس في وحدة قابلة للنشر: يمنع انجراف الإعدادات بين البيئات، ويُطبّق سياقات الأمان افتراضياً، ويتعامل مع عمليات النشر دون توقّف بفضل PDB واستراتيجية التحديث المتدرج، ويُنفّذ التهجيرات بصورة ذرية قبل انتقال حركة المرور، ويمنحك تتبّعاً كاملاً للتغييرات عبر سجلّ إصدارات Helm. كل نمط هنا — تعليق الـ checksum، والـ PDB الشرطي، ومسار تعليق IRSA، وملفات قيم كل بيئة — يعكس بالضبط كيف تُعبّئ فرق المنصات الناضجة التطبيقات على نطاق كبرى التقنية.