StackGres و Strimzi: درس‌های Day-Two از یه Rebuild واقعی

kubernetes operators postgresql kafka devops platform-engineering

داشتیم یه محیط pre-production رو از صفر می‌ساختیم. دو تا سیستم stateful باید جابجا می‌شدن: یه cluster PostgreSQL با TimescaleDB برای داده‌های time-series، و یه cluster Kafka برای event streaming در چند مرحله از pipeline.

برای هر دو، سوال یکی بود: StatefulSet معمولی با Helm chart، یا operator های اختصاصی Kubernetes؟

ما رفتیم سمت operator. StackGres برای PostgreSQL، Strimzi برای Kafka. هیچ‌کدوم از اول واضح نبودن. چند هفته بعد، هر دو ارزششون رو ثابت کردن، البته نه اون‌طوری که اول انتظار داشتم.

چی operator ها واقعاً قول می‌دن

تئوری اینه که operator ها دانش عملیاتی رو داخل یه controller قرار می‌دن. شما توصیف می‌کنید چی می‌خواید، یه cluster PostgreSQL با سه replica یا یه cluster Kafka با SCRAM authentication (Salted Challenge Response Authentication Mechanism)، و operator مداوم به سمت اون state حرکت می‌کنه. برخلاف یه Helm chart که YAML استاتیک render می‌کنه و می‌ره، یه operator دائما نظاره‌گر و واکنش‌گر هست.

استدلال بازاریابی اینه که Day-1 راحت‌تر می‌شه. استدلال واقعی اینه که Day-2 راحت‌تر می‌شه: backup، failover، چرخش certificate، recovery برای replica. اون‌جاست که تفاوت بین “یکی YAML برای StatefulSet شما نوشته” و “یکی که این سیستم رو عمیقاً می‌شناسه یه controller براش نوشته” خودش رو نشون می‌ده.

StackGres: لحظه‌ی backup

StackGres یه cluster PostgreSQL مدیریت‌شده با Patroni رو پشت یه سری CRD می‌بره. شما یه SGCluster تعریف می‌کنید و operator زیرساخت StatefulSet، sidecar ها، تنظیمات replication و connection pooling رو مدیریت می‌کنه. PgBouncer توکاره. Patroni leader election رو مدیریت می‌کنه. شما intent رو declare می‌کنید؛ operator بقیه رو انجام می‌ده.

پیکربندی backup declarative هست:

spec:
  configurations:
    backups:
      - sgObjectStorage: my-s3-storage
        cronSchedule: "0 2 * * *"
        retention: 7

شیء SGObjectStorage مشخص می‌کنه backup ها کجا می‌رن. WAL-G آپلود واقعی رو انجام می‌ده؛ operator lifecycle رو مدیریت می‌کنه. نیازی به نوشتن script، cron Job یا Deployment جداگانه برای backup نیست. policy رو توصیف می‌کنید و operator اعمالش می‌کنه.

چیزی که چند ساعت طول کشید تا بفهمیم: آپلود backup داخل pod اصلی database اجرا می‌شه، نه در یه Job جداگانه. این برای تزریق credential های cloud مهمه. اگه از IAM credentials بومی pod استفاده می‌کنید (یعنی یه cloud identity از طریق service account pod تزریق می‌کنید نه یه کلید استاتیک)، اون annotation باید روی service account pod دیتابیس باشه. فیلد allResources در spec cluster جای درستیه که این annotation رو بذارید، چون هر annotation ای که دستی بذارید در reconcile بعدی operator پاک می‌شه:

spec:
  metadata:
    annotations:
      allResources:
        "eks.amazonaws.com/role-arn": "arn:aws:iam::123456789012:role/pg-backup-role"

بعد از این تصحیح و rollout کردن pod ها، backup ای که روزها به صورت بی‌سروصدا شکست می‌خورد کامل شد. این اولین لحظه‌ی Day-2 هست: نه یه task راه‌اندازی، بلکه یه خرابی که monitoring operator مرئی کرد و ساختار operator یه مسیر واضح برای رفعش داد.

Reinit کردن replica

لحظه دوم دراماتیک‌تر بود.

یه replica رفت تو یه crash loop. چی شده بود: replica فایل pg_controlش رو از دست داده بود، نمی‌تونست از primary stream کنه، و به یه رفتار fallback خاص StackGres رسید، یعنی تلاش برای restore کردن خودش از آخرین WAL-G backup روی S3. اون restore timeout خورد. Operator یه failed-backup flag گذاشت و replica هر ۶۰ ثانیه restart می‌کرد.

با یه StatefulSet خام، این به معنی ورود به pod، فهمیدن وضعیت member Patroni، تصمیم‌گیری بین wipe-and-resync یا اجرای دستی pg_basebackup، و امیدواری به اینکه بدتر نکنیم بود. با StackGres، منطق fallback operator مسیر recovery رو صریح کرد:

# حذف marker restore ناموفق؛ operator به streaming از primary برمی‌گرده
kubectl exec -n data <cluster>-1 -c patroni -- \
  rm /var/lib/postgresql/replication/initialization-failed-backup
kubectl delete pod <cluster>-1 -n data

Replica برگشت، از primary stream کرد، و دوباره به cluster پیوست. Fallback ساختارمند operator، “اول S3 امتحان کن، بعد streaming از leader”، یه مسیر recovery قطعی داد نه یه session debugging بی‌نتیجه.

Migration کردن TimescaleDB بین operator ها

وقتی داده رو از deployment قدیمی به cluster جدید StackGres انتقال دادیم، pg_restore معمولی برای TimescaleDB کافی نیست. این extension پروتکل restore خاص خودش رو داره:

-- در دیتابیس مقصد قبل از pg_restore اجرا کنید
SELECT timescaledb_pre_restore();
pg_restore -Fc --no-owner < dump.pgfc | psql target_db
-- بعد از pg_restore اجرا کنید
SELECT timescaledb_post_restore();

نسخه‌های extension در مبدأ و مقصد باید دقیقاً یکی باشن؛ این gate اصلیه قبل از هر تلاشی برای restore. ما migration رو به صورت یه byte-pipe درون cluster انجام دادیم، یعنی خروجی kubectl exec روی cluster مبدأ رو مستقیماً به kubectl exec -i روی cluster مقصد pipe کردیم تا ترافیک database از طریق VPN عبور نکنه.

یه جزئیاتی که ما رو کند کرد: StackGres extension های PostgreSQL رو declarative نصب می‌کنه. یه database که به uuid-ossp وابسته‌ست بدون اینکه اون extension در cluster spec باشه، موفق به restore نمی‌شه:

spec:
  postgres:
    extensions:
      - name: uuid-ossp
        version: "1.1"

اعمال کردن cluster spec به‌روز شده یه نصب live بدون restart pod رو trigger می‌کنه. Extension در چند دقیقه در دسترسه.

Strimzi: coupling نسخه‌ها

Strimzi cluster های Kafka رو از طریق یه CRD به اسم Kafka مدیریت می‌کنه. شما cluster رو declare می‌کنید و operator جایگذاری broker، پیکربندی، مدیریت certificate و rolling restart رو انجام می‌ده.

اولین محدودیتی که بهش خوردیم coupling نسخه‌ها بود. Strimzi 1.0 که Kubernetes 1.30 تا 1.36 رو پشتیبانی می‌کنه، فقط Kafka 4.x رو می‌ده. Kafka 4.x فقط KRaft هست، یعنی پروتکل consensus مبتنی بر Raft داخلی Kafka، بدون ZooKeeper. اگه از manifest های Kafka 3.x کپی می‌کنید، inter.broker.protocol.version و log.message.format.version رو حذف کنید؛ Kafka 4.x اصلاً این تنظیمات رو نمی‌شناسه.

این بدتر از چیزیه که بنظر می‌رسه نیست. پروتکل wire Kafka نسخه‌ها رو دو طرفه negotiate می‌کنه. یه client library سه‌ایکس بدون تغییر با یه broker چهارایکس ارتباط برقرار می‌کنه.

TLS: چرا cert-manager، نه ACM

برای دسترسی خارجی، به client هایی نیاز داشتیم که بدون import کردن یه CA سفارشی، به certificate broker اعتماد کنن. گزینه‌ها یه سرویس certificate مدیریت‌شده cloud یا Let’s Encrypt از طریق cert-manager بودن.

Certificate های مدیریت‌شده cloud اینجا کار نمی‌کنن. نمی‌شه export شون کرد. می‌شه TLS رو در load balancer terminate کرد، اما نمی‌شه یه certificate مدیریت‌شده رو تو یه pod broker mount کرد. یه broker Kafka برای L4 TLS به certificate محلی نیاز داره؛ یه proxy که بالادست TLS رو terminate کنه کافی نیست. cert-manager با Let’s Encrypt تنها راه رسیدن به یه certificate با اعتماد عمومی بود که بتونه داخل pod mount بشه.

یه gotcha که چند دقیقه وقت گرفت: cert-manager به صورت پیش‌فرض کلیدهای PKCS#1 می‌سازه. پیاده‌سازی SSL کافکا PKCS#1 رو با algid parse error, not a sequence رد می‌کنه. تصحیح یه فیلده:

spec:
  privateKey:
    encoding: PKCS8

بعد از تصحیح encoding کلید و یه operator reconcile اجباری از طریق annotation strimzi.io/force-reconcile، broker ها سالم شروع به کار کردن.

Rotation خودکار certificate

Certificate Let’s Encrypt بعد از ۹۰ روز منقضی می‌شه. cert-manager اون رو در روز ۶۰ خودکار تجدید می‌کنه و همون Secret کوبرنتیز رو in-place بازنویسی می‌کنه. نام Secret تغییر نمی‌کنه، یعنی reference listener استریمزی هم تغییر نمی‌کنه.

Strimzi Secret رو hash می‌کنه و وقتی hash تغییر کرد broker ها رو roll می‌کنه، یکی یکی، بدون downtime. Client ها چیزی نمی‌فهمن: host و SAN ثابت می‌مونن و root Let’s Encrypt از قبل توی JVM و OS trust store هست.

موضع monitoring می‌شه: شیء Certificate cert-manager رو نزدیک تاریخ تجدید برای Ready=False زیر نظر بگیرید، و مراقب نبودن event CertificateRenewed باشید. اگه cert-manager نتونه DNS-01 challenge رو کامل کنه، broker ها certificate قدیمی که هنوز معتبره رو سرو می‌کنن تا expire بشه. یه پنجره ۳۰ روزه دارید تا مشکل تجدید رو قبل از اینکه client ها چیزی ببینن برطرف کنید.

برای مقایسه: rotation certificate بدون operator یعنی تولید cert جدید، آپدیت Secret، شناسایی pod هایی که باید restart بشن، هماهنگی restart بدون از دست دادن quorum، و تأیید اینکه client ها دوباره وصل شدن. با Strimzi، این توالی در هر چرخه تجدید رفتار پیش‌فرضه.

جایگذاری broker و deadlock در AZ

Broker های Kafka باید روی node های جداگانه باشن. بدون anti-affinity اجباری، هر سه broker روی یه node می‌نشینن و رفتن اون node cluster رو می‌بره:

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
            - key: strimzi.io/name
              operator: In
              values: ["my-cluster-kafka"]
        topologyKey: kubernetes.io/hostname

Anti-affinity یه مشکل ظریف‌تر می‌آره. اگه broker ها اول cluster-bring-up روی یه node co-locate بشن، volume های EBS شون در availability zone اون node provision می‌شن. وقتی قانون anti-affinity یه broker رو به node ای در zone دیگه می‌فرسته، volume نمی‌تونه دنبالش بیاد. EBS volume ها zone-local هستن. Broker منتقل‌شده در حالت pending می‌مونه با didn't match PersistentVolume's node affinity.

روی یه cluster تازه، تصحیح ساده‌ست: PVC خالی broker قفل‌شده رو حذف کنید. Strimzi اون رو در reconcile دوباره می‌سازه، و این بار volume در zone ای که broker واقعاً schedule شده provision می‌شه.

برای fault tolerance واقعی، broker ها باید در سه availability zone پخش بشن. دو zone یه مصالحه هزینه‌ای با یه failure mode شناخته‌شده‌ست: رفتن یه AZ می‌تونه دو تا از سه broker رو ببره و quorum رو از بین ببره.

User ها و ACL های declarative

KafkaUser CRD ها امکان مدیریت declarative احراز هویت و مجوز رو می‌دن:

apiVersion: kafka.strimzi.io/v1
kind: KafkaUser
metadata:
  name: event-producer
spec:
  authentication:
    type: scram-sha-512
  authorization:
    type: simple
    acls:
      - resource:
          type: topic
          name: "*"
          patternType: literal
        operations: [Write, Create, Describe]

Operator credential های SCRAM می‌سازه، اون‌ها رو در یه Secret به اسم user ذخیره می‌کنه و ACL ها رو با cluster live همگام نگه می‌داره. لغو دسترسی یه user یه kubectl delete kafkauserه. Operator credential ها و ACL ها رو حذف می‌کنه. نیازی به ویرایش تنظیمات broker یا rotation دستی پسورد نیست.

یه رفتار که باید بدونید: rename کردن یه KafkaUser نیاز به حذف صریح CR قدیمی داره. kubectl apply با یه نام جدید یه user جدید می‌سازه؛ قدیمی رو حذف نمی‌کنه. User قدیمی با credential های live کار می‌کنه تا CR قدیمی رو حذف کنید. این رفتار درسته، اما می‌تونه در هنگام cleanup به ghost user هایی با SCRAM credential های فعال منجر بشه.

چی operator ها درست نمی‌کنن

Operator ها toil رو کم می‌کنن؛ از بین نمی‌برن.

تغییرات SGInstanceProfile در StackGres کاملاً propagate نمی‌شن. Operator می‌تونه sidecar resource request ها رو cache کنه و موقع تغییر CPU یا memory یه تضاد requests > limits ایجاد کنه. تصحیح اینه که profile رو حذف و دوباره بسازید تا از صفر محاسبه بشه. این رفتار شناخته‌شده‌ست نه باگ، اما فرض “فقط YAML رو آپدیت کن” روی تغییرات resource شکست می‌خوره.

PVC resize در StackGres یه ترتیب مشخص می‌خواد: cluster spec رو آپدیت کنید قبل از اینکه pod ها رو حذف کنید. اگه pod رو قبل از آپدیت spec حذف کنید، cluster controller داخل pod سایز قدیمی رو می‌خونه و سعی می‌کنه PVC رو کوچک‌تر کنه. این یه ۴۲۲ می‌گیره و controller crash-loop می‌کنه.

Anti-affinity broker های Strimzi ترکیب با volume های zone-local به این معنیه که یه node group دو-AZ fault tolerance واقعی نداره. از node failure محافظت می‌کنید، نه zone failure.

الگویی که پیدا می‌شه

بعد از استفاده از هر دو operator در یه environment rebuild، migration داده، و اولین هفته‌های ترافیک production-like، الگو واضحه. Operator ها هزینه‌شون رو توجیه می‌کنن، که complexity CRD واقعی و یه لایه debugging جدیده، دقیقاً روی عملیاتی که سخته یه بار درست انجام بشن و زیر فشار تقریباً غیرممکنه همیشه درست انجام بشن.

Backup restore با StackGres. Rotation certificate با Strimzi. Replica reinit بعد از یه state transition بد. اینا همون task هایی هستن که “یکی که این سیستم رو عمیقاً می‌شناسه یه controller براش نوشته” بیشتر ارزش داره از هر shell script خوش‌نیتی.

مزیت setup واقعیه اما متوسط. مزیت Day-2 با هر حادثه‌ای که می‌دونید بعدش operator دقیقاً چی می‌کنه، تجمیع می‌شه.

$ cat KUBERNETES .md
· 5 دقیقه مطالعه

کنترل‌پلین برای ایجنت‌ها: Omnigent روی Kubernetes

Databricks یه Meta-Harness به اسم Omnigent رو open source کرد که هر agent harness رو پشت یه API واحد با policy، session و state مشترک جمع می‌کنه. اگه قبلاً یه سرویس stateful روی Kubernetes deploy کردی، مسیر self-hosting این ابزار خیلی کوتاه‌تر از چیزیه که فکرش رو بکنی.

kubernetes devops platform-engineering ai-agents
$ cat KUBERNETES .md
· 6 دقیقه مطالعه

Helm Deploy موفق بود. Config هیچ‌وقت اعمال نشد.

یه CI pipeline سبز در حالی که gateway زنده 404 برمی‌گردونه، یه test failure نیست. یه شکاف reload خاموشه. اینجا pattern checksum چند-release ای هست که توی helmfile این مشکل رو حل می‌کنه.

kubernetes helm helmfile devops platform-engineering