نادراً ما تسير كتب تشغيل 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/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. تُنتج هذه الأنماط معاً كتب تشغيل محددة ومُستعيدة لنفسها — شرط أساسي للوثوق بالأتمتة على نطاق الإنتاج.
نستخدم ملفات تعريف الارتباط لتشغيل هذا الموقع وتحليل الزيارات وعرض إعلانات مخصّصة. يمكنك قبول كل ملفات تعريف الارتباط أو رفض غير الأساسية منها.
سياسة الخصوصية