SSD Nodes Learn 🎉 VPS من $5.50/شهر
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-01

Ansible: متى تستخدم Playbook ومتى تستخدم Role؟

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

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

دفتر Ansible مقابل الدور: ما الفرق؟

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

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

إذا لم تكن قد كتبت دفترًا بعد، ابدأ بدفتر تشغيل أول يستهدف VPS واحدًا ثم عد عندما يبدأ في التوسع.

متى يكون playbook المسطّح هو الخيار الصحيح

يكون playbook المسطّح مناسبًا عندما تُنفَّذ المهمة مرة واحدة، أو على مضيف واحد، أو عندما لا يقرأه أي شخص آخر. لا تحتاج تهيئة خادم تطبيق واحد أو تثبيت التحديثات على جهاز قبل نافذة صيانة إلى شجرة مجلدات. يضيف الدور 7 مجلدات وطبقة واحدة من التجريد. إذا كان المستدعي الوحيد هو 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 يحتوي على المتغيرات التي لا يُتوقع من المستدعي تجاوزها. وتفوق أولويته أولوية inventory، وهذا مستوى عالٍ من الأولوية. استخدمه نادرًا.
  • handlers/main.yml يحتوي على المهام التي يفعّلها notify. ويُشغّل Ansible المعالج في نهاية 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، بينما تسمى sshd في أنظمة عائلة RHEL. يفشل المعالج الذي يحدد اسم الوحدة الخطأ فقط عند حدوث تغيير فعلي في القالب، ولذلك لا تظهر المشكلة عادةً إلا بعد أسابيع.

يعد سطر 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 الخاص بالوحدة قبل إلقاء اللوم على القالب.

كيفية استدعاء دور من ملف تشغيل

# 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

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

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

توجد قاعدة ترتيب تفاجئ الجميع تقريبًا. يمكن أن يحتوي ملف التشغيل على 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 الدور في وقت التحليل، وتصبح مهامه جزءًا من ملف التشغيل، لذلك يسرد ansible-playbook --list-tasks site.yml هذه المهام، وتُطبَّق الوسمة الموجودة على الاستيراد على كل مهمة داخله. أما include_role فهو ديناميكي. لا تتم قراءة أي شيء حتى تُنفذ المهمة، وهذا يتيح لك تحديد اسم الدور من متغير أو حلقة. لكن هذه المهام لا تظهر في --list-tasks ولا في --start-at-task.

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

أي متغير له الأولوية: القيم الافتراضية، وgroup_vars، و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 مناسبًا لتشغيل لمرة واحدة، لكنه غير مناسب في برنامج نصي تحتفظ به: فهو يتغلب بصمت على كل قرار مدروس في مستودعك.

القاعدة العملية: إذا أردت أن تكون القيمة قابلة للضبط، فضعها في 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 أن كل وحدة فحصت الحالة الحالية ووجدت أن العمل منفَّذ مسبقًا. ويعني ظهور 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 يتيح للوحدة البحث أولًا عن ناتج ظاهر. عندما لا يترك الأمر مثل هذا الناتج، سجّل مخرجاته، ثم اتخذ القرار بنفسك باستخدام changed_when.

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

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

يبحث Ansible عن مجلد roles/ بجوار ملف playbook، ثم في roles_path. ويتبع البحث مسار playbook، وليس مسار shell الذي تعمل منه.

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

عندما لا تعود الأدوار هي الحل

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

ويحتاج تعزيز الأمان الذي يثبّته دور common هذا إلى قرارات مستقلة أيضًا. يضبط ملف الإضافة أعلاه توجيهين فقط ولا يزيد عليهما. لذلك اقرأ إعدادات 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 وليس دليل العمل الحالي لـ shell. إذا كنت تعتمد على 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