Qwizin — DevOps والحاويات وموازنة الأحمال والتنسيق
الحالة: Draft v0.1 · التاريخ: 2026-07-20
يرتبط بـ: CON-14 (modular monolith)، CON-15 (Kubernetes)، ADR-0001 (Oracle OCI داخل المملكة)، QAS-OPS-01، QAS-OPS-02، QAS-SCL-01/02/03
مرجع البيئة (مُتحقَّق منه في 2026-07-20): Kubernetes v1.36.2؛ الإصدارات الفرعية المدعومة 1.34/1.35/1.36. Laravel 13 (صدر في 2026-03-17)؛ لا يوجد إصدار LTS — كان الأخير Laravel 6 في 2019. وجميع الإصدارات الآن تحصل على 18 شهرًا لإصلاح العلل و24 شهرًا للأمن. يُنصح بـ PHP 8.4 (الإصدار 8.3 أمني فقط منذ 2025-12-31؛ وLaravel 13 يتطلب ≥ 8.3).
⚠ لا إصدار LTS في Laravel. لهذا أثر مباشر على DevOps يسهل إغفاله: لا يوجد خيار «ثبّته واتركه ثلاث سنوات». يجب رصد ميزانية لوتيرة ترقية مستمرة من اليوم الأول — أي ترقية رئيسية سنوية تقريبًا. نافذة إصلاح علل Laravel 12 تنتهي في 2026-08-13، بعد ثلاثة أسابيع من الآن. ابدأ على Laravel 13.
1. استراتيجية الحاويات#
1.1 صورة واحدة، أدوار متعددة#
الـ API ومعالجات الطوابير (workers) والـ scheduler هي التطبيق نفسه. وبناء ثلاث صور يعني ثلاث فرص لانحراف الإصدارات — فوجود worker يشغّل كود الأمس ويعالج حمولات مهام اليوم صنف علل خبيث حقًا.
ابنِ صورة واحدة؛ ونوّع نقطة الدخول (entrypoint).
| عبء العمل | نقطة الدخول | النسخ | إشارة التوسّع |
|---|---|---|---|
| API | php-fpm + nginx | 3+ | RPS / زمن الاستجابة p95 |
| Queue workers | queue:work (أو Horizon) |
2+ | عمق الطابور |
| Scheduler | schedule:work |
واحد بالضبط | لا شيء — singleton |
| Migrations | migrate --force |
Job | تُنفَّذ مرة واحدة لكل إصدار |
1.2 البناء متعدد المراحل#
Stage 1 (composer) → composer install --no-dev --optimize-autoloader
--classmap-authoritative
Stage 2 (assets) → node build (admin portal assets)
Stage 3 (runtime) → php:8.4-fpm-alpine
+ opcache, pcntl, intl, gd, redis, pdo_pgsql
+ Arabic fonts (see §1.4)
+ artisan config:cache route:cache view:cache event:cache
+ USER 10001, chown app tree
أمور غير قابلة للتفاوض:
--no-dev— وجود PHPUnit وأشرطة التصحيح وfaker في صورة إنتاجية هو سطح هجوم بلا غرض. وشريط تصحيح مسرَّب هو مسار اختراق كلاسيكي في PHP.- ثبّت الصورة الأساسية بالـ digest، لا بالوسم (tag). فـ
php:8.4-fpm-alpineيتحرك؛ أماphp@sha256:…فلا. وRenovate/Dependabot يؤتمت الترقيات، فالتثبيت لا يعني التقادم. allow-pluginsصريح فيcomposer.json. إضافات Composer تنفّذ كودًا اعتباطيًا وقت التثبيت — وهذا نظير npm install scripts في عالم PHP، ومسار حقيقي في سلسلة التوريد.- عطّل دوال PHP الخطرة في
php.ini:exec,shell_exec,passthru,system,proc_open,popen. يغلق ذلك أشيع مسار في PHP من RCE إلى shell من منبعه. APP_DEBUG=false,APP_ENV=production— يُتحقق منهما بفحص آلي، لا بالأمل. صفحة أخطاء التصحيح في Laravel تسرّب متغيرات البيئة بما فيها بيانات اعتماد قاعدة البيانات. وهي من أكثر اختراقات Laravel شيوعًا في الواقع.
1.3 تسخين الـ cache وقت البناء — مطلوب لنظام ملفات جذر للقراءة فقط#
تشغيل config:cache وroute:cache وview:cache وevent:cache أثناء بناء الصورة يحوّل ملفات الإقلاع في Laravel — القابلة للكتابة وقت التشغيل — إلى مخرجات مخبوزة للقراءة فقط. وهذا متطلب تحصين (§5) ومكسب أداء في آن واحد.
⚠ فخ
config:cache. ما إن يُخزَّن الإعداد في الـ cache حتى تُرجعenv()القيمةnullفي كل مكان خارجconfig/*.php. دقّق في استدعاءاتenv()داخل كود التطبيق قبل تفعيل هذا — فهو يفشل بصمت وفي الإنتاج. وهذا أشيع خطأ مفرد في تشغيل Laravel على Kubernetes.
ولإزالة عمليات الكتابة المتبقية:
| مسار كتابة Laravel | الإزالة |
|---|---|
storage/framework/sessions |
SESSION_DRIVER=redis |
storage/framework/cache |
CACHE_STORE=redis |
storage/logs |
LOG_CHANNEL=stderr |
storage/app (الملفات المرفوعة) |
مشغّل تخزين كائني متوافق مع S3 |
bootstrap/cache/* |
مخبوز وقت البناء |
storage/framework/views |
مخبوز، مع emptyDir احتياطي للحزم التي تُصرِّف وقت التشغيل |
ونقل الملفات المرفوعة إلى التخزين الكائني ليس مجرد تحصين — فالمستندات القانونية والشهادات تحتاج إلى ديمومة وتشفير وسياسة دورة حياة لا يوفرها قرص محلي داخل الـ pod.
1.4 مجموعة الخطوط العربية#
يجب أن تُضمِّن صورة Document Renderer خطوطًا عربية بتغطية كاملة للرسوم وجداول تشكيل صحيحة (QAS-LOC-01). وهذا شأن يخص الصورة وقت البناء، لا شأن تطبيقي — فالمُصيِّر بلا خطوط يُنتج مربعات أو حروفًا مفكّكة، ولا يظهر هذا الفشل إلا في الشهادات المولَّدة، لا في اختبارات تتحقق فقط من أن ملف الـ PDF غير فارغ. واختبارات الصور الذهبية (golden-image) على عيّنات عربية مكانها CI.
1.5 وقت التشغيل: PHP-FPM مقابل Octane#
التوصية: ابدأ بـ PHP-FPM + OPcache preload. لا تتبنَّ Octane عند الإطلاق.
| PHP-FPM | Octane (FrankenPHP / RoadRunner / Swoole) | |
|---|---|---|
| عزل الطلبات | حالة جديدة لكل طلب | حالة دائمة بين الطلبات |
| نمط الفشل | محتوى | تسرّب الحالة، تلوّث الـ singletons، نمو الذاكرة |
| الملاءمة لـ K8s | بديهية | ممكنة، لكنها تتطلب عناية أكبر |
| مكسب الأداء | خط الأساس | كبير |
مبرر التأجيل: نموذج العمال الدائمين في Octane يعني أن singleton يحمل سياق المستأجر قد يسرّب بيانات شركة إلى طلب شركة أخرى — وهو انتهاك مباشر لـ QAS-SEC-01، السيناريو الأعلى أولوية في النظام، وصنف علل لا وجود له أصلًا تحت FPM. وتبنّي Octane قبل إثبات نموذج tenant isolation وتغطيته بالاختبارات يقايض سمة الجودة الأعلى بأداء لا يتطلبه حجم الإطلاق.
أعد النظر عندما: يقترب زمن الاستجابة p95 من ميزانية QAS-PERF-01 تحت حمل حقيقي، وتكون هناك تغطية اختبارية آلية للعزل عبر المستأجرين، وتتوفر طاقة لتدقيق كل singleton. وFrankenPHP مذكور أولًا في توثيق Octane لـ Laravel 13، وهو الخيار المرجّح عند تلك النقطة.
2. طوبولوجيا Kubernetes#
2.1 مساحات الأسماء (Namespaces)#
| Namespace | المحتويات | ملف PSA |
|---|---|---|
qwizin-app |
API، workers، scheduler | restricted |
qwizin-media |
Document renderer، ماسح البرمجيات الخبيثة | restricted |
qwizin-search |
محرك البحث (stateful) | restricted |
qwizin-video |
خوادم الوسائط، TURN — فقط في حال الاستضافة الذاتية | baseline ⚠ |
qwizin-observability |
مُجمِّع OTel، المقاييس، السجلات | restricted |
qwizin-platform |
Ingress، cert-manager، ESO، Kyverno | متفاوت |
qwizin-videoمعزول عن قصد. خوادم الوسائط ذاتية الاستضافة تتطلبhostNetwork، وهو غير متوافق مع PSArestricted. وبدلًا من إضعاف خط الأساس للعنقود بأكمله، يُحصر الاستثناء في namespace واحد بسياسة شبكة خاصة به. وهذا هو النمط الصحيح كلما احتاج عبء عمل واحد إلى صلاحية: اعزل الاستثناء، ولا تخفّض خط الأساس أبدًا.
2.2 مجموعات العُقد (node pools)#
| المجموعة | الغرض | ملاحظة على الحجم |
|---|---|---|
general |
API، workers، scheduler | قياسية، مع autoscaling |
memory |
Document renderer، البحث | العرض عبر متصفح بلا واجهة يستهلك مئات الميغابايتات لكل عملية عرض |
media |
خوادم الوسائط — في حال الاستضافة الذاتية | محسّنة للشبكة. pod وسائط واحد لكل عقدة (تعارض منافذ hostNetwork) ← وحدة التوسّع هي العقدة، لا الـ pod |
3. موازنة الأحمال#
3.1 ثلاثة مسارات حركة مستقلة#
الحقيقة الطوبولوجية الحاسمة: وسائط الفيديو لا تمر إطلاقًا عبر مسار الـ API. ومعاملتهما كنظام واحد هو أشيع خطأ في موازنة الأحمال في منصة كهذه.
graph LR
subgraph P1["Path 1 — API traffic (L7)"]
C1["📱 Client"] --> WAF["WAF / DDoS"] --> LB1["L4 Load Balancer"]
LB1 --> ING["Ingress Controller<br/>TLS 1.3 termination<br/>routing · rate limiting"]
ING --> SVC["Service → API pods<br/><i>round-robin, readiness-gated</i>"]
end
subgraph P2["Path 2 — Static & downloads"]
C2["📱 Client"] --> CDN["CDN"] --> OBJ["Object Storage<br/><i>signed URLs</i>"]
end
subgraph P3["Path 3 — Video media (L4/UDP)"]
C3["📱 Client"] -.->|"WebRTC SRTP<br/><b>bypasses ingress entirely</b>"| MED["Media servers<br/><i>hostNetwork, UDP</i>"]
C3 -.->|"TURN fallback<br/>TCP/TLS 443"| TURN["TURN relay"]
end
style MED fill:#d98c1f,stroke:#96610f,color:#fff
style TURN fill:#d98c1f,stroke:#96610f,color:#fff
المسار 1 — API. موازن أحمال سحابي على L4 ← ingress controller. يُنهى TLS 1.3 عند الـ ingress. وتُطبَّق هنا التوجيه وتحديد المعدل وحدود حجم الطلب. مدعوم بفحوص الجاهزية (readiness probes) كي تخرج الـ pods من الدوران قبل أن تفشل.
المسار 2 — الملفات الساكنة والتنزيلات الكبيرة. تُقدَّم ملفات القوالب والشهادات من التخزين الكائني عبر signed URLs قصيرة العمر، خلف CDN. ويجب ألا يمر نقل الملفات الضخمة عبر طبقة الـ API إطلاقًا — فذلك يستهلك عمال PHP-FPM طوال مدة كل تنزيل، وهي أسرع طريقة لاستنفاد طبقة الطلبات.
المسار 3 — وسائط الفيديو. UDP، وhostNetwork، بلا ingress وبلا service mesh. مسار شبكي موازٍ يتطلب قواعد جدار ناري خاصة به ومعالجة أمنية خاصة به. والارتداد إلى TURN عبر TCP/TLS 443 إلزامي في الإنتاج — فالمستخدمون خلف CGNAT (شائع في شبكات الجوال السعودية) وخلف جدران الشركات المتشددة لا يستطيعون الاتصال بدونه. وفي استشارة مدفوعة، المكالمة غير القابلة للاتصال تعني استردادًا للمبلغ وعميلًا مفقودًا.
3.2 التصاق الجلسة (session affinity)#
غير مطلوب، ومتجنَّب عن قصد. الـ API عديم الحالة: الجلسات في Redis، والـ cache في Redis، والملفات المرفوعة في التخزين الكائني. والجلسات اللاصقة ستقوّض توزيع الحمل المتوازن وتُعقّد النشر التدريجي. والمكوّن الوحيد ذو متطلبات الالتصاق هو طبقة الوسائط، وهي تتولى توجيه الغرف إلى العقد بنفسها.
4. التوسّع التلقائي (autoscaling)#
4.1 إشارات التوسّع لكل عبء عمل#
اختيار الإشارة الخاطئة هو أشيع فشل في الـ autoscaling. ونادرًا ما يكون المعالج هو الإشارة الصحيحة هنا.
| عبء العمل | الإشارة | لماذا لا المعالج |
|---|---|---|
| API | RPS + زمن الاستجابة p95 | المعالج يتأخر عن التدهور الذي يشعر به المستخدم. PHP-FPM يتشبّع عند استنفاد مجمّع العمال (worker pool) بينما يبدو المعالج سليمًا — فطابور من الطلبات ينتظر عاملًا شاغرًا يظهر كزمن استجابة، لا كاستهلاك معالج. |
| Queue workers | عمق الطابور / عمر أقدم مهمة (KEDA) | العمال الخاملون لا يستهلكون معالجًا. تراكم 10 000 مهمة مع استهلاك معالج 5 % هو بالضبط الوضع الذي يستدعي التوسّع. |
| Document renderer | عمق طابور العرض | متذبذب بطبيعته (QAS-SCL-03) |
| ماسح البرمجيات الخبيثة | معدل الرفع | — |
| خوادم الوسائط | عدد الجلسات / مستوى الإجهاد | الوسائط مقيَّدة بعرض النطاق لا بالمعالج |
| Scheduler | لا شيء — singleton | يجب ألا يعمل في نسخ متعددة أبدًا |
يُوصى بـ KEDA للتوسّع حسب عمق الطابور، بما في ذلك التقليص إلى صفر لـ document renderer خلال فترات الهدوء (QAS-OPS-02).
4.2 التوسّع التنبؤي للفيديو ⭐#
Qwizin تعرف حملها المستقبلي. فالاستشارات تُحجز مسبقًا، ومن ثم فالطلب على الفيديو مجدول لا عشوائي. وهذا أمر غير معتاد معماريًا ويستحق استثماره.
الـ autoscaling التفاعلي لا يلائم الوسائط لأن:
- pod وسائط واحد لكل عقدة ← وحدة التوسّع هي العقدة، بزمن إقلاع يُقاس بالدقائق
- التقليص لا يمكنه قطع المكالمات الجارية. فالتفريغ يعني رفض المؤتمرات الجديدة وانتظار انتهاء القائمة — إلى حد طول أطول مكالمة. وفي استشارات تمتد ساعة، يعني ذلك ساعة من الدفع مقابل عقدة تحاول إزالتها.
التوصية: يقرأ الـ scheduler تقويم المواعيد ويُسخِّن سعة الوسائط مسبقًا قبل النوافذ المحجوزة، مع HPA تفاعلي كشبكة أمان فقط. فتكون السعة جاهزة قبل الموعد بدلًا من أن تصل بعد ثلاث دقائق من مكالمة دفع العميل ثمنها.
4.3 الإغلاق السلس — الفخ الخاص بـ Laravel#
يجب على الـ workers إنهاء المهمة الحالية قبل الإنهاء. وإذا كان terminationGracePeriodSeconds أقصر من أطول مهمة، فإن Kubernetes يرسل SIGKILL في منتصف المهمة عند كل نشر وكل تقليص. وبالنسبة لمهمة تولّد شهادة أو تستدعي API دفع، يعني ذلك عملًا ضائعًا أو حالة خارجية غير محددة.
- يجب أن يتجاوز
terminationGracePeriodSecondsأطول مدة متوقعة للمهمة - يتعامل العمال مع
SIGTERMبإنهاء المهمة الحالية ثم الخروج — وqueue worker في Laravel يفعل ذلك بشكل صحيح إن أُعطي الوقت - ينبغي تقسيم المهام الطويلة إلى أجزاء كي تبقى فترة السماح محدودة
- خطاف
preStopللتفريغ من نقاط نهاية الخدمة قبل بدء الإغلاق
5. خط الأساس الأمني#
التفصيل الكامل في deep-dives/security-architecture.md. وفيما يلي الأساسيات الخاصة بالحاويات والـ orchestration:
5.1 Pod Security Admission#
أُزيلت PodSecurityPolicy في Kubernetes 1.25. والبديل المدمج هو Pod Security Admission، وهو GA منذ 1.25.
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36 # pin — never "latest"
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
ثبّت enforce-version: فبدونه قد تُشدِّد ترقية العنقود الإنفاذ بصمت وتكسر عمليات النشر. أطلق warn + audit أولًا، واقرأ التحذيرات، ثم أنفِذ.
ما الذي يكسره restricted في مكدّس PHP — وكيف يُصلَح:
| المشكلة | الإصلاح |
|---|---|
صورة nginx الرسمية تشغّل الـ master بصلاحيات root وتربط المنفذ :80 |
استخدم nginxinc/nginx-unprivileged (UID 101، المنفذ 8080)؛ وتُعيّن الـ Service التحويل 80←8080. لا تُعِد إضافة NET_BIND_SERVICE — فضّل المنفذ العالي. |
nginx يكتب في /var/cache/nginx و/var/run و/tmp |
تركيبات emptyDir (وهي ضمن قائمة الأحجام المسموح بها في restricted) |
PHP-FPM يكتب pid/socket في /run |
emptyDir؛ والسجلات إلى stdout/stderr |
Laravel يكتب في storage/ وbootstrap/cache/ |
تسخين الـ cache وقت البناء + Redis + stderr + التخزين الكائني (§1.3) |
readOnlyRootFilesystemليس جزءًا من ملفrestricted— وهذا سوء فهم شائع. يجب إنفاذه بشكل منفصل عبر Kyverno أو Validating Admission Policy.
5.2 سياسة الشبكة#
الرفض الافتراضي للدخول والخروج لكل namespace، مع استثناء إلزامي لـ DNS على المنفذ 53 عبر UDP وTCP معًا (TCP لازم للردود الكبيرة ولـ DNSSEC). ونسيان استثناء DNS هو السبب الأول لظاهرة «كل شيء تعطّل» بعد تفعيل الرفض الافتراضي للخروج.
⚠ لم تعد
AdminNetworkPolicyوBaselineAdminNetworkPolicyموجودتين بهذين الاسمين. ففي أكتوبر 2025 دُمجتا فيv1alpha2.ClusterNetworkPolicyواحدة. وهي لا تزال alpha، وخارج الشجرة الأساسية، وقد مرّت للتو بإعادة تسمية كاسرة — لا تبنِ النموذج الأمني عليها. استخدمNetworkPolicyالقياسية، وقارب قواعد طبقة المسؤول ببساطة عبر عدم منح المطوّرين صلاحيات RBAC للكتابة على كائنات NetworkPolicy.
مشكلة الخروج (egress) التي تستحق التخطيط لها: NetworkPolicy القياسية تأخذ CIDRs لا أسماء نطاقات. وبوابات الدفع ومزوّدو الفيديو يقفون خلف عناوين anycast متبدّلة، ما يجعل قائمة CIDR المسموح بها هشّة وستستدعي أحدهم في الساعة 02:00 حين يغيّر مزوّد عناوينه. ثلاثة خيارات:
- سياسة FQDN أصيلة في الـ CNI —
toFQDNsفي Cilium أو قواعد النطاقات في Calico. وهو الجواب النظيف، وسبب قوي لـ اختيار الـ CNI عن قصد بدلًا من قبول الخيار الافتراضي. - وكيل خروج أمامي (forward proxy) بقائمة نطاقات مسموح بها — محايد تجاه الـ CNI، ويُنتج سجل خروج واحدًا قابلًا للتدقيق (دليل مفيد في تدقيق المدفوعات).
ipBlockواسع يستثني RFC1918 — ضعيف، لكنه يمنع الحركة الجانبية؛ ومقبول كحل مؤقت في الأسبوع الأول.
5.3 RBAC#
- لم تعد أسرار رموز ServiceAccount المولَّدة تلقائيًا موجودة (أُزيلت في 1.24، ونُظّفت في 1.29). فأعباء العمل تتلقى رموزًا مُسقَطة (projected) قصيرة العمر ومقيَّدة بالجمهور وذاتية التدوير. ووجود Secret من نوع
kubernetes.io/service-account-tokenمُنشأ يدويًا في ملف manifest هو بيان اعتماد طويل العمر لا ينتهي — عامل أي ظهور له بوصفه ملاحظة تدقيقية. automountServiceAccountToken: false— فـ pods الخاصة بـ Laravel لا تتحدث إلى API server. اضبط هذا علىdefaultفي كل namespace وعلى مستوى كل عبء عمل.- ServiceAccounts مخصصة لكل عبء عمل، ولا تشارك
defaultأبدًا، كي يكون RBAC وسجلات التدقيق قابلة للعزو. - انتبه لمسارات التصعيد التالية التي تعادل عمليًا صلاحية cluster-admin:
secrets:get/list،pods/exec،pods/attach،pods:create،escalate/bind،serviceaccounts/token، وأي wildcard.
5.4 الأسرار (Secrets)#
تغيّر المشهد تغيّرًا جوهريًا، والخياران الرائدان يحملان تحفظات:
- External Secrets Operator أوقف إصداراته في أغسطس 2025 حين احترق المشرفون وتوقفت الشركة الداعمة. ثم تعافى — مشرفون جدد، وحوكمة رسمية، وواجهة v1 مستقرة، ووتيرة شهرية، وهو الآن عند v2.8.0 (يوليو 2026)، ومستضاف لدى CNCF. صحيح أنه عاد سليمًا، لكن هذه كانت حادثة تقترب من الموت خلال 12 شهرًا. تحقق من درجة استقرار المزوّد المحدد الذي تستخدمه — فنواة ESO مستقرة بينما تتفاوت المزوّدات الفردية بين مستقر وalpha مجتمعي.
- HashiCorp Vault انتقل إلى BUSL 1.1 في أغسطس 2023، وأغلقت IBM استحواذها عليه في فبراير 2025. ولم يُعكس الترخيص. وBUSL يخلق غموضًا حقيقيًا لـ SaaS تجاري.
- OpenBao — تفريعة من Linux Foundation تحت MPL 2.0، وهي الآن تتباعد جوهريًا عن Vault لا تتبعه فحسب. v2.6.0 (يوليو 2026)، مع دعم تجاري متاح.
التوصية: إن كانت خدمتا KMS المُدارة ومخزن الأسرار من OCI متوفرتين في me-riyadh-1 (⚠ غير مُتحقَّق منه — انظر الفجوة رقم 1 في ADR-0001)، فاستخدم ESO موجَّهًا إليهما إضافة إلى تشفير etcd بـ KMS v2. وإن لم تكونا متوفرتين، فشغّل OpenBao بدلًا من Vault — فـ MPL 2.0 يزيل الغموض الترخيصي كليًا، وطريقة مصادقة Kubernetes فيه توفّر هوية أعباء العمل باستقلال عن المنصة. وارصد الميزانية بصدق: OpenBao خدمة stateful عالية التوفر بمراسم فك ختم (unseal) والتزام حقيقي بالـ DR.
Sealed Secrets حل مؤقت معقول للأسبوع الأول لإخراج النصوص الصريحة من Git، ورخيص الانتقال منه لاحقًا.
5.5 التحكم بالقبول (admission control)#
بلغ Kyverno حالة CNCF Graduated في 2026-03-24 وهو الخيار الواضح لعام 2026 — سياسات بصيغة YAML بدلًا من Rego، وهو أمر حاسم لفريق بلا مهندس منصات متفرغ. كما أنه يعدّل ويولّد، ويحوي verifyImages من Cosign مدمجًا.
Validating Admission Policy وصلت إلى GA (منذ 1.30) وMutatingAdmissionPolicy أصبحت مستقرة في v1.36. استخدم الاثنين: VAP للثوابت البسيطة المدمجة (بلا webhook، وبلا تدوير شهادات، وبلا نمط فشل يجعل webhook متوقفًا يحجب كل عمليات الكتابة على الـ API)، وKyverno لأي شيء يتطلب بحثًا عبر الكائنات، أو التحقق من الصور، أو التوليد.
⚠ استثنِ
kube-systemوnamespace الخاص بـ Kyverno من نطاق الـ webhook، واخترfailurePolicyعن قصد. فـ webhook بـfailurePolicy: Failلا يستطيع الوصول إلى pods الخاصة به يُعطّل العنقود بطريقة يصعب إصلاحها بعد ذلك.
6. مسار CI/CD#
graph LR
A["Commit / PR"] --> B["Static analysis<br/>PHPStan · Pint<br/><b>Deptrac — module<br/>boundaries</b>"]
B --> C["Tests<br/>unit · feature<br/><b>cross-tenant suite</b>"]
C --> D["composer audit --locked"]
D --> E["Build image<br/>multi-stage, digest-pinned"]
E --> F["Trivy scan<br/>fail on HIGH/CRITICAL"]
F --> G["SBOM<br/>CycloneDX + SPDX"]
G --> H["Cosign sign<br/>+ attest"]
H --> I["Push to registry"]
I --> J["Deploy staging"]
J --> K["Smoke tests"]
K --> L["Deploy production<br/><i>manual gate</i>"]
L --> M["Kyverno verifies<br/>signature + identity"]
style B fill:#2d8659,stroke:#1c5638,color:#fff
style C fill:#2d8659,stroke:#1c5638,color:#fff
style F fill:#c94a4a,stroke:#8b2f2f,color:#fff
style M fill:#c94a4a,stroke:#8b2f2f,color:#fff
6.1 بوابتان تحملان ثقلًا غير معتاد هنا#
Deptrac (حدود الوحدات). يشترط QAS-MOD-03 أن يبقى استخراج المرحلة الثانية رخيصًا، وهو ما يستلزم حدودًا لا تتآكل. والحد الذي لا تفحصه الآلة مجرد تعليق. وDeptrac يُفشل البناء عند مخالفات العبور بين الوحدات — لا مفاتيح أجنبية عابرة للوحدات، ولا اجتياز ORM عابر للوحدات، ولا تجاوز لعقد منشور.
⚠ انتقال الحزمة:
qossmic/deptracمهجور؛ والحزمة المصانة هيdeptrac/deptrac(v4.6.2، 2026-07-01).
حزمة اختبارات العبور بين المستأجرين. يشترط QAS-SEC-01 أن يرفض كل endpoint مقيَّد بمستأجر الوصولَ بمعرّفات مستأجر آخر. تُنشئ الحزمة مستأجرَين برسوم كائنات كاملة، وتصادق كـ A، وتطلب كل مسار بمعرّفات B، وتتحقق من 403/404 ومن ألا يحتوي أي جسم استجابة على بيانات B. وأي endpoint جديد بلا تغطية يُفشل البناء. وهذا هو الضابط الذي يصمد فعلًا مع الزمن.
6.2 توقيع الصور#
التوقيع بلا مفاتيح (keyless) عبر Fulcio صار الآن الوضع الافتراضي في Cosign، مع تسجيل التواقيع في سجل الشفافية Rekor؛ وRekor v2 وصل إلى GA. ويعمل التوقيع بلا مفاتيح بسلاسة عند البناء في GitHub Actions أو GitLab CI — إذ يكون رمز OIDC الخاص بالـ CI هو الهوية ولا يوجد مفتاح يمكن تسريبه.
تحقّق من الهوية، لا من مجرد وجود توقيع. ينبغي أن يتحقق verifyImages في Kyverno من المستودع وسير العمل المحددين المسموح لهما بالتوقيع. فالتحقق من أن الصورة «موقّعة من أحدهم» يكاد يكون بلا قيمة.
التوقيع بلا مفاتيح يتطلب الوصول إلى Fulcio وRekor عند التوقيع وعند التحقق معًا. وإن كان الخروج من داخل المملكة مقيّدًا، فإما أن تسمح بهذين الطرفين في قائمة السماح، أو أن تستخدم التوقيع القائم على مفاتيح KMS.
6.3 ترحيلات قاعدة البيانات — فخ Laravel على Kubernetes#
لا تشغّل الترحيلات (migrations) في initContainer أبدًا. فكل pod يشغّل الـ init container الخاص به، ومن ثم فإن إطلاق 3 نسخ يشغّل الترحيلات ثلاث مرات بالتوازي — وهو تسابق يفسد حالة المخطط.
النهج الصحيح:
- تُشغَّل الترحيلات بوصفها
Jobفي Kubernetes، مرة واحدة لكل إصدار، قبل الإطلاق - جدول الترحيلات في Laravel يوفّر قفلًا استشاريًا، لكن يجب مع ذلك تقييد الـ Job بتنفيذ واحد
- يجب أن تكون الترحيلات متوافقة رجعيًا، لأن النشر التدريجي يشغّل الكود القديم والجديد على المخطط نفسه في الوقت ذاته
- تستخدم التغييرات المدمِّرة نمط التوسيع/الانكماش (expand/contract): أضف العمود الجديد ← انشر كودًا يكتب في الاثنين ← املأ البيانات القديمة ← انشر كودًا يقرأ من الجديد ← احذف القديم في إصدار لاحق. فالعمود المحذوف في الإصدار نفسه الذي توقف فيه الكود عن استخدامه سيكسر كل pod قديم لا يزال يخدم الحركة.
6.4 استراتيجية النشر#
تحديثات تدريجية بـ maxSurge: 1, maxUnavailable: 0 للـ API. فحوص الجاهزية تتحكم بمرور الحركة؛ وفحوص الحياة (liveness) تعيد تشغيل الـ pods العالقة فعلًا. ويجب أن يكون الـ rollback أمرًا واحدًا يكتمل في أقل من 10 دقائق (QAS-OPS-01) — وهذا إجراء مُختبَر، لا أمنية.
7. قابلية المراقبة (observability)#
يشترط QAS-OPS-01 أن يتمكن مهندس مناوبة واحد من تشخيص حادث في الساعة 02:00 دون معرفة قبلية متوارثة. وKubernetes يرفع الحد الأدنى التشغيلي، فهذا الاستثمار إلزامي لا اختياري — إنه ثمن قرار CON-15.
| الإشارة | النهج |
|---|---|
| معرّف الارتباط (correlation ID) | يُولَّد عند الـ ingress؛ ويُمرَّر عبر API ← المهام المُدرجة في الطوابير ← الـ sidecars ← الاستدعاءات الخارجية ← كل سطر سجل. وبدونه، فإن تصحيح تدفق رفع←فحص←مراجعة←اعتماد←إشعار يعني مطابقة الطوابع الزمنية عبر أربعة مكوّنات يدويًا. |
| التتبعات (Traces) | OpenTelemetry، بعيّنات. وتُتتبَّع تدفقات الدفع والفيديو بنسبة 100 %. |
| المقاييس | RED للـ API؛ وعمق الطابور وعمر أقدم مهمة للـ workers؛ وعدد الجلسات للوسائط. |
| السجلات | JSON مهيكل إلى stdout، وتُشحن خارج العنقود. تُحجب البيانات الشخصية عند طبقة التسجيل، لا بانضباط المطوّرين — لا تسجّل أبدًا أرقام الهوية الوطنية أو رموز OTP أو الرموز (tokens) أو محتويات المستندات. |
| التنبيهات | على SLOs التي يشعر بها المستخدم، لا على رسوم استهلاك المعالج. زمن الكشف < 5 دقائق. |
تسجيل التدقيق في Kubernetes يستحق ذكرًا صريحًا لأنه لا يمكن إضافته بأثر رجعي بعد وقوع الحادث — فلا يمكنك تسجيل تدقيق الماضي. الحد الأدنى للسياسة: RequestResponse للأسرار وconfigmaps وكائنات RBAC وpods/exec؛ وMetadata لكل ما عداها؛ مع إسقاط get/list/watch على الـ endpoints والـ leases والأحداث، وإلا لشكّلت 90 % من الحجم. اشحنها خارج العنقود فورًا — فسجلات التدقيق المخزَّنة على عنقود مخترَق فقط لا قيمة لها.
8. ⚠ قيد معماري: تسلسل ZATCA#
إذا انطبقت الفوترة الإلكترونية لـ ZATCA (OQ-11، غير مؤكد)، فإنها تفرض قيدًا يتعارض مباشرة مع التوسّع الأفقي ويجب التصميم له بدلًا من اكتشافه.
تشترط المرحلة الثانية من ZATCA تسلسل تجزئة الفواتير (invoice hash chaining) — كل فاتورة تتضمن تجزئة الفاتورة السابقة — إضافة إلى عدّاد تصاعدي (monotonic counter). وهذا ذو حالة، ومرتَّب بصرامة، ولا يمكن إنتاجه بالتوازي من عدة pods. والتنفيذ الساذج عبر 3 نسخ API أو أكثر يُنتج سلسلة مكسورة وفواتير مرفوضة.
التصميم المطلوب: نقطة تسلسل واحدة لكل متتالية فواتير — طابور مخصّص بمستهلك واحد مع قفل موزّع، لا مسار كود متوسّع أفقيًا. أثِر هذا الآن؛ فإقحام الترتيب لاحقًا في تصميم متزامن مكلف.
البناء مقابل الشراء: يوجد مزوّدو حلول ZATCA معتمدون. وبالنسبة لفريق صغير، فإن التكامل مع مزوّد شبه مؤكد أنه أصح من تنفيذ ختم ECDSA وonboarding الـ CSID وتوقيع XAdES داخليًا.
9. خارطة طريق التحصين المرحلية#
مرتَّبة حسب خفض المخاطر لكل ساعة، لا حسب ما يثير الاهتمام.
الأسبوع 1 — أعلى نسبة عائد، وأقل خطر كسر#
- التحقق من
APP_DEBUG=false/APP_ENV=productionفي الإنتاج - إخراج الأسرار النصية الصريحة من Git؛ و
gitleaksفي CI؛ وتدوير أي سر سبق إيداعه automountServiceAccountToken: falseعلى ServiceAccount الـdefaultفي كل namespace- تشفير etcd أثناء السكون
composer audit --locked+--no-dev، مع إفشال البناء- Trivy في CI (أسبوع واحد بـ
--exit-code 0لقياس حجم المتراكم، ثم الإنفاذ) - PSA بـ
warn+audit: restricted— اجمع قائمة ما سيُكسر، ولا تنفّذ شيئًا بعد - تدقيق RBAC بحثًا عن ارتباطات
cluster-adminوعن الـ wildcards - التأكد من أن API server غير قابل للوصول علنًا
الشهر 1 — الضوابط البنيوية#
- إنفاذ PSA
restricted— أعمال nginx/PHP-FPM من §5.1 readOnlyRootFilesystem— أعمال Laravel من §1.3- نقل الملفات المرفوعة والشهادات إلى التخزين الكائني مع SSE وسياسة دورة حياة
- NetworkPolicy بالرفض الافتراضي مع استثناء DNS، namespace واحدًا في كل مرة
- ServiceAccounts لكل عبء عمل مع Roles مقيَّدة بالـ namespace وبأقل الصلاحيات
- شحن سجلات تدقيق Kubernetes خارج العنقود
- Kyverno في وضع Audit
- صور أساسية مثبّتة بالـ digest + Renovate؛ و
allow-pluginsصريح - kube-bench (ملف CIS v1.12)؛ ووثّق الاستثناءات المقبولة
- احسم معمارية الأسرار — لا تنجرف على Sealed Secrets إلى ما لا نهاية
الربع 1 — العمق#
- نقل Kyverno إلى Enforce (باستثناء
kube-systemوnamespace الخاص به)؛ وأضف سياسات VAP - توقيع Cosign + التحقق من الهوية عبر Kyverno
- توليد SBOM، مع الإثبات (attestation) والاحتفاظ
- الانتقال إلى الخلفية المختارة للأسرار؛ واختبر عملية تدوير قبل أن تحتاج إليها
- التحكم بالخروج عبر FQDN نحو مزوّدي الدفع والفيديو
- اتحاد هوية أعباء العمل، أو مصادقة Kubernetes عبر OpenBao
- إعادة الفحص من جهة السجل (registry) كي تظهر الثغرات الجديدة في الصور المنشورة
- تحصين kubelet؛ وحجب pod ← :10250
- أمن وقت التشغيل فقط إن وُجد من سيفرز تنبيهاته — انظر أدناه
موقف مقصود من أمن وقت التشغيل (Falco/Tetragon): هذا هو المكان الذي تموت فيه برامج التحصين عادةً. والفشل المتوقع هو نشر Falco بالقواعد الافتراضية، وتلقّي مئات التنبيهات يوميًا من سلوك حاويات طبيعي، ثم كتم الـ DaemonSet أو إزالته خلال شهر — فيبقى أمن وقت التشغيل على المخطط المعماري ومعدومًا في الواقع، وهو أسوأ من غيابه لأنه يخلق طمأنينة كاذبة.
وإن تُبنِّي: ابدأ بـ ست قواعد عالية الإشارة، لا بالقواعد الافتراضية. وبالنسبة لعبء عمل PHP، فأعلى القواعد قيمة هي «تشغيل shell داخل حاوية» — ففي pod يعمل بـ PHP-FPM يكاد يكون ذلك مؤكدًا أنه RCE عبر webshell. أسبوعان في وضع التدقيق فقط، موجَّهان إلى قناة لها مالك مُسمّى. وإن لم يكن هناك من سيفرز التنبيهات، فاصرف الجهد على البنود 10–19 بدلًا من ذلك. وذلك ترتيب أولويات صحيح، لا تهرّب.
10. البنود المفتوحة#
| ID | البند | الأثر |
|---|---|---|
| ADR-0001 gap 1 | توفر Redis المُدار / السجل (registry) / الأسرار من OCI في me-riyadh-1 غير مُتحقَّق منه |
يحدد معمارية الأسرار وما إذا كان Redis ذاتي الإدارة |
| ADR-0005 | الفيديو ذاتي الاستضافة مقابل المُدار | يحدد ما إذا كان qwizin-video وhostNetwork وmedia node pool وTURN موجودة أصلًا |
OQ-11 |
انطباق ZATCA | قيد التسلسل في §8 |
| اختيار CNI | Cilium مقابل Calico مقابل الخيار الافتراضي للمزوّد | يحدد ما إذا كانت سياسة الخروج بـ FQDN متاحة (§5.2) |
OQ-08 |
تسجيل الاستشارات | يهيمن على سعة الفيديو وتكلفته (ADR-0005) |