الوحدة 10 — النشر والتحكّم في الوصول
تطبيق يعمل على حاسوب مطوّره لا يخدم أحدًا سواه. هذه الوحدة الأخيرة تُغلق الحلقة: نضع التطبيق على Community Cloud لاختبار سريع أو في حاوية Docker لاستعمال داخليّ، ونُدير الأسرار من دون إدراجها في المستودع، ونحمي الوصول بكلمة مرور أو بتفويض خارجيّ، ونتعرّف على حدود التزامن التي يفرضها Streamlit. بعد هذه الوحدة، تُصبح لوحة القيادة لتنبّؤ الترك متاحة فعليًّا لفريق المبيعات، لا مجرّد تجربة على الجهاز الشخصيّ.
Community Cloud، الطريق الأسرع
Streamlit Community Cloud يستضيف مجّانًا التطبيقات الموجودة في مستودع GitHub عموميّ. ثلاث خطوات تكفي: ربط حساب GitHub، تحديد المستودع والملفّ الرئيسيّ، إطلاق النشر. عنوان URL العموميّ يكون جاهزًا في ثلاث إلى خمس دقائق.
المستودع يحتاج ملفّ requirements.txt (أو pyproject.toml) يُعدِّد التبعيّات، وملفّ runtime.txt اختياريّ لتثبيت نسخة بايثون:
# requirements.txt
streamlit==1.38.0
pandas==2.2.2
scikit-learn==1.5.1
joblib==1.4.2
plotly==5.24.0
requests==2.32.3
# runtime.txt
python-3.11
Community Cloud يفرض حدَّين ينبغي معرفتهما. غيغابايت واحد من الذاكرة الحيّة لكلّ تطبيق، وهو ما يكفي للوحة قيادة متواضعة لكنّه يمنع النماذج العميقة الكبيرة. وإسبات التطبيق بعد ساعات دون زيارة، مع إعادة تشغيل تدوم عشر إلى عشرين ثانية عند العودة. لعرض توضيحيّ أو أداة داخليّة قليلة الاستعمال، هذه الحدود مقبولة؛ لاستعمال جدّيّ، ننتقل إلى حاوية.
حاوية Docker للاستعمال الداخليّ
ملفّ Dockerfile بسيط يتكوّن من عشرة أسطر ويعطي تحكّمًا كاملًا في بيئة التنفيذ:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
# server.address 0.0.0.0 لقبول الاتّصالات من خارج الحاوية
CMD ["streamlit", "run", "app.py", \
"--server.address=0.0.0.0", \
"--server.port=8501", \
"--server.headless=true"]
المعامل --server.headless=true يمنع Streamlit من محاولة فتح متصفّح عند البدء، وهو أمر لا معنى له داخل حاوية. بدون هذا المعامل، سجلّ Docker يمتلئ بآثار أخطاء دون أن تتأثّر أيّ وظيفة فعليًّا.
البناء والإطلاق يتّبعان النمط المعتاد:
docker build -t resiliation:latest .
docker run --rm -p 8501:8501 --name resiliation resiliation:latest
للنشر الداخليّ خلف خادم ويب مثل nginx أو Traefik، نُضيف نقطة استقبال /healthz تُجيب بالرمز 200 حالما يكون Streamlit جاهزًا؛ هذه النقطة يستعملها الخادم للتحقّق من حياة الحاوية. Streamlit يعرض _stcore/health أصلًا، وهي تلعب هذا الدور.
إدارة الأسرار دون إدراجها في المستودع
تطبيق ما يستدعي دائمًا تقريبًا واجهة API، أو قاعدة بيانات، أو خدمة تطلب مفتاحًا. هذا المفتاح لا يجب أبدًا أن يظهر في الشيفرة المصدريّة، ولا حتّى في ملفّ مُدرَج في git. Streamlit يوفّر st.secrets، الذي يقرأ من ملفّ .streamlit/secrets.toml مضاف إلى .gitignore:
# .streamlit/secrets.toml -- لا تُدرَج في git
[api_resiliation]
cle = "sk_abcd1234efgh5678"
[base_donnees]
url = "postgresql://user:motdepasse@hote:5432/base"
[admin]
motdepasse = "phrase_complexe_a_remplacer"
الوصول من الشيفرة يتمّ بالسمات أو بالفهرسة:
cle_api = st.secrets["api_resiliation"]["cle"]
url_bd = st.secrets.base_donnees.url
على Community Cloud، تُضبط الأسرار في واجهة النشر الويب؛ في Docker، نمرّرها عبر متغيّرات بيئة أو عبر مجلَّد مركَّب على /app/.streamlit/secrets.toml. القاعدة المطلقة: أيّ سرّ تسرّب بواسطة git push هو سرّ محروق، ينبغي تبديله فورًا، حتّى لو حُذف في الالتزام التالي. سجلّ Git لا يُمحى.
كلمة مرور بسيطة
لتطبيق داخليّ يستعمله عشرات الموظّفين، تكفي كلمة مرور واحدة في الغالب. النمط المرجعيّ يقع في عشرين سطرًا:
import streamlit as st
import hmac
def verifier_mot_de_passe() -> bool:
"""يعيد True إذا طابقت كلمة المرور المُدخَلة السرّ."""
def valider():
saisi = st.session_state.get("motdepasse", "")
attendu = st.secrets["admin"]["motdepasse"]
# مقارنة بزمن ثابت لتفادي هجمات التوقيت
st.session_state.authentifie = hmac.compare_digest(saisi, attendu)
del st.session_state["motdepasse"] # لا يُعاد عرض الحقل بالقيمة
if st.session_state.get("authentifie"):
return True
st.text_input(
"كلمة المرور",
type="password",
on_change=valider,
key="motdepasse",
)
if st.session_state.get("authentifie") is False:
st.error("كلمة مرور غير صحيحة.")
return False
if not verifier_mot_de_passe():
st.stop()
# --- من هنا يبدأ التطبيق الحقيقيّ. ---
st.title("لوحة قيادة ترك العملاء")
ثلاث احتياطات جديرة بالانتباه. الدالّة hmac.compare_digest تقارن سلسلتَين بزمن ثابت، ممّا يمنع مهاجمًا من استنتاج كلمة المرور حرفًا حرفًا عبر قياس زمن الاستجابة. كلمة المرور المُدخَلة تُمسح فورًا من session_state كي لا تبقى في الذاكرة. والدالّة st.stop() تمنع تنفيذ بقيّة السكربت ما لم يُصادَق على الدخول.
هذا النمط يناسب دائرة ضيّقة؛ لعشرة آلاف مستخدم يصبح قاصرًا تمامًا.
التفويض الخارجيّ
حالما نتحدّث عن وصول فرديّ ــ تتبّع الاستشارات، والإلغاء، والأدوار ــ نُفوّض المصادقة إلى مزوّد هويّة خارجيّ: Google Workspace، Microsoft Entra، Okta، Auth0. الطريقة الحديثة تعتمد على OpenID Connect، وStreamlit 1.42 قدَّم st.experimental_user وتكاملًا مباشرًا يُبسِّط الشيفرة بشكل كبير.
# .streamlit/secrets.toml
[auth]
client_id = "..."
client_secret = "..."
server_metadata_url = "https://accounts.google.com/.well-known/openid-configuration"
redirect_uri = "https://scoring.exemple.com/oauth2callback"
cookie_secret = "chaine_longue_et_aleatoire"
import streamlit as st
if not st.experimental_user.is_logged_in:
st.login() # يُحيل إلى المزوّد
st.stop()
email = st.experimental_user.email
st.caption(f"مسجَّل الدخول بحساب {email}.")
if not email.endswith("@entreprise.com"):
st.error("الوصول محجوز لموظّفي الشركة.")
st.stop()
قاعدتان مهمّتان. نُفوّض المصادقة (من هو هذا المستخدم؟) للمزوّد، لكنّ التفويض (هل يحقّ له الوصول؟) يبقى في التطبيق، غالبًا في هيئة تحقّق من نطاق البريد، أو دور مخزَّن في قاعدة، أو دليل يُستشار. وزرّ «تسجيل الخروج» ليس اختياريًّا: بدونه، من يُعير محطّته لغيره لا يملك وسيلة لقطع الجلسة.
حدود التزامن
Streamlit يُشغِّل كلّ جلسة في خيط تنفيذ خاصّ بها، لكنّه يتشارك عمليّة بايثون واحد ة. القفل العموميّ للمفسِّر (GIL) يُقيّد التوازي في الحسابات كثيفة المعالج: مستخدمان يُطلقان في اللحظة نفسها تسجيلًا ثقيلًا ينتظر أحدهما فعليًّا الآخر. للوحة قيادة استشاريّة، هذا ليس مشكلة؛ لخدمة تتعرّض لذروة حمل، ينبغي إطلاق عدّة عمليّات Streamlit خلف موازن تحميل، أو حساب النتائج مسبقًا في مهمّة خلفيّة.
التخزين المؤقّت @st.cache_resource يتشارَك بين الجلسات، وهو ما يضاعف الفعّاليّة: نموذج مُحمَّل مرّة واحدة يخدم عشرة مستخدمين متزامنين دون كلفة ذاكرة إضافيّة. في المقابل، st.session_state مستقلّ فعلًا لكلّ جلسة، دون تلوّث ممكن.
قبل مشاركة عنوان URL، نتحقّق منهجيًّا من ستّ نقاط. واحد: secrets.toml مُدرَج في .gitignore. اثنان: لا كلمة مرور ولا مفتاح ولا رمز يظهر في الشيفرة المصدريّة. ثلاث: المهلة timeout موجودة على كلّ استدعاء HTTP خارجيّ. أربع: الملفّات المرفوعة تُف حَص قبل المعالجة. خمس: الأخطاء تُظهر رسالة واضحة، لا أثرًا خامًا. ستّ: نسخة النموذج ونسخة التطبيق معروضتان في مكان ما. مربّع واحد غير مؤشَّر يُحوّل عرضًا توضيحيًّا إلى حادثة.
الخلاصة
- Community Cloud يستضيف مجّانًا تطبيقات المستودع العموميّ، بحدّ غيغابايت للذاكرة الحيّة وإسبات بعد الخمول.
- ملفّ
Dockerfileمن عشرة أسطر يكفي لنشر متحكَّم به؛--server.headless=trueو--server.address=0.0.0.0معاملان لا يُنسيان. - الأسرار تعيش في
.streamlit/secrets.toml، أبدًا في الشيفرة؛hmac.compare_digestيُقارن كلمة مرور بزمن ثابت للاحتماء من هجمات التوقيت. - التفويض الخارجيّ عبر OpenID Connect هو الطريق الموصى به حالما تحتاج فردنة الوصول؛ التفويض يبقى دائمًا مسؤوليّة التطبيق نفسه.
- التزامن محدود بعمليّة بايثون واحدة؛ لحمل كبير يلزم موازن أمام عدّة عمليّات، مع
cache_resourceمشترك يوفّر الذاكرة.
الوحدة التالية: المراجعة الشاملة والاختبار الختامي من أربعين سؤالًا.