الحاويات والنشر
درس 4 من 6
4الحاويات والنشر
الصورة الكبيرة«يعمل على جهازي» ليس نشرًا. النشر صورة حاوية (Container) فيها الكود والمكتبات بنسخ محدّدة، تعمل بنفس الطريقة على أي خادم. والأسرار خارجها في متغيّرات بيئة. وكل نسخة لها رقم، حتى تستطيع العودة للنسخة السابقة في دقيقة حين تسوء الجديدة.
ماذا تشاهد في الرسم: المكتبات تُثبَّت قبل نسخ الكود، فتغيير الكود لا يعيد تثبيتها ويبقى البناء سريعًا. ولا شيء في الصورة سرّي أو ثقيل: MODEL_PATH وAPI_KEY يصلان وقت التشغيل.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
الحاوية صندوق شحن فيه برنامجك وكل ما يحتاجه، فيعمل على حاسوب زميلك وعلى الخادم كما يعمل عندك تمامًا. لكن كلمات السر لا تُشحن داخل الصندوق، بل تُعطى للخادم منفصلة. وكل صندوق يحمل رقم إصدار، فإن ظهرت مشكلة ترجع للإصدار السابق.
Dockerfile يبني صورة بطبقات مع اعتماديات مثبتة الإصدارات، والأسرار تُمرر بمتغيرات البيئة أو مخزن أسرار لا داخل الصورة. توسم كل صورة بإصدار ثابت لا latest، وتنشر بالتدريج مع فحص الصحة وتستطيع الرجوع rollback.
قبل أن تبدأ
🌐من دفتر إلى خدمة
عندك خدمة FastAPI بفحص صحة ونسخة في كل رد.
💻سطر الأوامر
تشغّل أوامر في الطرفية وتعرف متغيّرات البيئة.
الفكرة الجوهرية
النشر حاوية تعمل بنفس الطريقة على أي خادم. تبني هنا الإعدادات من البيئة، ثم الصورة، ثم فصل الأوزان عنها، ثم الأرقام التي تجعل التراجع دقيقة واحدة.
1الإعدادات من البيئة
أول ما تفعله الخدمة عند البدء: تقرأ إعداداتها من متغيّرات البيئة، وتتأكّد أن كل المطلوب موجود. والأسرار مثل API_KEY تأتي من مدير الأسرار وقت التشغيل.
انظر إلى الرسم: خدمة ناقصة ترفض البدء برسالة واضحة. هذا أفضل بكثير من خدمة تبدأ ثم تفشل مع أول طلب.
def load_config(env):
required = ["MODEL_PATH", "API_KEY"]
missing = [k for k in required if not env.get(k)]
if missing:
raise SystemExit(f"cannot start: missing {', '.join(missing)}")
return {
"model_path": env["MODEL_PATH"],
"api_key": env["API_KEY"],
"max_input_chars": int(env.get("MAX_INPUT_CHARS", "2000")),
"version": env.get("APP_VERSION", "dev"),
}
def show(cfg):
safe = {k: ("***" if k == "api_key" else v) for k, v in cfg.items()}
print("starting with", safe)
show(load_config({"MODEL_PATH": "/models/sentiment-v3", "API_KEY": "sk-123", "APP_VERSION": "1.4.2"}))
try:
load_config({"MODEL_PATH": "/models/sentiment-v3"})
except SystemExit as e:
print(e)
الإعدادات المطلوبة تُفحص أولًا، والاختيارية لها قيم افتراضية، والسرّ يُخفى حتى في سجل البدء. في المرة الثانية نقص المفتاح فرفضت الخدمة البدء وقالت ماذا ينقص بالضبط. منصّة النشر سترى الفشل فورًا وتُبقي النسخة القديمة تعمل.
2الصورة: Dockerfile
الصورة حزمة فيها نظام صغير ومكتبات بإصدارات مثبّتة والكود. تُبنى مرة واحدة وتعمل بنفس الطريقة في كل مكان.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt # pinned versions: fastapi==0.115.0, ...
COPY app/ app/
RUN useradd --create-home appuser
USER appuser # do not run as root
ENV MAX_INPUT_CHARS=2000
EXPOSE 8000
HEALTHCHECK CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/ready')"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
requirements.txt يُنسخ ويُثبَّت قبل الكود، فتغيير الكود لا يعيد تثبيت المكتبات ويبقى البناء سريعًا. والخدمة تعمل بمستخدم عادي لا root. ولا يوجد سرّ ولا أوزان في الملف: MODEL_PATH وAPI_KEY تأتيان وقت التشغيل.
3الأوزان خارج الصورة
صورة فيها نموذج بعشرة غيغابايت بطيئة البناء والسحب، وكل تعديل صغير في الكود يعيد رفعها كلها.
انظر إلى الرسم: الصورة فيها الكود فقط، والأوزان في تخزين ملفات بمسار يحمل نسختها. الخدمة تحمّلها عند البدء من MODEL_PATH.
هكذا تغيّر الكود والنموذج كلًّا على حدة، وتتراجع عن أيٍّ منهما وحده.
4الإصدارات والتراجع
كل صورة لها وسم ثابت مثل 1.4.2، والنموذج له نسخته أيضًا. الوسم latest ممنوع في الإنتاج.
انظر إلى الرسم: التراجع ليس إعادة بناء كود قديم، بل تشغيل صورة موجودة بوسمها.
docker build -t sentiment-api:1.4.2 . docker run -d --name api -p 8000:8000 \ -e MODEL_PATH=s3://models/sentiment/v3 \ -e APP_VERSION=1.4.2 \ --env-file /secure/api.env \ sentiment-api:1.4.2 # 1.4.2 misbehaves in production: go back to the previous tag docker rm -f api && docker run -d --name api -p 8000:8000 ... sentiment-api:1.4.1
الرقم في الصورة هو نفسه في APP_VERSION، فيظهر في كل رد وكل سطر سجل. والتراجع ليس إعادة بناء من كود قديم، بل تشغيل صورة موجودة. في منصّات النشر الحقيقية هذا زر واحد أو أمر واحد، بشرط أنك لم تحذف الصور القديمة.
كيف يعمل، خطوة بخطوة: كيف تنشر نسخة وتتراجع عنها
تبني الصورة برقم ثابت مثل sentiment-api:1.4.2، لا latest.
تشغّلها وتمرّر الإعدادات والأسرار متغيّرات بيئة من مدير الأسرار، ومسار الأوزان بنسخته.
الخدمة تفحص الإعدادات المطلوبة عند البدء وترفض البدء برسالة واضحة إن نقص شيء.
إن ساءت النسخة الجديدة تشغّل الصورة السابقة برقمها، أو تعيد MODEL_PATH إلى النموذج السابق.
مثال محلول
أول ما تفعله الخدمة عند البدء: تقرأ إعداداتها من البيئة، وتتأكّد أن كل ما تحتاجه موجود، وترفض البدء برسالة واضحة إن نقص شيء. خدمة تبدأ ناقصة ثم تفشل مع أول طلب أسوأ بكثير من خدمة ترفض البدء.
pip install fastapi بلا نسخة في requirements.txt. بعد شهر تبني نفس الكود فتحصل على نسخة جديدة من مكتبة غيّرت سلوكها، والخدمة تنكسر دون أي تغيير منك. ثبّت كل نسخة، وحدّثها عمدًا باختبار.
جرّب بلا كود
API_KEY. الجواب: COPY requirements.txt ثم RUN pip install ثم COPY app/. والمفتاح يُمرَّر وقت التشغيل من مدير الأسرار، لا في الصورة.خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| حذف المفتاح من المستودع يزيله. | يبقى في تاريخ Git وفي أي صورة بُنيت به. الأسرار تصل وقت التشغيل فقط ولا تلمس الصورة. |
pip install fastapi بلا رقم يكفي. | بعد شهر تبني نفس الكود فتحصل على نسخة مكتبة جديدة غيّرت سلوكها. ثبّت كل نسخة وارفعها عمدًا مع اختبار. |
| التراجع يعني إعادة بناء الكود القديم. | التراجع تشغيل صورة موجودة برقمها، في دقيقة، بشرط ألّا تحذف الصور القديمة. |
جرّب بنفسك
أين يوضع مفتاح API الخاص بالخدمة؟
كل ما في الصورة أو المستودع قابل للاستخراج ويبقى في التاريخ. السرّ يصل للعملية فقط وقت تشغيلها.
لماذا لا تستخدم الوسم latest في الإنتاج؟
latest يشير لآخر ما بُني. خادمان سحبا الصورة في يومين مختلفين قد يشغّلان نسختين مختلفتين بنفس الاسم.
بطاقات مراجعة سريعة
1.4.2 أو رمز الالتزام)، والنموذج بنسخته أيضًا. لا latest في الإنتاج.تحقق من فهمك
النسخة الجديدة تعمل لكن دقتها أسوأ. هل تتراجع عن الكود أم عن النموذج؟
يعتمد على ما تغيّر، وهذا سبب فصلهما. إن تغيّر النموذج وحده، أعد MODEL_PATH للنسخة السابقة بنفس الصورة. وإن تغيّر الكود (مثل المعالجة المسبقة)، أعد الصورة السابقة بنفس النموذج. لو كان الاثنان في صورة واحدة، لاضطررت للتراجع عن كليهما معًا.
لماذا تُنسخ requirements.txt وتُثبَّت قبل نسخ مجلد الكود في Dockerfile؟
لأن Docker يعيد استخدام الطبقات التي لم تتغيّر. حين يتغيّر الكود وحده تبقى طبقة المكتبات كما هي، فلا يُعاد التثبيت ويبقى البناء سريعًا.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.