اولین بار که دیاگرام معماری Kubernetes رو دیدم، حس کردم یکی نقشه مترو رو با مدار مادربرد قاطی کرده. یه طرف API Server بود، یه طرف etcd و پایین تصویر هم چند تا Pod طوری نشسته بودن که انگار از اول همه چیز رو فهمیده بودن.
مشکل اینه که معمولا Kubernetes رو با حفظ کردن اسم کامپوننت ها یاد می گیریم. ولی هنوز نمی دونیم وقتی یه YAML می فرستیم چه اتفاقی می افته یا درخواست کاربر از کدوم کوچه ها رد می شه تا به برنامه برسه.
اینجا می خوام اول نقشه کلاستر رو بچینم، بعد برم سراغ Control Plane و Workerها، داستان Raft رو باز کنم و آخرش هم دنبال یه درخواست HTTP راه بیفتم. قرار نیست بعدش Kubernetes رو از صفر بنویسیم. همین که موقع دیدن Pending یا 502 بدونیم احتمالا باید یقه کدوم بخش رو بگیریم، کافیه.
اول چهار تا کلمه رو از هم جدا کنیم
Container محیط بسته بندی شده ایه که برنامه و وابستگی هاش داخلش اجرا می شن.
Pod کوچیک ترین واحدیه که Kubernetes دیپلوی می کنه. یه Pod می تونه یک یا چند کانتینر داشته باشه. کانتینرهای داخلش شبکه و localhost مشترک دارن و معمولا کنار هم یه کار مشخص انجام می دن.
Node یه ماشین فیزیکی یا مجازیه که Podها روش اجرا می شن. CPU و RAM واقعی از اینجا میاد. خود YAML هنوز به مرحله تولید پردازنده از هیچ نرسیده.
Cluster مجموعه Control Plane و Nodeهاست. Control Plane تصمیم می گیره چه چیزی کجا اجرا بشه و Worker Nodeها کار واقعی رو انجام می دن.
برای خودم این شکلی تصورش می کنم: Control Plane دفتر برنامه ریزیه، Workerها کارگاهن و Podها سفارش هایی هستن که باید توی کارگاه اجرا بشن.
نقشه بزرگ کلاستر
طبق مستندات رسمی کامپوننت های Kubernetes، هر کلاستر از یه Control Plane و حداقل یه Worker Node ساخته می شه. توی لپ تاپ ممکنه همه چیز روی یه ماشین باشه. در Production معمولا چند Worker داریم و برای High Availability هم Control Plane رو چند نسخه ای می کنیم.
سه بخش اصلی داریم:
- Control Plane وضعیت دلخواه کلاستر رو می گیره و تغییرها رو هماهنگ می کنه.
- Worker Node کانتینرها رو اجرا می کنه و شبکه لازم رو می ده.
- Add-onهایی مثل CoreDNS، مانیتورینگ و Ingress Controller قابلیت های بیشتر رو اضافه می کنن.
این یه معماری منطقیه. لازم نیست هر جعبه توی تصویر حتما یه سرور جدا داشته باشه.
Kubernetes دستور اجرا نمی کنه، وضعیت دلخواه می گیره
به نظرم مهم ترین چیزی که قبل از اسم کامپوننت ها باید بفهمم، مدل Declarative یا اعلانی Kubernetes هست. توی یه اسکریپت معمولی می گم اول این دستور رو اجرا کن، بعد اون فایل رو بساز و آخرش سرویس رو روشن کن. در Kubernetes بیشتر وقت ها نمی گم قدم بعدی چیه. می گم آخر کار چه وضعیتی می خوام.
مثلا می نویسم سه Replica از برنامه با این Image، این مقدار RAM و این Health Check می خوام. API این خواسته رو به عنوان Desired State نگه می داره. از اون به بعد چند Controller مدام وضعیت واقعی رو نگاه می کنن. اگه دو Pod وجود داشته باشه، سومی رو می سازن. اگه چهار تا شده باشه، یکی رو کم می کنن. اگه Image عوض شده باشه، Rollout رو جلو می برن.
این تفاوت کوچیک نیست. من یه دستور یک بار مصرف نمی فرستم که اگه وسطش برق رفت، نصف کار برای همیشه همون جا بمونه. من وضعیت مقصد رو ثبت می کنم و سیستم بارها برای رسیدن بهش تلاش می کنه.
البته اینجا هم جادو نداریم. Kubernetes فقط چیزهایی رو درست می کنه که براش Controller وجود داشته باشه و اطلاعات درست داشته باشه. اگه برنامه داخل Pod به خاطر یه Bug جواب اشتباه بده ولی Process همچنان زنده باشه، Kubernetes ذهن خوانی نمی کنه. باید Probe، Metric و Log مناسبی داشته باشم تا خرابی قابل دیدن بشه.
آبجکت ها چطور روی هم سوار می شن؟
وقتی برای یه برنامه Stateless از Deployment استفاده می کنم، چند لایه پشتش ساخته می شه:
- Deployment روش به روزرسانی و تعداد Replicaها رو تعریف می کنه.
- Deployment یه ReplicaSet می سازه.
- ReplicaSet تعداد Podهای لازم رو نگه می داره.
- هر Pod یه PodSpec داره که Container، Volume، Probe و تنظیمات دیگه داخلشه.
برای برنامه Stateful معمولا StatefulSet وارد می شه چون اسم پایدار، ترتیب و Storage اهمیت بیشتری دارن. برای کاری که باید روی هر Node اجرا بشه DaemonSet داریم. Job هم برای کاریه که باید اجرا بشه و تموم بشه، نه اینکه تا ابد مثل یه سرویس وب زنده بمونه.
پس Kubernetes فقط یه ابزار برای زیاد کردن Pod نیست. چند Controller مختلف داره که هر کدوم یه مدل از Workload رو می فهمن. انتخاب آبجکت اشتباه ممکنه ظاهرا کار کنه، ولی بعدا موقع Update یا خرابی Node تازه معلوم می شه چه آشی پختیم.
داخل Control Plane چه خبره؟
Control Plane مغز کلاستره، ولی یه مغز چند تکه که هر بخش فقط دنبال کار خودشه.
kube-apiserver، در اصلی
تقریبا همه ارتباط های مدیریتی از API Server رد می شن. وقتی kubectl apply می زنم، درخواست مستقیم نمی ره سراغ Scheduler یا kubelet. اول به kube-apiserver می رسه.
API Server هویت و مجوز درخواست رو بررسی می کنه، Admission Controllerها رو اجرا می کنه، ساختار داده رو می سنجه و وضعیت جدید رو ثبت می کنه. بقیه کامپوننت ها هم از راه همین API وضعیت کلاستر رو می خونن یا تغییر می دن.
etcd، حافظه کلاستر
etcd یه Key-Value Store توزیع شده و سازگاره. Deploymentها، Serviceها، Secretها، Nodeها و بقیه وضعیت Kubernetes آخر کار از راه API Server داخلش ثبت می شن.
etcd دیتابیس برنامه من نیست. نباید سفارش مشتری رو توش ذخیره کنم و خوشحال باشم که PostgreSQL رو دور زدم. این حافظه خود کلاستره و باید ازش بکاپ داشته باشم.
kube-scheduler، مسئول پیدا کردن جا
وقتی یه Pod هنوز Node نداره، Scheduler براش جای مناسب پیدا می کنه. CPU و RAM درخواستی، محدودیت ها، Affinity، Taint و Toleration و پراکندگی Podها روی انتخاب اثر دارن.
Scheduler کانتینر رو اجرا نمی کنه. فقط ثبت می کنه که مثلا این Pod باید روی worker-03 باشه. ادامه کار با kubelet همون Node هست.
kube-controller-manager، گروه آدم های پیگیر
Controller Manager چند Controller رو اجرا می کنه. هر Controller وضعیت فعلی رو با وضعیت دلخواه مقایسه می کنه و سعی می کنه فاصله رو کم کنه.
اگه من سه Replica بخوام و یکی از Podها حذف بشه، Controller مربوطه می بینه فقط دو تا مونده و یه Pod تازه می سازه. این همون Reconciliation Loop هست: الان چی دارم، چی می خواستم و چطور این دوتا رو شبیه هم کنم؟
cloud-controller-manager هم یه کامپوننت اختیاریه که Kubernetes رو به Cloud Provider وصل می کنه. ساخت بعضی Load Balancerها، شناخت Nodeهای Cloud و چند کار زیرساختی از این راه انجام می شه.
این کامپوننت ها چطور با هم حرف می زنن؟
بیشتر ارتباط مدیریتی از راه API Server انجام می شه. Scheduler برای تغییر مستقیم etcd رو باز نمی کنه. Controller Manager هم یواشکی فایل دیتابیس رو دستکاری نمی کنه. هر کدوم آبجکت ها رو از API می خونن، تغییرها رو Watch می کنن و نتیجه تصمیم خودشون رو دوباره به API می فرستن.
این مرز خیلی به درد امنیت و عیب یابی می خوره. Authentication، Authorization، Admission و Audit یه ورودی مرکزی دارن. اگه هر کامپوننت مستقیم سراغ etcd می رفت، کنترل دسترسی بیشتر شبیه در خونه ای می شد که برای هر مهمون یه سوراخ جدا توی دیوار زده باشیم.
در Control Plane چند نسخه ای، معمولا همه API Serverها می تونن همزمان درخواست بگیرن و پشت Load Balancer قرار می گیرن. اما Scheduler و Controller Manager برای بعضی کارها Leader Election دارن تا دو نسخه همزمان یه تصمیم تکراری نگیرن. این Leader Election رو نباید با Leader الگوریتم Raft یکی بگیرم. Raft اعضای etcd رو روی Log داده به توافق می رسونه. Leader Election کامپوننت های Kubernetes بیشتر برای اینه که از بین چند Replica فقط یکی حلقه کنترلی فعال رو جلو ببره.
Desired State چه زمانی ثبت شده حساب می شه؟
وقتی API Server درخواست معتبر رو در etcd ثبت کرد، معمولا پاسخ موفق به کاربر می ده. این موفقیت به این معنی نیست که Pod همون لحظه Running شده. فقط یعنی Kubernetes خواسته من رو پذیرفته و نگه داشته.
بعد Controller، Scheduler و kubelet هر کدوم در زمان خودشون کار رو جلو می برن. برای همینه که kubectl apply ممکنه موفق باشه ولی Pod چند ثانیه بعد ImagePullBackOff بگیره. ثبت سفارش موفق بوده، آشپزخونه هنوز می گه ماده اولیه پیدا نکردم.
روی Worker Node چه چیزهایی داریم؟
Worker جاییه که برنامه من واقعا اجرا می شه.
kubelet روی هر Node هست. Podهای سپرده شده به اون Node رو می بینه و تلاش می کنه کانتینرهاشون اجرا بشن. وضعیت و Probeها رو هم گزارش می ده.
Container Runtime موتور اجرای کانتینره. Kubernetes از راه CRI با Runtimeهایی مثل containerd و CRI-O حرف می زنه. Docker Engine دیگه Runtime مستقیم Kubernetes نیست، ولی Imageهای استانداردی که با Docker می سازیم همچنان قابل اجرا هستن.
CNI Plugin شبکه Podها رو می سازه. Kubernetes مدل شبکه رو تعریف می کنه، ولی پیاده سازیش با ابزارهایی مثل Cilium، Calico یا Flannel هست. هر Pod معمولا IP خودش رو می گیره و باید بتونه به Podهای Nodeهای دیگه برسه.
kube-proxy معمولا Serviceها و EndpointSliceها رو می بینه و روی Node قانون می سازه تا ترافیک به یه Pod آماده برسه. این کار می تونه با iptables یا nftables انجام بشه. بعضی CNIها هم Service Proxy رو با eBPF پیاده می کنن و اون وقت kube-proxy اختیاریه.
اسم kube-proxy یه کم گول زننده به نظر میاد. همیشه یه برنامه وسط هر درخواست نمی شینه که بسته رو با دست تحویل Pod بده. خیلی وقت ها فقط قانون های Data Plane رو می سازه و Kernel ادامه مسیر رو انجام می ده.
داخل خود Pod چه خبره؟
همه کانتینرهای یه Pod روی یه Node اجرا می شن و Network Namespace مشترک دارن. یعنی می تونن با localhost به هم وصل بشن و Portهاشون نباید با هم تداخل داشته باشه. این نزدیکی باعث می شه الگوهایی مثل Sidecar ممکن بشن. مثلا یه کانتینر برنامه رو اجرا کنه و یه کانتینر کمکی Log یا Proxy رو مدیریت کنه.
Pod عمر تضمین شده نداره. اگه حذف بشه، Pod جایگزین معمولا اسم و IP تازه می گیره. برای همین برنامه نباید IP یه Pod رو توی تنظیمات حک کنه. Service و DNS اومدن که همین بی ثباتی رو پشت یه اسم و آدرس پایدار پنهان کنن.
Probeها هم سه سوال متفاوت می پرسن:
- Startup Probe می پرسه برنامه بالا اومدن اولیه رو تموم کرده یا هنوز وقت لازم داره.
- Liveness Probe می پرسه Process گیر کرده و باید Restart بشه یا نه.
- Readiness Probe می پرسه همین الان اجازه داریم ترافیک کاربر رو به این Pod بفرستیم یا نه.
اشتباه گرفتن این سه تا می تونه یه سرویس سالم رو وارد چرخه Restart کنه. مثلا برنامه ای که دو دقیقه برای Warm Up وقت لازم داره با Liveness تهاجمی هر سی ثانیه کشته می شه و هیچ وقت فرصت نمی کنه سالم بالا بیاد. Kubernetes هم با پشتکار کامل خرابکاری تعریف شده در YAML رو تکرار می کنه.
Storage و Add-onها کجای تصویرن؟
برای Storage پایدار، Pod معمولا از PersistentVolumeClaim استفاده می کنه. CSI Driver اتصال Kubernetes به سیستم Storage رو انجام می ده. اگه Pod روی Node دیگه ساخته بشه، Controllerها و CSI تلاش می کنن Volume مناسب رو وصل کنن. نوع Storage تعیین می کنه این جابه جایی چقدر ممکن یا سریع باشه.
CoreDNS هم معمولا به عنوان Add-on داخل کلاستر اجرا می شه و اسم Serviceها رو قابل پیدا کردن می کنه. Metrics Server، سیستم جمع آوری Log، Ingress Controller و ابزارهای مانیتورینگ هم Add-on هستن. نبودن اینها Core اصلی Kubernetes رو خراب نمی کنه، ولی یه کلاستر Production بدون DNS و مشاهده پذیری خیلی زود تبدیل به اتاق تاریکی می شه که فقط از صداهاش می فهمیم یه چیزی افتاده.
کلاستر چطور چند ماشینه و مقاوم می شه؟
برای یادگیری می تونم با kind، minikube یا k3d یه کلاستر محلی بسازم. برای Production دو مسیر کلی دارم:
- سرویس مدیریت شده مثل EKS، GKE یا AKS بگیرم و نگهداری Control Plane رو تا حد زیادی به Cloud Provider بسپرم.
- کلاستر رو با ابزارهایی مثل kubeadm، Kubespray یا kOps بسازم و خودم مسئول آپدیت، گواهی ها، بکاپ و High Availability باشم.
با kubeadm معمولا روی اولین Control Plane دستور kubeadm init اجرا می شه. Control Planeها و Workerهای بعدی با kubeadm join عضو می شن. قبلش Runtime لازم داریم و بعدش هم باید CNI رو نصب کنیم. kubeadm سرور و شبکه و Load Balancer رو از کلاه شعبده بازی بیرون نمیاره.
برای High Availability چند API Server پشت یه Load Balancer قرار می گیرن. Scheduler و Controller Manager هم چند نسخه دارن و با Leader Election جلوی تصمیم همزمان رو می گیرن.
برای etcd دو مدل رایج داریم. در مدل Stacked، عضو etcd کنار کامپوننت های Control Plane روی همون ماشین اجرا می شه. در مدل External etcd، اعضای etcd ماشین های جدا دارن. مدل دوم جداسازی بیشتری می ده، ولی پیچیدگی و هزینه اش هم بیشتره. راهنمای رسمی HA با kubeadm هر دو مدل رو توضیح داده.
اگه Control Plane موقتا قطع بشه، خیلی از Podها و مسیرهای موجود ممکنه همچنان کار کنن. ولی تغییر تازه، Scheduling و ترمیم کامل وضعیت متوقف می شه. مغز خاموش شده، اما دست و پا هنوز حرکت قبلی رو ادامه می دن.
کلاستر چند Node لازم داره؟
جواب واقعی همون جواب دوست داشتنی مهندسیه: بستگی داره. برای آزمایش، یه Node هم کافیه. برای Production باید نوع خرابی قابل قبول رو مشخص کنم.
اگه فقط یه Control Plane داشته باشم و همون ماشین از دست بره، API و etcd هم از دسترس خارج می شن. برای HA معمولا حداقل سه Control Plane در Failure Domainهای جدا می ذارن و ورودی API Serverها رو پشت Load Balancer می برن. اگه هر سه VM روی یه Host فیزیکی باشن، روی کاغذ سه نسخه دارم ولی در عمل فقط سه تا تخم مرغ توی یه سبد گذاشتم.
تعداد Workerها هم به ظرفیت و تحمل خرابی بستگی داره. اگه کل برنامه روی یه Worker جا شده و همون Node خاموش بشه، Kubernetes جایی برای ساخت Pod جایگزین نداره. داشتن Replica بدون ظرفیت اضافه شبیه داشتن لاستیک زاپاس بدون جک می مونه. از نظر آماری امیدوار کننده به نظر میاد، از نظر عملی نه خیلی.
خراب شدن Node چطور تشخیص داده می شه؟
kubelet مرتب وضعیت Node رو گزارش می ده. اگه Control Plane برای مدتی این Heartbeatها رو نگیره، Node آماده نبودن خودش رو نشان می ده. Controllerهای مربوط به Node و Workload بعد از زمان های تعریف شده تصمیم می گیرن Podهای لازم رو جای دیگه بازسازی کنن.
این اتفاق فوری نیست و نباید هم باشه. شاید فقط شبکه برای چند ثانیه قطع شده باشه و خود برنامه روی Node هنوز کار کنه. اگه Kubernetes با اولین Packet Loss همه Podها رو جای دیگه بسازه، یه لرزش کوچیک شبکه می تونه خودش تبدیل به قطعی بزرگ بشه.
برای Workloadهای Stateful داستان سخت تره. قبل از بالا آوردن نسخه تازه باید معلوم بشه نسخه قبلی واقعا دیگه به Volume یا دیتابیس دسترسی نداره. وگرنه دو نمونه ممکنه همزمان خودشون رو صاحب یه داده بدونن. اینجا دیگه زیاد بودن Replica به تنهایی نجاتمون نمی ده و باید رفتار Storage و خود برنامه رو هم بشناسیم.
Update خود کلاستر هم بخشی از معماریه
کلاستر فقط روز نصب وجود نداره. Certificateها منقضی می شن، نسخه Kubernetes عوض می شه، Nodeها Kernel تازه می خوان و CNI هم Update داره. در کلاستر Self-managed باید برای Upgrade مرحله ای، Drain کردن Node، Backup گرفتن از etcd و برگشت از خرابی برنامه داشته باشم.
سرویس مدیریت شده بخشی از این بار رو کم می کنه، ولی همه چیز رو نه. نسخه Node Pool، Add-onها، سازگاری APIهای قدیمی و بودجه اختلال هنوز مسئله من هستن. Cloud فقط بعضی دردسرها رو با فاکتور ماهانه مرتب تری تحویل می ده.
Raft چیه و چرا etcd رای گیری می کنه؟
Raft الگوریتم پخش ترافیک بین Podها نیست و Scheduler هم باهاش Node انتخاب نمی کنه. Raft داخل etcd کمک می کنه چند عضو روی ترتیب تغییرهای داده توافق کنن.
هر عضو می تونه یکی از این نقش ها رو داشته باشه:
- Leader تغییرها رو می گیره و Log رو برای بقیه می فرسته.
- Follower Log خودش رو با Leader هماهنگ نگه می داره.
- Candidate وقتی Leader گم می شه وارد انتخابات می شه و رای می خواد.
فرض کنیم سه عضو etcd دارم. Leader تغییر تازه رو وارد Log می کنه و برای دو عضو دیگه می فرسته. وقتی اکثریت، یعنی حداقل دو عضو، اون رو تایید کردن، تغییر Commit می شه.
اگه Leader قطع بشه، Followerها بعد از یه زمان کوتاه انتخابات راه می ندازن و یکی Leader تازه می شه. زمان انتخابات کمی تصادفیه تا همه با هم نامزد نشن و جلسه به گروه خانوادگی تبدیل نشه.
کلمه مهم Quorum یا حد نصابه. کلاستر سه عضوی با دو رای کار می کنه و خرابی یه عضو رو تحمل می کنه. کلاستر پنج عضوی سه رای می خواد و دو خرابی رو تحمل می کنه. چهار عضو هم سه رای لازم دارن و باز فقط یه خرابی رو تحمل می کنن. برای همین تعداد فرد معمولا بهتره. FAQ رسمی etcd جدول کاملش رو داره.
Raft در بخش اقلیت اجازه Write تازه نمی ده. این کار شاید شبیه لجبازی باشه، ولی جلوی Split Brain رو می گیره. بهتره مدتی تغییر ثبت نشه تا اینکه دو نسخه متفاوت از حقیقت داشته باشیم.
Log و Term چه کمکی می کنن؟
Raft زمان رو به دوره هایی به اسم Term تقسیم می کنه. هر انتخابات یه Term تازه می سازه و هر Leader مربوط به یه Term مشخصه. ورودی های Log هم ترتیب دارن. این اطلاعات کمک می کنه اعضا بفهمن کدوم Leader و کدوم Log تازه تره.
Followerها فقط هر چیزی رو که به اسم Leader رسید قبول نمی کنن. پیام باید با Term معتبر و تاریخچه هماهنگ جور باشه. اگه یه عضو مدتی قطع بوده، بعد از برگشت Logهای عقب افتاده یا Snapshot لازم رو می گیره تا دوباره به بقیه برسه.
Snapshot اینجا عکس صفحه نمایش نیست. etcd هر چند وقت یه نمای فشرده از وضعیت می سازه تا مجبور نباشه برای بازیابی از روز اول تمام Logها رو یکی یکی اجرا کنه. Log همچنان برای توافق تغییرهای تازه مهمه، ولی تاریخچه قدیمی می تونه Compact بشه.
اگه شبکه نصف بشه چی؟
فرض کنم پنج عضو etcd دارم و شبکه اونها رو به دو گروه دو و سه تایی تقسیم می کنه. گروه سه تایی Quorum داره و می تونه Leader معتبر داشته باشه و Write رو جلو ببره. گروه دو تایی اجازه این کار رو نداره.
وقتی شبکه برگرده، گروه اقلیت خودش رو با Log معتبر هماهنگ می کنه. این محدودیت همون چیزیه که نمی ذاره دو طرف همزمان تغییرهای متناقض رو حقیقت نهایی اعلام کنن.
اگه فقط دو عضو از پنج عضو سالم بمونن، etcd داده رو بی دلیل حذف نمی کنه، ولی Write تازه هم Commit نمی شه. در این حالت APIهایی که به ثبت وضعیت تازه نیاز دارن گیر می کنن. Podهای قبلی شاید کار کنن، اما Control Plane نمی تونه مثل قبل تصمیم تازه رو ذخیره کنه.
چرا Backup هنوز لازمه؟
Raft خرابی چند عضو رو تحمل می کنه، ولی جای Backup نیست. اگه یه حذف اشتباه از راه API ثبت بشه، Raft با نظم و سرعت همون حذف اشتباه رو روی اعضای دیگه هم تکثیر می کنه. توافق جمعی لزوما به معنی تصمیم خوب نیست. پنج نفر هم می تونن با هماهنگی کامل فایل اشتباه رو پاک کنن.
برای همین Snapshot منظم etcd، نگهداری امن Certificateها و تمرین Restore مهمه. بکاپی که هیچ وقت Restore نشده بیشتر یه آرزوی فشرده شده به حساب میاد تا برنامه بازیابی.
کلاستر و برنامه با چی دیپلوی می شن؟
اینجا دو نوع دیپلوی داریم.
دیپلوی کلاستر یعنی Control Plane، Workerها، Runtime، شبکه و گواهی ها رو آماده کنیم. این کار با سرویس مدیریت شده، kubeadm یا ابزارهای اتوماسیون انجام می شه.
دیپلوی برنامه یعنی بعد از آماده شدن کلاستر، Deployment و Service و بقیه آبجکت ها رو بسازیم. ساده ترین راه Manifestهای YAML و kubectl apply هست. Helm برای Package و Template، Kustomize برای تغییر Manifest و ابزارهایی مثل Argo CD و Flux برای GitOps استفاده می شن. آخرش همه این ابزارها باید با Kubernetes API حرف بزنن.
یه نمونه خیلی خلاصه این شکلیه:
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-api
spec:
replicas: 3
selector:
matchLabels:
app: demo-api
template:
metadata:
labels:
app: demo-api
spec:
containers:
- name: api
image: registry.example.com/demo-api:1.0.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health/ready
port: 8080
---
apiVersion: v1
kind: Service
metadata:
name: demo-api
spec:
selector:
app: demo-api
ports:
- port: 80
targetPort: 8080
Deployment سه Pod رو از راه ReplicaSet نگه می داره. Service با Label مربوط به برنامه یه آدرس پایدار جلوی Podها می ذاره. Readiness Probe هم مشخص می کنه Pod فقط زنده مونده یا واقعا می تونه ترافیک بگیره.
بعد از kubectl apply، API Server درخواست رو بررسی و در etcd ثبت می کنه. Controllerها ReplicaSet و Podها رو می سازن. Scheduler برای هر Pod یه Node پیدا می کنه. kubelet با کمک Runtime کانتینر رو اجرا می کنه و CNI بهش شبکه می ده. وقتی Readiness موفق شد، Pod به EndpointSlice آماده های Service اضافه می شه.
Helm و GitOps دقیقا کجای کارن؟
Helm جای Kubernetes رو نمی گیره. Chart و Valueها رو می خونه، Manifest نهایی رو می سازه و اون رو به API می فرسته. خوبی این مدل اینه که مجموعه بزرگی از منابع رو Package می کنم. بدیش اینه که Templateهای پیچیده می تونن YAML رو به زبانی تبدیل کنن که حتی سازنده اش هم سه ماه بعد از دیدنش جا بخوره.
Kustomize بیشتر روی Base و Overlay کار می کنه. یه Manifest پایه دارم و برای محیط Development یا Production تغییرهایی مثل Replica، Image و Config رو روی اون اعمال می کنم.
در GitOps، Git منبع وضعیت دلخواه می شه. Controllerهایی مثل Argo CD یا Flux مخزن رو نگاه می کنن و اختلافش با کلاستر رو کم می کنن. این مدل تاریخچه و Review خوبی می ده، ولی اگه هر کسی همزمان با kubectl edit داخل کلاستر تغییر دستی بزنه، خیلی زود وارد مسابقه ای می شیم که یه طرف انسانه و یه طرف Controller خستگی ناپذیر.
Rollout چطور بدون قطع کامل جلو می ره؟
وقتی Image داخل Deployment عوض می شه، Deployment یه ReplicaSet تازه می سازه. بعد کم کم Podهای نسخه تازه رو بالا میاره و Podهای قدیمی رو کم می کنه. maxSurge می گه چند Pod اضافه موقت اجازه دارم و maxUnavailable می گه چند نسخه می تونن همزمان در دسترس نباشن.
Readiness اینجا نقش اصلی داره. تا وقتی Pod جدید Ready نشده، نباید ترافیک عادی بگیره و نباید با خیال راحت نسخه قدیمی حذف بشه. اگه Readiness فقط یه جواب ثابت 200 OK بده و وابستگی حیاتی رو چک نکنه، Rollout روی کاغذ موفقه ولی کاربرها نظر متفاوتی دارن.
اگه نسخه تازه خراب باشه، تاریخچه Deployment امکان Rollback رو می ده. با این حال Rollback برنامه لزوما Migration دیتابیس رو برنمی گردونه. قبل از دیپلوی باید سازگاری نسخه کد و Schema رو جدا طراحی کنم. Kubernetes نگهبان Replicaهاست، نه وکیل اختلاف بین نسخه قدیم و ستون تازه دیتابیس.
چرا مستقیم Pod نمی سازم؟
می تونم یه Pod خام بسازم، ولی اگه حذف بشه Controller بالادستی وجود نداره که دوباره درستش کنه. Deployment و ReplicaSet فقط تزئین YAML نیستن. اونها مالک چرخه عمر Pod هستن.
برای همین در کار عادی برنامه Stateless رو با Deployment اجرا می کنم، نه با Pod تنها. Pod چیزی مصرف شدنیه. اگه اسمش رو قاب گرفتم و به IP اون وابسته شدم، احتمالا هنوز با ذهنیت یه سرور سنتی دارم به کلاستر نگاه می کنم.
یه درخواست چطور به Pod می رسه؟
قبل از دنبال کردن بسته، بد نیست درهای ورودی رایج رو بشناسم.
ClusterIP نوع پیش فرض Service هست و فقط داخل شبکه کلاستر قابل دسترسیه. این همون آدرس پایداریه که بیشتر Serviceهای داخلی لازم دارن.
NodePort یه Port مشخص رو روی Nodeها باز می کنه و ترافیک اون Port رو به Service می رسونه. ساده به نظر میاد، ولی مدیریت Port، Firewall و ورودی Production رو کامل حل نمی کنه.
LoadBalancer Service از Integration محیط می خواد یه Load Balancer بیرونی بسازه یا تنظیم کنه. در Cloud معمولا Cloud Controller این درخواست رو به سرویس Load Balancer ارائه دهنده منتقل می کنه. روی Bare Metal باید پیاده سازی مناسب خودم رو داشته باشم.
Ingress برای Route کردن HTTP و HTTPS بر اساس Host و Path استفاده می شه. Gateway API مدل جدیدتر و گسترده تری برای تعریف ورودی و Routeها می ده. هر دو فقط API هستن و برای کار کردن Controller می خوان.
حالا فرض می کنم کاربر api.example.com رو باز کرده. مسیر دقیق به Cloud و CNI بستگی داره، ولی مدل رایج اینه:
- DNS بیرونی دامنه رو به IP ورودی کلاستر وصل می کنه.
- Load Balancer اتصال رو به Node یا Data Plane کنترلر ورودی می رسونه.
- Ingress Controller یا Gateway Controller قانون Host، Path و TLS رو پیدا می کنه.
- قانون، درخواست رو به یه Service می فرسته.
- Data Plane سرویس از بین Endpointهای آماده یه Pod IP رو انتخاب می کنه.
- شبکه CNI بسته رو به Pod می رسونه، حتی اگه روی Node دیگه ای باشه.
Ingress فقط یه Rule داخل API هست. برای جابه جا شدن ترافیک باید Ingress Controller نصب باشه. Gateway API هم Controller لازم داره. ساختن Ingress بدون Controller مثل نوشتن آدرس روی کاغذ و منتظر تاکسی موندنه.
Service هم یه Load Balancer کوچیک نیست که وسط شبکه نشسته باشه. ClusterIP معمولا یه IP مجازیه. Service و EndpointSlice به Data Plane اطلاعات می دن تا Kernel یا پیاده سازی شبکه بسته رو به Pod آماده هدایت کنه. مستندات شبکه Kubernetes هم می گه این Data Plane می تونه kube-proxy یا بخشی از Pod Network باشه.
برای ترافیک داخلی، CoreDNS اسم یه Service مثل demo-api.default.svc.cluster.local رو به ClusterIP تبدیل می کنه و Data Plane ادامه مسیر رو انجام می ده. NetworkPolicy هم می تونه رفت و آمد رو محدود کنه، البته فقط وقتی CNI واقعا اون رو پیاده کرده باشه.
DNS بیرونی با CoreDNS یکی نیست
وقتی کاربر بیرون کلاستر api.example.com رو باز می کنه، معمولا DNS عمومی اینترنت جواب می ده و IP ورودی رو برمی گردونه. CoreDNS کلاستر بیشتر برای اسم های داخلی مثل service.namespace.svc.cluster.local استفاده می شه.
این دوتا ممکنه در بعضی معماری ها به هم وصل بشن، ولی نقش پایه شون فرق داره. اگه دامنه عمومی اشتباه باشه، درخواست شاید اصلا به کلاستر نرسه. در این حالت نگاه کردن به Log برنامه مثل منتظر مهمونی موندنه که آدرس خونه دیگه ای رو گرفته.
Service مقصد رو از کجا پیدا می کنه؟
Service معمولا با Selector سراغ Labelهای Pod می ره. Control Plane از روی Podهای منطبق، EndpointSlice می سازه. EndpointSlice آدرس و وضعیت Backendها رو نگه می داره و Service Proxy از اون برای ساخت Data Plane استفاده می کنه.
خود بسته شبکه وارد EndpointSlice نمی شه. EndpointSlice یه آبجکت اطلاعاتیه. برای همین توی دیاگرام مسیر کنترلیش خط چینه. وقتی Pod Ready می شه، آدرسش می تونه به مجموعه Endpointهای آماده اضافه بشه. وقتی Ready نیست، بهتره ترافیک عادی بهش نرسه.
اگه Selector اشتباه باشه، Service ساخته می شه و ClusterIP هم می گیره، ولی Endpoint آماده نداره. ظاهر همه چیز رسمی و مرتب به نظر میاد، فقط هیچ کس پشت باجه نیست.
بسته برگشت چطور برمی گرده؟
جواب برنامه از شبکه Pod بیرون میاد و به سمت Client برمی گرده. بسته به نوع Service، تنظیم externalTrafficPolicy، CNI و Load Balancer ممکنه Source NAT انجام بشه یا IP اصلی Client حفظ بشه. بعضی پیاده سازی ها مسیر برگشت مستقیم تری دارن و بعضی جواب رو از همون Node ورودی رد می کنن.
برای فهم معماری لازم نیست همه حالت ها رو یه جا حفظ کنم. نکته مهم اینه که Source IP و مسیر رفت و برگشت به پیاده سازی وابسته ان. اگه برنامه IP کاربر رو لازم داره، باید تنظیم Load Balancer، Controller، Headerهایی مثل X-Forwarded-For و اعتماد به Proxyها رو درست بررسی کنم. قبول کردن هر Header از هر کسی هم راه سریعی برای ساختن IP دلخواه کاربره.
NetworkPolicy دقیقا کجا اثر می ذاره؟
NetworkPolicy می گه چه ترافیک ورودی یا خروجی برای Podها مجازه. خود API این قانون رو ذخیره می کنه، ولی CNI باید اجراش کنه. بعضی شبکه های ساده اصلا NetworkPolicy رو پیاده نمی کنن.
قانون اشتباه می تونه DNS، اتصال دیتابیس یا مسیر Health Check رو ببنده. برای همین بهتره Policy رو مرحله ای اضافه کنم و فقط باز شدن صفحه اصلی رو تست نکنم. ممکنه صفحه باز بشه ولی Worker پس زمینه دیگه نتونه به Queue وصل بشه و چند ساعت بعد تازه بوی سوختگی بلند بشه.
وقتی ترافیک نمی رسه، از کجا شروع کنم؟
من معمولا از نزدیک ترین نقطه به برنامه شروع می کنم و کم کم به سمت بیرون می رم. این روش جلوی پرش تصادفی بین Dashboard و YAML و Firewall رو می گیره.
اول خود Pod رو نگاه می کنم:
kubectl get pods -o wide
kubectl describe pod <pod-name>
kubectl logs <pod-name> --all-containers
Running بودن کافی نیست. ستون Ready، Eventهای پایین describe و Log برنامه می گن Container واقعا آماده شده یا فقط Process هنوز نمرده. بعد از داخل Pod یا یه Pod موقت، مستقیم Port برنامه رو تست می کنم تا Ingress و Service فعلا از معادله بیرون باشن.
قدم بعدی Service و EndpointSlice هست:
kubectl get service demo-api -o wide
kubectl get endpointslice \
-l kubernetes.io/service-name=demo-api
kubectl describe service demo-api
اگه Endpoint ندارم، معمولا باید Label، Selector یا Readiness رو بررسی کنم. اگه Endpoint هست، port و targetPort و دسترسی شبکه مهم می شن. می تونم از یه Pod دیگه اول اسم DNS سرویس و بعد خود ClusterIP رو تست کنم.
بعد سراغ لایه ورودی می رم:
kubectl get ingress -A
kubectl describe ingress <ingress-name>
kubectl get pods -n <controller-namespace>
kubectl logs -n <controller-namespace> <controller-pod>
اینجا Host، Path، TLS Secret، اسم Service و Port مقصد رو چک می کنم. سالم بودن خود Controller هم مهمه. یه Ingress بی نقص با Controller خاموش هنوز فقط یه متن قشنگ داخل etcd هست.
آخر مسیر DNS عمومی، IP بیرونی Load Balancer، Firewall و Health Check خود Load Balancer رو بررسی می کنم. اگه درخواست اصلا به Log کنترلر ورودی نمی رسه، احتمالا مشکل قبل از Controllerه. اگه به Controller می رسه ولی Upstream نداره، مشکل معمولا از Service، Endpoint یا Route هست. اگه به Pod می رسه و جواب 500 برمی گرده، دیگه Kubernetes احتمالا فقط پیک بدشانسی بوده و باید برم سراغ برنامه.
Eventهای مرتب شده هم خیلی وقت ها سرنخ خوبی می دن:
kubectl get events -A --sort-by=.lastTimestamp
این ترتیب کمک می کنه برای هر 502 مستقیم etcd رو متهم نکنم. رفتن سراغ Raft وقتی Service هیچ Endpointی نداره، مثل باز کردن موتور ماشین برای پیدا کردن باتری خالی کنترل تلویزیونه.
نقشه ای که توی ذهنم می مونه
API Server در ورودی Control Plane می ایسته. etcd حقیقت کلاستر رو نگه می داره. Controllerها وضعیت واقعی رو به چیزی که خواستم نزدیک می کنن. Scheduler برای Pod جا پیدا می کنه. kubelet روی Node اون رو با Runtime اجرا می کنه. CNI شبکه رو می سازه و Service Data Plane ترافیک رو به Endpoint آماده می رسونه.
Raft هم کمک می کنه اعضای etcd روی یه نسخه از حقیقت توافق کنن. نه درخواست HTTP رو پخش می کنه و نه Pod مناسب رو انتخاب می کنه. فقط نمی ذاره حافظه کلاستر با اولین قطعی شبکه دچار چند شخصیتی بشه.
وقتی این مدل توی ذهنم جا بیفته، دیاگرام Kubernetes دیگه نقشه مترو مریخ نیست. فقط یه شهر شلوغه که API Server شهرداریشه، etcd بایگانیه و kubeletها هم آخر شب باید کار واقعی رو جمع کنن. طبق معمول، کسی که YAML نوشته زودتر رفته خونه.