راه‌اندازی سرور CI: کوزه‌گری که از کوزه‌شکسته آب می‌خورد!

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

‏Self-Hosted CI Server چیست؟

گاهی به علت نیاز به منابع پردازشی بیشتر، شخصی‌سازی، صرفه‌جویی در هزینه‌ها، رعایت محرمانگی و ... نمی‌توان از زیرساخت‌های عمومیِ CI استفاده کرد. اکثر پلتفرم‌های این حوزه (نظیر Gitlab CI و Github Actions) در پاسخ به این نیازمندی‌ها امکان استفاده از Self-Hosted CI Server را فراهم کرده‌اند تا بتوان از امکانات همان پلتفرم در سرور شخصی بهره برد.

برای اطلاعات بیشتر در این باره می‌توانید به توضیحات گیت‌هاب و گیت‌لب مراجعه کنید.

مشکل چیست؟

وقتی مجبور می‌شویم برای پروژه‌ی خود یک یا چند Self-Hosted CI Server راه‌اندازی کنیم، غالبا دست به یک کار هنری می‌زنیم؛ مجموعه‌ای از آزمون‌و‌خطاها و اعمال تنظیمات و نصب ابزارهای مختلف، برای رسیدن به یک سرور مناسب در اسرع وقت.

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

مشکل به اینجا هم ختم نمی‌شود؛ بسته به نیاز پروژه در زمان‌های مختلف، تغییراتی روی سرور CI اعمال می‌کنیم و پس از مدتی به معنای واقعی کلمه با یک کار هنری مواجهیم: چیزی که سازنده‌اش هم نمی‌تواند بازتولیدش کند!

از اینجا به بعد، آپدیت زیرساخت CI، تغییر تنظیمات سرور CI، آپدیت ابزارهای نصب‌شده در آن، دیباگ مشکلات در سرور و ... نیازمند انرژی زیادی برای انجام خواهند بود. به طوری که پیشنهاد یک تغییر ساده در سرور CI احتمالا با این واکنش‌ها مواجه خواهد شد: «این را بگذاریم برای آخر هفته»، «فعلا فرصت این کار را نداریم»، «بچه‌ها امروز کامیت نکنید تا CI درست شود»، «این تغییر را باید فلانی انجام دهد» و ...

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

این‌ها دقیقا همان مشکلاتی هستند که برای جلوگیری از آن‌ها در محصول نهایی خود، دست به دامن CI شده بودیم. انگار فراموش کرده‌ایم که اصلا در راستای چه آرمان‌هایی سراغ مفهوم Continuous Integration رفته بودیم!


تحت داکر بودن کافی نیست!

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

اگرچه containerization برای محیط CI بسیار توصیه شده و لازم است، اما کافی نیست. حتی در شرایطی که همه‌ی جاب‌های CI تحت داکر اجرا می‌شوند، هنگام تغییر، ارتقا یا بازسازی سرور CI، ممکن است با رفتارهای ناسازگاری در آن مواجه شوید و روزها برای پاس کردن پایپلاینی که تا دیروز مشکلی نداشت تلاش کنید!

فهرست زیر شامل برخی موارد است که در سرور میزبان CI ممکن است بر اجرای جاب‌ها تاثیرگذار باشد:

- نسخه‌ی داکر

- نسخه‌ی کرنل سیستم‌عامل

- احراز هویت در رجیستری‌های private (برای دانلود ایمیج خود جاب)

- تنظیمات مربوط به registry mirror

- تنظیمات مربوط به insecure registries

- دسترسی شبکه به سرویس‌های جانبی

- تنظیمات مربوط به امکان / عدم امکان اجرای موازی جاب‌ها

- و...

راه‌حل

راه‌های مختلفی برای مقابله با مشکل یادشده وجود دارد. مثلا می‌توان از سرور CI (همان کار هنری!) snapshot گرفت و در مواقع لزوم از آن استفاده کرد.

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

این کار نه‌تنها باعث می‌شود وضعیت سرور CI قابل بازتولید باشد، بلکه امکان مرور جزئیات آن را در اختیار همه‌ی اعضای تیم قرار می‌دهد. بعلاوه تجربه نشان داده که طی این خودکارسازی، بسیاری از ابزارها و تنظیمات اضافی یا اشتباه، خود را نشان داده و حذف خواهند شد.

خودکارسازی راه‌اندازی سرور CI

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

  1. فایل‌های مربوط به ساخت و تنظیم سرور CI در یک فولدر در مخزن خود پروژه نگهداری شود.
  2. تنها پیش‌فرضی که از ماشین مقصد باید وجود داشته باشد، نسخه‌ی سیستم عامل و تنظیمات شبکه (برای دسترس‌پذیری ماشین میزبان) است.
  3. مطابق طراحی اکثر سرویس‌های CI، سرور مربوطه به صورت stateless نگهداری شود. یعنی ذخیره‌ی هرگونه داده توسط جاب‌های CI، خارج از سرور انجام شود تا فرایند update / rollback سرویس بی‌نیاز از data migration باشد.
    البته وجود cache در سرور CI بلامانع است. به شرط آن که به معنای واقعی کلمه cache باشد نه data. یعنی از بین رفتن آن خللی در functionality سیستم CI ایجاد نکند.
  4. در هنگام طراحی جاب‌های CI کمترین میزان وابستگی به سرور CI موجود باشد و حتی الامکان از روش‌های virtualization (نظیر Docker) بهره گرفته شود.
  5. با توجه به اینکه برای اجرای خود اسکریپت، طبیعتا سرور CI/CD وجود ندارد، نیاز است تا نکات زیر رعایت شود:
    - اسکریپت با این پیش‌فرض نوشته شود که قرار است در کامپیوتر شخصی اجرا گردد.
    - حتی‌الامکان اسکریپت مربوطه به حداقل پیش‌نیازها برای اجرا وابسته باشد.
    - اجرای اسکریپت به سادگی اجرای یک فایل bash یا شبیه آن باشد، بدون هیچگونه flag و تنظیمات اضافه. (هر آپشنی با توضیحات مرتبط به صورت interactive از کاربر گرفته شود.) این کار در راستای عمل به شیوه‌ی Document as Code است. به بیان دیگر، مستندات مربوط به راه‌اندازی سرور CI باید تنها یک جمله باشد: «فلان اسکریپت را اجرا کنید!»
  6. پیش‌فرض‌هایی که اسکریپت مطابق آن‌ها نوشته شده، assert شوند و در صورت مغایرت، خطای متناظر به صورت گویا چاپ شود.
    - پیش‌فرض‌های سرور:
    . نوع سیستم عامل
    . نسخه‌ی سیستم عامل
    . دسترسی تحت شبکه به سرویس‌های دیگر
    . (در صورت نیاز) حداقل منابع پردازشی مورد نیاز
    - پیش‌فرض‌های کلاینت (همان اسکریپت interactive):
    . پیشنیازهای اجرای اسکریپت (مانند نسخه‌ی انسیبل، ابزار sshpass و ...)
  7. هر تغییر در تنظیمات سرور CI باید توسط تغییر و اجرای اسکریپت مربوطه اعمال شود. (حتی الامکان خودتان ssh نزنید!)
  8. از عمومی و کلی کردن ساختار اسکریپت خودداری شود. اسکریپت تنها وظیفه دارد سرور CI مربوط به پروژه‌ی کنونی خودش را تنظیم کند.
    اگرچه عمومی بودن اسکریپت در ظاهر جذاب به نظر می‌رسد، اما باعث می‌شود پارامترهای وروردی بسیار زیاد و گیج‌کننده شوند. مضاف بر اینکه برخی تنظیمات واقعا خاص سرور CI پروژه است و عمومی کردن آن اساسا بی‌معنی است؛ مانند گواهی SSL مربوط به سرویس‌های داخلی، که اصلا برای یک پروژه‌ی دلخواه دیگر ممکن است به کل نیاز نباشد.
    به طور کلی، هدف اسکریپت خودکار، راه‌اندازی سرور CI در شرایط کنونی شرکت / پروژه است و طبیعتا با تغییر این شرایط، اسکریپت نیز می‌تواند تغییر کند.
  9. پارامترهای غیرمحرمانه (مانند گواهی SSL شرکت) حتی الامکان hard-code شوند. این کار باعث می‌شود اجرای اسکریپت برای اجراکننده (که ممکن است هرکسی باشد) ساده‌تر شود.

موارد آخر احتمالا با پیش‌فرض‌های بسیاری از ما هماهنگ نیست. مثلا همه‌ی ما عمدتا تلاش کرده‌ایم تا یک قابلیت را در صورت امکان به صورت عمومی‌تر فراهم کنیم (مورد ۸) یا اصرار داشته‌ایم که پارامتر‌های هاردکدشده را به بیرون کد منتقل کنیم (مورد ۹). اینجا هم، مثل هزاران موضوع دیگر در مهندسی نرم‌افزار، با یک trade-off روبرو هستیم که برآورد هزینه/فایده‌ی آن را به خودتان واگذار می‌کنیم.

ایام به کام و پایپلاین‌هایتان پاس!