کلاستر جدید روی هر node تا ۱۱۰ تا pod می‌برد. Production روی ۵۸ گیر کرده بود.

kubernetes aws eks terraform devops

دو تا کلاستر EKS، با instance type یکسان. اونی که امسال ساخته شده روی هر node تا ۱۱۰ تا pod زمان‌بندی می‌کنه. کلاستر production که قدیمی‌تره، سقفش ۵۸ است و دقیقاً هم به همون سقف خورده بود: کلی CPU و memory آزاد، ولی جا برای pod نه. هدف شبیه یه تغییر یک‌خطی بود: به production همون تراکم رو بده. تهش شد یه node group جدید، یه پیش‌نیاز CNI، و یه Terraform plan که خیلی بیشتر از چیزی که خواسته بودم می‌خواست انجام بده.

عدد ۵۸ از کجا میاد

به‌صورت پیش‌فرض، EKS مقدار max pods رو از شبکه درمیاره، نه از توان پردازشی. افزونه VPC CNI به هر pod یه IP واقعی از ENIهای node می‌ده، پس سقف یه حساب ENI است: یه m6g.xlarge چهار تا ENI با ۱۵ آدرس برای هر کدوم می‌گیره، و بعد از کنار گذاشتن یکی برای هر ENI به‌علاوه دو تا برای سیستم، می‌رسیم به ۵۸. یعنی node با 4 vCPU و 16 GB حافظه فقط ۵۸ تا pod؛ برای workloadهای درشت خوبه و برای پلتفرمی پر از سرویس‌های کوچیک افتضاح.

Prefix delegation این حساب رو عوض می‌کنه. به‌جای IPهای تکی روی هر slot از ENI، افزونه CNI پیشوندهای ‎/28 وصل می‌کنه، یعنی ۱۶ آدرس برای هر slot، و محدودیت عملی دیگه جدول ENI نیست. البته هنوز باید سقف جدید رو به kubelet گفت، و AWS توصیه می‌کنه برای هر چیزی زیر 30 vCPU روی ۱۱۰ نگهش داری. تراکم کلاستر greenfield هم دقیقاً از همین‌جا اومده بود: node groupهاش از روز اول با prefix delegation روشن و max_pods = 110 توی launch template به دنیا اومده بودن.

انحراف podsPerCore

اولین غریزه‌م روی یه کلاستر با Karpenter این بود: podsPerCore: 10. به نظر تطبیقی میاد: nodeهای کوچیک کمتر می‌گیرن، بزرگ‌ها بیشتر. توی این setup یه تله است، اون هم دوبار. kubelet مقدار podsPerCore رو به‌عنوان یه سقف اضافه روی max pods اعمال می‌کنه، پس یه xlarge با ۴ هسته می‌شه ۴۰، که از همون ۵۸ قبلی هم کمتره. و Karpenter ظرفیت node رو از محدودیت‌های ENI حساب می‌کنه و از prefix delegation خبر نداره، پس دید scheduler و دید kubelet از هم فاصله می‌گیرن. یه maxPods: 110 ساده با prefix delegation روشن، خسته‌کننده و درسته. دیدن اثباتش روی یه node تازه کیف داره:

$ kubectl get node <old-node> -o jsonpath='{.status.capacity.pods}'
8
$ kubectl get node <new-node> -o jsonpath='{.status.capacity.pods}'
110

این یه m6g.medium بود که از ۸ تا pod، یه پیش‌فرض تقریباً خنده‌دار برای یه node با 4 GB حافظه، رسید به ۱۱۰.

به یه node group زنده نمی‌شه launch template اضافه کرد

اینجاست که production با greenfield فرق می‌کنه. ست کردن max_pods روی یه managed node group یعنی ماژول براش یه launch template رندر می‌کنه، چون flag مربوط به kubelet باید از مسیر user data رد بشه. ولی یه managed node group که بدون launch template ساخته شده، هیچ‌وقت نمی‌تونه صاحبش بشه. API خود EKS آپدیت رو رک رد می‌کنه:

ValidationException: Cannot update launchTemplate

Terraform هم اینو می‌دونه و تغییر رو ForceNew مدل می‌کنه: node group رو نابود کن، یه جایگزین بساز. روی sandbox یه شونه بالا انداختنه. روی کلاستر production یعنی همه nodeهای اون گروه طبق زمان‌بندی Terraform جابه‌جا می‌شن، نه زمان‌بندی تو.

پس حرکت درست اینه که باهاش نجنگی: یه node group جدید کنار قبلی بساز که از لحظه تولد launch template لازمش رو داره.

general_arm_v2 = {
  instance_types = ["m6g.xlarge"]
  desired_size   = 3
  min_size       = 2
  max_size       = 20
  max_pods       = 110
  max_unavailable = 1
}

گروه قدیمی دست‌نخورده به کارش ادامه می‌ده. Workloadها کم‌کم منتقل می‌شن؛ گروه جدید scale می‌شه و قدیمی رو با زمان‌بندی خودت cordon و drain می‌کنی، و rollback هم فقط اینه که «drain رو متوقف کن». راستی پخش کردن گروه جدید بین availability zoneها از نظر instance هیچ هزینه‌ای نداره؛ تنها قبض، ترافیک بین AZهاست، همون قلمی که حساب‌وکتاب VPC peering در برابر Transit Gateway رو هم تعیین می‌کنه.

Prefix delegation رو قبل از nodeها روشن کن، نه بعدش

Launch template فقط نصف تغییره. افزونه vpc-cni باید قبل از بالا اومدن nodeهای جدید ENABLE_PREFIX_DELEGATION=true داشته باشه:

vpc-cni = {
  configuration_values = jsonencode({
    env = { ENABLE_PREFIX_DELEGATION = "true" }
  })
}

ترتیب رو برعکس کنی، یه دروغگو ساختی: kubelet اعلام می‌کنه ۱۱۰ تا جا دارم، scheduler هم با خوشحالی پرشون می‌کنه، ولی CNI در عمل فقط به اندازه محدودیت قدیمی ENI می‌تونه IP بده. هر podی که از اون خط رد بشه توی ContainerCreating می‌شینه و منتظر آدرسی می‌مونه که هیچ‌وقت نمیاد. هیچ‌چیزی توی پایپ‌لاین دیپلوی اینو نمی‌گیره؛ همون کلاس خطاییه که توی آپدیت ConfigMap که هیچ‌وقت به pod در حال اجرا نمی‌رسه دیدیم: همه‌چیز سبزه به‌جز واقعیت در runtime.

یه محدودیت دیگه هم توی لیست instanceها قایم شده: prefix delegation فقط روی Nitro کار می‌کنه. گروه spot توی production هنوز m4.large رو لیست کرده بود، یه نوع پیشا-Nitro از یه دوره دیگه، و هر nodeی که روش بالا می‌اومد بی‌سروصدا برمی‌گشت به محدودیت‌های قدیمی. Instance typeهای قدیمی توی یه mixed-instances policy دقیقاً همون رسوبی‌ان که هیچ‌کس بررسی‌شون نمی‌کنه تا روزی که یه قابلیت، بی‌صدا کنارشون بذاره.

Plan تغییرهایی داشت که من ننوشته بودم

خروجی terraform plan این بود: 6 add، 1 change، 2 destroy. تغییر من فقط سه‌تاش بود: node group، launch template و تنظیم CNI. بقیه‌ش یه ارتقای ماژول مال چند هفته قبل بود که هیچ‌وقت apply نشده بود، و توش یه مهاجرت از aws-auth ConfigMap به EKS access entries بسته‌بندی شده بود. امروز هیچ‌کس اینو سفارش نداده بود، ولی apply کردن تغییر من اون رو هم apply می‌کرد.

خوندن کاملش به اون یه ساعت می‌ارزید. دو تا destroy ترسناک به نظر می‌رسیدن و بی‌خطر بودن: null_resourceهایی که بلاک configشون از ماژول حذف شده بود، و provisioner زمان destroy فقط وقتی اجرا می‌شه که بلاکش هنوز وجود داشته باشه. پس Terraform فقط از state حذفشون می‌کرد بدون اینکه چیزی اجرا بشه. ConfigMap زنده می‌مونه و چون کلاستر روی حالت API_AND_CONFIG_MAP است، هیچ‌کس دسترسیش رو از دست نمی‌ده.

مین واقعی createها بودن. Plan می‌خواست یه access entry برای یه نقش SSO admin بسازه، و یه چک روی کلاستر زنده نشون داد دقیقاً همون entry از قبل وجود داره؛ چند ماه پیش با دست از توی console ساخته شده بود. عملیات CreateAccessEntry توی EKS هم idempotent نیست:

ResourceInUseException: The specified access entry resource is already in use on this cluster

یعنی apply وسط راه شکست می‌خورد؛ شاید با node group ساخته‌شده و یه مهاجرت auth نصفه‌کاره. راه‌حل، تقسیم شعاع انفجار بود: تغییرِ تراکم رو با -target دقیقاً روی همون سه resource خودم apply کن، و مهاجرت auth رو بکن یه تغییر مستقل؛ تغییری که به‌جای برخورد با entryهای دستی، با import کردنشون شروع می‌شه.

چی از این ماجرا یاد بگیریم

تراکم pod روی EKS یه ویژگی زمان تولده. Launch template، حالت CNI و سقف kubelet همه موقع ساخته شدن node group قفل می‌شن، پس راه تمیز عوض کردنشون روی یه کلاستر زنده اینه که نسل بعدی رو کنار نسل فعلی بسازی، نه اینکه درجا دستکاریش کنی. مثل پیش‌فرض enableServiceLinks، محدودیت pod مبتنی بر ENI هم چیزیه که بیشتر آدم‌ها روزی باهاش آشنا می‌شن که جلوی یه rollout رو گرفته.

و با هر خط از plan که خودت ننوشتی مثل یه سؤال برخورد کن، نه یه تشریفات. اون ارتقای ماژولِ apply‌نشده برای نفر بعدی که plan می‌زد کمین کرده بود، و کلاستر زنده هم حالتی داشت، همون access entryهای دستی، که با هیچ مقدار HCL خوندنی پیدا نمی‌شد. Plan بهت می‌گه Terraform می‌خواد چی کار کنه؛ فقط خود کلاستر بهت می‌گه می‌تونه یا نه.

$ cat AIAGENTS .md
· 8 دقیقه مطالعه

Hermes Agent روی EKS: از docker-compose تا Helm

ایجنت هوش مصنوعی خودآموز Nous Research در یه محیط پروداکشن-محور روی EKS. ECR خصوصی،، IRSA، مدیریت API Key به عنوان Secret، و یه Helm Chart سفارشی که از روی manifest خام ساختیم.

ai-agents aws eks kubernetes llm helm devops
$ cat DNS .md
· 7 دقیقه مطالعه

یه رکورد DNS رو دوبار پاک کردم. بار سوم پرسیدم چرا.

یه endpoint عمومی توی سه روز دو بار قطع شد، و هر دو بار همون fix پنج‌دقیقه‌ای جواب داد: پاک کردن یه رکورد AAAA یتیم. باگ واقعی یه رکورد ownership توی DNS بود که هیچ‌وقت نمی‌تونه خودش رو صاحب بشه، دقیقاً روی apex یه zone دلگیت‌شده.

dns aws route53 kubernetes devops
$ cat KUBERNETES .md
· 5 دقیقه مطالعه

اپ REDIS_PORT رو می‌خوند. Kubernetes قبلش مقدارش رو عوض کرده بود.

هر view که به cache دست می‌زد 500 برمی‌گردوند، ولی health check سبز بود. مقصر یه Service به اسم redis بود و enableServiceLinks؛ یه پیش‌فرض قدیمی Kubernetes از دوران Docker که متغیرهای محیطی‌ای تزریق می‌کنه که هیچ‌وقت نخواستی.

kubernetes devops debugging django