Skip to content

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

یک codebase زیرساخت ممکن است انواع مختلف کد را شامل شود، از جمله تعاریف پشته، پیکربندی سرور، ماژول‌ها، کتابخانه‌ها، تست‌ها، پیکربندی و ابزارهای کمکی. چگونه باید این کد را در بین و درون پروژه‌ها سازمان‌دهی کنید؟ چگونه باید پروژه‌ها را در بین repositoryها سازمان‌دهی کنید؟ آیا کد زیرساخت و برنامه با هم هستند، یا باید جدا شوند؟ چگونه باید کد را برای estate با چند بخش سازمان‌دهی کنید؟

سازمان‌دهی پروژه‌ها و Repositoryها

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

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

قانون Conway (ببینید «هم‌راستا کردن مرزها با ساختارهای سازمانی» در صفحه ۲۶۵) می‌گوید رابطه مستقیمی بین ساختار سازمان و سیستم‌هایی که می‌سازد وجود دارد. هم‌راستایی ضعیف ساختارهای تیم و مالکیت سیستم‌ها، و کدی که آن سیستم‌ها را تعریف می‌کند، اصطکاک و ناکارآمدی ایجاد می‌کند.

طرف دیگر کشیدن مرز بین پروژه‌ها، یکپارچه‌سازی پروژه‌ها وقتی وابستگی بین آن‌ها وجود دارد است، همان‌طور که برای پشته‌ها در فصل ۱۷ توصیف شد.

ببینید «یکپارچه‌سازی پروژه‌ها» در صفحه ۳۲۶ برای بحث اینکه چگونه و چه زمانی وابستگی‌های مختلف ممکن است با یک پروژه یکپارچه شوند.

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

یک Repository، یا چند تا؟

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

چند عامل trade-off برای در نظر گرفتن وجود دارد:

  • جدا کردن پروژه‌ها به repositoryهای مختلف، حفظ مرزها در سطح کد را آسان‌تر می‌کند.
  • داشتن چند تیم که روی کد در یک repository کار می‌کنند می‌تواند overhead اضافه کند و conflict ایجاد کند.
  • پخش کد در بین چند repository می‌تواند کار روی تغییراتی که از آن‌ها عبور می‌کنند را پیچیده کند.
  • کد نگه‌داشته‌شده در همان repository versioned می‌شود و می‌تواند با هم branch شود، که بعضی استراتژی‌های یکپارچه‌سازی و delivery پروژه را ساده می‌کند.
  • سیستم‌های مدیریت کد منبع مختلف (مثل Git، Perforce و Mercurial) ویژگی‌ها و scalability متفاوتی دارند و ویژگی‌هایی برای پشتیبانی سناریوهای پیچیده.

بیایید گزینه‌های اصلی سازمان‌دهی پروژه‌ها در بین repositoryها را در پرتو این عوامل ببینیم.

یک Repository برای همه چیز

بعضی تیم‌ها، و حتی بعضی سازمان‌های بزرگ‌تر، یک repository تکی با همه کد خود نگهداری می‌کنند. این نیاز به نرم‌افزار سیستم کنترل منبع دارد که به سطح استفاده شما scale کند. بعضی نرم‌افزارها با رشد codebase در اندازه، تاریخچه، تعداد کاربران و سطح فعالیت مشکل دارند.¹ پس تقسیم repositoryها مسئله مدیریت performance می‌شود.

یک repository تکی می‌تواند استفاده آسان‌تری داشته باشد. مردم می‌توانند همه پروژه‌هایی که برای کار روی آن‌ها نیاز دارند checkout کنند، تضمین می‌کنند نسخه consistent از همه چیز دارند. بعضی نرم‌افزارهای کنترل نسخه ویژگی‌هایی مثل sparse-checkout ارائه می‌دهند که به کاربر اجازه می‌دهد با زیرمجموعه‌ای از repository کار کند.

Monorepo — یک Repository، یک Build

یک repository تکی با یکپارچه‌سازی build-time (ببینید «الگو: یکپارچه‌سازی پروژه در زمان Build» در صفحه ۳۲۷) خوب کار می‌کند. استراتژی monorepo از الگوی یکپارچه‌سازی build-time برای پروژه‌های نگهداری‌شده در یک repository تکی استفاده می‌کند. نسخه ساده‌شده monorepo همه پروژه‌های repository را build می‌کند، همان‌طور که در شکل ۱۸-۱ نشان داده شده.

شکل ۱۸-۱. Build کردن همه پروژه‌های یک repository با هم

اگرچه پروژه‌ها با هم build می‌شوند، ممکن است چند artifact تولید کنند، مثل بسته‌های برنامه، پشته‌های زیرساخت و imageهای سرور.

یک repository، چند build

بیشتر سازمان‌هایی که همه پروژه‌هایشان را در یک repository نگه می‌دارند لزوماً یک build تکی در بین همه آن‌ها اجرا نمی‌کنند. اغلب چند build مختلف برای build کردن زیرمجموعه‌های مختلف سیستم دارند (شکل ۱۸-۲ را ببینید).

شکل ۱۸-۲. Build کردن ترکیبات مختلف پروژه‌ها از یک repository

اغلب، این buildها بعضی پروژه‌ها را به اشتراک می‌گذارند. مثلاً، دو build مختلف ممکن است از همان کتابخانه مشترک استفاده کنند (شکل ۱۸-۳ را ببینید).

شکل ۱۸-۳. اشتراک‌گذاری یک کامپوننت در بین buildها در یک repository تکی

یکی از دام‌های مدیریت چند پروژه این است که می‌تواند مرزهای بین پروژه‌ها را مبهم کند. مردم ممکن است کدی برای یک پروژه بنویسند که مستقیماً به فایل‌های پروژه دیگر در repository reference می‌دهد. انجام این کار به coupling محکم‌تر و visibility کمتر وابستگی‌ها منجر می‌شود. در طول زمان، پروژه‌ها tangle می‌شوند و نگهداری سخت می‌شوند، چون تغییر فایلی در یک پروژه ممکن است conflictهای غیرمنتظره با پروژه‌های دیگر داشته باشد.

یک Repository جداگانه برای هر پروژه (Microrepo)

داشتن repository جداگانه برای هر پروژه طرف دیگر افراط است (شکل ۱۸-۴).

شکل ۱۸-۴. هر پروژه در یک repository جداگانه

این استراتژی جدایی تمیز بین پروژه‌ها را تضمین می‌کند، به‌ویژه وقتی pipeline دارید که هر پروژه را جداگانه قبل از یکپارچه‌سازی build و test می‌کند. اگر کسی دو پروژه checkout کند و تغییری در فایل‌ها در بین پروژه‌ها بدهد، pipeline fail می‌کند و مشکل را expose می‌کند.

از نظر فنی، می‌توانید یکپارچه‌سازی build-time را در بین پروژه‌های مدیریت‌شده در repositoryهای جداگانه استفاده کنید، با checkout کردن اول همه buildها (شکل ۱۸-۵ را ببینید).

شکل ۱۸-۵. یک build تکی در بین چند repository

در عمل، build در بین چند پروژه در یک repository تکی عملی‌تر است، چون کد آن‌ها با هم versioned می‌شود. push کردن تغییرات برای یک build تکی به چند repository فرایند delivery را پیچیده می‌کند. مرحله delivery نیاز به راهی برای دانستن کدام نسخه‌های همه repositoryهای درگیر را checkout کند تا build consistent ایجاد کند.

repositoryهای تک‌پروژه‌ای وقتی یکپارچه‌سازی delivery-time و apply-time را پشتیبانی می‌کنند بهترین کار را می‌کنند. تغییر در هر repository فرایند delivery پروژه خودش را trigger می‌کند و آن را بعداً در flow با پروژه‌های دیگر کنار هم می‌آورد.

چند Repository با چند پروژه

در حالی که بعضی سازمان‌ها به سمت یکی از افراط‌ها — repository تکی برای همه چیز، یا repository جداگانه برای هر پروژه — فشار می‌آورند، بیشتر چند repository با بیش از یک پروژه نگهداری می‌کنند (شکل ۱۸-۶ را ببینید).

شکل ۱۸-۶. چند repository با چند پروژه

اغلب، گروه‌بندی پروژه‌ها به repositoryها به‌صورت organic اتفاق می‌افتد، نه اینکه توسط استراتژی مثل monorepo یا microrepo هدایت شود. با این حال، چند عامل وجود دارد که بر نرمی کار تأثیر می‌گذارد.

یک عامل، همان‌طور که در بحث‌های استراتژی‌های repository دیگر دیده شد، هم‌راستایی گروه‌بندی پروژه با استراتژی build و delivery آن است. پروژه‌ها را در یک repository نگه دارید وقتی نزدیک به هم مرتبط‌اند، و به‌ویژه وقتی پروژه‌ها را در زمان build یکپارچه می‌کنید. پروژه‌ها را به repositoryهای جداگانه جدا کنید وقتی مسیرهای delivery آن‌ها tightly integrated نیستند.

عامل دیگر مالکیت تیم است. اگرچه چند نفر و تیم می‌توانند روی پروژه‌های مختلف در همان repository کار کنند، می‌تواند حواس‌پرتی‌کننده باشد. Changelogها تاریخچه commit تیم‌های مختلف با workstreamهای نامرتبط را در هم می‌آمیزند. بعضی سازمان‌ها دسترسی به کد را محدود می‌کنند. کنترل دسترسی برای سیستم‌های کنترل منبع اغلب بر اساس repository مدیریت می‌شود، که عامل دیگری برای تصمیم اینکه کدام پروژه‌ها کجا بروند است.

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

سازمان‌دهی انواع مختلف کد

پروژه‌های مختلف در یک codebase زیرساخت انواع مختلف عناصر سیستم شما را تعریف می‌کنند، مثل برنامه‌ها، پشته‌های زیرساخت، ماژول‌های پیکربندی سرور و کتابخانه‌ها. و این پروژه‌ها ممکن است انواع مختلف کد را شامل شوند، از جمله declarationها، کد imperative، مقادیر پیکربندی، تست‌ها و اسکریپت‌های کمکی. داشتن استراتژی برای سازمان‌دهی این چیزها کمک می‌کند codebase شما قابل نگهداری بماند.

فایل‌های پشتیبانی پروژه

به‌طور کلی، هر کد پشتیبانی برای یک پروژه خاص باید با کد آن پروژه زندگی کند. یک layout پروژه معمولی برای پشته ممکن است شبیه مثال ۱۸-۱ باشد.

مثال ۱۸-۱. نمونه layout پوشه برای یک پروژه

├── build.sh
├── src/
├── test/
├── environments/
└── pipeline/

ساختار پوشه این پروژه شامل:

src/ کد پشته زیرساخت، که قلب پروژه است.

test/ کد تست. این پوشه می‌تواند به زیرپوشه‌ها برای تست‌هایی که در فازهای مختلف اجرا می‌شوند تقسیم شود، مثل تست‌های offline و online. تست‌هایی که از ابزارهای مختلف استفاده می‌کنند، مثل تحلیل static، تست‌های performance و functional، احتمالاً زیرپوشه‌های اختصاصی هم دارند.

environments/ پیکربندی. این پوشه شامل فایل جداگانه با مقادیر پیکربندی برای هر نمونه پشته است.

pipeline/ پیکربندی delivery. پوشه شامل فایل‌های پیکربندی برای ایجاد مراحل delivery در ابزار delivery pipeline (ببینید «نرم‌افزار و سرویس‌های Delivery Pipeline» در صفحه ۱۲۳).

build.sh/ اسکریپت برای پیاده‌سازی فعالیت‌های build. ببینید «استفاده از اسکریپت‌ها برای Wrap کردن ابزارهای زیرساخت» در صفحه ۳۳۵ برای بحث اسکریپت‌هایی مثل این.

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

نکته کلیدی توصیه است که فایل‌های خاص یک پروژه با پروژه زندگی کنند. این تضمین می‌کند وقتی کسی نسخه‌ای از پروژه checkout می‌کند، می‌داند کد زیرساخت، تست‌ها و delivery همه همان نسخه هستند و پس باید با هم کار کنند. اگر تست‌ها در پروژه جداگانه ذخیره شوند، mismatch آن‌ها آسان است، اجرای نسخه اشتباه تست‌ها برای کدی که تست می‌کنید.

با این حال، بعضی تست‌ها، پیکربندی یا فایل‌های دیگر ممکن است خاص یک پروژه تکی نباشند. چگونه باید با آن‌ها برخورد کنید؟

تست‌های بین‌پروژه‌ای

تست تدریجی (ببینید «تست تدریجی» در صفحه ۱۱۵) شامل تست هر پروژه جداگانه قبل از تست یکپارچه با پروژه‌های دیگر است، همان‌طور که در شکل ۱۸-۷ نشان داده شده.

شکل ۱۸-۷. تست جداگانه پروژه‌ها، سپس با هم

می‌توانید کد تست برای اجرا در مراحل فردی هر پروژه را با آن پروژه راحت بگذارید. اما کد تست برای مرحله یکپارچه‌سازی چطور؟ می‌توانید این تست‌ها را در یکی از پروژه‌ها بگذارید، یا پروژه جداگانه‌ای برای تست‌های یکپارچه‌سازی ایجاد کنید.

نگه داشتن تست‌های یکپارچه‌سازی درون یک پروژه

در بسیاری مواردی که چند پروژه را یکپارچه می‌کنید، یک پروژه نقطه ورود واضحی برای انواع خاصی از تست‌ها است. مثلاً، بسیاری تست‌های functional به سرویس front-end متصل می‌شوند تا ثابت کنند کل سیستم کار می‌کند. اگر کامپوننت backend، مثل database، درست پیکربندی نشده باشد، سرویس frontend نمی‌تواند به آن متصل شود، پس تست fail می‌شود.

در این موارد، کد تست یکپارچه‌سازی راحت با پروژه‌ای که سرویس frontend را provision می‌کند زندگی می‌کند. به احتمال زیاد، کد تست به آن سرویس coupled است. مثلاً، باید hostname و port آن سرویس را بداند.

این تست‌ها را از تست‌هایی که در مراحل delivery اولیه‌تر اجرا می‌شوند — مثلاً، وقتی با test doubleها تست می‌کنید — جدا کنید. می‌توانید هر مجموعه تست را در زیرپوشه جداگانه در پروژه نگه دارید.

تست‌های یکپارچه‌سازی هم در پروژه‌هایی که پروژه‌های دیگر را consume می‌کنند (ببینید «استفاده از Test Fixtureها برای مدیریت وابستگی‌ها» در صفحه ۱۳۷) خوب جا می‌گیرند، نه در پروژه provider.

مثال ShopSpinner شامل یک پروژه پشته است که نمونه‌های زیرساخت برنامه را تعریف می‌کند و ساختارهای شبکه تعریف‌شده در پشته متفاوت را به اشتراک می‌گذارد.

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

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

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

Versioning وقتی suiteهای تست یکپارچه‌سازی را جدا از کدی که تست می‌کنند مدیریت می‌کنید چالش‌برانگیز است. مردم ممکن است گیج شوند کدام نسخه تست‌های یکپارچه‌سازی را برای نسخه داده‌شده کد سیستم اجرا کنند. برای کاهش این، مطمئن شوید تست‌ها را همراه با کد بنویسید و تغییر دهید، نه اینکه آن فعالیت‌ها را جدا کنید. و راهی برای correlate کردن نسخه‌های پروژه پیاده‌سازی کنید؛ مثلاً، با استفاده از رویکرد fan-in توصیف‌شده در الگوی یکپارچه‌سازی delivery-time (ببینید «الگو: یکپارچه‌سازی پروژه در زمان Delivery» در صفحه ۳۳۰).

سازمان‌دهی کد بر اساس مفهوم دامنه

کد درون یک پروژه تکی می‌تواند چند قطعه شامل شود. پروژه زیرساخت برنامه در مثال ShopSpinner یک server cluster و نمونه database، و ساختارهای شبکه و سیاست‌های امنیتی برای هر کدام تعریف می‌کند. بسیاری تیم‌ها ساختارهای شبکه و سیاست‌های امنیتی را در فایل‌های خودشان تعریف می‌کنند، همان‌طور که در مثال ۱۸-۲ نشان داده شده.

مثال ۱۸-۲. فایل‌های منبع سازمان‌دهی‌شده بر اساس فناوری

└── src/
    ├── cluster.infra
    ├── database.infra
    ├── load_balancer.infra
    ├── routing.infra
    ├── firewall_rules.infra
    └── policies.infra

فایل firewall_rules.infra شامل قوانین firewall برای سرورهای مجازی ایجاد‌شده در cluster.infra و همچنین قوانین برای نمونه database تعریف‌شده در database.infra است.

سازمان‌دهی کد به این شکل روی عناصر functional بیشتر از نحوه استفاده تمرکز می‌کند. اغلب درک، نوشتن، تغییر و نگهداری کد برای عناصر مرتبط وقتی در همان فایل هستند آسان‌تر است. تصور کنید فایلی با سی قانون firewall مختلف برای دسترسی به هشت سرویس مختلف، در برابر فایلی که یک سرویس و سه قانون firewall مرتبط با آن را تعریف می‌کند.

این مفهوم از اصل طراحی طراحی حول مفاهیم دامنه نه فنی پیروی می‌کند (ببینید «طراحی کامپوننت‌ها حول مفاهیم دامنه، نه فنی» در صفحه ۲۵۵).

سازمان‌دهی فایل‌های مقدار پیکربندی

فصل ۷ الگوی فایل‌های پیکربندی برای مدیریت مقادیر پارامتر برای نمونه‌های مختلف یک پروژه پشته را توصیف کرد (ببینید «الگو: فایل‌های پیکربندی پشته» در صفحه ۸۷). توصیف دو روش مختلف برای سازمان‌دهی فایل‌های پیکربندی per-environment در بین چند پروژه پیشنهاد کرد. یکی ذخیره آن‌ها درون پروژه مربوطه:

├── application_infra_stack/
│   ├── src/
│   └── environments/
│       ├── test.properties
│       ├── staging.properties
│       └── production.properties

└── shared_network_stack/
    ├── src/
    └── environments/
        ├── test.properties
        ├── staging.properties
        └── production.properties

دیگری داشتن پروژه جداگانه با پیکربندی همه پشته‌ها، سازمان‌دهی‌شده بر اساس محیط:

├── application_infra_stack/
│   └── src/

├── shared_network_stack/
│   └── src/

└── configuration/
    ├── test/
    │ ├── application_infra.properties
    │ └── shared_network.properties
    ├── staging/
    │ ├── application_infra.properties
    │ └── shared_network.properties
    └── production/
        ├── application_infra.properties
        └── shared_network.properties

ذخیره مقادیر پیکربندی با کد یک پروژه، کد قابل بازاستفاده عمومی (با فرض پشته قابل بازاستفاده، طبق «الگو: پشته قابل بازاستفاده» در صفحه ۷۲) را با جزئیات نمونه‌های خاص مخلوط می‌کند. ایده‌آل این است که تغییر پیکربندی برای یک محیط نباید نیاز به تغییر پروژه پشته داشته باشد.

از طرف دیگر، arguably ردیابی و درک مقادیر پیکربندی وقتی نزدیک پروژه‌هایی که مربوط‌اند آسان‌تر است، نه اینکه در پروژه پیکربندی monolithic در هم آمیخته باشند. مالکیت تیم و هم‌راستایی عامل است، مثل همیشه. جدا کردن کد زیرساخت و پیکربندی آن می‌تواند از گرفتن مالکیت و مسئولیت در هر دو جلوگیری کند.

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

آیا کد برنامه و زیرساخت باید در repositoryهای جداگانه نگه داشته شوند، یا با هم؟ هر پاسخ برای افراد مختلف به‌وضوح درست به نظر می‌رسد. پاسخ درست به ساختار سازمان و تقسیم مالکیت شما بستگی دارد.

مدیریت کد زیرساخت و برنامه در repositoryهای جداگانه از مدل عملیاتی پشتیبانی می‌کند که تیم‌های جداگانه زیرساخت و برنامه‌ها را می‌سازند و مدیریت می‌کنند. اما اگر تیم‌های برنامه مسئولیت زیرساخت دارند، به‌ویژه زیرساخت خاص برنامه‌های خودشان، چالش ایجاد می‌کند.

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

اگر آن کد در repository متفاوتی از repositoryی که بیشتر در آن کار می‌کنند باشد، همان سطح راحتی برای dig کردن در آن ندارند. این به‌ویژه وقتی درست است که کدی است که با آن کمتر آشنا هستند، و وقتی با کد زیرساخت برای قسمت‌های دیگر سیستم مخلوط شده.

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

DevOps و ساختارهای تیم

جنبش DevOps سازمان‌ها را تشویق می‌کند با جایگزین‌هایی برای تقسیم سنتی بین development و operations آزمایش کنند. ببینید نوشته‌های Matthew Skelton و Manuel Pais درباره Team Topologies برای افکار عمیق‌تر درباره ساختاردهی تیم‌های برنامه و زیرساخت.

تحویل زیرساخت و برنامه‌ها

صرف‌نظر از اینکه کد برنامه و زیرساخت را با هم مدیریت می‌کنید یا نه، در نهایت آن‌ها را به همان سیستم deploy می‌کنید.² تغییرات کد زیرساخت باید در طول flow delivery برنامه با برنامه‌ها یکپارچه و تست شوند (شکل ۱۸-۸).

شکل ۱۸-۸. Delivery برنامه به محیط‌های مدیریت‌شده توسط کد زیرساخت

به‌عنوان مثال ضد، بسیاری سازمان‌ها دیدگاه legacy از زیرساخت production به‌عنوان silo جداگانه دارند. اغلب، یک تیم زیرساخت production، شامل محیط staging یا preproduction، را مالک است اما مسئولیت محیط‌های development و testing را ندارد (شکل ۱۸-۹).

شکل ۱۸-۹. مالکیت جداگانه زیرساخت production

این جدایی اصطکاک برای delivery برنامه، و همچنین برای تغییرات زیرساخت ایجاد می‌کند. تیم‌ها تا دیر در فرایند delivery conflictها یا شکاف‌های بین دو قسمت سیستم را کشف نمی‌کنند. همان‌طور که در فصل ۸ توضیح داده شد، یکپارچه‌سازی و تست مداوم همه قسمت‌های سیستم همان‌طور که مردم روی تغییرات کار می‌کنند مؤثرترین راه برای تضمین کیفیت بالا و delivery قابل اعتماد است.

پس استراتژی delivery شما باید تغییرات کد زیرساخت را در همه محیط‌ها تحویل دهد. چند گزینه برای flow تغییرات زیرساخت وجود دارد.

تست برنامه‌ها با زیرساخت

اگر تغییرات زیرساخت را در مسیر delivery برنامه تحویل دهید، می‌توانید از تست‌های خودکار برنامه بهره ببرید. در هر مرحله، بعد از اعمال تغییر زیرساخت، مرحله تست برنامه را trigger کنید (شکل ۱۸-۱۰ را ببینید).

رویکرد تست تدریجی («تست تدریجی» در صفحه ۱۱۵) از تست‌های برنامه برای تست یکپارچه‌سازی استفاده می‌کند. نسخه‌های برنامه و زیرساخت می‌توانند به هم گره بخورند و در بقیه flow delivery طبق الگوی یکپارچه‌سازی delivery-time (ببینید «الگو: یکپارچه‌سازی پروژه در زمان Delivery» در صفحه ۳۳۰) پیش بروند. یا تغییر زیرساخت می‌تواند بدون یکپارچه‌سازی با تغییرات برنامه در حال انجام به محیط‌های downstream push شود، با استفاده از یکپارچه‌سازی apply-time (ببینید «الگو: یکپارچه‌سازی پروژه در زمان Apply» در صفحه ۳۳۳).

شکل ۱۸-۱۰. اجرای تست‌های برنامه هنگام تغییرات زیرساخت

Push کردن تغییرات به برنامه‌ها و زیرساخت تا جایی که ممکن است سریع ایده‌آل است. اما در عمل، همیشه ممکن نیست اصطکاک را از همه انواع تغییرات در یک سازمان حذف کنید. مثلاً، اگر ذی‌نفعان نیاز به بررسی عمیق‌تر تغییرات برنامه user-facing دارند، ممکن است نیاز داشته باشید تغییرات زیرساخت routine را سریع‌تر push کنید. در غیر این صورت، فرایند release برنامه شما ممکن است تغییرات فوری مثل patchهای امنیتی و تغییرات جزئی مثل به‌روزرسانی‌های پیکربندی را گیر بیندازد.

تست زیرساخت قبل از یکپارچه‌سازی

ریسک اعمال کد زیرساخت به محیط‌های مشترک development و test برنامه این است که شکستن آن محیط‌ها بر تیم‌های دیگر تأثیر می‌گذارد. پس ایده خوبی است مراحل delivery و محیط‌هایی برای تست کد زیرساخت به‌تنهایی، قبل از promote کردن آن‌ها به محیط‌های مشترک داشته باشید (شکل ۱۸-۱۱ را ببینید).

شکل ۱۸-۱۱. مرحله تست زیرساخت

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

استفاده از کد زیرساخت برای Deploy کردن برنامه‌ها

کد زیرساخت تعریف می‌کند چه چیزی روی سرورها می‌رود. Deploy کردن برنامه شامل گذاشتن چیزها روی سرورها است. پس ممکن است نوشتن کد زیرساخت برای automate کردن فرایند deploy برنامه منطقی به نظر برسد. در عمل، مخلوط کردن نگرانی‌های deploy برنامه و پیکربندی زیرساخت messy می‌شود. رابط بین برنامه و زیرساخت باید ساده و واضح باشد.

سیستم‌های بسته‌بندی سیستم‌عامل مثل RPM، فایل‌های .deb و .msi رابط well-defined برای بسته‌بندی و deploy برنامه‌ها هستند. کد زیرساخت می‌تواند فایل بسته برای deploy را مشخص کند، سپس ابزار deploy کنترل را بگیرد.

مشکل وقتی می‌آید که deploy برنامه شامل چند فعالیت است، و به‌ویژه وقتی شامل چند قطعه متحرک است. مثلاً، یک‌بار cookbook Chef نوشتم برای deploy برنامه‌های Dropwizard Java تیم روی سرورهای مجازی Linux. cookbook نیاز داشت:

  1. نسخه جدید برنامه را دانلود و در پوشه جدید unpack کند
  2. فرایند نسخه قبلی برنامه را متوقف کند اگر در حال اجرا بود
  3. فایل‌های پیکربندی را در صورت نیاز به‌روز کند
  4. symlink اشاره‌کننده به نسخه فعلی برنامه را به پوشه جدید به‌روز کند
  5. اسکریپت‌های migration schema database را اجرا کند³
  6. فرایند نسخه جدید برنامه را شروع کند
  7. بررسی کند فرایند جدید درست کار می‌کند

این cookbook برای تیم ما دردسرآور بود، گاهی fail می‌کرد وقتی فرایند قبلی terminate نشده بود، یا فرایند جدید یک دقیقه بعد از شروع crash کرده بود.

اساساً، این یک اسکریپت procedural درون codebase زیرساخت declarative بود. بعد از تصمیم به بسته‌بندی برنامه‌ها به‌عنوان RPM موفق‌تر شدیم، که یعنی می‌توانستیم از ابزارها و اسکریپت‌هایی که مخصوصاً برای deploy و upgrade برنامه‌ها intended هستند استفاده کنیم. برای فرایند بسته‌بندی RPM تست نوشتیم، که به بقیه codebase Chef وابسته نبود، پس می‌توانستیم روی مسائل خاصی که deploy را unreliable می‌کرد drill in کنیم.

چالش دیگر استفاده از کد زیرساخت برای deploy برنامه‌ها وقتی فرایند deploy نیاز به orchestrate کردن چند قطعه دارد. فرایند تیم ما وقتی برنامه‌های Dropwizard را به یک سرور deploy می‌کردیم خوب کار می‌کرد. وقتی به load balancing برنامه در چند سرور رفتیم مناسب نبود.

حتی بعد از رفتن به بسته‌های RPM، cookbookها ترتیب deploy در بین چند سرور را مدیریت نمی‌کردند. پس cluster در طول عملیات deploy نسخه‌های مخلوط برنامه را اجرا می‌کرد. و اسکریپت migration schema database فقط یک‌بار باید اجرا شود، پس نیاز داشتیم locking پیاده‌سازی کنیم تا فقط فرایند deploy اولین سرور آن را اجرا کند.

راه‌حل ما انتقال عملیات deploy از کد پیکربندی سرور به اسکریپتی بود که برنامه‌ها را از مکان deploy مرکزی — build server ما — روی سرورها push می‌کرد. این اسکریپت ترتیب deploy به سرورها و migrationهای schema database را مدیریت می‌کرد و zero-downtime deployment را با تغییر پیکربندی load balancer برای rolling upgrade پیاده‌سازی می‌کرد.⁴

برنامه‌های cloud native توزیع‌شده چالش orchestrate کردن deploy برنامه‌ها را افزایش می‌دهند. Orchestrate کردن تغییرات به ده‌ها، صدها یا هزاران نمونه برنامه واقعاً messy می‌شود. تیم‌ها از ابزارهای deploy مثل Helm یا Octopus Deploy برای تعریف deploy گروهی برنامه‌ها استفاده می‌کنند. این ابزارها جداسازی نگرانی‌ها را با تمرکز روی deploy مجموعه برنامه‌ها enforce می‌کنند و provision cluster زیرین را به قسمت‌های دیگر codebase واگذار می‌کنند.

با این حال، محکم‌ترین استراتژی deploy برنامه نگه داشتن هر عنصر loosely coupled است. هر چه deploy یک تغییر به‌صورت مستقل از تغییرات دیگر آسان‌تر و ایمن‌تر باشد، کل سیستم قابل اعتمادتر است.

نتیجه‌گیری

Infrastructure as Code، همان‌طور که نامش نشان می‌دهد، معماری، کیفیت و قابلیت مدیریت زیرساخت یک سیستم را از codebase هدایت می‌کند. پس codebase باید طبق نیازمندی‌های کسب‌وکار و معماری سیستم ساختاردهی و مدیریت شود. باید اصول و practices مهندسی که تیم شما را مؤثر می‌کند پشتیبانی کند.


¹ Facebook، Google و Microsoft همه از repositoryهای بسیار بزرگ استفاده می‌کنند. هر سه یا تغییرات سفارشی به نرم‌افزار کنترل نسخه داده‌اند یا خودشان ساخته‌اند. ببینید «Scaling version control software» برای بیشتر. همچنین ببینید «Scaled trunk-based development» اثر Paul Hammant برای بینش درباره تاریخچه رویکرد Google.

² سوالات — و الگوها — حول زمان یکپارچه‌سازی پروژه‌ها برای یکپارچه‌سازی برنامه‌ها با زیرساخت مرتبط‌اند. ببینید «یکپارچه‌سازی پروژه‌ها» در صفحه ۳۲۶ برای بیشتر.

³ ببینید «Using Migration Scripts in Database Deployments» از Red Gate.

⁴ از الگوی expand and contract برای تغییرات schema database بدون downtime استفاده کردیم.