Docker المتقدم وأمن الحاويات

بيئات تشغيل الحاويات ومعيار OCI

18 دقيقة الدرس 8 من 28

بيئات تشغيل الحاويات ومعيار OCI

حين تُنفّذ الأمر docker run nginx، ربما تتخيّل أن Docker هو "الشيء الذي يُشغّل الحاويات". كان هذا التصوّر دقيقاً عام 2015، لكنّ البنية التحتية لبيئات الإنتاج اليوم أكثر تعقيداً بكثير. عُقد Kubernetes لا تتحدث مع Docker إطلاقاً — بل تتحدث مع containerd، الذي يتحدث مع runc، الذي يُنفّذ مجموعة من استدعاءات Linux للنظام. فهم هذه الطبقات ليس ترفاً أكاديمياً؛ بل يحدد أيّ علامات التشغيل يمكنك ضبطها، وكيف تُطبَّق سياسة الأمان، وأيّ بيئة تشغيل تختار حين تحتاج إلى عزل أقوى من الافتراضي.

المشكلة التي أوجدت معيار OCI

بحلول عام 2015، أصبح Docker مرادفاً للحاويات، غير أن بنيته المتكاملة أوجدت هشاشةً في النظام البيئي. أرادت كلٌّ من Kubernetes وCoreos rkt وغيرها تشغيل الحاويات، لكن لم يكن ثمة معيار موحّد — فكان كل طرف يُعيد تطوير صيغ الصور وسلوك بيئة التشغيل بشكل مستقل. أُسِّست مبادرة الحاويات المفتوحة (OCI)، تحت مظلة مؤسسة Linux، لحلّ هذه الإشكالية. وتُحدّد مواصفتين رئيسيتين:

  • مواصفة صورة OCI — صيغة صورة الحاوية: بيان (manifest)، وطبقات نظام الملفات (tar)، وملف JSON للإعدادات يصف نقطة الدخول والبيئة وغيرها.
  • مواصفة بيئة تشغيل OCI — ملف config.json يصف كل ما يلزم لتشغيل حاوية: مسار نظام الملفات الجذر، وسيطات العملية، ومساحات الأسماء، وcgroups، والصلاحيات، وملف seccomp، ونقاط التحميل، والخطافات.

أي أداة تُنتج صورة OCI يمكن تشغيلها بأي بيئة تشغيل متوافقة مع OCI، وأي بيئة متوافقة يمكن توصيلها بأي منسّق متوافق. Docker وBuildah وKaniko وBuildKit كلها تُنتج صور OCI. أما runc وcrun وrunsc الخاص بـgVisor وKata Containers، فجميعها تُطبّق مواصفة بيئة تشغيل OCI.

الطبقات الكاملة لبيئة التشغيل

تستخدم مجموعات Kubernetes الحديثة ثلاث طبقات لبيئة التشغيل، لكل طبقة مهمة محددة:

Container runtime stack: kubelet to runc kubelet Kubernetes node agent — schedules Pods, talks to the runtime via CRI CRI gRPC containerd (High-Level Runtime) Image pull, snapshot management, networking, storage — no container exec itself OCI bundle containerd-shim-runc-v2 (Shim) Owns the container process after runc exits — holds stdio, reports exit status fork/exec runc (Low-Level OCI Runtime) Reads OCI config.json, calls clone()/unshare() — sets up namespaces, cgroups, seccomp — then exec()s PID 1 Linux Kernel (namespaces · cgroups · seccomp · capabilities)
مكدس بيئة تشغيل الحاويات ثلاثي الطبقات: يتواصل kubelet مع containerd عبر CRI، ويُسلّم containerd حزمة OCI إلى runc عبر الـshim، ويُنفّذ runc استدعاءات النواة التي تُنشئ الحاوية.

containerd: بيئة التشغيل العالية المستوى

containerd مشروع متخرّج من CNCF وهو بيئة التشغيل الافتراضية في جميع خدمات Kubernetes المُدارة الكبرى (EKS, GKE, AKS). يُدير دورة حياة الحاويات كاملةً: سحب الصور وتخزينها (عبر نظام اللقطات)، وإعداد أنظمة ملفات overlay، وضبط شبكات CNI، وإدارة حالة الحاويات — لكنه لا يُنفّذ عملية الحاوية بنفسه؛ هذه المهمة منوطة ببيئة التشغيل منخفضة المستوى.

يُتيح containerd واجهة gRPC متوافقة مع CRI تتحدث إليها kubelet. يمكنك أيضاً التخاطب معه مباشرةً عبر أداة ctr (مستوى منخفض) أو الأكثر ودية nerdctl (واجهة سطر أوامر متوافقة مع Docker مدعومة بـcontainerd).

# فحص حالة containerd على عُقدة Kubernetes # (تحتاج إلى SSH إلى العُقدة أولاً — هذه أوامر على مستوى العُقدة) # عرض الحاويات قيد التشغيل عبر crictl crictl ps # سحب صورة مباشرةً عبر containerd ctr image pull docker.io/library/nginx:1.27-alpine # عرض الصور في فضاء أسماء containerd الافتراضي ctr -n k8s.io images ls # فحص إعداد بيئة تشغيل OCI الذي تُولّده containerd لحاوية معيّنة crictl inspect <container-id> | python3 -m json.tool | grep -A5 seccomp
فضاءات أسماء containerd: لدى containerd مفهوم فضاء الأسماء الخاص به (ليست فضاءات أسماء Linux). حاويات Kubernetes تسكن في فضاء الأسماء k8s.io؛ أوامر ctr المستقلة تستخدم default. دائماً حدّد -n k8s.io عند تصحيح أخطاء حاويات Kubernetes بأداة ctr.

runc: بيئة التشغيل المنخفضة المستوى OCI

runc هو التطبيق المرجعي لمواصفة بيئة تشغيل OCI، استُخرج من libcontainer الأصلية في Docker. إنه ملف Go صغير (~8 ميغابايت) يؤدي مهمة واحدة تحديداً: مُعطىً دليل يحتوي على rootfs/ وconfig.json، ينشئ فضاءات أسماء Linux، ويضبط حدود cgroup، ويُطبّق فلاتر seccomp ومجموعات ربط الصلاحيات، ثم يُنفّذ exec() على العملية. ينتهي بعدها تاركاً التحكم للـshim — لا يبقى في الذاكرة.

يمكنك استدعاء runc مباشرةً لفهم عمله أو لتصحيح حاوية متعطّلة:

# تشغيل حاوية يدوياً بـrunc (للفهم — ليس للعمليات الاعتيادية) # 1. إنشاء بنية دليل حزمة OCI mkdir -p /tmp/mycontainer/rootfs # 2. تصدير صورة Docker بوصفها rootfs docker export $(docker create alpine) | tar -C /tmp/mycontainer/rootfs -xf - # 3. توليد config.json الافتراضي لـOCI cd /tmp/mycontainer runc spec # 4. فحص المواصفة المُولَّدة — هذا بالضبط ما يقرأه runc cat config.json | python3 -m json.tool | head -60 # 5. تشغيل الحاوية runc run mycontainer-1 # 6. سرد حاويات runc قيد التشغيل (في طرفية أخرى) runc list # 7. حذف الحاوية runc delete mycontainer-1

أين يقع Docker اليوم؟

جرى إعادة هيكلة Docker (أداة سطر الأوامر والعفريت) اعتباراً من عام 2017. يُعدّ dockerd اليوم في جوهره طبقة تجربة مستخدم فوق containerd: يتولى Docker API وعمليات البناء عبر BuildKit وواجهة سطر الأوامر المألوفة. حين تُنفّذ docker run، تسير الاستدعاءات هكذا: أداة dockerdockerd ← containerd ← shim ← runc ← النواة. على عُقد Kubernetes، لا يكون dockerd في المسار إطلاقاً — إذ تتحدث kubelet مباشرةً مع containerd عبر CRI.

نظرة من الإنتاج: اعتباراً من Kubernetes 1.24، أُزيل dockershim (المحوّل الذي أتاح لـkubelet التحدث مع dockerd). إن كانت مجموعتك تستخدم Docker بوصفه بيئة تشغيل Kubernetes، كان عليها الانتقال إلى containerd أو CRI-O. معظم المجموعات السحابية المُدارة انتقلت تلقائياً. إذا رأيت عُقدة في حالة NotReady بعد ترقية Kubernetes، تحقق من أن مسار مقبس CRI في إعداد kubelet يطابق بيئة التشغيل المثبّتة (/run/containerd/containerd.sock لـcontainerd، أو /var/run/crio/crio.sock لـCRI-O).

بيئات تشغيل بديلة: حين لا يكفي runc

يشترك runc في نواة المضيف — كل حاوية على العُقدة تستخدم النواة ذاتها. بالنسبة لأعباء العمل متعددة المستأجرين حيث لا يمكن الثقة الكاملة بعبء العمل (مثل خدمة CI عامة أو منصة serverless)، يُشكّل هذا إشكاليةً في حدود الأمان. ثمة بديلان جاهزان للإنتاج يعالجان هذا:

  • gVisor (runsc) — بيئة تشغيل OCI من Google تضع نواةً وسيطة في فضاء المستخدم (تُسمى "Sentry") بين عملية الحاوية ونواة المضيف. استدعاءات النظام من الحاوية تصطدم بـSentry التي تُعيد تطبيق مجموعة واسعة من Linux بلغة Go. هذا يُقلّل بشكل جذري سطح هجوم نواة المضيف. عُقد GKE Sandbox تستخدم runsc. حمل الأداء حقيقي (~10-20% لأعباء المعالج، أعلى لأعباء العمل الكثيفة باستدعاءات النظام).
  • Kata Containers — يُشغّل كل حاوية (أو Pod) داخل جهاز افتراضي خفيف الوزن يستخدم QEMU أو Cloud Hypervisor. عزل كامل على مستوى الجهاز الافتراضي؛ عملية الحاوية لا تلمس نواة المضيف أبداً. مستخدمة في Azure Confidential Containers. زمن بدء التشغيل أعلى (~1 ثانية مقابل ~100 ميلي ثانية لـrunc).

كلاهما متوافق مع OCI، لذا يمكن استخدامهما بديلاً عن runc داخل containerd بتسجيل RuntimeClass في Kubernetes:

# RuntimeClass في Kubernetes لـgVisor (يتطلب تثبيت gVisor على العُقد) apiVersion: node.k8s.io/v1 kind: RuntimeClass metadata: name: gvisor handler: runsc # يطابق اسم معالج الـshim في containerd --- # استخدامه في مواصفة Pod — كل شيء آخر متطابق apiVersion: v1 kind: Pod metadata: name: sandboxed-app spec: runtimeClassName: gvisor # <-- التغيير الوحيد containers: - name: app image: gcr.io/myproject/myapp:v2.1.0 resources: requests: memory: "128Mi" cpu: "250m" limits: memory: "256Mi" cpu: "500m"
ليست كل أعباء العمل تعمل على gVisor: تُطبّق Sentry استدعاءات النظام الأكثر شيوعاً، لكن بعض التطبيقات تستخدم واجهات نواة نادرة أو جديدة لا تدعمها Sentry بعد. اختبر دائماً قبل الترحيل — قواعد البيانات الحالة، والأدوات المعتمدة على eBPF، وبعض تفاصيل وقت تشغيل Go واجهت تاريخياً مشاكل توافق. راجع قائمة توافق gVisor وشغّل مجموعة اختباراتك مع runsc في بيئة التدريج قبل النشر للإنتاج.

اختيار بيئة التشغيل: مصفوفة القرار

على نطاق الشركات الكبرى، يُحدَّد اختيار بيئة التشغيل انطلاقاً من نموذج التهديد ونوع عبء العمل:

  • أعباء عمل داخلية موثوقة على عُقد مخصصةrunc (الافتراضي). أقل حمل، أوسع توافق.
  • SaaS متعدد المستأجرين أو CI عامrunsc (gVisor) للمهام كثيفة المعالج/الذاكرة؛ Kata لكل ما يستلزم عزلاً قوياً على مستوى الجهاز الافتراضي.
  • البيئات الخاضعة للتنظيم (FedRAMP, PCI-DSS) — Kata Containers أو أجهزة افتراضية للحوسبة السرية؛ غالباً ما يشترط المدققون دليلاً على العزل على مستوى الأجهزة.
  • الحافة / إنترنت الأشياء ذات الموارد المحدودةcrun (إعادة تطبيق runc بلغة C، أسرع بـ15× في الإقلاع، وبصمة ذاكرة أقل بـ50×).
مواصفة OCI هي العقد: تبديل بيئات التشغيل لا يتطلب أي تغيير في صورك، أو ملفات Dockerfile، أو تظهيرات Kubernetes — يتغير فقط حقل RuntimeClass في مواصفة Pod. هذه هي الفائدة العملية الملموسة من جهود توحيد معيار OCI.