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

الشروط والحلقات ومعالجة الأخطاء

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

الشروط والحلقات ومعالجة الأخطاء

نادراً ما تسير كتب تشغيل Ansible في الإنتاج بخطوات مستقيمة فقط. البنية التحتية الحقيقية متغايرة في عائلات أنظمة التشغيل، ولديها علامات ميزات اختيارية، واستدعاءات خارجية قابلة لإعادة المحاولة، وسيناريوهات فشل جزئي حيث قد يكون إلغاء تشغيل كامل أكثر ضرراً من التعافي بلطف. يغطي هذا الدرس الآليات الأربع التي تمنح كتب تشغيل Ansible قوتها التعبيرية: when للتفريع، وloop للتكرار، وblock/rescue/always لمعالجة الاستثناءات المنظمة، وfailed_when/changed_when لتجاوز منطق نجاح Ansible الافتراضي والكشف عن التغييرات.

الشروط باستخدام when

يقبل توجيه when تعبير Jinja2 يُقيَّم إلى قيمة منطقية. عندما يكون التعبير خاطئاً، يتخطى Ansible المهمة ويُبلّغ عنها كـ مُتخطَّاة — لا فاشلة ولا متغيرة. هذا مختلف عن مهمة تُشغَّل ولا تفعل شيئاً؛ المهمة المُتخطَّاة لم تُنفَّذ على الإطلاق.

الأنماط الشائعة لـ when في كتب تشغيل الإنتاج:

  • التفريع حسب عائلة نظام التشغيل — استخدم حقائق ansible_os_family أو ansible_distribution لتحديد مدير الحزم الصحيح أو اسم الخدمة. هذا الاستخدام الأكثر شيوعاً لـ when في أتمتة الأسطول.
  • اختبارات المتغيرات المُسجَّلة — شغّل مهمة وسجّل مخرجاتها، ثم تصرف شرطياً بناءً على ما إذا وُجد شيء ما أو نُصِّب أو أعاد كوداً للخروج معيناً.
  • صدق المتغير — اجعل كتل الإعدادات الكاملة مشروطة بمتغير منطقي (enable_tls: true) يمرره المشغّلون وقت التشغيل عبر -e أو متغيرات مجموعة المخزون.
  • تركيب الشروط — يقبل when قائمة يربطها Ansible بـ AND؛ لمنطق OR استخدم or داخل Jinja2 مباشرة.
# --- أمثلة على الشروط --- # 1. التفريع حسب عائلة نظام التشغيل — تثبيت nginx بمدير الحزم الصحيح - name: Install nginx (Debian/Ubuntu) ansible.builtin.apt: name: nginx state: present when: ansible_os_family == "Debian" - name: Install nginx (RHEL/CentOS/Amazon Linux) ansible.builtin.dnf: name: nginx state: present when: ansible_os_family == "RedHat" # 2. تسجيل + شرط — أعد التحميل فقط إذا تغيّر الإعداد - name: Validate nginx config ansible.builtin.command: nginx -t register: nginx_test changed_when: false # التحقق لا يُغيّر أي شيء أبداً - name: Reload nginx only if validation passed and config was changed ansible.builtin.service: name: nginx state: reloaded when: - nginx_test.rc == 0 - nginx_config.changed # nginx_config مُسجَّل من مهمة قالب # 3. صدق المتغير — اشترط تهيئة TLS - name: Deploy TLS certificates ansible.builtin.copy: src: "certs/{{ inventory_hostname }}.pem" dest: /etc/nginx/ssl/ mode: '0640' when: enable_tls | bool # 4. شروط مركّبة مع OR - name: Restart service on change or first run ansible.builtin.service: name: myapp state: restarted when: myapp_config.changed or myapp_binary.changed
نصيحة مرشّح Jinja2: طبّق دائماً مرشّح | bool عند اختبار متغير قد يكون السلسلة "true" أو "false" (شائع عندما تأتي القيم من متغيرات البيئة أو ملفات YAML المحمّلة بـ include_vars). بدون المرشّح، السلسلة "false" تصادقية في Python وسيمر الشرط بشكل غير متوقع.

التكرار باستخدام loop

توجيه التكرار الحديث هو loop، الذي حلّ محل عائلة with_items / with_dict / with_fileglob الأقدم (لا تزال تعمل لكنها مُهمَلة). يقبل loop أي قائمة — من القيم البسيطة، أو القواميس، أو مخرجات إضافة البحث. داخل جسم المهمة، تُوصَل قيمة التكرار الحالية بـ item. عند التكرار على قواميس، صِل إلى الحقول بـ item.key وitem.value (أو أي مفتاح اعتباطي عرّفته).

الأنماط الإنتاجية لـ loop:

  • إنشاء مستخدمين متعددين — كرّر على قائمة من القواميس، كل منها يملك مفاتيح name وshell وgroups.
  • نشر ملفات إعداد متعددة من قوالب — كرّر على قائمة أسماء الخدمات، يُصيَّر إعداد مميز لكل تكرار.
  • تطبيق قواعد الجدار الناري — كرّر على قائمة من المنافذ أو كتل CIDR.
  • التحكم في مخرجات الحلقة — استخدم loop_control.label لعرض ملخص مقروء بدلاً من القاموس الكامل في مخرجات Ansible. هذا حرج لقواميس تحتوي كلمات مرور أو رموز.
# --- أمثلة على الحلقات --- # 1. إنشاء حسابات خدمة متعددة من قائمة قواميس - name: Ensure service accounts exist ansible.builtin.user: name: "{{ item.name }}" shell: "{{ item.shell | default('/bin/bash') }}" groups: "{{ item.groups | default([]) }}" append: true system: "{{ item.system | default(false) }}" state: present loop: - { name: deploy, shell: /bin/bash, groups: [docker, sudo] } - { name: monitor, shell: /usr/sbin/nologin, system: true } - { name: backup, shell: /usr/sbin/nologin, system: true } loop_control: label: "{{ item.name }}" # أظهر الاسم فقط في المخرجات # 2. فتح منافذ الجدار الناري (حلقة على قائمة مختلطة) - name: Open required ports in firewalld ansible.posix.firewalld: port: "{{ item }}/tcp" permanent: true state: enabled immediate: true loop: - 80 - 443 - 8080 # 3. نشر إعدادات لكل خدمة من قالب واحد - name: Deploy microservice configs ansible.builtin.template: src: templates/service.conf.j2 dest: "/etc/myapp/{{ item.name }}.conf" owner: deploy mode: '0644' loop: "{{ microservices }}" # microservices متغير قائمة من group_vars loop_control: label: "{{ item.name }}" notify: Reload myapp # 4. حلقة مع فهرس — مفيد حين يهم الترتيب - name: Write ordered config snippets ansible.builtin.copy: content: "{{ item.content }}" dest: "/etc/myapp/conf.d/{{ '%02d' | format(ansible_loop.index0) }}-{{ item.name }}.conf" loop: "{{ config_snippets }}" loop_control: extended: true # يُتيح ansible_loop.index0 و.first و.last
Ansible when + loop + block/rescue control flow Task Execution starts here when? evaluate condition false SKIPPED no-op true loop? iterate items single next item block / rescue / always block — run tasks rescue — on failure always — cleanup ok / changed / failed
تدفق تحكم مهمة Ansible: يحمي when التنفيذ بشرط، وloop يُكرّر، وblock/rescue/always يُهيكل معالجة الأخطاء — وكلها قابلة للتركيب على مهمة واحدة أو مجموعة مهام.

معالجة الأخطاء المنظمة باستخدام block وrescue وalways

يُماثل بناء block/rescue/always في Ansible مباشرةً try/except/finally في Python. هذه هي الأداة الصحيحة لأي موقف يجب أن يُطلق فيه فشل المهمة إجراءات تعويضية بدلاً من إيقاف التشغيل. على نطاق الشركات الكبرى، يظهر هذا النمط في كل مكان: ترحيل مخطط قاعدة البيانات الذي يحتاج للتراجع عند الفشل، نشر الخدمات التي يجب إلغاء تسجيلها من موازن التحميل قبل وبعد بغض النظر عن النتيجة، واستدعاءات API التي تحتاج لتحرير رموز التنظيف حتى لو ألغت العملية الرئيسية.

# --- مثال block / rescue / always: نشر خدمة مع التراجع التلقائي --- - name: Deploy application with automatic rollback hosts: app_servers tasks: - name: Application deployment with recovery block: # تُشغَّل المهام داخل block بشكل طبيعي؛ إذا فشلت أي منها، يُشغَّل rescue - name: Stop service for maintenance ansible.builtin.service: name: myapp state: stopped - name: Deploy new binary ansible.builtin.copy: src: "dist/myapp-{{ version }}" dest: /usr/local/bin/myapp mode: '0755' - name: Run database migrations ansible.builtin.command: /usr/local/bin/myapp migrate --yes register: migration_result - name: Start updated service ansible.builtin.service: name: myapp state: started rescue: # يُشغَّل فقط إذا فشلت مهمة في block - name: Log the failure for incident tracking ansible.builtin.uri: url: "{{ ops_webhook_url }}" method: POST body_format: json body: event: deploy_failed host: "{{ inventory_hostname }}" version: "{{ version }}" error: "{{ ansible_failed_result.msg | default('unknown') }}" delegate_to: localhost - name: Restore previous binary ansible.builtin.copy: src: /usr/local/bin/myapp.prev dest: /usr/local/bin/myapp remote_src: true mode: '0755' - name: Start service on previous version ansible.builtin.service: name: myapp state: started always: # يُشغَّل في كل الأحوال، نجاح أو فشل — مثالي للتنظيف - name: Re-enable health checks in load balancer ansible.builtin.uri: url: "{{ lb_api }}/hosts/{{ inventory_hostname }}/enable" method: PUT headers: Authorization: "Bearer {{ lb_token }}" delegate_to: localhost - name: Record deployment attempt in audit log ansible.builtin.lineinfile: path: /var/log/deployments.log line: "{{ lookup('pipe', 'date -Iseconds') }} host={{ inventory_hostname }} version={{ version }} result={{ ansible_failed_result is defined | ternary('FAILED', 'OK') }}" delegate_to: localhost
ممارسة احترافية — ansible_failed_result: داخل كتلة rescue، يضبط Ansible تلقائياً المتغير السحري ansible_failed_result على كائن نتيجة المهمة التي فشلت. سجّل هذا دائماً في نظام التنبيه أو متتبع الحوادث حتى يمتلك مهندسو الاستجابة الخطأ الدقيق دون الحاجة لـ SSH إلى الخوادم. هذا المصدر الأساسي لبيانات الفشل المنظمة في البنية التحتية التي يُديرها Ansible.

تجاوز كشف النجاح: failed_when وchanged_when

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

failed_when — تجاوز شرط الفشل. حالات الاستخدام الشائعة:

  • أداة CLI تخرج بكود غير صفري عند "غير موجود" (كود 1) لكن هذه حالة صالحة وليست خطأ.
  • سكريبت يطبع "ERROR" في المخرج القياسي لكنه يخرج بـ 0 (ينجح دائماً زيفاً).
  • أمر فحص يجب أن يفشل فقط إذا احتوت المخرجات نمطاً معيناً.

changed_when — تجاوز شرط التغيير. حالات الاستخدام الشائعة:

  • سكريبتات مثالية الحالة تطبع "already up to date" عندما لا يتغير شيء.
  • أوامر التحقق أو الفحص التي لا تُعدّل الحالة أبداً (اضبطه على false).
  • سكريبتات تطبع "Applied N changes" — حلّل N من المخرجات لضبط التغيير بدقة.
# --- أمثلة على failed_when و changed_when --- # 1. فحص الخدمة: الخروج بكود 3 ("غير نشطة") مقبول لأغراضنا - name: Check if legacy cron job is running ansible.builtin.command: systemctl is-active legacy-cron register: cron_status failed_when: cron_status.rc not in [0, 3] # 0=نشطة, 3=غير نشطة — كلاهما مقبول changed_when: false # قراءة الحالة لا تُغيّر أي شيء # 2. سكريبت يضمّن إشارة تغيير خاصة به في المخرجات - name: Run idempotent database seeder ansible.builtin.command: python3 /opt/scripts/seed_db.py register: seed_result changed_when: "'rows inserted' in seed_result.stdout" failed_when: - seed_result.rc != 0 - "'already seeded' not in seed_result.stdout" # خروج 1 + هذه الرسالة = مقبول # 3. CLI مخصص يُبلّغ عن أخطاء في stderr حتى عند النجاح - name: Run data sync script ansible.builtin.command: /usr/local/bin/sync-data --dry-run={{ dry_run | bool }} register: sync_out failed_when: - sync_out.rc != 0 - "'WARN' not in sync_out.stderr" # التحذيرات في stderr مقبولة changed_when: - not (dry_run | bool) # وضع الاختبار الجاف ليس تغييراً حقيقياً أبداً - "'Synced 0 records' not in sync_out.stdout" # 4. فحص الحزمة — "غير مُثبَّت" (rc=1) ليس خطأ هنا - name: Check if legacy package exists before removing ansible.builtin.command: rpm -q old-package-name register: rpm_check failed_when: rpm_check.rc not in [0, 1] changed_when: false - name: Remove legacy package if present ansible.builtin.dnf: name: old-package-name state: absent when: rpm_check.rc == 0
مصيدة إنتاجية — الإفراط في استخدام ignore_errors: true: اختصار شائع هو إضافة ignore_errors: true لمهمة تفشل أحياناً و"المضي قدماً". هذا خاطئ تقريباً دائماً. يبتلع الفشل الحقيقي الذي يجب أن يوقف التشغيل بصمت، ويمنع block/rescue من الإطلاق لأن المهمة تُعتبر ناجحة. استخدم failed_when لتحديد ما يعنيه الفشل بدقة لسكريبتك، وblock/rescue للإجراءات التعويضية. احتفظ بـ ignore_errors فقط للمهام الاختيارية وبأقصى جهد — وسجّل دائماً رسالة تحذير بعدها حتى يكون الفشل المتجاهل مرئياً في المخرجات.

تركيب الأربعة معاً: نمط إنتاجي متكامل

في كتب التشغيل الحقيقية تتركب هذه الآليات الأربع بشكل طبيعي. حلقة تُكرّر على الخوادم؛ when يحمي الخطوات الخاصة بنظام التشغيل داخل الحلقة؛ block/rescue يلتف حول خطوات التعديل لإمكانية التراجع؛ وfailed_when/changed_when تُوحّد دلالات الخروج للسكريبتات المخصصة. هذا النمط المُستخدَم في دليل SRE من Google المؤتمت بـ Ansible — كتاب التشغيل نفسه هو مسار التدقيق، وكل حالة خروج محددة بدلاً من الاعتماد على المشغّل البشري لمعرفة أكواد الخروج غير الصفرية المقبولة.

الخلاصة

استخدم when للتفريع على الحقائق والنتائج المُسجَّلة والمتغيرات المنطقية — طبّق دائماً | bool عندما قد يكون المصدر سلسلة نصية. استخدم loop مع loop_control.label للتكرار النظيف على قوائم القواميس دون تسريب قيم حساسة للمخرجات. التف حول مهام التعديل بـ block/rescue/always للتراجع المنظم والتنظيف المضمون، والتقط ansible_failed_result في rescue للحصول على سياق غني للحوادث. تجاوز منطق Ansible الافتراضي للنجاح/الفشل والتغيير بـ failed_when/changed_when بدلاً من كتم الأخطاء بـ ignore_errors. تُنتج هذه الأنماط معاً كتب تشغيل محددة ومُستعيدة لنفسها — شرط أساسي للوثوق بالأتمتة على نطاق الإنتاج.