ثقافة DevOps وأساسياتها

تطبيق الاثني عشر عاملاً

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

تطبيق الاثني عشر عاملاً

في عام 2012، صاغ مهندسو Heroku تجارب سنوات من تشغيل تطبيقات SaaS واسعة النطاق في وثيقة مرجعية: تطبيق الاثني عشر عاملاً. تُعرّف هذه المنهجية اثنتي عشرة ممارسة تُنتج معًا برمجيات قابلة للنقل والتوسع، ومتوقعة السلوك في بيئة الإنتاج. كل عامل يُجيب في جوهره على سؤال واحد: كيف نجعل هذه الخدمة آمنةً للتشغيل وسهلةَ الفهم في الساعة الثالثة صباحًا أثناء حادثة طارئة؟

يتناول هذا الدرس كل عامل بشكل تفصيلي، ويشرح المنطق التشغيلي وراءه، ويوضح كيف تبدو المطابقة وعدم المطابقة في الأنظمة الحقيقية. مقاييس DORA من الدرس الخامس تقيس النتائج؛ أما الاثنا عشر عاملاً فهم الانضباط الهندسي الذي يجعل تحقيق تلك النتائج ممكنًا.

العامل الأول — قاعدة الشيفرة: مستودع واحد، عمليات نشر متعددة

يُتتبَّع تطبيق الاثني عشر عاملاً في مستودع واحد للتحكم في الإصدارات. تُنشر قاعدة الشيفرة الواحدة هذه في بيئات متعددة — development وstaging وproduction — من نفس الـ commit. وجود قواعد شيفرة متعددة يعني نظامًا موزعًا لا تطبيقًا واحدًا. المكتبات المشتركة تنتمي إلى مدير الحزم، لا إلى كل خدمة على حدة.

نمط الفشل الشائع: الفرق التي تحتفظ بـ"فرع إنتاج" منفصل مع إصلاحات عاجلة لا تُدمج أبدًا تتراكم لديها فجوات. في غضون أسابيع، لا أحد يعرف ما يعمل فعلًا في الإنتاج.

العامل الثاني — التبعيات: أعلن عنها صراحةً وعزلها

لا تعتمد قط على الحزم المثبتة على مستوى النظام. أعلن عن جميع التبعيات في ملف بيان (package.json، requirements.txt، go.mod، Gemfile) واستخدم آلية عزل (venv، node_modules، Go modules، Bundler) حتى يُنتج استنساخ المستودع الجديد مع أمر تثبيت واحد بيئةً كاملةً قابلةً للتشغيل.

# Python — العزل الصريح python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt # تثبيت كل تبعية متعدية pip freeze > requirements.txt # Node npm ci # يستخدم package-lock.json بدقة — لا تستخدم npm install في CI
لا تستخدم أبدًا سكريبتات التثبيت من نوع curl | bash لأدوات النظام داخل وقت تشغيل تطبيقك. إذا كان Dockerfile الخاص بك يُشغّل apt-get install curl ثم يُنزّل مثبتًا، فلديك تبعية ضمنية غير مُعدَّلة الإصدار ستتعطل بصمت عند تغيير المثبت المصدر.

العامل الثالث — الإعدادات: خزنها في البيئة

كل ما يتغير بين عمليات النشر — عناوين URL لقواعد البيانات ومفاتيح API وأعلام الميزات ومستويات السجلات — يجب أن يعيش في متغيرات البيئة، لا في الشيفرة أو ملفات الإعداد المُودَعة في المستودع. اختبار المعيار: هل يمكنك الآن فتح الشيفرة المصدرية للعالم دون كشف أي سر؟ إذا كانت الإجابة لا، فقد تسرّبت الإعدادات إلى الشيفرة.

# ملف .env (محلي فقط، لا يُودَع في المستودع أبدًا) DATABASE_URL=postgres://user:pass@localhost/myapp REDIS_URL=redis://localhost:6379 LOG_LEVEL=debug STRIPE_SECRET_KEY=sk_test_... # في نشر Kubernetes — الأسرار تُحقن كمتغيرات بيئة apiVersion: v1 kind: Secret metadata: name: app-secrets type: Opaque stringData: DATABASE_URL: "postgres://user:pass@prod-db:5432/myapp" --- apiVersion: apps/v1 kind: Deployment spec: template: spec: containers: - name: api envFrom: - secretRef: name: app-secrets
صنّف متغيرات الإعداد حسب اختلافات النشر، لا حسب الخدمة. علامة جيدة: يمكنك تشغيل نفس صورة Docker في الاختبار مقابل الإنتاج بتغيير متغيرات البيئة فقط. لا إعادة بناء للصور، لا تبديل لملفات الإعداد.

العامل الرابع — الخدمات المساندة: تعامل معها كموارد مرفقة

قواعد البيانات والطوابير والذواكر المؤقتة وخوادم البريد — تعامل مع الجميع كـموارد مرفقة يُصل إليها عبر URL في البيئة. MySQL المحلية وRDS المستضاف متبادلان من منظور التطبيق. التبديل من Redis محلي إلى ElastiCache لا يتطلب سوى تغيير متغير بيئة، لا تغيير شيفرة.

العامل الخامس — البناء والإصدار والتشغيل: فصل صارم

حوّل قاعدة الشيفرة إلى نشر جارٍ عبر ثلاث مراحل منفصلة وغير قابلة للعكس:

  • البناء (Build): الترجمة وجلب التبعيات وإنتاج نتيجة (صورة Docker، JAR، ثنائي).
  • الإصدار (Release): دمج نتيجة البناء مع إعدادات النشر. كل إصدار يحصل على معرف ثابت لا يتغير.
  • التشغيل (Run): تشغيل العمليات من الإصدار في بيئة التنفيذ.

الشيفرة لا تتغير في وقت التشغيل أبدًا. نمط "النشر ثم تعديل الإعداد على الخادم" ينتهك هذا العامل ويجعل الرجوع إلى إصدار سابق أمرًا يستعصي على الفهم.

Build → Release → Run pipeline Build Compile + image Release Image + config (immutable ID) Run Processes start (from release) ↑ إعداد النشر يُحقن هنا
المراحل الثلاث غير القابلة للعكس — الشيفرة تتجمد عند Build؛ الإعداد يُحقن عند Release؛ العمليات تُشغَّل من Run.

العامل السادس — العمليات: نفّذها كعمليات عديمة الحالة لا تتشارك شيئًا

عمليات التطبيق عديمة الحالة ولا تتشارك شيئًا. أي بيانات يجب الاحتفاظ بها تعيش في خدمة مساندة — قاعدة البيانات أو ذاكرة التخزين المؤقت. حالة الجلسة في الذاكرة والرفع إلى نظام الملفات المحلي والذاكرات المؤقتة داخل العملية كلها تنتهك هذا العامل. عندما يوجّه موازن الحمل الطلب التالي إلى نسخة عملية مختلفة، تُفقد كل الحالة اللزجة.

التطبيق الفعلي: الجلسات اللاصقة نمط مضاد. الملفات المرفوعة إلى /tmp على pod واحد لن تكون مرئية لـpod آخر. انقل الجلسات إلى Redis أو DynamoDB والرفوعات إلى تخزين الكائنات (S3، GCS) من اليوم الأول.

العامل السابع — ربط المنافذ: صدّر الخدمات عبر ربط المنافذ

التطبيق مكتفٍ بذاته ويُصدّر HTTP (أو أي بروتوكول) بربط منفذ. لا يعتمد على حقن وقت تشغيل (مثل Apache mod_php). تطبيق Node يُشغّل خادم HTTP الخاص به؛ تطبيق Python يُشغّل Gunicorn داخليًا. هذا يجعل التطبيق قابلًا للتركيب بسهولة: يصبح خدمةً مساندةً لأي تطبيق آخر.

العامل الثامن — التزامن: وسّع أفقيًا عبر نموذج العمليات

وسّع التطبيق أفقيًا بتشغيل المزيد من العمليات، لا بضخ الموارد في عملية مونوليثية واحدة. قسّم العمل إلى أنواع عمليات مُسمّاة — web (يخدم HTTP)، worker (يعالج المهام المُدرجة)، clock (المهام المجدولة). Kubernetes Deployment replicas تُطبّق هذا العامل مباشرةً. كل نوع عملية يتوسع بشكل مستقل.

العامل التاسع — التخلص السريع: بدء سريع وإيقاف آمن

يجب أن تبدأ العمليات في ثوانٍ وتتوقف بأمان عند استقبال SIGTERM. تُنهي عملية الويب طلبها الحالي ثم تخرج. تُعيد عملية العامل مهمتها الحالية إلى الطابور قبل الخروج. هذا يُمكّن التوسع السريع والنشر والتعافي من الأعطال.

# Node.js — معالج SIGTERM الآمن const server = app.listen(PORT); process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); server.close(() => { console.log('HTTP server closed'); process.exit(0); }); }); # Kubernetes يمنح الـpods 30 ثانية افتراضيًا قبل SIGKILL # عدّل بـ terminationGracePeriodSeconds: 60

العامل العاشر — تماثل البيئات: ابقِ البيئات متشابهة قدر الإمكان

الفجوة بين بيئة التطوير والإنتاج هي السبب الجذري لأخطاء "يعمل على جهازي". ثلاثة أبعاد للتماثل: الوقت (انشر بكثرة لتقليص التأخر)، الأفراد (المطورون الذين يكتبون الشيفرة ينشرونها ويراقبونها)، الأدوات (نفس محرك قاعدة البيانات، نفس الذاكرة المؤقتة، نفس الطابور في التطوير والإنتاج). استخدام SQLite في التطوير وPostgreSQL في الإنتاج انتهاك كلاسيكي — فروق طفيفة في لهجة SQL تُسبب أخطاءً لا تظهر إلا في الإنتاج.

Docker Compose يجعل تماثل الأدوات أمرًا سهلًا. شغّل نفس صور Postgres وRedis وKafka محليًا التي تستخدمها في الإنتاج. لا يوجد سبب وجيه لاستخدام محرك خدمة مساندة مختلف بين البيئات.

العامل الحادي عشر — السجلات: تعامل معها كتدفقات أحداث

لا يهتم التطبيق بتوجيه إخراجه أو تخزينه. يكتب ببساطة إلى stdout، دون تخزين مؤقت. تلتقط بيئة التنفيذ هذا التدفق وتوجهه إلى وجهته — مجمعات السجلات (Datadog، Splunk، Loki) أو التخزين طويل الأمد أو الطرفية أثناء التطوير. لا تفتح ملفات السجلات مباشرةً، ولا تدير تدوير السجلات داخل التطبيق.

العامل الثاني عشر — مهام الإدارة: شغّلها كعمليات منفردة

ترحيلات قواعد البيانات وإصلاحات البيانات الفردية وفحوصات وحدة التحكم — شغّلها كعمليات منفردة في نفس بيئة عمليات التطبيق العادية، مستخدمًا نفس نتيجة البناء وإعداداتها. يجب أن تكون مُودَعة في المستودع، لا منفَّذة كجلسات SSH عشوائية.

# Kubernetes — تشغيل مهمة ترحيل منفردة kubectl run migrations \ --image=myapp:v1.42 \ --restart=Never \ --env-from=secret/app-secrets \ -- python manage.py migrate # نفس الصورة، نفس البيئة، نفس الإعداد — الأسلوب الصحيح # لا تقم بـ ssh داخل pod وتُنفّذ أوامر يدويًا

تطبيق الاثني عشر عاملاً عمليًا: قائمة تشخيص

عند التعامل مع خدمة جديدة، يمر المهندسون في الشركات الكبرى على هذه الأسئلة بسرعة:

  1. هل يمكنني استنساخ التطبيق وتشغيله بدون خطوات يدوية سوى docker compose up؟ (I، II)
  2. هل يمكنني النشر في الاختبار والإنتاج بتغيير متغير بيئة فقط؟ (III، IV)
  3. هل كل إصدار ثابت ومُوسوم؟ هل يمكنني الرجوع للخلف بأمر واحد؟ (V)
  4. هل سينجو التطبيق من موت أي عملية وإعادة تشغيلها على مضيف مختلف؟ (VI، IX)
  5. هل تذهب السجلات إلى stdout وتلتقطها المنصة؟ (XI)
  6. هل مهام الإدارة سكريبتات قابلة للتكرار في التحكم بالإصدارات؟ (XII)
الامتثال لمبادئ الاثني عشر عاملاً ليس ثنائيًا. تعامل معه كمقياس صحة متدرج. يجب أن تحصل الخدمة الجديدة على 12/12 منذ اليوم الأول — إعادة هيكلة الشيفرة القديمة المقترنة بالإعداد والحالة مشروع مكلف يمتد لأشهر. مقاييس DORA ستُظهر العبء: تكرار النشر الأقل ومعدل فشل التغيير الأعلى يرتبطان ارتباطًا وثيقًا بانتهاكات هذه المبادئ.