فاصلهٔ میان «تمام شد» و «منتشر شد»: خط تحویل خودکار چه میکند
یک قابلیت ممکن است یک هفته تمام شده باشد و هنوز به دست هیچ کاربری نرسیده باشد. چیزی که در این فاصله نشسته، معمولاً یک نفر است، یک سری مرحله که در ذهن دارد، و شبی که کسی دلش نمیخواست صرفش کند. این متن دربارهٔ چیزی است که میتواند این فاصله را پر کند.
مراسمی که هیچجا نوشته نشده
از یک تیم بپرسید چطور نسخه منتشر میکنند. معمولاً جوابی میگیرید که ساده به نظر میرسد و چهل دقیقه طول میکشد.
یک نفر تستها را روی سیستم خودش اجرا میکند، یا قصد داشته اجرا کند. یک نفر از پروژه Build میگیرد. یک نفر خروجی را با SSH روی سرور کپی میکند یا در پنل بالا میاندازد. یک نفر یادش میآید پایگاه داده هم باید تغییر کند و آن را دستی اجرا میکند، امیدوارانه به ترتیب درست. یک نفر سرویس را ریاستارت میکند. بعد همه ده دقیقه سایت را نگاه میکنند تا ببینند جایی آتش میگیرد یا نه.
این روش کار میکند. در واقع بیشتر وقتها کار میکند و خطرش هم دقیقاً از همینجاست. مراحل در ذهن یک نفر است، پس انتشار وقتی انجام میشود که آن یک نفر باشد. چون هر انتشار گران است، تغییرها دستهای میشوند: سه هفته تغییر، یکجا. و چون دستهٔ بزرگ پرخطر است، انتشار شب هنگام انجام میشود؛ وقتی کمترین تعداد آدم خرابی را میبینند و کمترین تعداد آدم بیدارند که درستش کنند.
اگر یک انتشار چهل تغییر داشته باشد و چیزی خراب شود، شما یک باگ ندارید. چهل مظنون دارید.
این دو حرف دقیقاً یعنی چه
CI — یکپارچهسازی پیوسته. هر تغییر مرتب در شاخهٔ اصلی مشترک ادغام میشود و هر ادغام بهصورت خودکار بررسی میشود. کلمهٔ اصلی *یکپارچهسازی* است: مشکلی که حل میکند، شاخههایی است که سه هفته جدا میمانند و بعد به هم میخورند. وقتی همه هر روز ادغام کنند، تعارضها کوچک و همیشگیاند، نه نادر و عظیم. کار خودکار — اجرای تست روی هر Push — همان چیزی است که این ادغام مکرر را امن میکند.
CD — تحویل پیوسته، یا انتشار پیوسته. این دو یکی نیستند و تفاوتشان مهم است:
- تحویل پیوسته یعنی شاخهٔ اصلی *همیشه* در وضعیت قابل انتشار است؛ بررسیشده و بستهبندیشده. انتشار تصمیمی است که یک نفر میگیرد و با یک کلیک انجام میشود.
- انتشار پیوسته یعنی آن کلیک هم نیست. هر تغییری که از همهٔ بررسیها رد شود، خودش روی محیط عملیاتی میرود.
بیشتر تیمها بهتر است اول به تحویل پیوسته برسند. انتشار پیوسته همان ماشین است با یک مرحلهٔ دستیِ کمتر، و فقط وقتی منطقی است که بررسیها واقعاً قابل اعتماد شده باشند.
خط تحویل، مرحله به مرحله
خط تحویل چیزی نیست جز همین مراحل، به همین ترتیب، هر بار، بدون اینکه آدمی تصمیم بگیرد.
۱. تغییر Push میشود کوچک و مکرر بهتر از بزرگ و دیرهنگام است. تغییری که چهار فایل را لمس میکند، در چند دقیقه بازبینی میشود و اگر بد رفتار کند، در چند ثانیه ردیابی میشود.
۲. بررسیهای سریع اول اجرا میشوند قالببندی کد، Lint، بررسی نوع دادهها، تست واحد. اینها زیر یک دقیقه طول میکشند، پس ترتیب مهم است: بررسی ارزان باید پیش از بررسی گران انجام شود. یک غلط املایی در کد نباید پشت تست مرورگر منتظر بماند.
روی هر Runner ابری، دو تنظیم از همان روز اول هزینهشان را برمیگردانند. نصب وابستگیها را بر اساس هشِ فایل قفل وابستگیها (lockfile) کش کنید، تا شاخهای که به وابستگیها دست نزده اصلاً نصبشان نکند. و اجراهای منسوخ را لغو کنید — در GitHub Actions یعنی یک گروه concurrency بر اساس نام شاخه بههمراه cancel-in-progress — وگرنه سه Push در ده دقیقه یعنی سه خط تحویل کامل که سر همان Runnerها با هم رقابت میکنند و آن یکی که برایتان مهم است آخر صف میماند.
۳. Build فقط یک بار گرفته میشود این همان مرحلهای است که فرایندهای دستی معمولاً اشتباه انجامش میدهند. از پروژه یک بار Build گرفته میشود، یک خروجی تولید میکند، و دقیقاً همان خروجی است که اول به پیشنمایش و بعد به محیط عملیاتی میرود. برای هیچ محیطی دوباره Build گرفته نمیشود.
دلیلش ساده است: اگر برای محیط آزمایشی و محیط عملیاتی جداگانه Build بگیرید، چیزی را تست کردهاید و چیز دیگری را منتشر کردهاید. تنظیمات — آدرس پایگاه داده، کلیدها، و کلیدهای فعالسازی قابلیت — در زمان اجرا از محیط خوانده میشوند، نه اینکه داخل یک Build دوم کامپایل شوند. این تنها قاعده از فهرست دوازدهعاملی (Twelve-Factor) است که در همان ماه اول هزینهاش را پس میدهد.
دو جزئیات باعث میشود «همان خروجی» واقعاً همان باشد، نه تقریباً همان. خروجی را با هشِ محتوایش آدرس بدهید، نه با یک برچسب متغیر: latest نسخه نیست، و برچسبی که کسی میتواند جابهجایش کند هم نیست. و بهجای اینکه یک کلید ابری دائمی را در Secretهای مخزن نگه دارید، به خط تحویل اعتبار کوتاهمدت از طریق OIDC بدهید — کلید انتشار ثابت، ارزشمندترین رمز بیشتر سازمانهاست و همان رمزی است که هیچوقت کسی عوضش نمیکند.
۴. محیط پیشنمایش ساخته میشود تغییر روی آدرس موقت خودش با دادهٔ خودش منتشر میشود؛ محیطی که کسی دیگر نمیتواند خرابش کند. بازبین بهجای خواندن توضیح تغییر، نسخهٔ واقعی و کارکنندهٔ آن را باز میکند.
این دستکمگرفتهشدهترین مرحلهٔ کل مسیر است. «به نظرم درست است» را به «کلیک کردم و دیدم» تبدیل میکند.
۵. بررسیهای کند روی پیشنمایش اجرا میشوند تست سرتاسری یک مرورگر واقعی را روی مسیرهای مهم میراند. چند دقیقه طول میکشد و به همین دلیل اینجا اجرا میشود نه اول مسیر، و به همین دلیل هم باید تعدادش کم باشد.
۶. ادغام، و بعد انتشار وقتی همهچیز سبز شد و یک نفر تغییر را تأیید کرد، ادغام انجام میشود — و همین ادغام است که انتشار روی محیط عملیاتی را فعال میکند. نه یک آدم. نه اسکریپتی که کسی اجرا میکند. خودِ ادغام.
۷. تغییرهای پایگاه داده داخل خط تحویل اجرا میشوند تغییر ساختار پایگاه داده — همان چیزی که به آن Migration میگویند — هم کد است: نوشته میشود، بازبینی میشود، نسخه میگیرد و خط تحویل آن را به ترتیب اجرا میکند. هیچوقت نیمهشب در کنسول محیط عملیاتی تایپ نمیشود.
الگویی که این کار را امن میکند گسترش، سپس جمع کردن (expand then contract) است — یعنی چهار انتشار، برای کاری که از بیرون فقط یک تغییر نام به نظر میرسد:
- ستون جدید را اضافه کنید، بهصورت nullable. هنوز هیچکس از آن نمیخواند.
- کدی منتشر کنید که در هر دو ستون مینویسد و هنوز از ستون قدیمی میخواند.
- دادهٔ قدیمی را دستهدسته منتقل کنید، بعد خواندن را به ستون جدید منتقل کنید.
- در انتشاری بعدی، وقتی دیگر چیزی به آن ارجاع نمیدهد، ستون قدیمی را حذف کنید.
هر مرحله جداگانه برگشتپذیر است و در هیچ لحظهای کدِ در حال اجرا با ساختار پایگاه داده اختلاف ندارد. انتشاری که یکباره نام را عوض کند فقط روی کاغذ برگشتپذیر است: کد را که برگردانید، حالا با ستونی حرف میزند که دیگر وجود ندارد.
همین دقت برای خود دستورهای تغییر ساختار هم لازم است، چون این تغییرها قفل میگیرند. CREATE INDEX تا پایان کارش جلوی نوشتن را میگیرد، پس روی جدول زنده باید CREATE INDEX CONCURRENTLY باشد. افزودن کلید خارجی یا محدودیت CHECK، کل جدول را زیر قفل انحصاری اعتبارسنجی میکند، مگر اینکه اول آن را NOT VALID اضافه کنید و بعد VALIDATE CONSTRAINT بزنید. و پیش از هر تغییر ساختاری یک lock_timeout بگذارید: تغییری که نمیتواند قفلش را بگیرد باید در دو ثانیه شکست بخورد و دوباره تلاش شود، نه اینکه پشت یک کوئری طولانی صف بکشد و همهٔ درخواستها هم پشت *آن* جمع شوند. افزودن ستون با مقدار پیشفرض همان موردی است که قبلاً از آن میترسیدند و دیگر لازم نیست — PostgreSQL 11 آن را از بازنویسی کل جدول به یک تغییر سادهٔ فراداده تبدیل کرد.
۸. تست سلامت روی محیط عملیاتی اجرا میشود سه چهار بررسی روی سایت زنده، بلافاصله بعد از انتشار. صفحهٔ اصلی جواب میدهد. ورود کار میکند. یک رکورد خوانده و نوشته میشود. این کار کل آن دستهٔ خرابی را میگیرد که از همهٔ تستها رد میشود و باز سایت را پایین میآورد: متغیر محیطی که در محیط عملیاتی تنظیم نشده، تغییر ساختاری که همهجا اجرا شده جز آنجا، و Buildی که روی پروژهٔ اشتباه رفته.
۹. راه بازگشت وجود دارد اگر تست سلامت رد شود، نسخهٔ قبلی خودکار برمیگردد، و اگر هم رد نشود یک نفر میتواند در یک مرحله برش گرداند. نکتهٔ مهم این است که بازگشت تمرین شده باشد. روالی که هیچوقت اجرا نشده، یک امید است نه یک برنامه.
اینکه چقدر میشود عقب برگشت، به روش انتشاری بستگی دارد که انتخاب کردهاید؛ پس ارزش دارد آگاهانه انتخابش کنید:
- تدریجی (Rolling): سرورها چندتاچندتا با نسخهٔ جدید عوض میشوند. ارزان است و ظرفیت اضافه نمیخواهد، اما برای چند دقیقه دو نسخه همزمان زندهاند — که فقط وقتی اشکالی ندارد که ساختار پایگاه داده هر دو را تحمل کند؛ یعنی باز همان انضباط گسترش و جمع کردن.
- آبی–سبز (Blue-Green): نسخهٔ جدید کنار نسخهٔ قدیمی بالا میآید و ترافیک روی مسیریاب جابهجا میشود. بازگشت، همان جابهجایی در جهت عکس است و به همین دلیل سریعترین بازگشت ممکن را میدهد. هزینهاش دو برابر ظرفیت در طول تغییر است.
- قناری (Canary): سهم کوچکی از ترافیک به نسخهٔ جدید میرود، نرخ خطا و زمان پاسخ دیده میشود، و بر اساس همان ادامه میدهد یا متوقف میشود. به ترافیک کافی نیاز دارد تا نمونه معنا داشته باشد.
و چیزی که بیشترِ فشار را از هر سهٔ اینها برمیدارد: تغییرهای پرخطر را پشت یک کلید فعالسازی قابلیت (feature flag) بگذارید، تا انتشار فنی و رسیدن قابلیت به کاربر دیگر یک اتفاق واحد نباشند. کد سهشنبه خاموش منتشر میشود. پنجشنبه یک نفر آن را برای کارکنان روشن میکند، بعد برای پنج درصد، بعد برای همه — و خاموش کردنش یک کلید است، نه یک انتشار.
وقتی این مسیر وجود داشته باشد، چه چیزی عوض میشود
توضیح فنی، اثر واقعی را کم نشان میدهد؛ چون اثر واقعی به آدمها مربوط است.
اندازهٔ دستهها کوچک میشود. انتشار ارزان میشود، پس بهجای هر سه هفته، روزی چند بار انجام میشود. دستهٔ کوچک یعنی وقتی چیزی خراب میشود، علت واضح است؛ فقط یک تغییر تازه وجود دارد که نگاهش کنید.
ترس از انتشار از بین میرود. کسی از انتشاری که این ماه چهل بار دیده و دو بار بدون دردسر برگردانده، نمیترسد. تیمی که از انتشار میترسد، کمکم منتشر نمیکند؛ و محصولی که فصلی یک بار بهروز شود، دیگر به کاربر خودش پاسخ نمیدهد.
انتشار به یک نفر وابسته نمیماند. مراحل در فایلی داخل مخزن است، نه در حافظهٔ کسی. آدمها میتوانند مرخصی بروند. آدمها میتوانند بروند.
بازبینی واقعی میشود. آدرس پیشنمایش، تأیید را از یک تشریفات به یک بررسی واقعی تبدیل میکند.
پنجشنبهها عادی میشوند. توصیهٔ «آخر هفته منتشر نکنید» قانون طبیعت نیست. نشانهٔ فرایندی است که نمیشود سریع برش گرداند.
اگر میخواهید بدانید این کارها واقعاً جواب میدهند یا نه، چهار عدد ارزش پیگیری دارند و سالهاست مجموعهٔ استاندارد صنعتاند، چون دور زدنشان سخت است: چند وقت یک بار منتشر میکنید؛ یک تغییر چقدر طول میکشد تا از commit به محیط عملیاتی برسد؛ چند درصد انتشارها مشکل ایجاد میکنند؛ و وقتی مشکلی پیش آمد، برگشتن به حالت سالم چقدر طول میکشد. نکتهٔ مهمی که از پژوهشهای این حوزه درمیآید این است که دو عدد اول و دو عدد آخر *با هم* حرکت میکنند. تیمهایی که بیشتر منتشر میکنند، سریعتر هم بازمیگردند؛ چون تغییر کوچک هم فهمیدنش سادهتر است و هم برگرداندنش. کم منتشر کردن برای «احتیاط»، دقیقاً همان چیزی است که انتشار را خطرناک میکند.
هزینهاش چیست و چه چیزی را درست نمیکند
خط تحویل نه رایگان است و نه معجزه.
هزینهٔ زمان اجرا دارد و هزینهٔ نگهداری؛ خط تحویل هم کد است و مثل کد خراب میشود. هزینهٔ انضباط هم دارد، چون همان لحظهای که رد کردن یک بررسیِ قرمز عادی شود، کل ماجرا به نمایش تبدیل میشود.
و بهتنهایی هیچ چیزی را درست نمیکند. خط تحویل دقیقاً به اندازهٔ بررسیهای داخلش معتبر است. Build سبزی که پشتش تستی نباشد، چراغ سبزی است که هیچ معنایی ندارد؛ فقط میگوید کد کامپایل شد، نه اینکه محصول کار میکند. به همین دلیل خط تحویل و مجموعهٔ تست یک کارند، نه دو کار: تست به خط تحویل حرفی برای گفتن میدهد و خط تحویل مطمئن میشود که تستها واقعاً اجرا میشوند.
محیطی را که خودش خراب است هم درست نمیکند. اگر محیط عملیاتی یک سرور تکی باشد که سه سال دستی دستکاری شده، اولین قدم صادقانه این است که همان محیط قابل بازسازی شود.
شروع روی پروژهای که از قبل وجود دارد
هیچکدام از اینها بازنویسی لازم ندارد. روی پروژهای که همین حالا زنده است، ترتیبی که زودتر جواب میدهد این است:
- Build را در CI راه بیندازید، حتی بدون هیچ تستی. همین که ثابت شود پروژه از روی یک کلون تمیز Build میشود، فایلی را پیدا میکند که کسی یادش رفته commit کند.
- بررسی نوع دادهها و Lint را به همان اجرا اضافه کنید. ارزان، سریع، و بلافاصله مفید.
- یک تست سلامت روی محیط عملیاتی اضافه کنید که بعد از هر انتشار اجرا شود.
- خود انتشار را خودکار کنید، تا یک دستور باشد نه مجموعهای از مراحل که در ذهن کسی است — و بازگشت را هم همان دستور با نسخهٔ قبلی قرار دهید.
- محیط پیشنمایش اضافه کنید، تا بازبینی چیزی برای دیدن داشته باشد.
- بعد لایههای تست را پر کنید، از پرخطرترین بخش به سمت پایین.
هر مرحله بهتنهایی مفید است و هر کدام مرحلهٔ بعدی را ارزانتر میکند. تا مرحلهٔ چهارم، انتشار دیگر یک اتفاق پرتنش نیست — و هدف هم از ابتدا همین بود.