فاصلهٔ میان «تمام شد» و «منتشر شد»: خط تحویل خودکار چه میکند
یک قابلیت ممکن است یک هفته تمام شده باشد و هنوز به دست هیچ کاربری نرسیده باشد. چیزی که در این فاصله نشسته، معمولاً یک نفر است، یک سری مرحله که در ذهن دارد، و شبی که کسی دلش نمیخواست صرفش کند. این متن دربارهٔ چیزی است که جای این فاصله مینشیند.
مراسمی که هیچجا نوشته نشده
از یک تیم بپرسید چطور نسخه منتشر میکنند. معمولاً جوابی میگیرید که ساده به نظر میرسد و چهل دقیقه طول میکشد.
یک نفر تستها را روی سیستم خودش اجرا میکند، یا قصد داشته اجرا کند. یک نفر پروژه را بیلد میگیرد. یک نفر خروجی را با SSH روی سرور کپی میکند یا در پنل بالا میاندازد. یک نفر یادش میآید پایگاه داده هم باید تغییر کند و آن را دستی اجرا میکند، امیدوارانه به ترتیب درست. یک نفر سرویس را ریاستارت میکند. بعد همه ده دقیقه سایت را نگاه میکنند تا ببینند جایی آتش میگیرد یا نه.
این روش کار میکند. در واقع بیشتر وقتها کار میکند و خطرش هم دقیقاً از همینجاست. مراحل در ذهن یک نفر است، پس انتشار وقتی انجام میشود که آن یک نفر باشد. چون هر انتشار گران است، تغییرها دستهای میشوند: سه هفته تغییر، یکجا. و چون دستهٔ بزرگ پرخطر است، انتشار شب هنگام انجام میشود؛ وقتی کمترین تعداد آدم خرابی را میبینند و کمترین تعداد آدم بیدارند که درستش کنند.
اگر یک انتشار چهل تغییر داشته باشد و چیزی خراب شود، شما یک باگ ندارید. چهل مظنون دارید.
این دو حرف دقیقاً یعنی چه
CI — یکپارچهسازی پیوسته. هر تغییر مرتب در شاخهٔ اصلی مشترک ادغام میشود و هر ادغام بهصورت خودکار بررسی میشود. کلمهٔ اصلی *یکپارچهسازی* است: مشکلی که حل میکند، شاخههایی است که سه هفته جدا میمانند و بعد به هم میخورند. وقتی همه هر روز ادغام کنند، تعارضها کوچک و همیشگیاند، نه نادر و عظیم. کار خودکار — اجرای تست روی هر پوش — همان چیزی است که این ادغام مکرر را امن میکند.
CD — تحویل پیوسته، یا انتشار پیوسته. این دو یکی نیستند و تفاوتشان مهم است:
- تحویل پیوسته یعنی شاخهٔ اصلی *همیشه* در وضعیت قابل انتشار است؛ بررسیشده و بستهبندیشده. انتشار تصمیمی است که یک نفر میگیرد و با یک کلیک انجام میشود.
- انتشار پیوسته یعنی آن کلیک هم نیست. هر تغییری که از همهٔ بررسیها رد شود، خودش روی محیط عملیاتی میرود.
بیشتر تیمها بهتر است اول به تحویل پیوسته برسند. انتشار پیوسته همان ماشین است با یک دروازهٔ دستیِ کمتر، و فقط وقتی منطقی است که بررسیها واقعاً قابل اعتماد شده باشند.
خط تحویل، مرحله به مرحله
خط تحویل چیزی نیست جز همین مراحل، به همین ترتیب، هر بار، بدون اینکه آدمی تصمیم بگیرد.
۱. تغییر پوش میشود کوچک و مکرر بهتر از بزرگ و دیرهنگام است. تغییری که چهار فایل را لمس میکند، در چند دقیقه بازبینی میشود و اگر بد رفتار کند، در چند ثانیه ردیابی میشود.
۲. بررسیهای سریع اول اجرا میشوند قالببندی، لینت، بررسی تایپ، تست واحد. اینها زیر یک دقیقه طول میکشند، پس ترتیب مهم است: بررسی ارزان باید قبل از بررسی گران رد شود. یک اشتباه تایپی نباید پشت تست مرورگر منتظر بماند.
روی هر اجراکنندهٔ ابری، دو تنظیم از همان روز اول هزینهشان را برمیگردانند. نصب وابستگیها را بر اساس هشِ فایل قفل کش کنید، تا شاخهای که به وابستگیها دست نزده اصلاً نصب نکند. و اجراهای منسوخ را لغو کنید — در GitHub Actions یعنی یک گروه concurrency بر اساس نام شاخه بههمراه cancel-in-progress — وگرنه سه پوش در ده دقیقه یعنی سه خط لولهٔ کامل که سر همان اجراکنندهها با هم رقابت میکنند و آن یکی که برایتان مهم است آخر صف است.
۳. بیلد فقط یک بار گرفته میشود این همان مرحلهای است که فرایندهای دستی معمولاً اشتباه انجامش میدهند. پروژه یک بار بیلد میشود، یک خروجی تولید میکند، و دقیقاً همان خروجی است که اول به پیشنمایش و بعد به محیط عملیاتی میرود. برای هیچ محیطی دوباره بیلد گرفته نمیشود.
دلیلش ساده است: اگر برای محیط آزمایشی و محیط عملیاتی جداگانه بیلد بگیرید، چیزی را تست کردهاید و چیز دیگری را منتشر کردهاید. تنظیمات — آدرس پایگاه داده، کلیدها، کلیدهای فعالسازی قابلیت — در زمان اجرا از محیط خوانده میشوند، نه اینکه داخل یک بیلد دوم کامپایل شوند. این تنها قاعدهای از فهرست دوازدهعاملی است که در همان ماه اول هزینهاش را پس میدهد.
دو جزئیات باعث میشود «همان خروجی» واقعاً همان باشد، نه تقریباً همان. خروجی را با هشِ محتوایش آدرس بدهید، نه با یک برچسب متغیر: latest نسخه نیست، و برچسبی که کسی میتواند جابهجایش کند هم نیست. و به خط تحویل بهجای کلید ابری دائمی که در اسرار مخزن نشسته، اعتبار کوتاهمدت از طریق OIDC بدهید — کلید انتشار ثابت، ارزشمندترین رازِ بیشتر سازمانهاست و همان رازی است که کسی عوضش نمیکند.
۴. محیط پیشنمایش ساخته میشود تغییر روی آدرس موقت خودش با دادهٔ خودش منتشر میشود؛ محیطی که کسی دیگر نمیتواند خرابش کند. بازبین بهجای خواندن توضیح تغییر، نسخهٔ واقعی و کارکنندهٔ آن را باز میکند.
این کمارزشگذاریشدهترین مرحلهٔ کل مسیر است. «به نظرم درست است» را به «کلیک کردم و دیدم» تبدیل میکند.
۵. بررسیهای کند روی پیشنمایش اجرا میشوند تست سرتاسری یک مرورگر واقعی را روی مسیرهای مهم میراند. چند دقیقه طول میکشد و به همین دلیل اینجا اجرا میشود نه اول مسیر، و به همین دلیل هم باید تعدادش کم باشد.
۶. ادغام، و بعد انتشار وقتی همهچیز سبز شد و یک نفر تغییر را تأیید کرد، ادغام انجام میشود — و همین ادغام است که انتشار روی محیط عملیاتی را فعال میکند. نه یک آدم. نه اسکریپتی که کسی اجرا میکند. خودِ ادغام.
۷. مهاجرتها داخل خط تحویل اجرا میشوند تغییر پایگاه داده هم کد است: نوشته میشود، بازبینی میشود، نسخه میگیرد و خط تحویل آن را به ترتیب اجرا میکند. هیچوقت نیمهشب در کنسول محیط عملیاتی تایپ نمیشود.
الگویی که این کار را امن میکند گسترش، سپس جمع کردن است — یعنی چهار انتشار، برای کاری که از بیرون یک تغییر نام به نظر میرسد:
- ستون جدید را اضافه کنید، بهصورت nullable. هنوز هیچکس از آن نمیخواند.
- کدی منتشر کنید که در هر دو ستون مینویسد و هنوز از ستون قدیمی میخواند.
- دادهٔ قدیمی را دستهدسته منتقل کنید، بعد خواندن را به ستون جدید منتقل کنید.
- در انتشاری بعدی، وقتی دیگر چیزی به آن ارجاع نمیدهد، ستون قدیمی را حذف کنید.
هر مرحله جداگانه برگشتپذیر است و در هیچ لحظهای کدِ در حال اجرا با ساختار پایگاه داده اختلاف ندارد. انتشاری که یکباره نام را عوض کند فقط روی کاغذ برگشتپذیر است: کد را که برگردانید، حالا با ستونی حرف میزند که دیگر وجود ندارد.
همین دقت برای خود دستورهای تغییر ساختار هم لازم است، چون مهاجرتها قفل میگیرند. CREATE INDEX تا پایان کارش قفل نوشتن نگه میدارد، پس روی جدول زنده باید CREATE INDEX CONCURRENTLY باشد. افزودن کلید خارجی یا محدودیت بررسی، کل جدول را زیر قفل انحصاری اعتبارسنجی میکند، مگر اینکه آن را NOT VALID اضافه کنید و بعد VALIDATE CONSTRAINT بزنید. و پیش از هر تغییر ساختاری یک lock_timeout بگذارید: مهاجرتی که نمیتواند قفلش را بگیرد باید در دو ثانیه شکست بخورد و دوباره تلاش شود، نه اینکه پشت یک کوئری طولانی صف بکشد و همهٔ درخواستها هم پشت *آن* جمع شوند. افزودن ستون با مقدار پیشفرض همان موردی است که قبلاً از آن میترسیدند و دیگر لازم نیست — PostgreSQL 11 آن را از بازنویسی کل جدول به یک تغییر فراداده تبدیل کرد.
۸. تست دودی روی محیط عملیاتی اجرا میشود سه چهار بررسی روی سایت زنده، بلافاصله بعد از انتشار. صفحهٔ اصلی جواب میدهد. ورود کار میکند. یک رکورد خوانده و نوشته میشود. این کار کل آن خانوادهٔ خرابی را میگیرد که از همهٔ تستها رد میشود و باز سایت را پایین میآورد: متغیر محیطی که در محیط عملیاتی تنظیم نشده، مهاجرتی که همهجا جز آنجا اجرا شده، بیلدی که روی پروژهٔ اشتباه رفته.
۹. راه بازگشت وجود دارد اگر تست دودی رد شود، نسخهٔ قبلی خودکار برمیگردد، و اگر هم رد نشود یک نفر میتواند در یک مرحله برش گرداند. نکتهٔ مهم این است که بازگشت تمرین شده باشد. روالی که هیچوقت اجرا نشده، یک امید است نه یک برنامه.
اینکه چقدر میشود عقب برگشت، به روش انتشاری بستگی دارد که انتخاب کردهاید؛ پس ارزش دارد آگاهانه انتخابش کنید:
- غلتان: نمونهها چندتاچندتا جایگزین میشوند. ارزان است و ظرفیت اضافه نمیخواهد، اما برای چند دقیقه دو نسخه همزمان زندهاند — که فقط وقتی اشکالی ندارد که ساختار پایگاه داده هر دو را تحمل کند؛ یعنی باز همان انضباط گسترش و جمع کردن.
- آبی–سبز: نسخهٔ جدید کنار نسخهٔ قدیمی بالا میآید و ترافیک روی مسیریاب جابهجا میشود. بازگشت، همان جابهجایی در جهت عکس است و به همین دلیل سریعترین بازگشت ممکن را میدهد. هزینهاش دو برابر ظرفیت در طول تغییر است.
- قناری: سهم کوچکی از ترافیک به نسخهٔ جدید میرود، نرخ خطا و زمان پاسخ دیده میشود، و بر اساس همان ادامه میدهد یا متوقف میشود. به ترافیک کافی نیاز دارد تا نمونه معنا داشته باشد.
و چیزی که بیشترِ فشار را از هر سهٔ اینها برمیدارد: تغییرهای پرخطر را پشت کلید فعالسازی قابلیت بگذارید، تا انتشار فنی و انتشار برای کاربر دیگر یک رویداد نباشند. کد سهشنبه خاموش منتشر میشود. پنجشنبه یک نفر آن را برای کارکنان روشن میکند، بعد برای پنج درصد، بعد برای همه — و خاموش کردنش یک کلید است، نه یک انتشار.
وقتی این مسیر وجود داشته باشد، چه چیزی عوض میشود
توضیح فنی، اثر واقعی را کم نشان میدهد؛ چون اثر واقعی به آدمها مربوط است.
اندازهٔ دستهها کوچک میشود. انتشار ارزان میشود، پس بهجای هر سه هفته، روزی چند بار انجام میشود. دستهٔ کوچک یعنی وقتی چیزی خراب میشود، علت واضح است؛ فقط یک تغییر تازه وجود دارد که نگاهش کنید.
ترس از اتاق بیرون میرود. کسی از انتشاری که این ماه چهل بار دیده و دو بار بدون دردسر برگردانده، نمیترسد. تیمی که از انتشار میترسد، کمکم منتشر نمیکند؛ و محصولی که فصلی یک بار بهروز شود، دیگر به کاربر خودش پاسخ نمیدهد.
انتشار به یک نفر وابسته نمیماند. مراحل در فایلی داخل مخزن است، نه در حافظهٔ کسی. آدمها میتوانند مرخصی بروند. آدمها میتوانند بروند.
بازبینی واقعی میشود. آدرس پیشنمایش، تأیید را از یک تشریفات به یک بررسی واقعی تبدیل میکند.
پنجشنبهها عادی میشوند. توصیهٔ «آخر هفته منتشر نکنید» قانون طبیعت نیست. نشانهٔ فرایندی است که نمیشود سریع برش گرداند.
اگر میخواهید بدانید این کارها واقعاً جواب میدهند یا نه، چهار عدد ارزش پیگیری دارند و سالهاست مجموعهٔ استاندارد صنعتاند، چون دور زدنشان سخت است: چند وقت یک بار منتشر میکنید؛ یک تغییر چقدر طول میکشد تا از کامیت به محیط عملیاتی برسد؛ چند درصد انتشارها مشکل ایجاد میکنند؛ و وقتی مشکلی پیش آمد، برگشتن به حالت سالم چقدر طول میکشد. نکتهٔ مهمی که از پژوهشهای این حوزه درمیآید این است که دو عدد اول و دو عدد آخر *با هم* حرکت میکنند. تیمهایی که بیشتر منتشر میکنند، سریعتر هم بازمیگردند؛ چون تغییر کوچک هم فهمیدنش سادهتر است و هم برگرداندنش. کم منتشر کردن برای «احتیاط»، دقیقاً همان چیزی است که انتشار را خطرناک میکند.
هزینهاش چیست و چه چیزی را درست نمیکند
خط تحویل نه رایگان است و نه معجزه.
هزینهٔ زمان اجرا دارد و هزینهٔ نگهداری؛ خط تحویل هم کد است و مثل کد خراب میشود. هزینهٔ انضباط هم دارد، چون همان لحظهای که رد کردن یک بررسیِ قرمز عادی شود، کل ماجرا به نمایش تبدیل میشود.
و بهتنهایی هیچ چیزی را درست نمیکند. خط تحویل دقیقاً به اندازهٔ بررسیهای داخلش معتبر است. بیلد سبزی که پشتش تستی نباشد، چراغ سبزی است که هیچ معنایی ندارد؛ فقط میگوید کد کامپایل شد، نه اینکه محصول کار میکند. به همین دلیل خط تحویل و مجموعهٔ تست یک کارند، نه دو کار: تست به خط تحویل حرفی برای گفتن میدهد و خط تحویل مطمئن میشود که تستها واقعاً اجرا میشوند.
مقصد خرابِ انتشار را هم درست نمیکند. اگر محیط عملیاتی یک سرور تکی باشد که سه سال دستی دستکاری شده، اولین قدم صادقانه این است که آن محیط قابل بازسازی شود.
شروع روی پروژهای که از قبل وجود دارد
هیچکدام از اینها بازنویسی لازم ندارد. روی پروژهای که همین حالا زنده است، ترتیبی که زودتر جواب میدهد این است:
- بیلد را در CI راه بیندازید، حتی بدون هیچ تستی. همین که ثابت شود پروژه از یک کلون تمیز بیلد میشود، فایلی را پیدا میکند که کسی یادش رفته کامیت کند.
- بررسی تایپ و لینت را به همان اجرا اضافه کنید. ارزان، سریع، و بلافاصله مفید.
- یک تست دودی روی محیط عملیاتی اضافه کنید که بعد از هر انتشار اجرا شود.
- خود انتشار را خودکار کنید، تا یک دستور باشد نه یک دنبالهٔ بهخاطرسپرده — و بازگشت را همان دستور با نسخهٔ قبلی قرار دهید.
- محیط پیشنمایش اضافه کنید، تا بازبینی چیزی برای دیدن داشته باشد.
- بعد لایههای تست را پر کنید، از پرخطرترین بخش به سمت پایین.
هر مرحله بهتنهایی مفید است و هر کدام مرحلهٔ بعدی را ارزانتر میکند. تا مرحلهٔ چهارم، انتشار دیگر یک رویداد نیست — و هدف هم از ابتدا همین بود.