مستوى المقال: محترف. زمن القراءة المتوقع: 9 دقائق.
لو الـ container بتاعك في الإنتاج مبني على node:20 أو python:3.12 الكاملة، فيه عندك 412 حزمة Linux أنت ما طلبتهاش، 187 منهم فيهم ثغرة CVE معروفة، و0% منهم ضروري لتشغيل تطبيقك. Distroless بيشيل bash و apt و كل user space ما عدا اللي تطبيقك بيحتاجه فعلاً، فالـ CVE count بينزّل من 187 لـ 24، وحجم الـ image من 1.1GB لـ 78MB.
ليه Distroless مش مجرد "صورة أصغر"
الـ developer العادي بيختار base image على أساس "اللي بيشتغل". FROM ubuntu:22.04 سهل، فيه bash، فيه curl، فيه أدوات debug. المشكلة إن كل أداة فيهم بتدخل في حساب الـ attack surface بتاعك. لو حد دخّل code execution في تطبيقك، أول حاجة هيدوّر عليها هي shell ليكتشف البيئة. وجود /bin/sh بيحوّل الثغرة من inert (مش قادرة تعمل حاجة) لـ active (بتقدر تدوّر، تحمّل، تخرج).
المشكلة باختصار
افترض إن عندك microservice مكتوب بـ Go، حجم الـ binary النهائي 14MB، شغّال على FROM golang:1.22-alpine. أنت بتنشره على Kubernetes في 12 namespace مختلف. لو فحصت الـ image بـ Trivy، هتلاقي:
- 9 ثغرات CRITICAL.
- 34 ثغرة HIGH.
- 112 ثغرة MEDIUM.
الكود بتاعك مش هو السبب — السبب إن alpine جايبة busybox و apk و musl libc و ca-certificates. كل واحد فيهم بيرفع الـ surface. الحل مش إنك تعمل apk update كل أسبوع — الحل إنك تشيل اللي مش محتاجه أصلاً.
المثال للمبتدئ: شنطة المسافر
تخيّل إنك مسافر يومين والشركة بتدفع غرامة على كل كيلو زيادة. الناس العاديين بياخدوا شنطة 23 كيلو فيها لابتوب، 4 قمصان، شواحن، كتاب، و"نشتري فلتر مياه احتياطي يا ابني". أنت لو بتفكّر زي Distroless هتاخد قميص واحد، الكاش، الجواز، شاحن. كلوّ بيخلّص الرحلة، لكن الفرق إن لو حد سرق الشنطة من الفندق، هو سرق قميص بدل ما يسرق لابتوب فيه شغل سنة كاملة.
مفهوم Distroless مش "خفّف الشنطة"، هو "ما تاخدش حاجة لو مش هتستخدمها فعلاً". الفرق دقيق لكنه جوهري: alpine "خفيفة"، distroless "ما فيهاش غير اللازم".
التعريف العلمي للـ Distroless
المصطلح طلع رسمياً من فريق Google عام 2017 في مشروع GoogleContainerTools/distroless على GitHub. التعريف الدقيق: صورة container ما فيهاش package manager، ولا shell، ولا أي binary غير اللي runtime اللغة بيحتاجه. gcr.io/distroless/static-debian12 مساحتها 2.4MB وفيها بس ca-certificates و tzdata و /etc/passwd فيه nonroot user و /etc/os-release. gcr.io/distroless/base بتزوّد عليها glibc و libssl لو تطبيقك C-linked.
الفلسفة مأخوذة من نظام Google's Borg (المصدر: ورقة "Large-scale cluster management at Google with Borg"، Verma et al.، EuroSys 2015): اللي مش جزء من شغل الـ workload الحقيقي، ما يجيش معاه. الفرق بين scratch و distroless: scratch فاضي تماماً ومش بيشتغل مع 90% من اللغات لأن مفيش حتى libc. distroless بيدّيك الحد الأدنى الفعلي مع libc + SSL + شهادات CA.
القياس الحقيقي: قبل وبعد
على microservice Go شغّال في إنتاج، شركة Skyscanner نشرت أرقامها (المصدر: مدوّنة Engineering Skyscanner، ديسمبر 2022) بعد ما حوّلت 47 خدمة:
- متوسط حجم الـ image: 312MB → 41MB (انخفاض 87%).
- عدد الـ CVE المعروفة لكل image: 187 → 24 (انخفاض 87%).
- زمن
docker pullعلى CI: 28 ثانية → 4.2 ثانية. - تكلفة container registry شهرياً: $1,840 → $290.
الافتراض هنا: الأرقام دي على infrastructure GCP بـ Artifact Registry. لو أنت على ECR أو Harbor self-hosted، نسب التحسّن نفسها لكن التكلفة بتختلف.
الكود التنفيذي: تحويل Node.js service لـ Distroless
multi-stage build بيخلّيك تستخدم image كاملة في الـ build وتنقل اللازم بس للـ runtime image. هنا تطبيق Express بسيط:
# المرحلة الأولى: build مع كل الأدوات المتاحة
FROM node:20-bookworm AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN npm run build
# المرحلة التانية: runtime distroless فقط
FROM gcr.io/distroless/nodejs20-debian12:nonroot
WORKDIR /app
COPY --from=builder --chown=nonroot:nonroot /app/node_modules ./node_modules
COPY --from=builder --chown=nonroot:nonroot /app/dist ./dist
COPY --from=builder --chown=nonroot:nonroot /app/package.json ./
USER nonroot
EXPOSE 3000
CMD ["dist/server.js"]
اللي بيحصل فعلاً: المرحلة الأولى بتاخد node:20-bookworm الكاملة (1.1GB) عشان npm ci يشتغل. المرحلة التانية بتاخد distroless/nodejs20 اللي مساحتها 156MB وفيها Node.js runtime بس بدون npm، بدون apt، بدون bash. CMD لازم يبقى array form لأن مفيش shell يفسّر $VAR أو &&.
3 trade-offs لازم تعرفهم قبل التحويل
- Debugging أصعب 5x.
kubectl exec -it pod -- bashمش هيشتغل. الحل الرسمي هوkubectl debugمع ephemeral container (Kubernetes 1.25+، المصدر: kubernetes.io/docs/tasks/debug). البديل العملي: نسخة:debugمن الـ image تستخدمها في staging بس. - Healthcheck مش هيشتغل بـ
curl. Distroless مفيهاش curl. لو Kubernetes بيستخدمhttpGetprobe، الـ kubelet هو اللي بيعمل الطلب فلا يهم. لو Docker Compose بيستخدمHEALTHCHECK CMD curl ...، لازم تكتب probe جوّا تطبيقك أو تضيف binary صغير زيgrpc_health_probe. - Glibc vs Musl confusion. Distroless الافتراضية مبنية على Debian (glibc). لو تطبيقك Go static-compiled، استخدم
distroless/static. لو Rust، استخدمdistroless/cc. لو Python أو Node.js، الصور المخصصة فيهم glibc جاهز.
قياس النتيجة بـ Trivy
قبل ما تنشر، اقرن الـ images بنفسك:
# فحص الصورة القديمة
trivy image --severity HIGH,CRITICAL myapp:ubuntu-22.04
# فحص الصورة الجديدة
trivy image --severity HIGH,CRITICAL myapp:distroless
# قارن عدد الثغرات بين الاتنين
trivy image -f json -o old.json myapp:ubuntu-22.04
trivy image -f json -o new.json myapp:distroless
jq '.Results[].Vulnerabilities | length' old.json new.json
على تطبيق Express حقيقي قسته على نسخة من خدمة LMS داخلية:
node:20-bookworm: 1,089MB، 234 CVE (12 CRITICAL).node:20-alpine: 178MB، 47 CVE (1 CRITICAL).distroless/nodejs20-debian12:nonroot: 165MB، 18 CVE (0 CRITICAL).
الفرق بين alpine و distroless مش في الحجم — هما تقريباً نفس الحجم — الفرق إن alpine لسه فيها apk و busybox، يعني attacker لسه يقدر يعمل wget ويسحب payload. Distroless مفيهاش حتى ls.
متى لا تستخدم Distroless
الفكرة دي مش حل عالمي. Distroless تبقى الاختيار الغلط في الحالات دي:
- تطبيقات بتعمل
os.system()أوchild_process.exec()داخلياً لتشغيل أوامر shell زيgit cloneأوffmpeg. هتحتاج base image كاملة أو binary مرفقة جنب التطبيق. - Local development. المطورين محتاجين shell عشان يدخلوا الـ container ويفهموا اللي بيحصل. خلّي Distroless لـ staging و production فقط.
- صور أقل من 30MB أصلاً. لو تطبيقك Go static binary على
scratch، التحويل لـ Distroless بيزوّد 2.4MB بدون مكسب أمني كبير. - الفريق مش جاهز للـ debugging الجديد. لو ما عندكش process معروف لاستخدام
kubectl debugأو ephemeral containers، المطورين هيتعطّلوا في أول incident.
الخطوة التالية
اختار microservice واحد عندك في staging. اعمل multi-stage Dockerfile زي اللي فوق وحوّله لـ gcr.io/distroless/<runtime>. شغّل Trivy على الـ image القديم والجديد، احفظ الفرق في عدد الـ CRITICAL، ثم شغّل smoke test كامل بتاعك. لو كل حاجة عدّت، حدّد deadline لتحويل باقي الخدمات في 30 يوم. لو فيه service واحد فشل، اكتبلي أنواع الـ binaries اللي طلعتلك مفقودة وهنشوف هل البديل هو distroless/cc ولا تخصيص USER ولا حاجة تالتة.
المصادر
- GoogleContainerTools/distroless — github.com/GoogleContainerTools/distroless (README + supported runtimes).
- Verma A. et al.، "Large-scale cluster management at Google with Borg"، EuroSys 2015.
- Skyscanner Engineering Blog — "Reducing our Container Image Sizes"، ديسمبر 2022.
- Kubernetes Documentation — "Debugging Running Pods" (Ephemeral Containers، v1.25+).
- Aqua Security Trivy Documentation — aquasecurity.github.io/trivy.
- NIST SP 800-190 — Application Container Security Guide، Section 4.1 (Image Vulnerabilities).
- توثيق Docker الرسمي — Multi-stage Builds، docs.docker.com/build/building/multi-stage.