SSD Nodes Learn Hosting plans →
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-23

Playbook أم Role في Ansible: متى تستخدم كلًّا منهما؟

تعرّف إلى متى يكفي Playbook مسطّح، ومتى تحتاج Role ببنيته ذات المجلدات السبعة، مع ansible-galaxy init واستدعاء الأدوار وأسبقية المتغيرات.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

الـplaybook مقابل role في Ansible: ما الفرق بينهما؟

ملف Ansible playbook هو الملف الذي تشغّله باستخدام ansible-playbook. وهو يربط مجموعة من المضيفين بالمهام التي تحتاج إلى تنفيذها عليها. أما Ansible role فهو مجلد ذو بنية ثابتة يحتوي على المهام والقوالب وhandlers والمتغيرات الافتراضية، ويستدعيه playbook باسمه. صياغة المهام داخل كل منهما متطابقة، لذا لا يتعلق الأمر بما يمكنك التعبير عنه. بل يتعلق بإمكانية إعادة الاستخدام.

ابدأ بـplaybook مسطّح. يُعد ملف site.yml واحد يحتوي على قائمة tasks: الشكل المناسب لأول عملية أتمتة تنفذها، ويظل مناسباً لفترة أطول مما يتوقع معظم الأشخاص. حوّل المهام إلى role عندما تحتاج كتلة المهام نفسها إلى التنفيذ على مجموعة ثانية من المضيفين، أو عندما يتجاوز الملف نحو 100 سطر ولا تعود قادراً على العثور على مهمة بمجرد التمرير.

إذا لم تكتب واحداً بعد، ابدأ بإنشاء playbook أول مقابل VPS واحد ثم عُد عندما يبدأ حجمه بالازدياد.

عندما يكون الـplaybook المسطّح هو الخيار الصحيح

يكون الـplaybook المسطّح مناسباً عندما تُنفَّذ المهمة مرة واحدة، أو على خادم واحد، أو عندما لا يقرأه أي شخص آخر. لا يحتاج تجهيز خادم تطبيق واحد أو تثبيت التحديثات على خادم قبل نافذة صيانة إلى شجرة مجلدات. يضيف الـrole سبعة مجلدات وطبقة من الإحالة غير المباشرة. إذا كان المستدعي الوحيد هو الـplaybook الموجود بجانبه، فلن تفيدك هذه الإحالة غير المباشرة، بل ستضطرك إلى الانتقال بين الملفات في كل مرة تريد فيها قراءة ما يُنفَّذ فعلياً.

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

ما الذي يحتويه دليل الدور فعلياً

roles/common/
  defaults/main.yml
  vars/main.yml
  tasks/main.yml
  handlers/main.yml
  templates/99-hardening.conf.j2
  files/
  meta/main.yml
  • tasks/main.yml هو نقطة الدخول. يشغّل Ansible هذا الملف عند استدعاء الدور، وجميع الأدلة الأخرى اختيارية.
  • defaults/main.yml يحتوي على المتغيرات التي يُتوقع من المستدعي تجاوزها. وهو مصدر المتغيرات ذي الأولوية الأدنى في Ansible، لذلك تتغلب عليه تقريباً أي مصادر أخرى.
  • vars/main.yml يحتوي على المتغيرات التي لا يُتوقع من المستدعي تجاوزها. وتكون أولويته أعلى من أولوية الجرد، وهذا تصريح قوي. استخدمه نادراً.
  • handlers/main.yml يحتوي على المهام التي يفعّلها notify. يعمل المعالج في نهاية play، ومرة واحدة، بغض النظر عن عدد المهام التي أبلغته.
  • files/ يحتوي على الملفات التي تنسخها وحدة copy كما هي، بينما يحتوي templates/ على قوالب Jinja2 التي تعرضها وحدة template. داخل الدور، تشير إلى كليهما باسم الملف المجرد من دون مسار، لأن Ansible يبحث أولاً في أدلة الدور نفسه.
  • meta/main.yml يعرّف تبعيات الدور والبيانات الوصفية التي يقرأها Ansible Galaxy.

هذا التخطيط ليس تفضيلاً أسلوبياً. يبحث Ansible في هذه المسارات المحددة، لذلك لا يعثر مطلقاً على قالب تضعه في roles/common/template/ (بصيغة المفرد).

إنشاء الدور المشترك باستخدام ansible-galaxy init

mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles common

يكتب ذلك الهيكل الكامل ضمن roles/common، بما في ذلك أدلة لن تستخدمها وملفات main.yml أولية لا تحتوي إلا على ---. احذف العناصر التي ستتركها فارغة. وجود vars/main.yml فارغ لا يسبب مشكلة لـAnsible، لكنه يخفي الملفات المهمة فعلياً في الدور.

املأ الآن الملفات التي تنفذ العمل. ابدأ بالقيم الافتراضية، لأنها الواجهة العامة للدور.

# roles/common/defaults/main.yml
---
common_packages:
  - ufw
  - fail2ban
  - unattended-upgrades
common_admin_group: admins
common_permit_root_login: "no"
common_password_authentication: "no"

ضع "no" و"yes" بين علامتي اقتباس. يحلل Ansible لغة YAML باستخدام PyYAML، الذي يقرأ no غير الموضوع بين علامتي اقتباس على أنه القيمة المنطقية false، ولذلك يصبح سطر الإعداد الناتج PermitRootLogin False، ويرفضه sshd. تحافظ علامات الاقتباس على القيمة كسلسلة نصية.

# roles/common/tasks/main.yml
---
- name: Install the base packages
  ansible.builtin.apt:
    name: "{{ common_packages }}"
    state: present
    update_cache: true
    cache_valid_time: 3600

- name: Create the admin group
  ansible.builtin.group:
    name: "{{ common_admin_group }}"
    state: present

- name: Install the sshd hardening drop-in
  ansible.builtin.template:
    src: 99-hardening.conf.j2
    dest: /etc/ssh/sshd_config.d/99-hardening.conf
    owner: root
    group: root
    mode: "0644"
    validate: /usr/sbin/sshd -t -f %s
  notify: Restart sshd
# roles/common/handlers/main.yml
---
- name: Restart sshd
  ansible.builtin.service:
    name: ssh
    state: restarted
# roles/common/templates/99-hardening.conf.j2
# Managed by Ansible. Local edits are overwritten on the next run.
PermitRootLogin {{ common_permit_root_login }}
PasswordAuthentication {{ common_password_authentication }}

في Debian وUbuntu، تُسمى وحدة systemd ssh، بينما تُسمى في أنظمة عائلة RHEL sshd. يفشل المعالج الذي يذكر الاسم الخطأ فقط عند حدوث تغيير فعلي في القالب، ولذلك لا تظهر المشكلة عادةً إلا بعد أسابيع.

يُعد السطر validate أهم جزء في هذه المهمة. يعرض Ansible القالب في ملف مؤقت، ويستبدل %s بمسار ذلك الملف، ثم ينفذ الأمر. لا يُستبدل الملف الهدف إلا إذا انتهى الأمر بالرمز 0. ضع توجيهاً غير صالح في القالب وشغّل المهمة مرة أخرى: تفشل المهمة مع failed to validate، ويبقى /etc/ssh/sshd_config.d/99-hardening.conf الفعلي دون تغيير، ويظل بإمكانك تسجيل الدخول إلى الخادم. انتبه إلى أن الفحص يختبر أكثر من صحة بناء الجملة. إذا تعذر على sshd -t قراءة مفاتيح المضيف، فسينتهي بالرمز sshd: no hostkeys available -- exiting.، وسيبلغ Ansible عن failed to validate نفسه. لذلك اقرأ msg الخاص بالوحدة قبل أن تلقي اللوم على القالب.

كيفية استدعاء دور من playbook

# site.yml
---
- name: Base configuration for every server
  hosts: all
  become: true
  roles:
    - common
# inventory.ini
[local]
localhost ansible_connection=local
ansible-playbook -i inventory.ini site.yml

يجب أن تنتهي الـplay بـfailed=0 في الملخص. مرّر المعلمات في موضع الاستدعاء باستخدام الصيغة الموسعة. بهذه الطريقة يخدم الدور نفسه مجموعتين من المضيفين:

  roles:
    - role: common
      common_admin_group: ops
      common_permit_root_login: prohibit-password

هناك قاعدة ترتيب تفاجئ معظم المستخدمين. يمكن أن تحتوي الـplay على pre_tasks وroles وtasks وpost_tasks، ويشغّلها Ansible بهذا الترتيب مهما كان ترتيب كتابتها في الملف. ضع tasks: فوق roles:، وستظل الأدوار تعمل أولاً. لذلك، إذا كان يجب تنفيذ شيء قبل أحد الأدوار، فضعه في pre_tasks:، وليس في بداية tasks:.

- name: Ordering demonstration
  hosts: local
  gather_facts: false
  pre_tasks:
    - name: Runs first
      ansible.builtin.debug:
        msg: pre
  roles:
    - common
  tasks:
    - name: Runs after the role
      ansible.builtin.debug:
        msg: task
  post_tasks:
    - name: Runs last
      ansible.builtin.debug:
        msg: post

لاستدعاء دور من داخل قائمة مهام بدلاً من المفتاح roles:، استخدم import_role أو include_role.

  tasks:
    - name: Static, read when the playbook is parsed
      ansible.builtin.import_role:
        name: common

    - name: Dynamic, resolved when the task runs
      ansible.builtin.include_role:
        name: postgres
      when: "'db' in group_names"

إنّ import_role ثابت. يقرأ Ansible الدور في وقت التحليل، وتصبح مهامه جزءاً من الـplay، لذلك يعرض ansible-playbook --list-tasks site.yml هذه المهام، وتطبَّق العلامة الموجودة على الاستيراد على كل مهمة داخله. أمّا include_role فهو ديناميكي. لا يقرأ Ansible شيئاً حتى تُنفَّذ المهمة، وهذا ما يتيح لك تحديد اسم الدور من متغير أو من حلقة. لكن هذه المهام لا تظهر في --list-tasks ولا في --start-at-task.

توجد هنا نقطة شائعة تسبب الأخطاء. يُقيَّم when: في مهمة include_role قبل أن يصبح defaults/main.yml الخاص بالدور المُضمَّن ضمن النطاق. إذا كتبت when: common_packages | length > 0 في عملية التضمين، يتوقف التنفيذ مع 'common_packages' is undefined، رغم أن هذا المتغير معرّف داخل الدور الذي تُضمّنه نفسه. الحل هو نقل مفتاح التفعيل خارج الدور: ضعه في group_vars/all.yml، حيث يكون متاحاً ضمن النطاق في كل المواضع، واترك القيم الافتراضية للدور للقيم التي يستخدمها الدور نفسه.

أي متغيّر تكون له الأولوية: الإعدادات الافتراضية، أم roles/<name>/defaults/main.yml، أم group_vars/، أم host_vars/؟

تُوثّق Ansible أكثر من عشرين مستوى لأولوية المتغيّرات. أربعة منها تحسم معظم الحالات العملية، وهي مرتبة هنا من الأضعف إلى الأقوى.

  • يوجد roles/<name>/defaults/main.yml بالقرب من أسفل الترتيب. تتغلب عليه تقريباً أي قيمة تضبطها في موضع آخر، ولذلك فهو المكان المناسب لإعدادات الدور القابلة للضبط.
  • يقع group_vars/ وhost_vars/ في الوسط. هنا تضع القيم الخاصة بموقعك، وهما يتغلبان بوضوح على الإعدادات الافتراضية للدور.
  • يأتي roles/<name>/vars/main.yml فوق host_vars. لا يمكن تجاوز قيمة تضعها هنا من خلال الجرد. احجزها للقيم التي يجب أن يحافظ الدور على اتساقها الداخلي، مثل اسم حزمة يجب أن يطابق اسم خدمة.
  • تتغلب معلمة الدور التي تمررها عند استدعائه على vars/main.yml، كما تتغلب -e في سطر الأوامر على كل شيء، بما في ذلك معلمات الدور.

يمكنك رؤية كيفية حسم هذه الأولويات خلال نحو دقيقة. امنح دوراً صغيراً قيمة افتراضية واحدة ومتغيّر دور واحداً، ثم اضبط الاسمين نفسيهما في host_vars.

# roles/prec/defaults/main.yml
---
prec_tunable: from-defaults
prec_internal: from-defaults
# roles/prec/vars/main.yml
---
prec_internal: from-rolevars
# host_vars/localhost.yml
---
prec_tunable: from-hostvars
prec_internal: from-hostvars
# roles/prec/tasks/main.yml
---
- name: Show which value survived
  ansible.builtin.debug:
    msg: "tunable={{ prec_tunable }} internal={{ prec_internal }}"
ansible-playbook -i inventory.ini prec.yml
ansible-playbook -i inventory.ini prec.yml -e prec_internal=from-cli

تطبع عملية التشغيل الأولى tunable=from-hostvars internal=from-rolevars. فقد تغلب الجرد على الإعداد الافتراضي للدور، لكنه خسر أمام متغيّر الدور. وتطبع عملية التشغيل الثانية internal=from-cli، لأن المتغيّرات الإضافية تقع في أعلى الترتيب، ولا يمكن لأي مستوى أدنى تجاوزها. ولهذا السبب يكون -e مناسباً لعملية تشغيل لمرة واحدة، لكنه غير مناسب في script تحتفظ به: إذ يتغلب بصمت على كل قرار ذي صلة في مستودعك.

القاعدة العملية: إذا أردت أن تكون القيمة قابلة للضبط، فضعها في defaults/. ووضعها في vars/ يخبر كل مستخدم لاحق للدور بأن الجرد لا يمكنه تغييرها. قد يكون هذا مقصوداً أحياناً، لكنه يكون في الغالب خطأً غير مقصود.

أثبت أن الدور idempotent: شغّله مرتين

ينتج تشغيل Ansible الذي يمكن الوثوق به النتيجة نفسها في المرة الثانية، ويُبلغ عن عدم تغيير أي شيء. شغّل playbook مرتين واقرأ الملخص.

ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.yml

يجب أن يبدو الملخص الثاني كما يلي:

PLAY RECAP *********************************************************************
localhost   : ok=4  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

يعني changed=0 أن كل module فحص الحالة الحالية ووجد أن العمل قد أُنجز مسبقاً. ويعني changed=2 في التشغيل الثاني أن مهمتين لا تستطيعان تمييز الحالة، ولذلك ستواصلان إعادة كتابة الملفات وإعادة تشغيل الخدمات إلى ما لا نهاية. والسبب المعتاد هو command أو shell، لأن Ansible لا يملك طريقة لمعرفة ما نفّذه أمر عشوائي.

# traps.yml
---
- name: Command modules do not know what they changed
  hosts: local
  gather_facts: false
  tasks:
    - name: This appends a line on every run
      ansible.builtin.shell: "echo run >> /tmp/grow.txt"

    - name: This appends a line only once
      ansible.builtin.shell: "echo run >> /tmp/guarded.txt"
      args:
        creates: /tmp/guarded.txt

شغّل ذلك playbook مرتين، ثم عدّ الأسطر التي تحتوي على wc -l /tmp/grow.txt /tmp/guarded.txt. يحتوي /tmp/grow.txt على سطرين، ويحتوي /tmp/guarded.txt على سطر واحد. في التشغيل الثاني، لم تُنفّذ المهمة المحمية إطلاقاً، وتحمل نتيجتها الرسالة skipped, since /tmp/guarded.txt exists، لأن creates يزوّد module بمنتج ظاهر يبحث عنه أولاً. عندما لا يترك الأمر هذا المنتج، سجّل ناتجه واتخذ القرار بنفسك باستخدام changed_when.

يتنبأ ansible-playbook --check --diff site.yml بالتغييرات من دون تنفيذها، ويطبع --diff الأسطر الدقيقة التي سيعيد template كتابتها. اقرأ الناتج مع مراعاة قيد واحد: يتم تخطي مهمتَي shell وcommand في check mode، لذلك قد يخفي المخطط الذي يبدو نظيفاً عملاً ما زال مطلوباً.

يستحق عمود آخر في ذلك الملخص القدر نفسه من الانتباه: يُحتسب المضيف الذي تعذر على Ansible الاتصال به ضمن unreachable بدلاً من failed، ولم تُنفّذ أي من مهامه، لذلك حدّد مسبقاً ما إذا كان يجب لمضيف واحد يتعذر الوصول إليه إيقاف التشغيل بالكامل قبل توجيه هذا الدور إلى أكثر من جهازين.

لماذا يقول Ansible إن الدور غير موجود

يبحث Ansible عن مجلد roles/ بجوار ملف playbook، ثم في roles_path. ويتبع البحث مسار playbook، وليس الصدفة التي شغّلت منها الأمر.

ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely

تعني هذه الرسالة أن site.yml وroles/ لم يعودا متوافقين، وتطبع المسارات التي حاول استخدامها. ضع الاثنين في الدليل نفسه. لا بأس من التشغيل من دليل أب، لأن مسار playbook هو العامل المحدد:

ansible-playbook -i infra/inventory.ini infra/site.yml

توجد نسخة أقل وضوحاً من المشكلة نفسها. يتجاهل Ansible ملف ansible.cfg في الدليل الحالي عندما يكون هذا الدليل قابلاً للكتابة من جميع المستخدمين، لأن أي مستخدم على الخادم يمكنه وضع إعدادات فيه وتغيير سلوك التشغيل.

[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.

عندئذٍ تصبح إعدادات roles_path وinventory غائبة بصمت، ويفشل البحث عن الدور لسبب لا علاقة له بالأدوار. يطبع ansible --version ملف config file الذي حمّله فعلياً، بينما يطبع ansible-config dump --only-changed كل إعداد يختلف عن القيم الافتراضية المضمّنة. تحقّق من كليهما كلما تصرّف التشغيل كما لو أن إعداداتك غير موجودة.

مشاركة الأدوار: requirements.yml وإصدار مثبت

يُثبَّت الدور الذي كتبه شخص آخر، ولا يُنسخ. صرّح عنه مرة واحدة:

# requirements.yml
---
roles:
  - name: postgres
    src: https://github.com/example/ansible-role-postgres
    scm: git
    version: v1.4.0
ansible-galaxy install -r requirements.yml -p galaxy_roles

اضبط version دائماً. من دونه، ستحصل على محتوى الفرع الافتراضي في يوم تنفيذ الأمر. لذلك قد يفشل نشر نجح الشهر الماضي من دون أي تغيير في مستودعك نفسه. وجّه roles_path إلى دليل التنزيل، وأبقِ هذا الدليل خارج git:

# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_roles

تظل الأدوار الموجودة في roles/ بجانب ملف playbook قابلة للعثور عليها، لأن هذا المسار يُبحث فيه دائماً بالإضافة إلى roles_path. لذلك تبقى أدوارك الخاصة محفوظة في المستودع وخاضعة للمراجعة، بينما تكون أدوار الجهات الخارجية تنزيلات قابلة لإعادة الإنتاج ومثبتة على tag.

متى لا يعود الدور هو الحل

الدور هو وحدة لإعادة الاستخدام ضمن تنفيذ واحد لـAnsible. لا ينشئ خوادم أو سجلات DNS لدى مزود الخدمة، ومحاولة استخدامه لهذا الغرض هي ما يحوّل ملفات playbook إلى شيء لا يرغب أحد في صيانته. من المفيد قراءة تقسيم العمل بين Ansible وTerraform قبل أن تبدأ. لا يحل الدور أيضاً محل تصميم inventory: بعد تجاوز عدد محدود من الأجهزة، تصبح طريقة تجميع تلك الخوادم والوصول إليها أهم من طريقة توزيع المهام على الملفات.

تستحق إجراءات التقوية التي يثبّتها هذا الدور common قرارات مستقلة أيضاً. يضبط ملف drop-in أعلاه توجيهين فقط ولا يزيد عليهما، لذلك اقرأ إعدادات SSH التي تستحق التغيير فعلاً وكيفية جعل Ubuntu يطبّق تحديثات الأمان تلقائياً قبل أن تقرر ما يجب أن يندرج في الدور لكل مضيف تملكه.

FAQ

متى أحوّل Ansible playbook إلى role؟

عندما تحتاج الكتلة نفسها من المهام إلى التنفيذ في play ثانية أو على مجموعة ثانية من المضيفين. يُعد نسخ المهام بين ملفات playbook إشارة واضحة، لأن كل إصلاح سيجب تطبيقه مرتين منذ تلك اللحظة، وسيأتي يوم يُطبَّق فيه مرة واحدة فقط. لا تستفيد playbook واحدة يقل طولها تقريباً عن 100 سطر وتستهدف دائماً مجموعة واحدة من role، كما أن الأدلة الإضافية تجعل قراءتها أصعب.

هل تُنفَّذ roles قبل المهام الموجودة في play نفسها؟

نعم. ينفّذ Ansible pre_tasks، ثم كل ما هو مدرج ضمن roles:، ثم tasks:، ثم post_tasks:، ويتجاهل ترتيب ظهور هذه المفاتيح في ملفك. إن كتابة tasks: فوق roles: لا تجعل تلك المهام تُنفَّذ أولاً. إذا كان يجب تنفيذ شيء قبل role، فضعه في pre_tasks:.

لماذا لا تتجاوز قيمة group_vars قيمة المتغير في role؟

تحقق مما إذا كان المتغير مضبوطاً في vars/main.yml ضمن role بدلاً من defaults/main.yml. يحتل vars/ أولوية أعلى من group_vars وhost_vars في ترتيب أولوية Ansible، لذلك لا يستطيع inventory تجاوز قيمته. انقل المتغير إلى defaults/main.yml، فهو قريب من أدنى ترتيب الأولوية، وهو المكان الصحيح لأي قيمة يجب أن يتمكن المستدعي من تغييرها. للتأكد من أن أولوية المتغير هي السبب وليس خطأً مطبعياً، شغّل التنفيذ مرة واحدة باستخدام -e name=value، الذي تتجاوز أولويته أي مصدر آخر.

لماذا يقول Ansible إن role غير موجود؟

يبدأ البحث بجوار ملف playbook، لذلك يجب أن يكون site.yml وroles/ في الدليل نفسه. يعرض الخطأ المسارات التي حاول البحث فيها، كما في the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely. لا مشكلة في تشغيل playbook من دليل أب، لأن البحث يتبع مسار playbook وليس دليل العمل الحالي في الصدفة. إذا كنت تعتمد على roles_path من ansible.cfg، فتحقق من تحميل ذلك الملف باستخدام ansible --version، لأن Ansible يتجاهل دليل العمل القابل للكتابة عالمياً.

هل أحتاج إلى ansible-galaxy init لإنشاء role؟

لا. تتكون role من أدلة تحمل الأسماء المتوقعة فقط، لذلك يكفي mkdir -p roles/common/tasks مع tasks/main.yml للحصول على role عاملة. يوفّر ansible-galaxy init --init-path roles common وقت الكتابة ويمنحك الهيكل الكامل، بما في ذلك meta/main.yml ومسودة README. احذف الأدلة التي تتركها فارغة، لأن vars/main.yml الفارغ يخفي الملفات التي تنفّذ عملاً فعلياً في role.

#ansible#roles#playbook#structure#automation