۳۱ خرداد ۲۵۸۵ · 6 دقیقه مطالعه
Helm Deploy موفق بود. Config هیچوقت اعمال نشد.
یه route جدید به gateway service اضافه کردیم. pipeline توی helmfile اجرا شد، همه مرحلهها سبز شدن، و deploy بدون هیچ خطایی تموم شد.
route برگشت 404.
یه port-forward و یه curl تأیید کرد: تعریف route جدید روی gateway زنده نبود. pod داشت با config آخرین restart کار میکرد، نه اونی که تازه deploy کرده بودیم. ConfigMap رو آپدیت کرده بودیم. pod اصلاً خبر نداشت.
چیزی که تشخیص رو سختتر میکرد این بود که قبلاً کار کرده بود. دو ماه پیش، یه تغییر config مشابه بدون مشکل live شده بود. چیزی که اون موقع نمیدونستیم اینه که اون تغییر اتفاقاً یه volume mountPath رو هم دست زده بود، که به عنوان یه side effect باعث شده بود Kubernetes pod رو roll کنه. آپدیت config به نظر علّی میرسید. تصادف بود.
چرا ConfigMapها restart رو trigger نمیکنن
Kubernetes یه pod رو وقتی roll میکنه که pod template spec اش عوض بشه. ConfigMap یه object جداگانه توی Kubernetes API هست. آپدیت کردنش pod template رو mutate نمیکنه. pod به اجرا ادامه میده با هر چیزی که موقع آخرین start روی اون mount شده بود.
این by design هست. Kubernetes عمداً “داده عوض شد” رو از “consumer باید restart کنه” جدا میکنه. برای appهایی که file watcher داخلی دارن، این رفتار درسته: میتونن config رو بدون restart pod reload کنن. برای appهایی که موقع startup فایلهای config رو میخونن و در حافظه نگه میدارن، یه تلهست.
gateway service توی دسته دوم قرار میگیره. API definition ها رو از فایلهای mounted موقع start میخونه. هیچ file watcher ای نیست. یه ConfigMap update روی disk میشینه و میمونه، نامرئی برای process در حال اجرا، تا وقتی که چیزی pod رو restart کنه.
راهحل استاندارد: checksum annotation
راهحل canonical اینه که یه annotation checksum/config به pod template توی Deployment اضافه کنیم، که از محتوای ConfigMap گرفته میشه:
# templates/deployment.yaml — standard single-chart pattern
spec:
template:
metadata:
annotations:
checksum/config: {{ include "gateway.configmap" . | sha256sum | quote }}
وقتی محتوای ConfigMap عوض میشه، مقدار annotation هم عوض میشه. annotation توی spec.template.metadata زندگی میکنه، که بخشی از pod spec هست. Kubernetes میبینه pod template عوض شده و یه rolling restart trigger میکنه. مقدار annotation به عنوان metadata بیمعنیه؛ تنها کارش اینه که signal تغییر رو به pod spec منتقل کنه.
این pattern توی Helm docs و بیشتر writeupهای مربوط به force کردن pod restart روی ConfigMap change ظاهر میشه. توی یه Helm chart تکی که include "gateway.configmap" میتونه به named template برسه خوب کار میکنه.
چرا این روش با چند release کار نمیکنه
setup ما دو Helm release جدا داره: یکی که مالک gateway Deployment هست و یکی که API-definition ConfigMap ها رو از فایلهای تعریف جداگانه generate میکنه.
include به chart جاری scope داره. نمیتونی از یه Deployment template بنویسی include "other-release.configmap"؛ اون named template توی rendering context اون chart وجود نداره. محتوای ConfigMap موقع render زمان برای Deployment chart در دسترس نیست.
این بخشیه که بیشتر writeupهای checksum-annotation رد میکنن. یه chart monolithic رو فرض میگیرن که Deployment و ConfigMap یه template namespace مشترک دارن. لحظهای که اونها رو به release های جدا تقسیم میکنی، که برای config به صورت dynamic generate شده رایجه، pattern استاندارد هیچ راهی برای reach کردن به اون طرف نداره.
راهحل helmfile
فایلهای values توی helmfile از Go templating با مجموعهای از functionها فراتر از Helm استاندارد پشتیبانی میکنن. دو تا از اونها این مشکل رو مستقیم حل میکنن.
readFile محتوای خام یه فایل رو میخونه و به عنوان string برمیگردونه. readDirEntries entry های یه directory رو برمیگردونه. هر دو pathها رو نسبت به directory خود values file resolve میکنن، نه نسبت به ریشه repo. اون جزئیت آخر مهمه.
توی values file مربوط به gateway release، هر فایل source ای که به ConfigMap ها میره رو میخونیم، محتوا رو concat میکنیم، هش میزنیم، و نتیجه رو به عنوان یه values key expose میکنیم:
# helmfile-values/gateway/values.yaml.gotmpl
{{- $apiDir := "./apis" -}}
{{- $content := "" -}}
{{- range readDirEntries $apiDir -}}
{{- $content = cat $content (readFile (printf "%s/%s" $apiDir .Name)) -}}
{{- end -}}
configChecksum: {{ $content | sha256sum | quote }}
path ./apis نسبت به location فایل values resolve میشه. اگه values.yaml.gotmpl توی helmfile-values/gateway/values.yaml.gotmpl باشه، پس ./apis باید روی disk helmfile-values/gateway/apis/ باشه. اگه این رو اشتباه بزنی readDirEntries یه لیست خالی برمیگردونه، $content خالی میمونه، و هش ثابت میمونه. annotation render میشه ولی هیچوقت عوض نمیشه، صرف نظر از اینکه توی فایلهای source چی بذاری.
Deployment template بعدش هش از پیش محاسبه شده رو از values میخونه:
# gateway chart — templates/deployment.yaml
spec:
template:
metadata:
annotations:
checksum/config: {{ .Values.configChecksum }}
نکته کلیدی: محاسبه هش توی لایه values توی helmfile اتفاق میافته، قبل از اینکه هر chart-scoped templating ای اجرا بشه. Deployment chart نیازی نداره از ConfigMap chart بدونه. دو release از هم decoupled هستن؛ checksum از طریق interface مربوط به values اونها رو به هم وصل میکنه.
تأیید اینکه کار میکنه
helmfile template Deployment YAML رو با annotation پر شده render میکنه. هر فایلی رو توی directory APIs ویرایش کن، دوباره helmfile template رو اجرا کن، و تأیید کن که هش توی annotation عوض میشه. اگه عوض نشد، path اشتباهه. برای تأیید اینکه اصلاً محتوا داره خونده میشه {{ $content | len }} رو به values template اضافه کن.
helmfile diff آپدیت annotation رو به عنوان یه تغییر تمیز pod-template نشون میده: فقط مقدار annotation، هیچ چیز دیگهای. بعد از apply، یه port-forward و یه curl روی route جدید تأیید میکنه که config live شده.
چی بگیریم ازش
معماریهای چند release، assumption های single-release رو میشکنن. checksum annotation برای chart های تکی مستند شده؛ variant چند-release تقریباً هیچوقت توی docهای رسمی یا blog postها ظاهر نمیشه.
readFile و readDirEntries توی helmfile escape hatch درست هستن چون توی لایه values عمل میکنن، قبل از اینکه هر chart-scoped template context ای اجرا بشه. این باعث میشه برای هر release ای که نیاز داره محتوا رو از فایلهای source یه release دیگه هش کنه در دسترس باشن.
رفتار relative-path تنها جاییه که abstraction نشت میکنه. یه بار mapping path رو توی values file repo با یه comment مستند کن، و حل میشه.
همین pattern برای هر سرویسی که موقع startup از فایلهای mounted config میخونه اعمال میشه: message broker ها، proxy ها، certificate bundle ها، policy engine ها. اگه سرویس موقع runtime فایلها رو watch نمیکنه، یه ConfigMap update خاموشه تا وقتی که چیزی pod رو restart کنه. checksum annotation اون restart رو automatic و deterministic میکنه.