إدارة الإعدادات مع Ansible

ملفات Playbook

18 دقيقة الدرس 4 من 30

ملفات Playbook

إذا كانت الأوامر المباشرة (ad-hoc) هي مفك البراغي في Ansible، فملفات Playbook هي مخطط البنية المعمارية. الـ playbook هو ملف YAML يُعلن عن ما يجب أن يكون صحيحاً على مجموعة من المضيفين — أي الحزم مثبّتة، أي الخدمات تعمل، وأي ملفات الإعداد تحتوي على أي محتوى. تقرأ Ansible الـ playbook وتدفع المضيفين نحو تلك الحالة بشكل متكرر ومرتّب. في شركات مثل Stripe وCloudflare وShopify، تُرمّز الـ playbooks عقوداً من الخبرة التشغيلية ويمكنها إحضار أسطول كامل من الأجهزة الخام إلى حالة الإعداد الكاملة في دقائق.

يغطي هذا الدرس العناصر الأربعة البنيوية التي تُشكّل كل playbook احترافية: المسرحيات (plays) والمهام (tasks) والمعالجات (handlers) وتوجيه notify الذي يربطها. أتقن هذه العناصر الأربعة ولديك النموذج الذهني لكل ما تفعله Ansible.

تشريح الـ Playbook: الـ Play

الـ playbook هو قائمة من الـ plays. كل play يُعيّن مجموعة من المضيفين إلى مجموعة من المهام ويحدد سياق التنفيذ لتلك المهام. يمكن أن يحتوي الـ playbook على play واحد أو خمسين — كل play يعمل بالتسلسل، وجميع المضيفين في play واحد يعملون بالتوازي (حتى حد forks، الافتراضي 5).

المفاتيح الأساسية لأي play:

  • name — تسمية مقروءة بشرياً؛ تظهر في المخرجات وهي أفضل توثيق لما يفعله الـ play.
  • hosts — نمط المخزون (اسم مجموعة، glob، all، أو قائمة مفصولة بفواصل).
  • become — الترقي إلى root عبر sudo للـ play بأكمله. يمكن تجاوزه لكل مهمة.
  • gather_facts — الافتراضي true؛ يجمع معلومات النظام (النظام، IP، الذاكرة) قبل تشغيل المهام. عطّله بـ false عندما لا تُستخدم المعلومات وتحتاج السرعة.
  • vars — متغيرات محدودة النطاق بالـ play (تُغطى بعمق في الدرس 5).
  • tasks — قائمة مرتّبة من الإجراءات لتنفيذها على المضيفين المُعيّنين.
  • handlers — مهام تُشغَّل فقط عند الإخطار بها (تُغطى أدناه).
playbook واحد، plays متعددة: يمكن لملف YAML واحد أن يحتوي على plays تستهدف مجموعات مضيفين مختلفة بالتسلسل. على سبيل المثال: الـ play 1 يُعدّ خوادم قواعد البيانات، الـ play 2 يُعدّ خوادم التطبيقات (التي قد تعتمد على جاهزية قواعد البيانات). يتيح لك هذا تنسيق عمليات النشر متعددة الطبقات في ملف واحد باستدعاء ansible-playbook واحد.

المهام: وحدة العمل

المهمة هي استدعاء واحد لوحدة Ansible. كل مهمة لها name (مطلوب في الإنتاج — لا تتخطاه أبداً)، ومفتاح الوحدة، وحجج الوحدة. تعمل المهام من الأعلى للأسفل داخل الـ play. إذا فشلت مهمة، تتوقف Ansible عن الـ play على ذلك المضيف بشكل افتراضي (فشل سريع لكل مضيف؛ المضيفون الآخرون في الـ play يستمرون إلا إذا ضبطت any_errors_fatal: true).

إليك playbook كاملة وعالية الجودة للإنتاج تثبّت Nginx وتضع ملف إعداد وتضمن تشغيل الخدمة وتمكينها:

--- # site.yml — إعداد خادم الويب الأساسي - name: Configure web servers hosts: webservers become: true gather_facts: true vars: nginx_worker_processes: "auto" nginx_worker_connections: 1024 tasks: - name: Ensure Nginx is installed ansible.builtin.package: name: nginx state: present - name: Deploy Nginx main config ansible.builtin.template: src: templates/nginx.conf.j2 dest: /etc/nginx/nginx.conf owner: root group: root mode: "0644" validate: "nginx -t -c %s" notify: Reload Nginx - name: Ensure Nginx is started and enabled ansible.builtin.service: name: nginx state: started enabled: true handlers: - name: Reload Nginx ansible.builtin.service: name: nginx state: reloaded

شغّلها بالأوامر التالية:

# تشغيل جاف أولاً — شاهد ما سيتغيّر دون لمس المضيفين ansible-playbook site.yml --check --diff # تشغيل حقيقي على مخزون الإنتاج ansible-playbook -i inventories/production/hosts.ini site.yml # تحديد مضيف واحد لاختبار الكناري ansible-playbook -i inventories/production/hosts.ini site.yml --limit web01.prod.example.com # مخرجات مفصّلة — طباعة نتائج المهام ansible-playbook site.yml -v # نتائج الوحدات ansible-playbook site.yml -vvv # تفاصيل الاتصال و SSH
استخدم دائماً --check --diff قبل التشغيل على الإنتاج. وضع التحقق يشغّل الـ playbook دون إجراء تغييرات ويطبع ما سيتغيّر. --diff يُظهر الفرق قبل وبعد محتوى الملف. معاً هما تشغيلك الجاف. على نطاق واسع، أفرض هذا في CI: شغّل --check على كل PR، وطبّق فقط عند الدمج في main.

المعالجات و notify: التأثيرات الجانبية المُحرَّكة بالأحداث

المعالجات هي مهام تعمل في نهاية الـ play — لكن فقط إذا أخطرها على الأقل مهمة واحدة خلال تنفيذ الـ play. يحل هذا النمط مشكلة تشغيلية أساسية: تريد إعادة تشغيل خدمة فقط عندما يتغيّر إعدادها فعلاً، وليس في كل تشغيل.

الآليات:

  1. تُعلن مهمة عن notify: <اسم المعالج>. يجب أن يطابق الاسم حقل name للمعالج تماماً (حساس لحالة الأحرف).
  2. إذا أبلغت تلك المهمة عن changed (لا متخطاة، لا ok — changed)، تُعلّم Ansible المعالج المُسمّى كمعلّق.
  3. بعد اكتمال جميع المهام، تُصرف Ansible المعالجات المعلّقة مرة واحدة، بالترتيب الذي تُعلن فيه في قسم handlers (ليس بالترتيب الذي أُخطرت به).
  4. حتى لو أخطرت عشر مهام نفس المعالج، فإنه يعمل مرة واحدة فقط.
Ansible playbook execution flow: tasks then handlers Play: Configure web servers Task 1 Install Nginx Task 2 Deploy nginx.conf Task 3 Start Nginx service CHANGED notify Handler Reload Nginx (once) OK (no change) OK بعد جميع المهام: تصريف المعالجات — Reload Nginx يعمل مرة واحدة فقط لو أبلغت Task 2 بـ OK بدلاً من CHANGED، سيُتخطى المعالج بالكامل.
تعمل المهام بالتسلسل؛ تُصرف المعالجات مرة واحدة في النهاية — فقط عندما تُخطرها مهمة واحدة على الأقل بنتيجة CHANGED.

أخطار المعالجات في الإنتاج

المعالجات أنيقة لكن لها حواف حادة تعثّر المهندسين في الإنتاج:

  • لا تعمل المعالجات إذا فشل الـ play في منتصف الطريق. إذا أخطأت مهمة 3 قبل اكتمال المهام، تُتخطى المعالجات المعلّقة. استخدم meta: flush_handlers كمهمة لإجبار المعالجات على العمل في نقطة محددة في الـ play — على سبيل المثال، مباشرةً بعد نشر ملف إعداد لكن قبل بدء الخدمات التابعة.
  • اسم المعالج يجب أن يتطابق تماماً. خطأ مطبعي في notify لا يفعل شيئاً بصمت — لا ترفع Ansible خطأ لمعالج لم يُشغَّل قط. اختبر دائماً بـ --check وابحث عن NOTIFIED في المخرجات.
  • ترتيب المعالجات هو ترتيب الإعلان، وليس ترتيب الإخطار. إذا كان المعالج ب يعتمد على اكتمال المعالج أ أولاً، أعلن أ قبل ب.
  • إعادة تحميل واحدة، وليس عشراً. يمكن لعشر مهام تغيّر أجزاء الإعداد أن تُخطر نفس المعالج — يعمل مرة واحدة في النهاية. هذا هو النمط الصحيح لتجميع الإعداد من مصادر متعددة.
لا تعد تشغيل خدمة داخل كتلة مهام للتحايل على المعالجات. يكتب المهندسون الجدد على Ansible أحياناً مهمة تعيد تشغيل خدمة دون شرط — لتجنب تعلّم المعالجات. هذا يسبب توقفاً غير ضروري في كل تشغيل للـ playbook، حتى عندما لم يتغيّر شيء. المعالجات موجودة تحديداً لحل هذا: أعد التشغيل فقط عندما يتغيّر شيء فعلاً.

نمط الـ Playbook متعدد الـ Plays الكامل

تُنسّق ملفات الـ playbook الاحترافية الحقيقية طبقات متعددة. إليك النمط الأساسي لنشر مكدس تطبيق ذي طبقتين — قواعد البيانات أولاً، ثم خوادم التطبيقات:

--- # full-stack.yml - name: Configure database tier hosts: dbservers become: true gather_facts: true tasks: - name: Install PostgreSQL ansible.builtin.package: name: postgresql state: present - name: Deploy pg_hba.conf ansible.builtin.template: src: templates/pg_hba.conf.j2 dest: /etc/postgresql/15/main/pg_hba.conf owner: postgres group: postgres mode: "0640" validate: "pg_hba: %s" notify: Reload PostgreSQL - name: Ensure PostgreSQL is started and enabled ansible.builtin.service: name: postgresql state: started enabled: true handlers: - name: Reload PostgreSQL ansible.builtin.service: name: postgresql state: reloaded - name: Configure application tier hosts: appservers become: true gather_facts: true tasks: - name: Deploy application config ansible.builtin.template: src: templates/app.env.j2 dest: /opt/myapp/.env owner: myapp group: myapp mode: "0600" notify: Restart application - name: Ensure application service is running ansible.builtin.service: name: myapp state: started enabled: true handlers: - name: Restart application ansible.builtin.service: name: myapp state: restarted
استخدم FQCN (الاسم الكامل للمجموعة) لجميع الوحدات. اكتب ansible.builtin.package، وليس فقط package. تجعل FQCNs واضحاً أي مجموعة تأتي منها الوحدة، وتصمد أمام ترقيات إصدارات المجموعة دون تظليل صامت، وتُفرضها أدوات الفحص مثل ansible-lint. Google وHashiCorp وكل متجر Ansible مؤسسي يُلزم باستخدام FQCNs في قواعد كود الـ playbook المشتركة.

الـ Idempotency: العقد الذي يجب أن تحافظ عليه

كل مهمة في playbook احترافية يجب أن تكون idempotent — تشغيل الـ playbook عشر مرات ينتج نفس النتيجة كتشغيله مرة واحدة. وحدات package وservice وtemplate هي idempotent بالتصميم. مهام Shell والأوامر ليست كذلك — استخدم حراس creates أو removes أو changed_when لجعلها idempotent أو لقمع تقارير التغيير الإيجابية الزائفة:

# مهمة shell متكررة — تعمل فقط إذا لم يكن ملف المخرجات موجوداً - name: Generate TLS certificate ansible.builtin.command: cmd: openssl req -x509 -newkey rsa:4096 -keyout /etc/ssl/private/app.key -out /etc/ssl/certs/app.crt -days 365 -nodes -subj "/CN=app.internal" creates: /etc/ssl/certs/app.crt # تخطّ إذا كان هذا الملف موجوداً بالفعل # قمع حالة changed الزائفة للفحوصات للقراءة فقط - name: Check kernel parameter ansible.builtin.command: sysctl net.ipv4.ip_forward register: sysctl_result changed_when: false # هذه المهمة لا تغيّر الحالة أبداً — أبلغ عنها دائماً بـ OK

كسر الـ idempotency هو أحد أكثر أخطاء Ansible الإنتاجية شيوعاً. الـ playbook التي تقلّب الإعداد في كل تشغيل ستعيد تشغيل الخدمات باستمرار، وتولّد ضجيجاً في أحداث التغيير في CMDB الخاص بك، وتجعل من المستحيل معرفة ما إذا كان شيء ما قد تغيّر فعلاً من تقرير التشغيل. عامل كل سطر CHANGED في تشغيل الـ playbook كحدث حقيقي يتطلب تفسيراً.