GitHub Actions به VPN شما نمیرسد: runner اختصاصی یا تونل
یک runner میزبانیشده، ماشین مجازی تازهای بیرون از شبکه شماست. یا runner را داخل آن شبکه بگذارید، یا از job تونل بسازید و منتظر یک مسیر واقعی بمانید.
مرحله deploy شما روی یک نام میزبان داخلی گیر میکند، در حالی که همان فرمان از لپتاپ شما فوراً پاسخ میدهد و build کلاینت روی هر دو ماشین یکسان است. job روی runner میزبانیشده GitHub اجرا میشود: ماشین مجازیای که برای همان یک job در ابر GitHub ساخته شده، بدون مسیر به فضای آدرس خصوصی شما، بدون رکورد resolver برای ناحیه داخلی و بدون گواهیای که endpoint خصوصی بپذیرد. فقط دو طراحی در این محدودیت جا میشود و هیچکدام را نمیتوان بعداً بدون بازسازی workflow روشن کرد. پیش از اولین خط YAML بین آنها انتخاب کنید، چون مهاجرت بعدی بیشتر از تلاش اول هزینه دارد.
انتخاب بین runnerی است که آنجا قرار میگیرد که شبکه از قبل تمام میشود و تونلی که به اندازه یک job زنده میماند. هر دو سالها deployهای تولیدی را حمل کردهاند و به شکلهای متفاوتی شکست میخورند.
مرز واقعاً کجاست
هر چیزی که روی لپتاپ نصب کنید به لپتاپ محدود میشود و هر چیزی که در طول job نصب کنید با runner از بین میرود. هیچ namespace شبکه پایدارای وجود ندارد که کلاینت بین اجراها به آن وصل بماند، پس هر اجرا از همان وضعیت دستنیافتنی شروع میشود. Azure این مرز را برای agentهای میزبانیشده Microsoft مستند میکند، جایی که پیوندهای VPN و ExpressRoute به شبکههای خصوصی پشتیبانی نمیشوند، و runnerهای میزبانیشده GitHub همان شکل محدودیت را دارند نه مکانیزمی جداگانه. این را ویژگی محصول بخوانید، نه نقصی که باید دور زد.
هیچ تنظیمی در repository آن را شل نمیکند. برچسب job تعیین میکند کدام pool کار را بردارد و برچسب میزبانیشده یعنی ماشینی بیرون از محیط شما برای تمام اجرا.
دسترسی خصوصی بنابراین باید از چیزی بیاید که مالک آن هستید. runnerی که آنجا قرار میگیرد که شبکه از قبل تمام میشود، یا تونلی که یک job زنده میماند. بقیه مقاله درباره پرداخت آگاهانه برای یکی از این دو است، به جای کشف صورتحساب در تولید.
بر اساس فراوانی اجرا تصمیم بگیرید، نه بر اساس آشنایی
وقتی deployها چند بار در روز میرسند، وقتی job بیش از یک پورت خصوصی را لمس میکند، یا وقتی تیم پلتفرم از قبل مسئول patch و چرخش اعتبارنامههاست، به runner اختصاصی در VPC یا شبکه دفتر روی بیاورید. job روی آن ماشین مثل هر میزبان دیگری پشت firewall حل و مسیریابی میکند. کار شبکه یک بار، هنگام نصب، انجام میشود نه در هر اجرا.
وقتی دسترسی گاهبهگاه است و نمیخواهید agentی دیرزیست اعتبارنامهای به شبکه شما نگه دارد، تونل را داخل job بسازید. job برای چند دقیقه کلاینت شبکه خودش میشود و چیزی پشت سر نمیگذارد، که اغلب تمام هدف است.
هزینه تونل را پیش از انتخاب آن تخمین بزنید. هر اجرا برای تحقق profile، شروع کلاینت، آمادگی و برچیدن هزینه میپردازد. تونلی که بالا میآید اما هرگز تأیید نمیشود، deployی میسازد که در میانه راه شکست میخورد و وضعیت خصوصی را به شکلی رها میکند که کسی برنامهریزی نکرده بود. تلاش دوباره پس از آن به ندرت پاک است و شکست دوم معمولاً به نسخه کلاینت نسبت داده میشود.
نوشتن profile طوری که مرحله نتواند قطع شود
profileها و اعتبارنامهها را بیرون از repository نگه دارید و آنها را از secretها در مرحلهای که لازم است بسازید. ترتیب تعیین میکند job زنده میماند یا نه: فایلی که زیر sudo ساخته میشود متعلق به root است و shell بدون امتیاز مرحله دیگر نمیتواند در آن بنویسد.
- name: Bring up OpenVPN
run: |
umask 077
profile="$RUNNER_TEMP/client.ovpn"
printf '%s\n' "$VPN_PROFILE" > "$profile"
: > "$RUNNER_TEMP/openvpn.log"
printf '%s\n' "$VPN_CREDENTIALS" > "$RUNNER_TEMP/credentials.txt"
sudo openvpn --daemon openvpn-job \
--config "$profile" \
--auth-user-pass "$RUNNER_TEMP/credentials.txt" \
--writepid "$RUNNER_TEMP/openvpn.pid" \
--log "$RUNNER_TEMP/openvpn.log"هر دو جزئیات آن بلوک برای مراحل بعدی وجود دارند. با umask 077 profile با حالت 0600 و مالکیت کاربر runner مینشیند، sudo هنوز میتواند آن را بخواند و کلاینت log خود را در فایلی مینویسد که runner از قبل مالک آن است، پس حلقه آمادگی میتواند آن log را بدون sudo دوم بخواند. --writepid PIDی را ثبت میکند که برچیدن به آن هدف میگیرد.
profileی که با گواهی کلاینت احراز هویت میکند کلیدش را به همان شکل نوشته لازم دارد و profile با چالش ایستا حالت احراز هویت متناظر را میخواهد. profileهایی که گزینههای DNS را push میکنند به openvpn-systemd-resolved روی Ubuntu و Debian نیاز دارند، چون push به تنهایی روی تصویر پایه چیزی را تغییر نمیدهد.
آمادگی یک کاوش است، نه یک توقف
یک sleep ثابت دقیقاً تا وقتی دوام میآورد که concentrator کند شود و آنگاه مرحله deploy پیش از وجود مسیر اجرا میشود. شکست سپس داخل ابزار deploy به صورت timeout به میزبان خصوصی ظاهر میشود که درباره تونل چیزی نمیگوید.
- name: Await private endpoint
run: |
for attempt in $(seq 1 30); do
if getent hosts private.example.internal > /dev/null && \
timeout 3 bash -c 'exec 3<>/dev/tcp/private.example.internal/8443' 2>/dev/null; then
echo "endpoint answered after $attempt attempts"
exit 0
fi
sleep 2
done
grep -Ei 'error|verify|auth' "$RUNNER_TEMP/openvpn.log" | tail -40
exit 1حلقه دو چیز را میپاید چون هر یک میتواند نیمه خراب باشد. getent hosts ثابت میکند resolver pushشده برای آن نام پاسخ میدهد و باز شدن socket ثابت میکند چیزی روی پورتی که deploy استفاده خواهد کرد گوش میدهد؛ profileی که فقط مسیر دارد به آدرس تحتاللفظی و حذف خط getent نیاز دارد.
وقتی حلقه منقضی میشود، log تنها چیزی است که سه خطای متفاوت را جدا میکند: گواهی ردشده، حالت احراز هویت اشتباه و مسیری که هرگز نرسید. فیلتر کردن پیش از tail آن خروجی را به اندازه کافی کوتاه نگه میدارد تا وقتی job هنوز شکست میخورد خوانده شود. خطای اول را بخوانید نه خط آخر را، چون کلاینتی که نمیتواند احراز هویت کند خود را تکرار میکند تا حلقه تسلیم شود. ip -4 route show dev tun0 به پرسش سادهتر پاسخ میدهد که آیا اصلاً مسیری push شده و یک خط داخل همان حلقه هزینه دارد.
برچیدن و پرسش artifact
job را با مرحلهای ببندید که با if: always() محافظت میشود و sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" و هیچ چیز دیگری را اجرا میکند. روی runner اختصاصی مشترک هرگز بر اساس نام فرایند نکشید: job دیگری میتواند کلاینتی با همان باینری و آرگومانهای مشابه داشته باشد. شکست آنگاه روی jobی میافتد که دوم اجرا شد و log آن به workflowی اشاره میکند که هیچ کار اشتباهی نکرده است.
محافظ اهمیت دارد چون deploy شکستخورده هنوز تونلی زنده باقی میگذارد. بدون always() مرحله پاکسازی دقیقاً وقتی لازم است رد میشود و اجرای بعدی مقابل کلاینتی شروع میشود که از قبل متصل است.
یک پرسش پیش از پذیرش هر یک از دو طراحی میماند: آیا مرحله build اصلاً به مسیر خصوصی نیاز دارد؟ agent استقراری که از قبل داخل شبکه نشسته میتواند artifact تأییدشدهای بردارد و این buildهای معمولی را از مسیر خصوصی دور نگه میدارد. jobهای کمتر که تونل نگه میدارند یعنی مکانهای کمتری که یک مسیر کهنه میتواند در آن پنهان شود.