انتشار نسخهٔ تازه و بازگشت مطمئن؛ از r1 تا r2
در این درس ۶ بخش
اجرای نسخهٔ اول فقط آغاز نگهداری بکاند است. دیر یا زود باید کد را تغییر دهید و نسخهٔ تازهای منتشر کنید. اگر هنگام کار سرویس، فایلهای پوشهٔ فعلی را یکییکی جایگزین کنید، ممکن است بخشی از برنامه از نسخهٔ قبلی و بخشی از نسخهٔ تازه باشد. همچنین وقتی خطا رخ دهد، معلوم نیست دقیقاً به چه چیزی باید برگردید. در این درس از پوشههای مستقل انتشار استفاده میکنیم: r1 نسخهٔ فعلی، r2 نامزد تازه و current یک پیوند نمادین به نسخهٔ انتخابشده است. پیوند نمادین یک ورودی فایلسیستم است که به مسیر دیگری اشاره میکند؛ کپی کد یا فایل میانبر مخصوص ویندوز نیست. نتیجهٔ مورد انتظار، یک انتشار کنترلشده با آزمون قبل و بعد و مسیر بازگشت مشخص است. فرمانها فقط برای همان ماشین آزمایشی تازهٔ دورهاند؛ روی سروری که برنامههای دیگری دارد، مسیرها و سیاست انتشار باید جداگانه طراحی شوند.
قرارداد انتشار را پیش از تغییر بنویسید
انتشار را یک معاملهٔ ساده با چند شرط تصور کنید: ورودی آن یک پوشهٔ کامل و آزموده است؛ خروجی آن اجرای نسخهٔ انتخابشده و پاسخ سالم است. پیش از تغییر باید بدانید نسخهٔ فعلی کدام پوشه است، کدام فایلها قرار است تغییر کنند و در چه وضعیتی انتشار را متوقف میکنید. در این تمرین، رفتار API ثابت میماند و شمارهٔ بسته را از ۱٫۰٫۰ به ۱٫۰٫۱ تغییر میدهیم تا فرایند ساخت بستهٔ دوم را تمرین کنیم؛ اضافهکردن قابلیت تازه، تمرین جداگانهٔ منطق برنامه است. نام r2 شناسهٔ این آزمایش است و در پروژهٔ واقعی میتواند با شناسهٔ commit یا شمارهٔ انتشار جایگزین شود. کد اجراشونده همچنان root-owned است و کاربر darsnode اجازهٔ ویرایش آن را ندارد. برنامه فقط از متغیرهای محیطی واحد خود استفاده میکند؛ حساب سرویس نباید برای انتشار کد به sudo دسترسی بگیرد. در #modules-import-export علت کاملبودن مجموعهٔ فایلهای ماژول را مرور کنید: داشتن server.mjs بدون app.mjs یا model.mjs یک بستهٔ کامل نیست.
قدم اول: نسخهٔ فعلی را شناسایی و نامزد را در پوشهٔ شخصی بسازید
در Bash اوبونتو با حساب مدیر معمولی شروع کنید. فرمان بعدی فقط اگر current یک پیوند باشد و به r1 شناختهشده اشاره کند ادامه میدهد. این محدودیت عمدی است تا فرمان نمونه، انتشار متفاوت یا اجرای دوبارهٔ نامشخص را بیخبر تغییر ندهد. نامزد در پوشهٔ موقت شخصی ساخته میشود؛ هنوز هیچ فایلی در مسیر اجرای سرویس عوض نشده است. کدها و فایلهای نمونه از نسخهٔ فعلی خوانده میشوند، اما فایل واقعی app.env بیرون این درخت است و وارد نامزد نمیشود. فایل package.json دادهٔ JSON است؛ آن را با یک برنامهٔ کوچک میخوانیم و فقط نسخه را تغییر میدهیم. بعد از اجرا باید مسیر پوشهٔ نامزد را ببینید. مقدار همین متغیرها را در همان ترمینال نگه دارید؛ برای اجرای نیمهٔ بعد در ترمینال تازه، باید مرحلهٔ شناسایی را آگاهانه تکرار کنید، نه اینکه به متغیرهای خالی تکیه کنید.
prepare_r2() {
test -L /opt/darsavand-node/current || { printf 'current is not a symlink\n'; return 1; }
previous_release=$(readlink -f /opt/darsavand-node/current) || return 1
test "$previous_release" = /opt/darsavand-node/releases/r1 || { printf 'Unexpected current release\n'; return 1; }
stage_release=$(mktemp -d "$HOME/node-r2.XXXXXX") || return 1
cp -R -- "$previous_release/." "$stage_release/" || return 1
/opt/node-current/bin/node --input-type=module - "$stage_release" <<'JS'
import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
const file = join(process.argv[2], 'package.json');
const pkg = JSON.parse(await readFile(file, 'utf8'));
if (pkg.version !== '1.0.0') throw new Error('Unexpected package version');
pkg.version = '1.0.1';
await writeFile(file, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
JS
test "$?" -eq 0 || return 1
printf 'Candidate directory: %s\n' "$stage_release"
}
prepare_r2تغییر نسخه به معنی سازگاری یا سلامت خودکار نیست. شمارهٔ جدید فقط بخشی از شناسنامهٔ بسته است و کد باید دوباره بررسی شود. اگر فایل JSON خراب بود یا نسخهٔ اولیه با انتظار فرق داشت، در همین نقطه توقف کنید. برای پنهانکردن خطا، یک package.json خالی نسازید. اگر فرمانی مانند cp شکست خورد، ادامهدادن با نامزد ناقص خطای دوم تولید میکند. هر بلوک را تا نتیجهٔ موفق دنبال کنید و پیش از قدم بعد، وجود فایلهای لازم را بررسی کنید. پوشهٔ مرحلهٔ آمادهسازی متعلق به حساب شماست تا آزمون و ویرایش بدون امتیاز مدیریتی انجام شود. این تصمیم، خطر اجرای ابزار توسعه با دسترسی root را کم میکند. بعد از آزمون، نسخهٔ تأییدشده به مسیر تحت مدیریت root منتقل میشود. این جداسازی، وظیفهٔ نویسندهٔ کد و هویت اجرای سرویس را روشن نگه میدارد.
(
set -euo pipefail
cd "$stage_release"
/opt/node-current/bin/node --check app.mjs
/opt/node-current/bin/node --check model.mjs
/opt/node-current/bin/node --check server.mjs
/opt/node-current/bin/node --test app.test.mjs
)در پایان باید همهٔ آزمونهای پروژه پاس شوند. --check نحو فایل را میسنجد و اجرای HTTP کامل نیست؛ node --test آزمونهای قرارداد، مدل و درخواستهای واقعی محلی پروژه را اجرا میکند. شکست هرکدام، مجوز ادامهٔ انتشار نیست. در این بسته وابستگی خارجی نداریم و نصب npm لازم نیست. در پروژهٔ دارای وابستگی، بستهٔ قفل، نصب کنترلشده و سازگاری باینریهای بومی نیز باید بررسی شوند؛ node_modules ساختهشده روی ویندوز را به اوبونتو کپی نکنید. از نظر زبان، انتظار برای نتیجهٔ آزمون نمونهای از وابستگی مراحل است: مرحلهٔ بعد فقط به پایان مرحلهٔ قبلی وابسته نیست، به موفقیت آن وابسته است. همین تفاوت در #promise و #async-await نیز مهم بود. آزمون واحد موفق، سلامت Nginx، DNS یا گواهی را ثابت نمیکند؛ برای همین در ادامه یک آزمون نامزد روی سرور و یک آزمون پس از تغییر مسیر داریم.
قدم سوم: نامزد کامل را در مسیر انتشار تازه قرار دهید
(
set -euo pipefail
test -d "$stage_release"
test ! -e /opt/darsavand-node/releases/r2
test ! -L /opt/darsavand-node/releases/r2
sudo install -d -m 0755 /opt/darsavand-node/releases/r2
sudo cp -R -- "$stage_release/." /opt/darsavand-node/releases/r2/
sudo chown -R root:root /opt/darsavand-node/releases/r2
sudo find /opt/darsavand-node/releases/r2 -type d -exec chmod 0755 {} +
sudo find /opt/darsavand-node/releases/r2 -type f -exec chmod 0644 {} +
sudo -u darsnode test -r /opt/darsavand-node/releases/r2/server.mjs
)وجود قبلی r2 باعث توقف میشود؛ یک انتشار موجود را با کپی دوباره مخلوط نمیکنیم. اگر انتقال ناقص شد، نامزد را منتشر نکنید و اول علت کمبود فضا یا دسترسی را رفع کنید. فرمان find فقط زیر همان مسیر مشخص کار میکند و این الگو مجوز تغییر بازگشتی کل /opt نیست. فایلهای این پروژه داده و کد خواندنیاند و برای اجرای Node به بیت اجرایی خود فایل mjs نیاز ندارند؛ فایل اجرایی واقعی، باینری Node است. در فایلهای آینده، نیاز مجوزها میتواند متفاوت باشد و نباید این مقادیر را قانون همهٔ برنامهها دانست. بررسی test -r نشان میدهد حساب سرویس فایل ورودی را میخواند؛ وجود خطای دیگر هنگام import هنوز ممکن است و باید با اجرای واقعی نامزد بررسی شود. در هیچ مرحلهای رمز یا محتوای واقعی فایل محیطی را روی ترمینال چاپ نکنید.
قدم چهارم: نامزد را روی پورت جداگانه اجرا کنید
# Ubuntu Bash, terminal A: keep this foreground process open.
sudo -u darsnode env PORT=3001 /opt/node-current/bin/node \
/opt/darsavand-node/releases/r2/server.mjsدر ترمینال اول باید نشانی 127.0.0.1:3001/health را ببینید و برنامه باز بماند. این رفتار طبیعی یک سرور منتظر درخواست است. سرویس اصلی روی ۳۰۰۰ ادامه دارد و Nginx هنوز نامزد را نمیبیند. در اتصال SSH دوم، آزمون سلامت درس دهم را با پورت ۳۰۰۱ اجرا کنید؛ در کنار آن یک کار ساختگی بسازید و قرارداد پاسخ را کنترل کنید. سپس در همان ترمینال اول Ctrl+C بزنید تا نامزد پایان یابد. برای خاموشکردن نامزد، همهٔ فرایندهای node را نکشید و پورت ۳۰۰۱ را در فایروال عمومی باز نکنید. اگر پورت نامزد اشغال بود، ابتدا مالک همان سوکت را شناسایی کنید؛ اشغال پورت دلیل مناسبی برای پایاندادن به یک برنامهٔ ناشناس نیست. این اجرای پیشنمایش پیکربندی محیطیِ محدود دارد و جای آزمون کامل واحد systemd را نمیگیرد، اما مسیر اجرا، خواندن ماژولها و رفتار اصلی کد را با هویت darsnode بررسی میکند.
قدم پنجم: انتخاب نسخه را عوض کنید و نتیجه را بسنجید
اکنون باید پیوند current بهصورت یکمرحلهای جایگزین شود و سرویس با نسخهٔ تازه دوباره شروع کند. اتمیبودن جایگزینی پیوند یعنی در نام current، یک انتخاب کامل یا انتخاب قبلی دیده میشود؛ به معنی انتقال بدون وقفهٔ تمام درخواستهای شبکه نیست. فرایند در حال اجرا قبلاً ماژولهایش را بار کرده است و تغییر پیوند، آن فرایند را خودکار به کد تازه تبدیل نمیکند؛ restart لازم است. در بلوک بعدی، ابزار mktemp یک پوشهٔ موقت در همان فایلسیستم میسازد، پیوند نامزد در آن ساخته میشود و mv با گزینهٔ -T خودِ پیوند را جابهجا میکند. دستورها در زیرپوستهاند تا شکست، ترمینال SSH شما را نبندد. در موفقیت، مسیر واقعی current باید r2 باشد و آزمون سلامت پس از restart نیز باید پاس شود. اگر restart یا سلامت شکست خورد، مسیر برگشت بلافاصله در متن بعدی آمده است؛ این مرحله را بدون آمادهبودن آن شروع نکنید.
(
set -euo pipefail
test -L /opt/darsavand-node/current
test "$(readlink -f /opt/darsavand-node/current)" = /opt/darsavand-node/releases/r1
test -f /opt/darsavand-node/releases/r2/server.mjs
switch_dir=$(sudo mktemp -d /opt/darsavand-node/.switch.XXXXXX)
trap 'sudo rmdir -- "$switch_dir" 2>/dev/null || true' EXIT
sudo ln -s /opt/darsavand-node/releases/r2 "$switch_dir/current"
sudo mv -Tf -- "$switch_dir/current" /opt/darsavand-node/current
sudo systemctl restart darsavand-node.service
sudo systemctl is-active darsavand-node.service
readlink -f /opt/darsavand-node/current
)
# Now repeat the JSON health check from lesson 10; expect PASS.پس از تغییر، آزمون سلامت را چند بار با فاصلهٔ کوتاه و سقف زمان مشخص اجرا کنید تا فرصت شروع اولیه را در نظر بگیرید. سپس مسیر HTTPS عمومی و کدهای مجاز و ممنوع را بررسی کنید. اگر سلامتی برقرار نشد، وضعیت و چند سطر لاگ را برای شواهد ذخیره کنید و انتخاب نسخه را به r1 برگردانید. بلوک بازگشت دقیقاً همان الگوی جایگزینی پیوند را برای مقصد قبلی انجام میدهد. بعد از آن نیز آزمون سلامت لازم است؛ پیام پایان فرمان mv اثبات بازیابی برنامه نیست. اگر خود r1 دیگر قابل اجرا نیست، بازگشت فقط انتخاب کد قبلی را انجام داده و رخداد باز میماند. بهروزرسانی باینری Node را با انتشار برنامه ترکیب نکنید؛ اگر هر دو همزمان تغییر کنند، تشخیص علت و سازگاری بازگشت دشوار میشود. نسخهٔ runtime و مقصد /opt/node-current باید در گزارش انتشار ثبت شوند و بهروزرسانی runtime، برنامه و آزمون برگشت خودش را داشته باشد.
(
set -euo pipefail
test -L /opt/darsavand-node/current
test "$(readlink -f /opt/darsavand-node/current)" = /opt/darsavand-node/releases/r2
test -f /opt/darsavand-node/releases/r1/server.mjs
rollback_dir=$(sudo mktemp -d /opt/darsavand-node/.rollback.XXXXXX)
trap 'sudo rmdir -- "$rollback_dir" 2>/dev/null || true' EXIT
sudo ln -s /opt/darsavand-node/releases/r1 "$rollback_dir/current"
sudo mv -Tf -- "$rollback_dir/current" /opt/darsavand-node/current
sudo systemctl restart darsavand-node.service
readlink -f /opt/darsavand-node/current
)
# Repeat the JSON health check; record its actual result.این فرایند وقفهٔ کوتاه دارد و در برنامهٔ حافظهای، هر restart دادههای کارها را پاک میکند. تغییر پیوند، محتوای Map در فرایند را به نسخهٔ تازه منتقل نمیکند و rollback هم آن دادهها را بازنمیگرداند. در سامانهٔ دارای پایگاه داده، مسئلهٔ دیگری اضافه میشود: نسخهٔ قبلی کد باید با ساختار فعلی داده سازگار باشد. تغییر ساختار پایگاه داده را migration مینامیم؛ مهاجرت ناموفق یا ناسازگار میتواند باعث شود بازگرداندن فایلهای کد کافی نباشد. برای همین پیش از محصول واقعی، طراحی سازگاری نسخهها، پشتیبان داده و آزمون بازیابی لازم است. نمونهٔ این دوره عمداً از دادهٔ واقعی استفاده نمیکند تا بتوانید خطای فرایند انتشار را بدون آسیب به اطلاعات تمرین کنید. معنای #try-catch نیز همینجا کاربرد دارد: مدیریت شکست به معنی داشتن اقدام بعدی روشن است، نه فقط چاپ یک پیام و وانمودکردن موفقیت.
تمرین انتشار و گزارش قابل تحویل
یک بار بستهٔ r2 را بسازید، آزمون نامزد را انجام دهید، به r2 بروید و سپس به r1 برگردید. در هر مرحله مقصد current، نتیجهٔ آزمون سلامت و زمان تغییر را ثبت کنید. تمرین تحلیلی: اگر فقط نشانی HTTPS سالم است اما readlink هنوز r1 را نشان میدهد، آیا انتشار r2 ثابت شده است؟ پاسخ: خیر؛ سلامت یک مسیر، هویت نسخهٔ اجراشده را ثابت نمیکند. مقصد پیوند و نسخهٔ بسته باید همراه با لاگ شروع و آزمون پس از restart بررسی شوند. تمرین دوم: چرا chmod 777 راهحل خطای مجوز نیست؟ پاسخ: اختیار نوشتن کد اجراشونده را بیدلیل گسترش میدهد و علت ناهماهنگی هویت و مسیر را پنهان میکند. معیار تحویل، نسخهٔ مشخص، آزمون قبل، اقدام انتشار، آزمون بعد، شرط برگشت و نتیجهٔ واقعی برگشت است. اگر قصد دارید آزمایشگاه را در r2 تحویل دهید، پس از تمرین rollback، مرحلهٔ انتشار را دوباره با همان پیششرطهای روشن انجام دهید و نتیجهٔ نهایی را ثبت کنید.
برای پیوند این مسیر با آموختههای قبلی، مالکیت و مجوز را از دورهٔ لینوکس و آزمون API را از دورهٔ Node مرور کنید. ارجاع سوم کمک میکند رابطهٔ یک تغییر کوچک با برنامهٔ بازیابی را در سناریوی دیگری ببینید.
مجوزها، مالکیت و استفادهٔ سنجیده از sudo: https://www.darsavand.com/courses/linux-server-from-zero?lesson=a6cafdad-8b05-44a8-bfc6-f7ba647ee095
آزمون خودکار API؛ موفقیت، شکست و حفظ داده: https://www.darsavand.com/courses/nodejs-windows-backend-start?lesson=96fd0f9a-28ae-41f3-a5fd-76d21bae6b93
پروژهٔ پایانی؛ بازیابی یک رخداد: https://www.darsavand.com/courses/linux-advanced-administration?lesson=5ba98f16-5eab-424e-8241-4281a60bd1fc
منابع رسمی انگلیسی:
GNU Coreutils — mv: https://www.gnu.org/software/coreutils/manual/coreutils.html#mv-invocation
GNU Coreutils — ln: https://www.gnu.org/software/coreutils/manual/coreutils.html#ln-invocation
systemd service: https://www.freedesktop.org/software/systemd/man/255/systemd.service.html
Node.js test runner: https://nodejs.org/docs/latest-v24.x/api/test.html
Node.js HTTP shutdown: https://nodejs.org/docs/latest-v24.x/api/http.html
تألیف و سناریوهای اختصاصی: تحریریه درسآوند. دستورها برای آزمایشگاه مستقل دوره و نسخههای مشخصاند؛ گزارش نتیجه باید بر مشاهدهٔ واقعی استوار باشد.
متن درسهای عمومی را بدون ورود بخوانید.