Ansible: متى تستخدم Playbook ومتى تستخدم Role؟
تعرّف على الفرق العملي بين Playbook وRole في Ansible: متى يكفي ملف مسطّح، ومتى تحتاج بنية الدور، وأمر ansible-galaxy init وأولوية المتغيرات.
دفتر 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.ymltasks/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=localansible-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.0ansible-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.