تتعطل خطوة النشر لديك عند اسم مضيف داخلي، بينما يستجيب الأمر نفسه فورًا من حاسوبك المحمول، ونسخة العميل على الجهازين متطابقة. المهمة تعمل على مشغّل مستضاف من GitHub: جهاز افتراضي أُنشئ لتلك المهمة وحدها داخل سحابة GitHub، بلا مسار إلى فضاء عنوانك الخاص، وبلا مدخل محلل للمنطقة الداخلية، وبلا شهادة سيقبلها الطرف الخاص. تصميمان فقط يلائمان هذا القيد، ولا يمكن تشغيل أي منهما لاحقًا دون إعادة بناء سير العمل. اختر بينهما قبل أول سطر من YAML، لأن الترحيل بعد ذلك يكلف أكثر من المحاولة الأولى.

الاختيار بين مشغّل يوضع حيث تنتهي الشبكة بالفعل، ونفق يوجد طوال مدة مهمة واحدة. كلاهما حمل عمليات نشر إنتاجية لسنوات، وكلاهما يفشل بطرق مختلفة.

أين تقع الحدود فعليًا

أي شيء تثبّته على حاسوبك المحمول يتوقف عند حاسوبك، وأي شيء تثبّته أثناء مهمة يزول مع المشغّل. لا توجد مساحة أسماء شبكية دائمة يظل العميل متصلًا بها بين التشغيلات، لذلك يبدأ كل تشغيل من الحالة نفسها غير القابلة للوصول. توثّق Azure هذه الحدود للوكلاء المستضافين من Microsoft، حيث لا تُدعم روابط VPN وExpressRoute إلى الشبكات الخاصة، ومشغّلات GitHub المستضافة تحمل الشكل نفسه من القيود بدلًا من آلية منفصلة. اقرأ ذلك كخاصية للمنتج، لا كعيب يجب الالتفاف حوله.

لا يوجد إعداد في المستودع يخفف ذلك. تسمية المهمة تحدد أي مجموعة تلتقط العمل، والتسمية المستضافة تعني جهازًا خارج محيطك طوال التشغيل.

لذلك يجب أن يأتي الوصول الخاص من شيء تملكه. مشغّل يوضع حيث تنتهي الشبكة بالفعل، أو نفق يعيش لمهمة واحدة. بقية هذا المقال عن دفع ثمن أحد هذين عن قصد بدلًا من اكتشاف الفاتورة في الإنتاج.

قرّر بحسب تكرار التشغيل، لا بحسب الألفة

الجأ إلى مشغّل مستضاف ذاتيًا داخل VPC أو شبكة المكتب عندما تهبط عمليات النشر عدة مرات يوميًا، أو عندما تلمس المهمة أكثر من منفذ خاص، أو عندما يملك فريق منصة بالفعل التصحيح وتدوير بيانات الاعتماد. المهمة على ذلك الجهاز تُحلّ وتوجّه مثل أي مضيف آخر خلف الجدار الناري. عمل الشبكة يحدث مرة واحدة عند التثبيت، لا في كل تشغيل.

ابنِ النفق داخل المهمة عندما يكون الوصول متقطعًا ولا تريد وكيلًا طويل العمر يحتفظ ببيانات اعتماد داخل شبكتك. تصبح المهمة عميل شبكتها الخاص لبضع دقائق ولا تترك شيئًا خلفها، وهذا غالبًا هو المقصود كله.

قدّر تكلفة النفق قبل اختياره. كل تشغيل يدفع ثمن تجسيد الملف الشخصي، وبدء العميل، والجاهزية، والتفكيك. النفق الذي ينهض ولا يُتحقق منه أبدًا ينتج نشرًا يفشل في منتصفه ويترك حالة خاصة بشكل لم يخطط له أحد. إعادة المحاولة بعد ذلك نادرًا ما تكون نظيفة، والفشل الثاني يُلام عليه عادة إصدار العميل.

اكتب الملف الشخصي بحيث لا تستطيع الخطوة الإجهاض

أبقِ الملفات الشخصية وبيانات الاعتماد خارج المستودع وجسّدها من الأسرار في الخطوة التي تحتاجها. الترتيب يحدد ما إذا كانت المهمة ستنجو: الملف المُنشأ تحت sudo يملكه root، ولا تستطيع صدفة الخطوة غير المتميزة الكتابة فيه بعد ذلك.

- 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 يهبط الملف الشخصي على الوضع 0600 المملوك لمستخدم المشغّل، ويظل sudo قادرًا على قراءته، ويكتب العميل سجله في ملف يملكه المشغّل بالفعل، لذا تستطيع حلقة الجاهزية قراءة ذلك السجل دون sudo ثانٍ. --writepid يسجّل معرّف العملية الذي سيستهدفه التفكيك.

الملف الشخصي الذي يصادق بشهادة عميل يحتاج مفتاحه مكتوبًا بالطريقة نفسها، وملف التحدي الثابت يحتاج وضع المصادقة المطابق. الملفات التي تدفع خيارات DNS تحتاج openvpn-systemd-resolved على Ubuntu وDebian، لأن الدفع وحده لا يغيّر شيئًا على صورة أساسية.

الجاهزية مسبار، لا وقفة

النوم الثابت يصمد حتى يكون المجمّع بطيئًا، وحينها تعمل خطوة النشر قبل وجود المسار. يظهر الفشل بعد ذلك داخل أداة النشر كمهلة ضد المضيف الخاص، وهذا لا يقول شيئًا عن النفق.

- 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 يثبت أن المحلل المدفوع يجيب عن ذلك الاسم، وفتح المقبس يثبت أن شيئًا يستمع على المنفذ الذي سيستخدمه النشر؛ الملف المعتمد على المسار وحده يحتاج العنوان الحرفي ويحتاج getent إلى الحذف.

عندما تنتهي الحلقة، يكون السجل الشيء الوحيد الذي يفصل بين ثلاثة أعطال مختلفة: شهادة مرفوضة، ووضع مصادقة خاطئ، ومسار لم يصل قط. الترشيح قبل الذيل يبقي ذلك الناتج قصيرًا بما يكفي للقراءة بينما لا تزال المهمة تفشل. اقرأ أول خطأ بدلًا من السطر الأخير، لأن العميل الذي لا يستطيع المصادقة يكرر نفسه حتى تستسلم الحلقة. ip -4 route show dev tun0 يجيب على السؤال الأبسط عما إذا كان قد دُفع مسار أصلًا، ويكلف سطرًا واحدًا داخل الحلقة نفسها.

التفكيك، ومسألة الأثر

أغلق المهمة بخطوة محمية بـ if: always() تشغّل sudo kill "$(sudo cat "$RUNNER_TEMP/openvpn.pid")" ولا شيء آخر. على مشغّل مستضاف ذاتيًا مشترك، لا تقتل أبدًا باسم العملية: قد تملك مهمة أخرى عميلًا بالملف التنفيذي نفسه والوسائط المشابهة. يهبط الفشل بعد ذلك على أي مهمة عملت ثانية، ويشير سجلها إلى سير عمل لم يخطئ.

الحماية مهمة لأن النشر الفاشل يترك نفقًا حيًا. بدون always() تُتخطى خطوة التنظيف بالضبط عندما تكون مطلوبة، ويبدأ التشغيل التالي مقابل عميل متصل بالفعل.

يبقى سؤال واحد قبل أن تتبنى أيًا من التصميمين: هل تحتاج مرحلة البناء إلى مسار خاص أصلًا؟ وكيل النشر الموجود بالفعل داخل الشبكة يمكنه سحب أثر معتمد، وهذا يبقي البناءات العادية بعيدًا عن المسار الخاص. عدد أقل من المهام التي تحمل نفقًا يعني أماكن أقل يمكن أن يختبئ فيها مسار قديم.

مقالات ذات صلة