Skip to content

فصل ۱۸ — سازمان‌دهی برنامه

در سراسر این کتاب اجزای مختلف یک برنامهٔ ساخته‌شده روی Kubernetes را توصیف کردیم. گفتیم چگونه برنامه‌ها را به‌صورت container بسته‌بندی کنید، آن containerها را در Pod قرار دهید، آن Podها را با ReplicaSetها تکرار کنید و هر هفته نرم‌افزار را با deploymentها rollout کنید. حتی توضیح دادیم چگونه برنامه‌های stateful و دنیای واقعی را مستقر کنید که مجموعه‌ای از این اشیاء را در یک سیستم توزیع‌شدهٔ واحد کنار هم می‌گذارند. اما نگفتیم چگونه در عمل با چنین برنامه‌ای کار کنید. چگونه می‌توانید پیکربندی‌های مختلفی که برنامهٔ شما را می‌سازند چیدمان، به اشتراک بگذارید، مدیریت و به‌روز کنید؟ موضوع این فصل همین است.

اصولی که ما را راهنما می‌کنند

قبل از ورود به جزئیات مشخص ساختار برنامه، ارزش دارد اهدافی که این ساختار را هدایت می‌کنند در نظر گرفته شوند. واضح است که قابلیت اطمینان و چابکی اهداف کلی توسعهٔ یک برنامهٔ cloud-native در Kubernetes هستند، اما وقتی به سطح جزئیات بعدی می‌رویم، این واقعاً چگونه به طراحی نگهداری و استقرار برنامهٔ شما مربوط می‌شود؟ بخش‌های بعدی اصول مختلفی را توصیف می‌کنند که می‌توانیم به‌عنوان راهنما برای طراحی ساختاری استفاده کنیم که این اهداف را بهترین برآورده کند. اصول عبارت‌اند از:

  • filesystem به‌عنوان منبع حقیقت
  • code review برای اطمینان از کیفیت تغییرات
  • feature flagها برای rollout و rollback مرحله‌ای

filesystem به‌عنوان منبع حقیقت

وقتی برای اولین بار Kubernetes را کاوش می‌کنید، همان‌طور که در ابتدای این کتاب کردیم، معمولاً به‌صورت imperative با آن تعامل دارید. دستوراتی مثل kubectl run یا kubectl edit اجرا می‌کنید تا Podها یا اشیاء دیگر در حال اجرا در کلاستر بسازید و تغییر دهید. حتی وقتی شروع به کاوش نوشتن و استفاده از فایل‌های YAML یا JSON کردیم، این به‌صورت ad-hoc ارائه شد، انگار فایل خود فقط یک ایستگاه میانی در مسیر تغییر state کلاستر است. در واقعیت، در یک برنامهٔ واقعاً productionized، برعکس باید درست باشد.

به‌جای دیدن state کلاستر — داده در etcd — به‌عنوان منبع حقیقت، بهینه است filesystem اشیاء YAML را منبع حقیقت برنامهٔ شما بدانید. اشیاء API مستقر در کلاستر(های) Kubernetes شما سپس بازتاب حقیقت ذخیره‌شده در filesystem هستند.

دلایل متعددی وجود دارد که چرا این دیدگاه درست است. اول و مهم‌ترین این است که تا حد زیادی به شما امکان می‌دهد کلاستر را انگار زیرساخت immutable در نظر بگیرید. با ورود به معماری‌های cloud-native، به‌طور فزاینده با این مفهوم که برنامه‌ها و containerهایشان زیرساخت immutable هستند راحت شده‌ایم، اما در نظر گرفتن کلاستر به‌عنوان چنین چیزی کمتر رایج است. و با این حال، همان دلایل انتقال برنامه‌ها به زیرساخت immutable برای کلاسترها هم صدق می‌کند. اگر کلاستر شما یک snowflake ساخته‌شده از اعمال ad-hoc فایل‌های YAML تصادفی دانلودشده از اینترنت باشد، به‌اندازهٔ یک ماشین مجازی ساخته‌شده از اسکریپت‌های bash imperative خطرناک است.

علاوه بر این، مدیریت state کلاستر از طریق filesystem همکاری با چند عضو تیم را بسیار آسان می‌کند. سیستم‌های source control به‌خوبی فهمیده شده‌اند و به‌راحتی می‌توانند به چند نفر مختلف همزمان اجازه دهند state کلاستر را ویرایش کنند در حالی که conflictها (و حل آن conflictها) برای همه روشن است.

ترکیب این انگیزه‌ها به این معناست که مطلقاً اصل اول این است که همهٔ برنامه‌های مستقر به Kubernetes ابتدا باید در فایل‌های ذخیره‌شده در filesystem توصیف شوند. اشیاء API واقعی سپس فقط تصویر این filesystem در یک کلاستر خاص هستند.

نقش code review

چند وقت پیش code review برای کد منبع برنامه ایدهٔ تازه‌ای بود. اما حالا روشن است که اینکه چند نفر قبل از commit شدن به برنامه به یک قطعه کد نگاه کنند، بهترین روش برای تولید کد باکیفیت و قابل اعتماد است.

بنابراین جالب است که همین تا حدی کمتر برای پیکربندی‌هایی که آن برنامه‌ها را مستقر می‌کنند درست است. همهٔ همان دلایل بررسی کد مستقیماً به پیکربندی‌های برنامه هم می‌خورد. اما وقتی به آن فکر می‌کنید، واضح است که code review این پیکربندی‌ها برای استقرار قابل اعتماد سرویس‌ها حیاتی است. در تجربهٔ ما، بیشتر outageهای سرویس خودزده هستند — از طریق پیامدهای غیرمنتظره، غلط‌های تایپی یا اشتباهات سادهٔ دیگر. اطمینان از اینکه حداقل دو نفر به هر تغییر پیکربندی نگاه کنند، احتمال چنین خطاهایی را به‌طور قابل توجهی کاهش می‌دهد.

در نتیجه، اصل دوم چیدمان برنامهٔ ما این است که باید بررسی هر تغییری که به مجموعهٔ فایل‌هایی merge می‌شود که منبع حقیقت کلاستر ما را نمایندگی می‌کند را تسهیل کند.

دروازه‌ها و محافظ‌های feature

وقتی کد منبع برنامه و فایل‌های پیکربندی استقرار در source control هستند، یکی از رایج‌ترین سؤالاتی که پیش می‌آید این است که این مخازن چگونه به هم مربوط می‌شوند. آیا باید از همان مخزن برای کد منبع برنامه و هم پیکربندی استفاده کنید؟ برای پروژه‌های کوچک می‌تواند جواب بدهد، اما در پروژه‌های بزرگ‌تر اغلب جداسازی کد منبع از پیکربندی برای جداسازی دغدغه‌ها منطقی است. حتی اگر همان افراد مسئول ساخت و استقرار برنامه باشند، دیدگاه سازنده در مقابل استقراردهنده به‌اندازه‌ای متفاوت است که این جداسازی دغدغه‌ها منطقی به نظر می‌رسد.

اگر چنین است، پس چگونه توسعهٔ featureهای جدید در source control را با استقرار آن featureها در محیط production پیوند می‌دهید؟ اینجاست که دروازه‌ها و محافظ‌های feature نقش مهمی بازی می‌کنند.

ایده این است که وقتی feature جدیدی توسعه داده می‌شود، آن توسعه کاملاً پشت یک feature flag یا gate انجام می‌شود. این gate چیزی شبیه این است:

javascript
if (featureFlags.myFlag) {
    // Feature implementation goes here
}

مزایای متنوعی برای این رویکرد وجود دارد. اول، امکان commit کردن کد به شاخهٔ production مدت‌ها قبل از آماده بودن feature برای ship را فراهم می‌کند. این به توسعهٔ feature اجازه می‌دهد بسیار نزدیک‌تر به HEAD یک مخزن بماند، و از conflictهای merge هولناک یک شاخهٔ بلندمدت اجتناب می‌کنید.

علاوه بر این، یعنی فعال‌سازی یک feature فقط یک تغییر پیکربندی برای فعال کردن flag است. این بسیار روشن می‌کند در محیط production چه چیزی تغییر کرده، و به همان اندازه rollback فعال‌سازی feature را در صورت بروز مشکل بسیار ساده می‌کند.

استفاده از feature flagها هم debugging مشکلات در production را ساده‌تر می‌کند و هم اطمینان می‌دهد غیرفعال کردن یک feature نیازی به rollback باینری به نسخهٔ قدیمی‌تر کد ندارد که همهٔ bug fixها و بهبودهای نسخهٔ جدیدتر را حذف کند.

اصل سوم چیدمان برنامه این است که کد به‌طور پیش‌فرض در source control می‌نشیند، خاموش، پشت یک feature flag، و فقط از طریق تغییر code-reviewed در فایل‌های پیکربندی فعال می‌شود.

مدیریت برنامه در source control

حالا که تعیین کردیم filesystem باید منبع حقیقت کلاستر شما را نمایندگی کند، سؤال مهم بعدی این است که واقعاً فایل‌ها را در filesystem چگونه بچینید. واضح است filesystemها دایرکتوری‌های سلسله‌مراتبی دارند، و سیستم source control مفاهیمی مثل tag و branch اضافه می‌کند، بنابراین این بخش توضیح می‌دهد چگونه این‌ها را کنار هم بگذارید تا برنامهٔ خود را نمایندگی و مدیریت کنید.

چیدمان filesystem

برای اهداف این بخش، توضیح می‌دهیم چگونه یک نمونه از برنامهٔ خود را برای یک کلاستر بچینید. در بخش‌های بعدی توضیح می‌دهیم چگونه این چیدمان را برای چند نمونه از برنامه پارامتریزه کنید. ارزش دارد توجه کنید وقتی شروع می‌کنید گرفتن این سازماندهی درست مهم است. مثل تغییر چیدمان packageها در source control، تغییر پیکربندی‌های استقرار بعداً refactor پیچیده و پرهزینه‌ای است که احتمالاً هرگز انجام نمی‌دهید.

اولین cardinality که می‌خواهید برنامه را بر اساس آن سازماندهی کنید، component یا لایهٔ معنایی است (مثلاً frontend، صف کار batch و غیره). هرچند در ابتدا ممکن است زیاده‌روی به نظر برسد، چون یک تیم همهٔ این componentها را مدیریت می‌کند، زمینه را برای scale تیم آماده می‌کند — در نهایت ممکن است تیم (یا زیرتیم) متفاوتی مسئول هر component باشد.

بنابراین برای برنامه‌ای با frontend که از دو سرویس استفاده می‌کند، filesystem ممکن است شبیه این باشد:

frontend/
service-1/
service-2/

در هر یک از این دایرکتوری‌ها، پیکربندی‌های هر برنامه ذخیره می‌شوند. این‌ها فایل‌های YAML هستند که مستقیماً state فعلی کلاستر را نمایندگی می‌کنند. معمولاً مفید است هم نام سرویس و هم نوع شیء در همان فایل باشد.

اگرچه Kubernetes اجازهٔ ساخت فایل‌های YAML با چند شیء در همان فایل را می‌دهد، این معمولاً باید anti-pattern در نظر گرفته شود. تنها دلیل خوب برای گروه‌بندی چند شیء در همان فایل این است که از نظر مفهومی یکسان باشند. وقتی تصمیم می‌گیرید چه چیزی در یک فایل YAML باشد، اصول طراحی مشابه تعریف class یا struct را در نظر بگیرید. اگر گروه‌بندی اشیاء با هم یک مفهوم واحد نمی‌سازد، احتمالاً نباید در یک فایل باشند.

بنابراین با گسترش مثال قبلی، filesystem ممکن است شبیه این باشد:

frontend/
    frontend-deployment.yaml
    frontend-service.yaml
    frontend-ingress.yaml
service-1/
    service-1-deployment.yaml
    service-1-service.yaml
    service-1-configmap.yaml
...

مدیریت نسخه‌های دوره‌ای

بخش قبلی ساختار فایلی برای چیدن لایه‌های مختلف برنامه را توصیف کرد، اما مدیریت releaseهای برنامه چطور؟ بسیار مفید است بتوانید به‌صورت تاریخی نگاه کنید و ببینید استقرار برنامه قبلاً چگونه بوده. به همان اندازه مفید است بتوانید یک پیکربندی را به جلو iterate کنید در حالی که هنوز بتوانید پیکربندی release پایدار مستقر کنید.

در نتیجه، handy است بتوانید همزمان چند revision مختلف از پیکربندی را ذخیره و نگه‌داری کنید. با رویکرد فایل و version control، دو رویکرد مختلف می‌توانید استفاده کنید. اولی استفاده از tag، branch و ویژگی‌های source control است. این راحت است چون به همان روشی که مردم revisionها را در source control مدیریت می‌کنند map می‌شود، و به ساختار دایرکتوری ساده‌تری منجر می‌شود. گزینهٔ دیگر clone کردن پیکربندی در filesystem و استفاده از دایرکتوری‌ها برای revisionهای مختلف است. این رویکرد راحت است چون مشاهدهٔ همزمان پیکربندی‌ها را بسیار ساده می‌کند.

در واقعیت، رویکردها بیش یا کم یکسان‌اند، و در نهایت انتخاب زیبایی‌شناختی بین دو تاست. بنابراین هر دو را بحث می‌کنیم و شما یا تیمتان تصمیم بگیرید کدام را ترجیح می‌دهید.

نسخه‌بندی با branch و tag

وقتی از branch و tag برای مدیریت revisionهای پیکربندی استفاده می‌کنید، ساختار دایرکتوری از مثال بخش قبلی تغییر نمی‌کند. وقتی برای release آماده‌اید، یک tag source control (مثلاً git tag v1.0) در سیستم source control پیکربندی می‌گذارید. tag نمایندهٔ پیکربندی استفاده‌شده برای آن نسخه است، و HEAD source control به iterate کردن به جلو ادامه می‌دهد.

دنیا وقتی باید پیکربندی release را به‌روز کنید کمی پیچیده‌تر می‌شود، اما رویکرد همان چیزی را مدل می‌کند که در source control انجام می‌دهید. اول تغییر را به HEAD مخزن commit می‌کنید. سپس branch جدیدی به نام v1 در tag v1.0 می‌سازید. سپس تغییر مورد نظر را روی شاخهٔ release cherry-pick می‌کنید (git cherry-pick <edit>)، و در نهایت این branch را با tag v1.1 tag می‌کنید تا point release جدید را نشان دهد.

یک خطای رایج هنگام cherry-pick کردن fixها به شاخهٔ release این است که فقط تغییر را به آخرین release بردارند. ایدهٔ خوبی است آن را به همهٔ releaseهای فعال cherry-pick کنید، در صورتی که به هر دلیلی نیاز به rollback نسخه‌ها داشته باشید اما fix هنوز لازم باشد.

نسخه‌بندی با دایرکتوری‌ها

جایگزین استفاده از ویژگی‌های source control، استفاده از ویژگی‌های filesystem است. در این رویکرد، هر استقرار نسخه‌دار در دایرکتوری خودش وجود دارد. مثلاً filesystem برنامهٔ شما ممکن است شبیه این باشد:

frontend/
  v1/
    frontend-deployment.yaml
    frontend-service.yaml
  current/
    frontend-deployment.yaml
    frontend-service.yaml
service-1/
  v1/
      service-1-deployment.yaml
      service-1-service.yaml
  v2/
      service-1-deployment.yaml
      service-1-service.yaml
  current/
      service-1-deployment.yaml
      service-1-service.yaml
...

بنابراین هر revision در ساختار دایرکتوری موازی در دایرکتوری مرتبط با release وجود دارد. همهٔ استقرارها از HEAD انجام می‌شوند نه از revision یا tagهای مشخص. وقتی پیکربندی جدید اضافه می‌شود، روی فایل‌های دایرکتوری current انجام می‌شود.

وقتی release جدید می‌سازید، دایرکتوری current کپی می‌شود تا دایرکتوری جدید مرتبط با release جدید ساخته شود.

وقتی تغییر bugfix روی یک release انجام می‌دهید، pull request باید فایل YAML را در همهٔ دایرکتوری‌های release مرتبط تغییر دهد. این تجربهٔ کمی بهتر از رویکرد cherry-pick توصیف‌شده قبلی است، چون در یک change request واحد روشن است همهٔ نسخه‌های مرتبط با همان تغییر به‌روز می‌شوند، به‌جای نیاز به cherry-pick به ازای هر نسخه.

ساختاردهی برنامه برای توسعه، آزمایش و استقرار

علاوه بر ساختاردهی برنامه برای cadence release دوره‌ای، می‌خواهید برنامه را برای توسعهٔ چابک، آزمایش باکیفیت و استقرار ایمن ساختار دهید. این به توسعه‌دهندگان اجازه می‌دهد به‌سرعت تغییرات را روی برنامهٔ توزیع‌شده بسازند و آزمایش کنند، و آن تغییرات را به‌ایمنی به مشتریان rollout کنند.

اهداف

دو هدف برای برنامهٔ شما دربارهٔ توسعه و آزمایش وجود دارد. اول اینکه هر توسعه‌دهنده بتواند به‌راحتی featureهای جدید برای برنامه بسازد. در بیشتر موارد، توسعه‌دهنده فقط روی یک component کار می‌کند، و با این حال آن component به همهٔ microserviceهای دیگر در کلاستر متصل است. بنابراین برای تسهیل توسعه ضروری است توسعه‌دهندگان بتوانند در محیط خودشان کار کنند، اما با همهٔ سرویس‌ها در دسترس.

هدف دیگر ساختاردهی برنامه برای آزمایش، توانایی آزمایش آسان و دقیق برنامه قبل از استقرار است. این برای rollout سریع featureها در حالی که قابلیت اطمینان بالا حفظ می‌شود ضروری است.

پیشرفت یک release

برای دستیابی به هر دو هدف، مهم است مراحل توسعه را به نسخه‌های release توصیف‌شده قبلی مرتبط کنید. مراحل یک release عبارت‌اند از:

HEAD : لبهٔ تیز پیکربندی؛ آخرین تغییرات.

Development : تا حد زیادی پایدار، اما آمادهٔ استقرار نیست. مناسب برای توسعه‌دهندگان جهت ساخت featureها.

Staging : آغاز آزمایش، بعید است تغییر کند مگر مشکلی پیدا شود.

Canary : اولین release واقعی به کاربران، برای آزمایش مشکلات با ترافیک دنیای واقعی و همچنین به کاربران فرصت آزمایش آنچه بعد می‌آید.

Release : release فعلی production.

معرفی tag توسعه

صرف‌نظر از اینکه releaseها را با filesystem یا version control ساختار می‌دهید، روش درست مدل‌سازی مرحلهٔ development از طریق tag source control است. دلیلش این است که development لزوماً سریع‌حرکت است چون فقط کمی پشت HEAD از پایداری عقب می‌ماند.

برای معرفی مرحلهٔ development، tag جدید development به سیستم source control اضافه می‌شود و فرایند خودکار برای جلو بردن این tag استفاده می‌شود. در cadence دوره‌ای، HEAD از طریق آزمایش integration خودکار آزمایش می‌شود. اگر این آزمایش‌ها pass شوند، tag development به HEAD جلو برده می‌شود. بنابراین توسعه‌دهندگان می‌توانند نسبتاً نزدیک به آخرین تغییرات هنگام استقرار محیط‌های خودشان بمانند، اما همچنین مطمئن باشند پیکربندی‌های مستقرشده حداقل یک smoke test محدود را pass کرده‌اند.

نگاشت مراحل به revisionها

ممکن است وسوسه‌انگیز باشد مجموعهٔ جدیدی از پیکربندی‌ها برای هر یک از این مراحل معرفی کنید، اما در واقعیت، حاصلضرب دکارتی نسخه‌ها و مراحل messی می‌سازد که استدلال دربارهٔ آن بسیار سخت است. به‌جای آن، روش درست معرفی نگاشت بین revisionها و مراحل است.

صرف‌نظر از اینکه از revisionهای filesystem یا source control برای نمایندگی نسخه‌های مختلف پیکربندی استفاده می‌کنید، پیاده‌سازی map از stage به revision آسان است. در مورد filesystem می‌توانید از symbolic link برای نگاشت نام stage به revision استفاده کنید:

frontend/
   canary/ -> v2/
   release/ -> v1/
   v1/
     frontend-deployment.yaml
...

در مورد version control، فقط یک tag اضافی در همان revision نسخهٔ مناسب است.

در هر دو مورد، نسخه‌بندی releaseها با فرایندهای توصیف‌شده قبلی پیش می‌رود، و جداگانه مراحل در صورت لزوم به نسخه‌های جدید جلو برده می‌شوند. عملاً یعنی دو فرایند همزمان وجود دارد: اول برای برش نسخه‌های release جدید و دوم برای واجد شرایط کردن یک نسخهٔ release برای stage خاصی در lifecycle برنامه.

پارامتریزه کردن برنامه با templateها

وقتی حاصلضرب دکارتی محیط‌ها و مراحل را دارید، روشن می‌شود نگه‌داشتن همهٔ آن‌ها کاملاً یکسان غیرعملی یا غیرممکن است. و با این حال، مهم است تلاش کنید محیط‌ها تا حد ممکن یکسان بمانند. واریانس و drift بین محیط‌های مختلف snowflake و سیستم‌هایی می‌سازد که استدلال دربارهٔ آن‌ها سخت است. اگر محیط staging با محیط release متفاوت باشد، واقعاً می‌توانید به load testهایی که در staging اجرا کردید برای واجد شرایط کردن release اعتماد کنید؟ برای اطمینان از اینکه محیط‌ها تا حد ممکن مشابه بمانند، مفید است از محیط‌های پارامتریزه‌شده استفاده کنید. محیط‌های پارامتریزه‌شده از template برای بخش عمدهٔ پیکربندی استفاده می‌کنند، اما مجموعهٔ محدودی از پارامترها را مخلوط می‌کنند تا پیکربندی نهایی تولید شود. به این ترتیب بیشتر پیکربندی در template مشترک است، در حالی که پارامتریزه‌سازی محدود به دامنه است و در فایل پارامترهای کوچک برای visualization آسان تفاوت‌های بین محیط‌ها نگه‌داری می‌شود.

پارامتریزه‌سازی با Helm و templateها

زبان‌های مختلفی برای ساخت پیکربندی‌های پارامتریزه‌شده وجود دارد. به‌طور کلی همهٔ آن‌ها فایل‌ها را به فایل template که بخش عمدهٔ پیکربندی را دارد، و فایل parameters که می‌تواند با template ترکیب شود تا پیکربندی کامل تولید شود، تقسیم می‌کنند. علاوه بر پارامترها، بیشتر زبان‌های templating اجازه می‌دهند پارامترها مقادیر پیش‌فرض داشته باشند اگر مقداری مشخص نشده باشد.

در ادامه مثال‌هایی از پارامتریزه کردن پیکربندی‌ها با Helm، یک package manager برای Kubernetes، می‌آید. صرف‌نظر از آنچه پیروان زبان‌های مختلف ممکن است بگویند، همهٔ زبان‌های پارامتریزه‌سازی تا حد زیادی معادل‌اند، و مثل زبان‌های برنامه‌نویسی، کدام را ترجیح می‌دهید تا حد زیادی مسئلهٔ سبک شخصی یا تیمی است. بنابراین همان الگوهای توصیف‌شده اینجا برای Helm صرف‌نظر از زبان templatingی که انتخاب می‌کنید اعمال می‌شوند.

زبان template Helm از syntax «mustache» استفاده می‌کند، بنابراین مثلاً:

yaml
metadata:
  name: {{ .Release.Name }}-deployment

نشان می‌دهد Release.Name باید در نام deployment جایگزین شود.

برای پاس دادن پارامتر برای این مقدار از فایل values.yaml با محتوایی مثل این استفاده می‌کنید:

yaml
Release:
  Name: my-release

که پس از جایگزینی پارامتر نتیجه می‌دهد:

yaml
metadata:
  name: my-release-deployment

چیدمان filesystem برای پارامتریزه‌سازی

حالا که می‌دانید چگونه پیکربندی‌ها را پارامتریزه کنید، چگونه آن را به چیدمان‌های filesystem که قبلاً توصیف کردیم اعمال می‌کنید؟ برای دستیابی به این، به‌جای اینکه هر stage lifecycle استقرار را اشاره‌گر به یک نسخه بدانید، هر lifecycle استقرار ترکیبی از فایل parameters و اشاره‌گر به نسخهٔ مشخص است. مثلاً در چیدمان مبتنی بر دایرکتوری ممکن است شبیه این باشد:

frontend/
  staging/
    templates -> ../v2
    staging-parameters.yaml
  production/
    templates -> ../v1
    production-parameters.yaml
  v1/
    frontend-deployment.yaml
    frontend-service.yaml
  v2/
    frontend-deployment.yaml
    frontend-service.yaml
...

انجام این با version control شبیه است، به‌جز اینکه پارامترهای هر stage lifecycle در root درخت دایرکتوری پیکربندی نگه‌داری می‌شوند:

frontend/
  staging-parameters.yaml
  templates/
     frontend-deployment.YAML
...

استقرار برنامه در سراسر جهان

حالا که چند نسخه از برنامه از چند stage استقرار عبور می‌کنند، گام نهایی در ساختاردهی پیکربندی‌ها استقرار برنامه در سراسر جهان است. اما فکر نکنید این رویکردها فقط برای برنامه‌های در مقیاس بزرگ‌اند. در واقعیت، می‌توانند از دو region مختلف تا ده‌ها یا صدها region در سراسر جهان scale شوند. در دنیای cloud، جایی که یک region کامل می‌تواند از کار بیفتد، استقرار در چند region (و مدیریت آن استقرار) تنها راه دستیابی به uptime کافی برای کاربران پرتوقع است.

معماری‌ها برای استقرار جهانی

به‌طور کلی، هر کلاستر Kubernetes قرار است در یک region واحد زندگی کند، و انتظار می‌رود هر کلاستر Kubernetes یک استقرار کامل و واحد از برنامهٔ شما را داشته باشد. در نتیجه، استقرار جهانی یک برنامه شامل چند کلاستر Kubernetes مختلف است، هر کدام با پیکربندی برنامهٔ خودش.

توصیف چگونگی ساخت واقعی یک برنامهٔ جهانی، به‌ویژه با موضوعات پیچیده‌ای مثل replication داده، خارج از محدودهٔ این فصل است، اما توضیح می‌دهیم چگونه پیکربندی‌های برنامه را در filesystem بچینید.

در نهایت، پیکربندی یک region خاص از نظر مفهومی همان stage در lifecycle استقرار است. بنابراین اضافه کردن چند region به پیکربندی شما با اضافه کردن stageهای lifecycle جدید یکسان است. مثلاً به‌جای:

  • Development
  • Staging
  • Canary
  • Production

ممکن است داشته باشید:

  • Development
  • Staging
  • Canary
  • EastUS
  • WestUS
  • Europe
  • Asia

مدل‌سازی این در filesystem برای پیکربندی شبیه این است:

frontend/
  staging/
    templates -> ../v3/
    parameters.yaml
  eastus/
    templates -> ../v1/
    parameters.yaml
  westus/
    templates -> ../v2/
    parameters.yaml
  ...

اگر به‌جای آن از version control و tag استفاده می‌کنید، filesystem شبیه این خواهد بود:

frontend/
  staging-parameters.yaml
  eastus-parameters.yaml
  westus-parameters.yaml
  templates/
    frontend-deployment.yaml
...

با این ساختار، برای هر region tag جدید معرفی می‌کنید و از محتوای فایل در آن tag برای استقرار در آن region استفاده می‌کنید.

پیاده‌سازی استقرار جهانی

حالا که پیکربندی برای هر region در سراسر جهان دارید، سؤال این می‌شود چگونه آن regionهای مختلف را به‌روز کنید. یکی از اهداف اصلی استفاده از چند region اطمینان از قابلیت اطمینان و uptime بسیار بالا است. در حالی که وسوسه‌انگیز است فرض کنیم outageهای cloud و دیتاسنتر علل اصلی downtime هستند، حقیقت این است که outageها معمولاً از rollout نسخه‌های جدید نرم‌افزار ناشی می‌شوند. به همین دلیل، کلید یک سیستم با availability بالا محدود کردن اثر یا «شعاع انفجار» هر تغییری است که ممکن است انجام دهید. بنابراین، هنگام rollout یک نسخه در regionهای مختلف، منطقی است با دقت از region به region حرکت کنید تا در یک region اعتبارسنجی کنید و قبل از رفتن به بعدی اطمینان بگیرید.

rollout نرم‌افزار در سراسر جهان معمولاً بیشتر شبیه workflow است تا یک به‌روزرسانی declarative واحد: با به‌روز کردن نسخه در staging به آخرین نسخه شروع می‌کنید و سپس از همهٔ regionها عبور می‌کنید تا همه‌جا rollout شود. اما چگونه باید regionهای مختلف را ساختار دهید، و چقدر باید بین regionها برای اعتبارسنجی صبر کنید؟

برای تعیین مدت زمان بین rolloutها به regionها، می‌خواهید «میانگین زمان تا دود» (mean time to smoke) نرم‌افزار را در نظر بگیرید. این زمانی است که به‌طور میانگین پس از rollout release جدید به یک region طول می‌کشد تا مشکل (اگر وجود داشته باشد) کشف شود. واضح است هر مشکل منحصربه‌فرد است و زمان متفاوتی می‌تواند طول بکشد تا خود را نشان دهد، و به همین دلیل می‌خواهید زمان میانگین را بفهمید. مدیریت نرم‌افزار در مقیاس کسب‌وکار احتمال است، نه قطعیت، بنابراین می‌خواهید زمانی صبر کنید که احتمال خطا را به‌قدری پایین کند که برای رفتن به region بعدی راحت باشید. چیزی شبیه دو تا سه برابر mean time to smoke احتمالاً نقطهٔ شروع معقولی است، اما بسته به برنامهٔ شما بسیار متغیر است.

برای تعیین ترتیب regionها، مهم است ویژگی‌های regionهای مختلف را در نظر بگیرید. مثلاً احتمالاً regionهای پرترافیک و کم‌ترافیک دارید. بسته به برنامهٔ شما، ممکن است featureهایی داشته باشید که در یک منطقهٔ جغرافیایی محبوب‌تر از دیگری است. همهٔ این ویژگی‌ها باید هنگام تهیهٔ برنامهٔ release در نظر گرفته شوند. احتمالاً می‌خواهید با rollout به یک region کم‌ترافیک شروع کنید. این اطمینان می‌دهد هر مشکل زودهنگامی که می‌گیرید به منطقه‌ای با تأثیر کم محدود می‌شود. هرچند قانون سخت‌وگیرانه نیست، مشکلات زودهنگام اغلب شدیدترین‌اند، چون به‌قدری سریع خود را نشان می‌دهند که در اولین regionی که rollout می‌کنید گرفته شوند. بنابراین کمینه کردن تأثیر چنین مشکلاتی روی مشتریان منطقی است. بعد، احتمالاً می‌خواهید به یک region پرترافیک rollout کنید. وقتی با موفقیت اعتبارسنجی کردید release در region کم‌ترافیک درست کار می‌کند، می‌خواهید اعتبارسنجی کنید در مقیاس هم درست کار می‌کند. تنها راه این است rollout به یک region پرترافیک واحد. وقتی با موفقیت به هر دو region کم‌ترافیک و پرترافیک rollout کردید، ممکن است اطمینان داشته باشید برنامه می‌تواند همه‌جا به‌ایمنی rollout شود. با این حال، اگر واریانس‌های منطقه‌ای وجود دارد، ممکن است بخواهید آهسته‌تر در جغرافیاهای مختلف آزمایش کنید قبل از rollout گسترده‌تر release.

وقتی برنامهٔ release را کنار هم می‌گذارید، مهم است برای هر release، هرچقدر بزرگ یا کوچک، کاملاً آن را دنبال کنید. outageهای زیادی از شتاب دادن releaseها ایجاد شده — یا برای رفع مشکل دیگر، یا چون «ایمن» باور شده بود.

dashboardها و مانیتورینگ برای استقرارهای جهانی

وقتی در مقیاس کوچک توسعه می‌دهید ممکن است مفهوم عجیبی به نظر برسد، اما یکی از مشکلات قابل توجهی که احتمالاً در مقیاس متوسط یا بزرگ با آن روبه‌رو می‌شوید داشتن نسخه‌های مختلف برنامه مستقر در regionهای مختلف است. این به دلایل مختلف می‌تواند رخ دهد (مثلاً چون release در region خاصی شکست خورده، لغو شده یا مشکل داشته)، و اگر چیزها را با دقت track نکنید به‌سرعت به snowflake غیرقابل مدیریتی از نسخه‌های مختلف مستقر در سراسر جهان می‌رسید.

علاوه بر این، وقتی مشتریان دربارهٔ fix باگ‌هایی که تجربه می‌کنند سؤال می‌کنند، سؤال رایجی می‌شود: «آیا مستقر شده؟»

بنابراین ضروری است dashboardهایی بسازید که یک نگاه به شما بگویند کدام نسخه در کدام region در حال اجراست، و همچنین alerting که وقتی نسخه‌های فعال زیادی از برنامه مستقر شده‌اند fire کند. بهترین روش محدود کردن تعداد نسخه‌های فعال به حداکثر سه است: یکی در حال آزمایش، یکی در حال rollout، و یکی در حال جایگزینی توسط rollout. بیش از این نسخهٔ فعال درخواست دردسر است.

خلاصه

این فصل راهنمایی دربارهٔ مدیریت یک برنامهٔ Kubernetes از طریق نسخه‌های نرم‌افزار، stageهای استقرار و regionها در سراسر جهان ارائه می‌دهد. اصولی را برجسته می‌کند که پایهٔ سازماندهی برنامه‌اند: تکیه بر filesystem برای سازماندهی، استفاده از code review برای اطمینان از تغییرات باکیفیت، و تکیه بر feature flagها یا gateها برای افزودن و حذف تدریجی قابلیت‌ها.

مثل همه چیز، دستورالعمل‌های این فصل باید به‌عنوان الهام گرفته شوند، نه حقیقت مطلق. راهنما را بخوانید و ترکیبی از رویکردها را پیدا کنید که برای شرایط خاص برنامهٔ شما بهترین کار را بکند. اما به خاطر داشته باشید در چیدمان برنامه برای استقرار، فرایندی را تعیین می‌کنید که احتمالاً سال‌ها با آن زندگی خواهید کرد.