Skip to content

فصل ۱۹ — تحویل کد زیرساخت

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

اما استفاده از کد برای تعریف زیرساخت فرصتی ایجاد می‌کند تا تغییرات را با فرایند جامع‌تری مدیریت کنید. تغییر در سیستمی که دستی ساخته شده ممکن است تکرار آن در محیط توسعه بی‌معنا به نظر برسد — مثلاً تغییر مقدار RAM یک سرور. اما وقتی تغییر در کد پیاده‌سازی شود، می‌توانید به‌راحتی آن را در مسیر تا production با pipeline rollout کنید (به «Pipelineهای تحویل زیرساخت» در صفحه ۱۱۹ مراجعه کنید). این کار نه‌تنها مشکل خود تغییر را — که ممکن است پیش‌پاافتاده به نظر برسد (افزودن RAM ریسک شکستن چیزی را بسیار کم دارد) — بلکه هر مشکلی در فرایند اعمال تغییر را هم می‌گیرد. همچنین تضمین می‌کند همه محیط‌ها در مسیر تا production به‌طور یکنواخت پیکربندی شده‌اند.

تحویل کد زیرساخت

استعاره pipeline توصیف می‌کند چگونه تغییر در کد زیرساخت از فردی که تغییر می‌دهد تا نمونه‌های production پیش می‌رود. فعالیت‌های لازم برای این فرایند تحویل بر نحوه سازمان‌دهی codebase تأثیر می‌گذارد.

pipeline برای تحویل نسخه‌های کد چند نوع فعالیت دارد، از جمله build، promote، apply و validate. هر مرحله مشخص در pipeline ممکن است شامل چند فعالیت باشد، همان‌طور که در شکل ۱۹-۱ نشان داده شده.

شکل ۱۹-۱. فازهای تحویل پروژه کد زیرساخت

Building (ساخت) کد را برای استفاده آماده و برای فازهای دیگر در دسترس قرار می‌دهد. ساخت معمولاً یک‌بار در pipeline انجام می‌شود، هر بار که کد منبع تغییر کند.

Promoting (ارتقا) نسخه‌ای از کد را بین مراحل تحویل جابه‌جا می‌کند، همان‌طور که در «تست تدریجی» در صفحه ۱۱۵ توضیح داده شده. مثلاً وقتی نسخه پروژه stack از مرحله تست stack عبور کرد، ممکن است برای نشان دادن آمادگی مرحله تست یکپارچه‌سازی سیستم ارتقا یابد.

Applying (اعمال) ابزار مربوط را اجرا می‌کند تا کد را روی نمونه زیرساخت مربوط اعمال کند. نمونه می‌تواند محیط تحویل برای فعالیت‌های تست یا نمونه production باشد.

«Pipelineهای تحویل زیرساخت» در صفحه ۱۱۹ جزئیات بیشتری درباره طراحی pipeline دارد. اینجا روی ساخت و ارتقای کد تمرکز داریم.

ساخت پروژه زیرساخت

ساخت پروژه زیرساخت کد را برای استفاده آماده می‌کند. فعالیت‌ها می‌توانند شامل موارد زیر باشند:

  • بازیابی وابستگی‌های build-time، مانند کتابخانه‌ها، از جمله از پروژه‌های دیگر در codebase و کتابخانه‌های خارجی
  • حل پیکربندی build-time، مثلاً وارد کردن مقادیر پیکربندی مشترک بین چند پروژه
  • کامپایل یا تبدیل کد، مانند تولید فایل‌های پیکربندی از قالب‌ها
  • اجرای تست‌ها، شامل تست‌های offline و online («مراحل تست Offline برای Stackها» در صفحه ۱۳۱ و «مراحل تست Online برای Stackها» در صفحه ۱۳۴)
  • آماده‌سازی کد برای استفاده، قرار دادن آن در قالبی که ابزار زیرساخت مربوط برای اعمال استفاده می‌کند
  • در دسترس قرار دادن کد برای استفاده

چند روش مختلف برای آماده‌سازی کد زیرساخت و در دسترس قرار دادن آن وجود دارد. بعضی ابزارها مستقیماً از روش‌های خاصی پشتیبانی می‌کنند، مانند قالب artifact استاندارد یا repository. دیگران به تیم‌هایی که ابزار را استفاده می‌کنند واگذار می‌کنند روش تحویل کد خود را پیاده‌سازی کنند.

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

با بعضی ابزارها، «آماده‌سازی کد برای استفاده» شامل جمع‌آوری فایل‌ها در فایل بسته با قالب مشخص، یعنی artifact، می‌شود. این فرایند با زبان‌های برنامه‌نویسی عمومی مانند Ruby (gems)، JavaScript (NPM) و Python (بسته‌های Python با نصب‌کننده pip) معمول است. قالب‌های بسته دیگر برای نصب فایل‌ها و برنامه‌ها برای سیستم‌عامل‌های خاص شامل .rpm، .deb، .msi و NuGet (Windows) است.

ابزارهای زیرساخت زیادی قالب بسته برای پروژه‌های کدشان ندارند. با این حال، بعضی تیم‌ها artifactهای خود را می‌سازند و کد stack یا کد سرور را در فایل‌های ZIP یا «tarball» (آرشیو tar فشرده با gzip) می‌بندند. بعضی تیم‌ها از قالب‌های بسته OS استفاده می‌کنند و مثلاً فایل‌های RPM می‌سازند که فایل‌های Chef Cookbook را روی سرور استخراج می‌کنند. تیم‌های دیگر imageهای Docker می‌سازند که کد پروژه stack را همراه با executable ابزار stack شامل می‌شود.

تیم‌های دیگر کد زیرساخت را در artifact بسته‌بندی نمی‌کنند، به‌ویژه برای ابزارهایی که قالب بسته بومی ندارند. انجام یا نکردن این کار به نحوه publish، share و استفاده از کد بستگی دارد که به نوع repositoryیی که استفاده می‌کنند بستگی دارد.

استفاده از Repository برای تحویل کد زیرساخت

تیم‌ها از repository کد منبع برای ذخیره و مدیریت تغییرات کد منبع زیرساخت استفاده می‌کنند. اغلب repository جداگانه‌ای برای ذخیره کدی که آماده تحویل به محیط‌ها و نمونه‌هاست استفاده می‌کنند. بعضی تیم‌ها، همان‌طور که خواهیم دید، همان repository را برای هر دو هدف استفاده می‌کنند.

از نظر مفهومی، مرحله build این دو نوع repository را جدا می‌کند؛ کد را از repository کد منبع می‌گیرد، آن را می‌چیند و سپس در repository تحویل publish می‌کند (شکل ۱۹-۲ را ببینید).

شکل ۱۹-۲. مرحله build کد را در repository تحویل publish می‌کند

repository تحویل معمولاً چند نسخه از کد هر پروژه را ذخیره می‌کند. فازهای promotion، همان‌طور که به‌زودی توضیح داده می‌شود، نسخه‌های کد پروژه را علامت‌گذاری می‌کنند تا نشان دهند به کدام مرحله رسیده‌اند؛ مثلاً آماده تست یکپارچه‌سازی یا production.

فعالیت apply نسخه‌ای از کد پروژه را از repository تحویل می‌کشد و روی نمونه مشخصی اعمال می‌کند، مانند محیط SIT یا محیط PROD.

چند روش مختلف برای پیاده‌سازی repository تحویل وجود دارد. یک سیستم مشخص ممکن است پیاده‌سازی‌های repository متفاوتی برای انواع مختلف پروژه استفاده کند. مثلاً ممکن است repository مختص ابزار، مانند Chef Server، برای پروژه‌هایی که آن ابزار را استفاده می‌کنند به‌کار برود. همان سیستم ممکن است سرویس ذخیره فایل عمومی، مانند S3 bucket، برای پروژه‌ای با ابزاری مانند Packer که قالب بسته یا repository تخصصی ندارد استفاده کند.

چند نوع پیاده‌سازی repository کد تحویل وجود دارد.

Repository artifact تخصصی

بیشتر قالب‌های بسته‌ای که در بخش قبل بحث شد، محصول، سرویس یا استاندارد repository بسته دارند که چند محصول و سرویس ممکن است پیاده‌سازی کنند. چند محصول و سرویس میزبانی‌شده repository برای فایل‌های .rpm، .deb، .gem و .npm وجود دارد.

بعضی محصولات repository، مانند Artifactory و Nexus، از چند قالب بسته پشتیبانی می‌کنند. تیم‌هایی در سازمانی که یکی از این‌ها را اجرا می‌کند گاهی از آن‌ها برای ذخیره artifactهای خود، مانند فایل‌های ZIP و tarball، استفاده می‌کنند. بسیاری از پلتفرم‌های ابری repositoryهای artifact تخصصی دارند، مانند ذخیره image سرور.

پروژه ORAS (OCI Registry As Storage) راهی برای استفاده از repositoryهای artifact که اصلاً برای imageهای Docker طراحی شده‌اند به‌عنوان repository برای انواع دلخواه artifact ارائه می‌دهد.

اگر repository artifact از tag یا label پشتیبانی کند، می‌توانید برای promotion از آن‌ها استفاده کنید. مثلاً برای ارتقای artifact پروژه stack به مرحله تست یکپارچه‌سازی سیستم، ممکن است آن را با SIT_STAGE=true یا Stage=SIT tag کنید.

در غیر این صورت، ممکن است چند repository درون سرور repository بسازید، با یک repository برای هر مرحله تحویل. برای ارتقای artifact، آن را به repository مربوط کپی یا منتقل می‌کنید.

Repository مختص ابزار

بسیاری از ابزارهای زیرساخت repository تخصصی دارند که artifact بسته‌بندی‌شده در بر ندارد. در عوض، ابزاری اجرا می‌کنید که کد پروژه را به سرور آپلود می‌کند و نسخه‌ای به آن اختصاص می‌دهد. این تقریباً همان‌طور کار می‌کند که repository artifact تخصصی، اما بدون فایل بسته.

نمونه‌ها شامل Chef Server (self-hosted)، Chef Community Cookbooks (عمومی) و Terraform Registry (ماژول‌های عمومی) است.

Repository ذخیره فایل عمومی

بسیاری از تیم‌ها، به‌ویژه کسانی که قالب‌های خود را برای ذخیره پروژه‌های کد زیرساخت برای تحویل استفاده می‌کنند، آن‌ها را در سرویس یا محصول ذخیره فایل عمومی نگه می‌دارند. این می‌تواند file server، web server یا سرویس object storage باشد.

این repositoryها قابلیت خاصی برای مدیریت artifact، مانند شماره‌گذاری نسخه release، ارائه نمی‌دهند. پس خودتان شماره نسخه اختصاص می‌دهید، شاید با گنجاندن آن در نام فایل (مثلاً my-stack-v1.3.45.tgz). برای ارتقای artifact، ممکن است آن را به پوشه مرحله تحویل مربوط کپی یا لینک کنید.

تحویل کد از repository کد منبع

با توجه به اینکه کد منبع از قبل در repository منبع ذخیره شده و بسیاری از ابزارهای کد زیرساخت قالب بسته و toolchain برای treat کردن کدشان به‌عنوان release ندارند، بسیاری از تیم‌ها به‌سادگی کد را از repository کد منبع روی محیط‌ها اعمال می‌کنند.

اعمال کد از شاخه main (trunk) روی همه محیط‌ها مدیریت نسخه‌های مختلف کد را دشوار می‌کند. پس بیشتر تیم‌هایی که این کار را می‌کنند از branch استفاده می‌کنند و اغلب branch جداگانه‌ای برای هر محیط نگه می‌دارند. نسخه‌ای از کد را با merge به branch مربوط به محیط ارتقا می‌دهند. GitOps این روش را با همگام‌سازی مداوم ترکیب می‌کند (برای جزئیات بیشتر «اعمال مداوم کد» در صفحه ۳۵۰ و «GitOps» در صفحه ۳۵۱ را ببینید).

استفاده از branch برای ارتقای کد می‌تواند تمایز بین ویرایش کد و تحویل آن را محو کند. اصل اصلی CD این است که هرگز کد را بعد از مرحله build تغییر ندهید.¹ در حالی که تیم شما ممکن است متعهد شود هرگز کد را در branchها ویرایش نکند، اغلب حفظ این انضباط دشوار است.

یکپارچه‌سازی پروژه‌ها

همان‌طور که در «سازمان‌دهی پروژه‌ها و Repositoryها» در صفحه ۳۰۳ ذکر شد، پروژه‌ها در یک codebase معمولاً وابستگی بین یکدیگر دارند. سؤال بعدی این است که چه زمانی و چگونه نسخه‌های مختلف پروژه‌های وابسته به هم ترکیب شوند.

به‌عنوان مثال، چند پروژه از codebase تیم ShopSpinner را در نظر بگیرید. آن‌ها دو پروژه stack دارند. یکی از این‌ها، application_infrastructure-stack، زیرساخت مختص یک برنامه را تعریف می‌کند، از جمله pool ماشین‌های مجازی و قوانین load balancer برای ترافیک برنامه. پروژه stack دیگر، shared_network_stack، شبکه مشترک مشترک بین چند نمونه از application_infrastructure-stack را تعریف می‌کند، از جمله بلوک‌های آدرس (VPC و subnet) و قوانین firewall که اجازه ترافیک به سرورهای برنامه را می‌دهند.

تیم همچنین دو پروژه پیکربندی سرور دارد: tomcat-server که نرم‌افزار application server را پیکربندی و نصب می‌کند، و monitor-server که همین کار را برای agent مانیتورینگ انجام می‌دهد.

پنجمین پروژه زیرساخت، application-server-image، image سرور را با استفاده از ماژول‌های پیکربندی tomcat-server و monitor-server می‌سازد (شکل ۱۹-۳ را ببینید).

شکل ۱۹-۳. نمونه وابستگی‌ها بین پروژه‌های زیرساخت

¹ «فقط یک‌بار بسته بسازید.» به الگوهای CD جِز Humble مراجعه کنید.

پروژه application_infrastructure-stack زیرساخت خود را در ساختارهای شبکه‌ای که shared_network_stack ایجاد کرده می‌سازد. همچنین از imageهای سروری که پروژه application-server-image ساخته برای ایجاد سرورها در cluster سرور برنامه استفاده می‌کند. و application-server-image imageهای سرور را با اعمال تعاریف پیکربندی سرور در tomcat-server و monitor-server می‌سازد.

وقتی کسی تغییری در یکی از این پروژه‌های کد زیرساخت می‌دهد، نسخه جدیدی از کد پروژه ایجاد می‌شود. آن نسخه پروژه باید با نسخه‌ای از هر یک از پروژه‌های دیگر یکپارچه شود. نسخه‌های پروژه می‌توانند در build-time، delivery-time یا apply-time یکپارچه شوند.

پروژه‌های مختلف یک سیستم مشخص می‌توانند در نقاط مختلف یکپارچه شوند، همان‌طور که با مثال ShopSpinner در توصیف الگوهای زیر نشان داده شده.

Linking و یکپارچه‌سازی ترکیب عناصر یک برنامه کامپیوتری linking نامیده می‌شود. موازی‌هایی بین linking و یکپارچه‌سازی پروژه‌های زیرساخت همان‌طور که در این فصل توضیح داده شده وجود دارد. کتابخانه‌ها هنگام ساخت فایل executable به‌صورت statically linked هستند، مشابه یکپارچه‌سازی پروژه در build-time. کتابخانه‌های نصب‌شده روی ماشین هر بار که از executable فراخوانی می‌شود dynamically linked هستند، که تا حدی شبیه یکپارچه‌سازی پروژه در apply-time است. در هر دو حالت، provider یا consumer می‌تواند به نسخه متفاوتی در محیط runtime تغییر کند. قیاس ناقص است، چون تغییر برنامه رفتار executable را تحت تأثیر قرار می‌دهد، در حالی که تغییر زیرساخت (به‌طور کلی) وضعیت منابع را تحت تأثیر قرار می‌دهد.

الگو: یکپارچه‌سازی پروژه در Build-Time

الگوی یکپارچه‌سازی پروژه در build-time فعالیت‌های build را در چند پروژه انجام می‌دهد. این کار شامل یکپارچه‌سازی وابستگی‌ها بین آن‌ها و تنظیم نسخه‌های کد در پروژه‌هاست.

فرایند build اغلب شامل ساخت و تست هر یک از پروژه‌های مؤلفه قبل از ساخت و تست آن‌ها با هم است (شکل ۱۹-۴ را ببینید). آنچه این الگو را از جایگزین‌ها متمایز می‌کند این است که یا یک artifact واحد برای همه پروژه‌ها تولید می‌کند، یا مجموعه‌ای از artifactها که به‌عنوان گروه نسخه‌گذاری، ارتقا و اعمال می‌شوند.

شکل ۱۹-۴. نمونه یکپارچه‌سازی پروژه‌ها در build-time

در این مثال، یک مرحله build واحد image سرور را با استفاده از چند پروژه پیکربندی سرور تولید می‌کند.

مرحله build ممکن است چند گام داشته باشد، مانند ساخت و تست ماژول‌های پیکربندی سرور جداگانه. اما خروجی — image سرور — از کد همه پروژه‌های مؤلفه آن ساخته شده.

انگیزه (Motivation)

ساخت پروژه‌ها با هم مشکلات وابستگی را زود حل می‌کند. این کار بازخورد سریع درباره تعارض‌ها می‌دهد و سطح بالایی از سازگاری در codebase از طریق فرایند تحویل تا production ایجاد می‌کند. کد پروژه یکپارچه‌شده در build-time در کل چرخه تحویل سازگار است. همان نسخه کد در هر مرحله فرایند تا production اعمال می‌شود.

کاربرد (Applicability)

استفاده از این الگو در برابر یکی از جایگزین‌ها عمدتاً مسئله ترجیح است. به مجموعه trade-offهایی که ترجیح می‌دهید و توانایی تیم برای مدیریت پیچیدگی buildهای بین‌پروژه‌ای بستگی دارد.

پیامدها (Consequences)

ساخت و یکپارچه‌سازی چند پروژه در runtime پیچیده است، به‌ویژه برای تعداد بسیار زیاد پروژه. بسته به نحوه پیاده‌سازی build، ممکن است به زمان‌های بازخورد کندتر منجر شود.

استفاده از یکپارچه‌سازی پروژه در build-time در مقیاس به ابزار پیچیده برای orchestrate کردن buildها نیاز دارد. سازمان‌های بزرگ‌تری که این الگو را در codebaseهای بزرگ استفاده می‌کنند، مانند Google و Facebook، تیم‌هایی دارند که به نگهداری ابزار داخلی اختصاص دارند.

بعضی ابزارهای build برای ساخت تعداد بسیار زیاد پروژه نرم‌افزاری در دسترس هستند، همان‌طور که در بخش پیاده‌سازی بحث شد. اما این رویکرد در صنعت به‌اندازه ساخت جداگانه پروژه‌ها رایج نیست، پس ابزار و منابع مرجع کمتری برای کمک وجود دارد.

چون پروژه‌ها با هم ساخته می‌شوند، مرزها بین آن‌ها کمتر از الگوهای دیگر قابل مشاهده است. این ممکن است به coupling تنگ‌تر بین پروژه‌ها منجر شود. وقتی این اتفاق می‌افتد، سخت است تغییر کوچکی بدون تأثیر روی بخش‌های زیاد codebase بدهید و زمان و ریسک تغییرات افزایش می‌یابد.

پیاده‌سازی (Implementation)

ذخیره همه پروژه‌ها برای build در یک repository، اغلب monorepo نامیده می‌شود، ساخت آن‌ها با هم را با یکپارچه‌سازی نسخه‌گذاری کد در آن‌ها ساده می‌کند («Monorepo — یک Repository، یک Build» در صفحه ۳۰۵ را ببینید).

بیشتر ابزارهای build نرم‌افزار، مانند Gradle، Make، Maven، MSBuild و Rake، برای orchestrate کردن buildها در تعداد متوسط پروژه استفاده می‌شوند. اجرای build و تست در تعداد بسیار زیاد پروژه ممکن است زمان زیادی ببرد.

موازی‌سازی می‌تواند با ساخت و تست چند پروژه در thread، process یا حتی در compute grid این فرایند را تسریع کند. اما به منابع محاسباتی بیشتری نیاز دارد.

راه بهتر برای بهینه‌سازی build در مقیاس بزرگ استفاده از directed graph برای محدود کردن build و تست به بخش‌هایی از codebase که تغییر کرده است. اگر خوب انجام شود، باید زمان لازم برای build و تست بعد از commit را کاهش دهد تا فقط کمی بیشتر از اجرای build برای پروژه‌های جداگانه طول بکشد.

چند ابزار build تخصصی برای مدیریت build چندپروژه‌ای در مقیاس بسیار بزرگ طراحی شده‌اند. بیشتر این‌ها از ابزارهای داخلی ساخته‌شده در Google و Facebook الهام گرفته‌اند. بعضی از این ابزارها شامل Bazel، Buck، Pants و Please است.

جایگزین‌های یکپارچه‌سازی نسخه‌های پروژه در build-time انجام آن در delivery-time (به «الگو: یکپارچه‌سازی پروژه در Delivery-Time» در صفحه ۳۳۰) یا apply-time (به «الگو: یکپارچه‌سازی پروژه در Apply-Time» در صفحه ۳۳۳) است.

استراتژی مدیریت چند پروژه در یک repository («Monorepo — یک Repository، یک Build» در صفحه ۳۰۵)، اگرچه الگو نیست، از این الگو پشتیبانی می‌کند.

مثالی که برای این الگو استفاده کردم، شکل ۱۹-۴، هنگام ایجاد image سرور کد پیکربندی سرور را اعمال می‌کند («پخت Image سرور» در صفحه ۱۸۷ را ببینید). الگوی سرور immutable («الگو: Immutable Server» در صفحه ۱۹۵) نمونه دیگری از یکپارچه‌سازی build-time در برابر delivery-time است.

اگرچه در این کتاب به‌عنوان الگو مستند نشده، بسیاری از buildهای پروژه وابستگی‌ها به کتابخانه‌های third-party را در build-time حل می‌کنند، آن‌ها را دانلود و با deliverable می‌بندند. تفاوت این است که آن وابستگی‌ها با پروژه‌ای که از آن‌ها استفاده می‌کند ساخته و تست نمی‌شوند. وقتی آن وابستگی‌ها از پروژه‌های دیگر در همان سازمان می‌آیند، نمونه‌ای از یکپارچه‌سازی پروژه در delivery-time است (به «الگو: یکپارچه‌سازی پروژه در Delivery-Time» در صفحه ۳۳۰ مراجعه کنید).

آیا Monorepo است یا یکپارچه‌سازی پروژه در Build-Time؟ بیشتر توصیفات استراتژی monorepo برای سازمان‌دهی codebase شامل ساخت همه پروژه‌های repository با هم است — آنچه من یکپارچه‌سازی پروژه در build-time نامیدم. تصمیم گرفتم این الگو را monorepo ننامم چون آن نام به استفاده از یک repository کد منحصر به فرد وابسته است که گزینه پیاده‌سازی است. تیم‌هایی را دیده‌ام که چند پروژه را در یک repository مدیریت می‌کنند بدون ساخت آن‌ها با هم. اگرچه این تیم‌ها اغلب codebase خود را monorepo می‌نامند، از الگوی توصیف‌شده اینجا استفاده نمی‌کنند. از طرف دیگر، از نظر فنی ممکن است پروژه‌ها را از repositoryهای جدا checkout کنید و با هم بسازید. این با الگوی توصیف‌شده اینجا سازگار است چون نسخه‌های پروژه را در build-time یکپارچه می‌کند. با این حال، انجام این کار همبستگی و ردیابی نسخه‌های کد منبع تا buildها را پیچیده می‌کند، مثلاً هنگام debug مشکلات production.

الگو: یکپارچه‌سازی پروژه در Delivery-Time

با چند پروژه با وابستگی بین آن‌ها، یکپارچه‌سازی پروژه در delivery-time هر پروژه را جداگانه می‌سازد و تست می‌کند قبل از ترکیب آن‌ها. این رویکرد نسخه‌های کد را دیرتر از یکپارچه‌سازی build-time ترکیب می‌کند.

وقتی پروژه‌ها ترکیب و تست شدند، کدشان با هم در بقیه چرخه تحویل پیش می‌رود.

به‌عنوان مثال، پروژه application-infrastructure-stack ShopSpinner cluster ماشین‌های مجازی را با استفاده از image سرور تعریف‌شده در پروژه application-server-image تعریف می‌کند (شکل ۱۹-۵ را ببینید).

وقتی کسی تغییری در کد stack زیرساخت می‌دهد، pipeline تحویل پروژه stack را به‌تنهایی می‌سازد و تست می‌کند، همان‌طور که در فصل ۹ توضیح داده شد.

اگر نسخه جدید پروژه stack از آن تست‌ها عبور کند، به فاز تست یکپارچه‌سازی می‌رود که stack یکپارچه‌شده با آخرین image سروری که تست‌های خود را پاس کرده تست می‌کند. این مرحله نقطه یکپارچه‌سازی دو پروژه است. سپس نسخه‌های پروژه با هم به مراحل بعدی pipeline پیش می‌روند.

شکل ۱۹-۵. نمونه یکپارچه‌سازی پروژه‌ها در حین تحویل

انگیزه (Motivation)

ساخت و تست جداگانه پروژه‌ها قبل از یکپارچه‌سازی یکی از راه‌ها برای اعمال مرزهای روشن و coupling شل بین آن‌هاست.

مثلاً عضوی از تیم ShopSpinner قانون firewall در application-infrastructure-stack پیاده‌سازی می‌کند که پورت TCP تعریف‌شده در فایل پیکربندی در application-server-image را باز می‌کند. کدی می‌نویسد که شماره پورت را مستقیماً از آن فایل پیکربندی می‌خواند. اما وقتی کد را push می‌کند، مرحله تست stack fail می‌شود، چون فایل پیکربندی از پروژه دیگر روی build agent در دسترس نیست.

این شکست چیز خوبی است. coupling بین دو پروژه را آشکار می‌کند. عضو تیم می‌تواند کد را تغییر دهد تا از مقدار پارامتر برای شماره پورت بازشونده استفاده کند و مقدار را بعداً تنظیم کند (با یکی از الگوهای توصیف‌شده در فصل ۷). کد قابل نگهداری‌تر از codebase با ارجاع مستقیم به فایل‌ها در پروژه‌های مختلف خواهد بود.

کاربرد (Applicability)

یکپارچه‌سازی delivery-time وقتی مفید است که به مرزهای روشن بین پروژه‌ها در codebase نیاز دارید، اما همچنان می‌خواهید نسخه‌های هر پروژه را با هم تست و تحویل دهید. الگو برای مقیاس‌دهی به تعداد زیاد پروژه دشوار است.

پیامدها (Consequences)

یکپارچه‌سازی delivery-time پیچیدگی حل و هماهنگی نسخه‌های مختلف پروژه‌های مختلف را به فرایند تحویل منتقل می‌کند. این به پیاده‌سازی تحویل پیچیده، مانند pipeline، نیاز دارد («نرم‌افزار و سرویس‌های Pipeline تحویل» در صفحه ۱۲۳ را ببینید).

پیاده‌سازی (Implementation)

pipelineهای تحویل پروژه‌های مختلف را با طراحی pipeline «fan-in» یکپارچه می‌کنند. مرحله‌ای که پروژه‌های مختلف را کنار هم می‌آورد fan-in stage یا مرحله یکپارچه‌سازی پروژه نامیده می‌شود.²

نحوه یکپارچه‌سازی پروژه‌های مختلف در مرحله به انواع پروژه‌هایی که ترکیب می‌کند بستگی دارد. در مثال پروژه stack که از image سرور استفاده می‌کند، کد stack اعمال می‌شود و ارجاعی به نسخه مربوط image داده می‌شود. وابستگی‌های زیرساخت از repository تحویل کد بازیابی می‌شوند («استفاده از Repository برای تحویل کد زیرساخت» در صفحه ۳۲۳ را ببینید).

همان مجموعه نسخه‌های ترکیب‌شده پروژه باید در مراحل بعدی فرایند تحویل اعمال شوند. دو رویکرد رایج برای مدیریت این وجود دارد.

یک رویکرد بسته‌بندی همه کد پروژه در یک artifact واحد برای استفاده در مراحل بعدی است. مثلاً وقتی دو پروژه stack مختلف یکپارچه و تست می‌شوند، مرحله یکپارچه‌سازی می‌تواند کد هر دو پروژه را در یک آرشیو zip کند و آن را به مراحل downstream pipeline ارتقا دهد. جریان GitOps پروژه‌ها را به branch مرحله یکپارچه‌سازی merge می‌کند و سپس از آن branch به branchهای downstream.

رویکرد دیگر ایجاد فایل descriptor با شماره نسخه هر پروژه است. مثلاً:

yaml
descriptor-version: 1.9.1

stack-project:
  name: application-infrastructure-stack
  version: 1.2.34

server-image-project:
  name: application-server-image
  version: 1.1.1

فرایند تحویل فایل descriptor را به‌عنوان artifact treat می‌کند. هر مرحله‌ای که کد زیرساخت را اعمال می‌کند artifact پروژه جداگانه را از repository تحویل می‌کشد.

رویکرد سوم tag کردن منابع مربوط با شماره نسخه تجمیع‌شده است.

² الگوی fan-in مشابه (یا شاید نام دیگری برای) الگوی pipeline Aggregate Artifact است.

الگوی یکپارچه‌سازی پروژه در build-time («الگو: یکپارچه‌سازی پروژه در Build-Time» در صفحه ۳۲۷) پروژه‌ها را در ابتدا یکپارچه می‌کند، نه بعد از اینکه فعالیت‌های تحویل روی پروژه‌های جداگانه انجام شده. الگوی یکپارچه‌سازی پروژه در apply-time پروژه‌ها را در هر مرحله تحویلی که با هم استفاده می‌شوند یکپارچه می‌کند اما نسخه‌ها را «قفل» نمی‌کند.

الگو: یکپارچه‌سازی پروژه در Apply-Time

همچنین شناخته‌شده به‌عنوان: تحویل decoupled یا pipelineهای decoupled.

یکپارچه‌سازی پروژه در apply-time شامل عبور چند پروژه از مراحل تحویل به‌صورت جداگانه است. وقتی کسی کد پروژه را تغییر می‌دهد، pipeline کد به‌روزشده را به هر محیط در مسیر تحویل آن پروژه اعمال می‌کند. این نسخه کد پروژه ممکن است در هر یک از این محیط‌ها با نسخه‌های مختلف پروژه‌های upstream یا downstream یکپارچه شود.

در مثال ShopSpinner، پروژه application-infrastructure-stack به ساختارهای شبکه‌ای که پروژه shared-network-stack ایجاد کرده وابسته است. هر پروژه مراحل تحویل خود را دارد، همان‌طور که در شکل ۱۹-۶ نشان داده شده.

شکل ۱۹-۶. نمونه یکپارچه‌سازی پروژه‌ها هنگام اعمال آن‌ها

یکپارچه‌سازی بین پروژه‌ها با اعمال کد application-infrastructure-stack به محیط انجام می‌شود. این عملیات cluster سرور ایجاد یا تغییر می‌دهد که از ساختارهای شبکه (مثلاً subnet) از شبکه مشترک استفاده می‌کند.

این یکپارچه‌سازی صرف‌نظر از اینکه کدام نسخه stack شبکه مشترک در محیط مشخص قرار دارد انجام می‌شود. پس یکپارچه‌سازی نسخه‌ها هر بار که کد اعمال می‌شود جداگانه اتفاق می‌افتد.

انگیزه (Motivation)

یکپارچه‌سازی پروژه‌ها در apply-time coupling بین پروژه‌ها را به حداقل می‌رساند. تیم‌های مختلف می‌توانند تغییرات سیستم‌های خود را به production push کنند بدون نیاز به هماهنگی و بدون مسدود شدن به‌خاطر مشکلات تغییر در پروژه تیم دیگر.

کاربرد (Applicability)

این سطح decoupling برای سازمان‌ها با ساختار تیم خودمختار مناسب است. همچنین به سیستم‌های در مقیاس بزرگ‌تر کمک می‌کند، جایی که هماهنگی releaseها و تحویل lock-step در میان صدها یا هزاران مهندس عملی نیست.

پیامدها (Consequences)

این الگو ریسک شکستن وابستگی‌ها بین پروژه‌ها را به عملیات apply-time منتقل می‌کند. سازگاری در pipeline را تضمین نمی‌کند. اگر کسی تغییری در یک پروژه را سریع‌تر از تغییرات پروژه‌های دیگر از pipeline عبور دهد، در production با نسخه متفاوتی نسبت به محیط‌های تست یکپارچه می‌شود.

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

پیاده‌سازی (Implementation)

از بعضی جنبه‌ها، طراحی و پیاده‌سازی build و pipelineهای decoupled با یکپارچه‌سازی apply-time ساده‌تر از الگوهای جایگزین است. هر pipeline یک پروژه را می‌سازد، تست و تحویل می‌دهد.

فصل ۱۷ استراتژی‌های یکپارچه‌سازی stackهای زیرساخت مختلف را بحث می‌کند. مثلاً وقتی مرحله‌ای application-infrastructure-stack را اعمال می‌کند، باید به ساختارهای شبکه‌ای که shared-network-stack ایجاد کرده ارجاع دهد. آن فصل بعضی تکنیک‌ها برای اشتراک‌گذاری شناسه‌ها بین stackهای زیرساخت را توضیح می‌دهد.

تضمینی نیست که کدام نسخه کد پروژه دیگر در هر محیط مشخص استفاده شده. پس تیم‌ها باید وابستگی‌ها بین پروژه‌ها را به‌وضوح شناسایی و آن‌ها را به‌عنوان قرارداد treat کنند. shared-network-stack شناسه‌های ساختارهای شبکه‌ای که پروژه‌های دیگر ممکن است استفاده کنند را expose می‌کند. باید آن‌ها را به روش استاندارد، با یکی از الگوهای توصیف‌شده در فصل ۱۷، expose کند.

همان‌طور که در «استفاده از Test Fixture برای مدیریت وابستگی‌ها» در صفحه ۱۳۷ توضیح داده شد، می‌توانید از test fixture برای تست هر stack به‌تنهایی استفاده کنید. با مثال ShopSpinner، تیم می‌خواهد پروژه application-infrastructure-stack را بدون استفاده از نمونه shared-network-stack تست کند. stack شبکه زیرساخت redundant و پیچیده‌ای تعریف می‌کند که برای پشتیبانی test caseها لازم نیست. پس setup تست تیم می‌تواند مجموعه stripped-down شبکه ایجاد کند. این کار همچنین ریسک تکامل stack برنامه به فرض جزئیات پیاده‌سازی stack شبکه را کاهش می‌دهد.

تیمی که پروژه‌ای را مالک است که پروژه‌های دیگر به آن وابسته‌اند می‌تواند contract test پیاده‌سازی کند که ثابت کند کدش انتظارات را برآورده می‌کند. shared-network-stack می‌تواند تأیید کند ساختارهای شبکه — subnetها — ایجاد شده‌اند و شناسه‌هایشان با مکانیزمی که پروژه‌های دیگر برای مصرف استفاده می‌کنند expose شده‌اند.

مطمئن شوید contract testها به‌وضوح label شده‌اند. اگر کسی تغییر کدی بدهد که باعث fail شدن تست شود، باید بفهمد ممکن است پروژه‌های دیگر را بشکند، نه اینکه فکر کند فقط باید تست را با تغییرش به‌روز کند.

بسیاری از سازمان‌ها consumer-driven contract (CDC) testing را مفید می‌یابند. با این مدل، تیم‌هایی که روی پروژه consumer که به منابع ایجادشده در پروژه provider وابسته است کار می‌کنند، تست‌هایی می‌نویسند که در pipeline پروژه provider اجرا می‌شوند. این به تیم provider کمک می‌کند انتظارات تیم‌های consumer را بفهمد.

الگوی یکپارچه‌سازی پروژه در build-time («الگو: یکپارچه‌سازی پروژه در Build-Time» در صفحه ۳۲۷) در انتهای طیف مقابل این الگو است. آن الگو پروژه‌ها را یک‌بار در ابتدای چرخه تحویل یکپارچه می‌کند، نه هر بار. یکپارچه‌سازی پروژه در delivery-time («الگو: یکپارچه‌سازی پروژه در Delivery-Time» در صفحه ۳۳۰) هم پروژه‌ها را یک‌بار یکپارچه می‌کند، اما در نقطه‌ای در چرخه تحویل نه در ابتدا.

«سرخ کردن نمونه سرور» در صفحه ۱۸۶ یکپارچه‌سازی apply-time برای provisioning سرور را نشان می‌دهد. وابستگی‌هایی مانند ماژول‌های پیکربندی سرور هر بار که نمونه سرور جدید ایجاد می‌شود اعمال می‌شوند، معمولاً با گرفتن آخرین نسخه ماژول سرور که به مرحله مربوط ارتقا یافته.

استفاده از اسکریپت برای Wrap کردن ابزارهای زیرساخت

بیشتر تیم‌هایی که کد زیرساخت را مدیریت می‌کنند اسکریپت‌های سفارشی برای orchestrate و اجرای ابزارهای زیرساخت می‌سازند. بعضی از ابزارهای build نرم‌افزار مانند Make، Rake یا Gradle استفاده می‌کنند. دیگران اسکریپت در Bash، Python یا PowerShell می‌نویسند. در بسیاری موارد، این کد پشتیبان حداقل به‌اندازه کدی که زیرساخت را تعریف می‌کند پیچیده می‌شود و تیم‌ها زمان زیادی را صرف debug و نگهداری آن می‌کنند.

تیم‌ها ممکن است این اسکریپت‌ها را در build-time، delivery-time یا apply-time اجرا کنند. اغلب اسکریپت‌ها بیش از یکی از این فازهای پروژه را handle می‌کنند. اسکریپت‌ها انواع مختلفی از وظایف را handle می‌کنند که می‌تواند شامل موارد زیر باشد:

Configuration (پیکربندی) مقادیر پارامتر پیکربندی را جمع می‌کند، احتمالاً سلسله‌مراتبی از مقادیر را حل می‌کند. بیشتر درباره این به‌زودی.

Dependencies (وابستگی‌ها) کتابخانه‌ها، providerها و کد دیگر را حل و بازیابی می‌کند.

Packaging (بسته‌بندی) کد را برای تحویل آماده می‌کند، چه بسته‌بندی در artifact یا ایجاد یا merge branch.

Promotion (ارتقا) کد را از یک مرحله به بعدی منتقل می‌کند، چه با tag یا جابه‌جایی artifact یا ایجاد یا merge branch.

Orchestration (هماهنگی) stackها و عناصر زیرساخت دیگر را به ترتیب صحیح، بر اساس وابستگی‌هایشان، اعمال می‌کند.

Execution (اجرا) ابزارهای زیرساخت مربوط را اجرا می‌کند و آرگومان‌های خط فرمان و فایل‌های پیکربندی را مطابق نمونه‌ای که کد روی آن اعمال می‌شود می‌چیند.

Testing (تست) تست‌ها را setup و اجرا می‌کند، از جمله provisioning fixture و داده‌های تست، و جمع‌آوری و publish نتایج.

جمع‌آوری مقادیر پیکربندی

marshal و حل مقادیر پیکربندی می‌تواند یکی از پیچیده‌ترین وظایف اسکریپت wrapper باشد. سیستمی مانند مثال ShopSpinner را در نظر بگیرید که شامل چند محیط تحویل، چند نمونه production مشتری و چند جزء زیرساخت است.

مجموعه ساده یک‌سطحی مقادیر پیکربندی، با یک فایل برای هر ترکیب جزء، محیط و مشتری، به فایل‌های زیادی نیاز دارد. و بسیاری از مقادیر تکراری هستند.

مقدار store_name برای هر مشتری را تصور کنید که باید برای هر نمونه هر جزء تنظیم شود. تیم به‌سرعت تصمیم می‌گیرد آن مقدار را در یک مکان با مقادیر مشترک تنظیم کند و کدی به اسکریپت wrapper اضافه کند که مقادیر را از پیکربندی مشترک و پیکربندی per-component بخواند.

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

این نوع سلسله‌مراتب پارامتر کمی برای کدنویسی messy است. برای افراد هنگام معرفی پارامترهای جدید، پیکربندی مقادیر درست و ردیابی و debug مقادیر استفاده‌شده در هر نمونه سخت‌تر فهمیدن است.

استفاده از configuration registry پیچیدگی را با طعم دیگری می‌آورد. به‌جای دنبال کردن مقادیر پارامتر در آرایه‌ای از فایل‌ها، آن‌ها را در زیردرخت‌های مختلف registry دنبال می‌کنید. اسکریپت wrapper شما ممکن است حل مقادیر از بخش‌های مختلف registry را handle کند، مانند فایل‌های پیکربندی. یا ممکن است از اسکریپتی برای تنظیم مقادیر registry از قبل استفاده کنید تا منطق حل سلسله‌مراتب مقادیر پیش‌فرض برای تنظیم مقدار نهایی هر نمونه را مالک باشد. هر رویکرد سردردی برای تنظیم و ردیابی منشأ هر مقدار پارامتر مشخص ایجاد می‌کند.

ساده‌سازی اسکریپت‌های Wrapper

تیم‌هایی را دیده‌ام که زمان بیشتری صرف رفع باگ در اسکریپت‌های wrapper می‌کنند تا بهبود کد زیرساخت. این وضعیت از mixing و coupling نگرانی‌هایی که می‌توان و باید جدا شوند ناشی می‌شود. بعضی حوزه‌ها برای در نظر گرفتن:

تقسیم چرخه عمر پروژه یک اسکریپت واحد نباید وظایف در فازهای build، promotion و apply چرخه عمر پروژه را handle کند. اسکریپت‌های مختلف برای هر یک از این فعالیت‌ها بنویسید و استفاده کنید. مطمئن شوید درک روشنی از جایی که اطلاعات باید از یکی از این فازها به بعدی منتقل شود دارید. مرزهای روشن بین این فازها پیاده‌سازی کنید، مانند هر API یا قرارداد.

جداسازی وظایف وظایف مختلف درگیر در مدیریت زیرساخت را جدا کنید، مانند جمع‌آوری مقادیر پیکربندی، بسته‌بندی کد و اجرای ابزارهای زیرساخت. باز هم نقاط یکپارچه‌سازی بین این وظایف را تعریف کنید و آن‌ها را loosely coupled نگه دارید.

Decouple کردن پروژه‌ها اسکریپتی که اقدامات را در چند پروژه orchestrate می‌کند باید از اسکریپت‌هایی که وظایف درون پروژه‌ها را اجرا می‌کنند جدا باشد. و باید بتوان وظایف هر پروژه را به‌تنهایی اجرا کرد.

نگه داشتن کد wrapper نادان اسکریپت‌های شما نباید چیزی درباره پروژه‌هایی که پشتیبانی می‌کنند بدانند. از hardcode کردن اقداماتی که به آنچه کد زیرساخت انجام می‌دهد وابسته‌اند در اسکریپت‌های wrapper خودداری کنید. اسکریپت‌های wrapper ایده‌آل generic هستند، قابل استفاده برای هر پروژه زیرساخت با شکل مشخص (مثلاً هر پروژه‌ای که از ابزار stack خاصی استفاده می‌کند).

Treat کردن اسکریپت‌های wrapper مانند کد «واقعی» به همه این کمک می‌کند. اسکریپت‌ها را با ابزارهایی مانند shellcheck تست و validate کنید. قوانین طراحی نرم‌افزار خوب را روی اسکریپت‌هایتان اعمال کنید، مانند قانون composition، اصل single responsibility و طراحی حول مفاهیم domain. فصل ۱۵ و ارجاعات به منابع دیگر اطلاعات درباره طراحی نرم‌افزار خوب را ببینید.

جمع‌بندی

ایجاد فرایند محکم و قابل اعتماد برای تحویل کد زیرساخت کلید دستیابی به عملکرد خوب در برابر چهار معیار کلیدی است (به «چهار معیار کلیدی» در صفحه ۹ مراجعه کنید). سیستم تحویل شما پیاده‌سازی تحت‌اللفظی تحویل سریع و قابل اعتماد تغییرات به سیستم شماست.