SSD Nodes Learn Hosting plans →
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-08-31

Ansible playbook לעומת role: מתי להשתמש בכל אחד?

הבינו מתי להסתפק ב-playbook שטוח ומתי לעבור למבנה של role. המדריך סוקר את פקודת ansible-galaxy init, ניהול משתנים, סדר קדימויות ומתי מורכבות הקוד מצדיקה ארגון מחדש של הפרויקט.

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

ההבדל בין Ansible playbook לבין role

Ansible playbook הוא הקובץ שאתם מריצים באמצעות ansible-playbook. הוא ממפה קבוצת מארחים (hosts) לעבודה שהם צריכים לבצע. Ansible role הוא ספרייה בעלת מבנה קבוע המכילה משימות (tasks), תבניות (templates), מטפלים (handlers) ומשתני ברירת מחדל, ו־playbook קורא לה בשמה. תחביר המשימות בתוך שניהם זהה, לכן זו אינה שאלה של מה ניתן להגדיר. זו שאלה של שימוש חוזר.

התחילו עם playbook שטוח. site.yml אחד המכיל רשימת tasks: הוא המבנה הנכון לאוטומציה הראשונה שלכם, והוא נשאר מתאים למשך זמן רב יותר ממה שרוב האנשים מצפים. עברו לשימוש ב־role כאשר יש להריץ את אותו בלוק משימות עבור קבוצת מארחים שנייה, או כאשר הקובץ גדל מעבר ל-100 שורות בערך ואתם כבר לא מצליחים למצוא משימה באמצעות גלילה.

אם עדיין לא כתבתם אחד כזה, התחילו עם playbook ראשון מול VPS בודד וחזרו לכאן כשהוא יתחיל לגדול.

מתי Playbook שטוח הוא הפתרון הנכון

Playbook שטוח הוא הבחירה הנכונה כאשר הפעולה מתבצעת פעם אחת, על שרת יחיד, או כאשר איש מלבדכם לא יקרא את הקוד. הקצאת שרת יישומים בודד או עדכון שרת לפני חלון תחזוקה אינם מצדיקים מבנה של עץ תיקיות. Role מוסיף שבע תיקיות ושכבת עקיפות אחת. אם הקורא היחיד הוא ה-playbook שנמצא לצדו, העקיפות הזו אינה מועילה, אלא רק מחייבת אתכם לבצע קפיצה נוספת בכל פעם שתרצו לקרוא מה באמת רץ.

ה-playbook השטוח מפסיק להיות הפתרון הנכון ברגע מסוים, וקל לזהות אותו. הרגע הזה מגיע כשאתם מעתיקים בלוק של משימות ל-playbook שני. ההעתקה הזו היא הסימן. מרגע זה ואילך, כל תיקון חייב להתבצע פעמיים, ויום אחד הוא יתבצע רק פעם אחת.

מה מכילה בפועל ספריית תפקיד (role)

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. Handler רץ בסוף ה-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, אך הוא מקשה על זיהוי הקבצים החשובים באמת בתפקיד.

כעת מלאו את הקבצים המבצעים את העבודה. התחילו ב-defaults, שכן הם מהווים את ממשק הציבורי של התפקיד.

# 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. Handler שמציין שם שגוי ייכשל רק כאשר משהו אכן משנה את ה-template, וזו הסיבה שתקלות מסוג זה צפות לעיתים קרובות רק לאחר שבועות.

השורה validate היא השימושית ביותר במשימה זו. Ansible מרנדר את ה-template לקובץ זמני, מחליף את נתיב הקובץ ב-%s, ומריץ את הפקודה. קובץ היעד יוחלף רק אם הפקודה מסתיימת עם קוד יציאה 0. הכניסו הוראה שגויה ל-template והריצו שוב: המשימה תיכשל עם failed to validate, ה-/etc/ssh/sshd_config.d/99-hardening.conf האמיתי יישאר ללא שינוי, ועדיין תהיה לכם גישה לשרת. שימו לב שהבדיקה בוחנת יותר מאשר רק את ה-syntax שלכם. אם sshd -t אינו יכול לקרוא את מפתחות ה-host, הוא יסתיים עם sshd: no hostkeys available -- exiting. ו-Ansible ידווח על אותו failed to validate, לכן קראו את ה-msg של המודול לפני שאתם מאשימים את ה-template.

כיצד Playbook קורא ל־Role

# 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 בסיכום. העבירו פרמטרים בנקודת הקריאה באמצעות הצורה המורחבת; כך Role אחד יכול לשרת שתי קבוצות של מארחים:

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

יש כלל סדר אחד שמפתיע כמעט את כולם. Play יכול להכיל pre_tasks, roles, tasks ו־post_tasks, ו־Ansible מריץ אותם בסדר הזה ללא קשר לסדר שבו כתבתם אותם בקובץ. הציבו tasks: מעל roles:, וה־Roles עדיין ירוצו ראשונים. לכן, אם משהו חייב לקרות לפני Role, מקומו ב־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

כדי לקרוא ל־Role מתוך רשימת משימות במקום להשתמש במפתח 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 קורא את ה־Role בזמן הניתוח (parse time) והמשימות שלו הופכות לחלק מה־Play, לכן ansible-playbook --list-tasks site.yml מציג אותן, ותג (tag) על ה־import חל על כל משימה בפנים. include_role הוא דינמי. שום דבר לא נקרא עד שהמשימה רצה, וזה מה שמאפשר לכם להגדיר את שם ה־Role מתוך משתנה או לולאה. המחיר הוא שהמשימות הללו אינן גלויות ל־--list-tasks ול־--start-at-task.

מלכודת אחת נמצאת כאן. when: על משימת include_role מוערך לפני שה־defaults/main.yml של ה־Role הכלול נמצא בטווח (scope). כתבו when: common_packages | length > 0 על ה־include וההרצה תיעצר עם 'common_packages' is undefined, גם אם המשתנה הזה מוגדר בתוך ה־Role שאתם כוללים. הפתרון הוא להוציא את המתג מחוץ ל־Role: הציבו אותו ב־group_vars/all.yml, שם הוא נמצא בטווח בכל מקום, והשאירו ב־defaults של ה־Role ערכים שה־Role צורך בעצמו.

איזה משתנה גובר: defaults, group_vars, vars, extra vars

Ansible מתעדת יותר מעשרים רמות של קדימות משתנים. ארבע מהן מכריעות כמעט בכל ויכוח מעשי, והן מפורטות כאן מהחלשה לחזקה ביותר.

  • roles/<name>/defaults/main.yml נמצא בתחתית הסולם. כמעט כל ערך שתגדירו בכל מקום אחר יגבר עליו, וזו בדיוק הסיבה שהוא המקום הנכון עבור הגדרות הניתנות לכוונון בתוך role.
  • group_vars/ ו-host_vars/ נמצאים באמצע. כאן מקומן של התשובות הספציפיות לאתר שלכם, והן דורסות את ברירות המחדל של ה-role בצורה נקייה.
  • roles/<name>/vars/main.yml נמצא מעל host_vars. ערך שתציבו כאן לא ניתן לדריסה מתוך ה-inventory. שמרו אותו לדברים שה-role זקוק להם כדי לשמור על עקביות פנימית, כמו שם חבילה שחייב להתאים לשם של שירות.
  • פרמטר של role שמועבר בנקודת הקריאה גובר על vars/main.yml, ו--e בשורת הפקודה גובר על הכל, כולל פרמטרים של ה-role.

ניתן לראות את תהליך ההכרעה הזה בתוך דקה. הגדירו ל-role קטן ברירת מחדל אחת ומשתנה role אחד, ולאחר מכן הגדירו את אותם שמות ב-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. ה-inventory גבר על ברירת המחדל של ה-role אך הפסיד למשתנה ה-role. ההרצה השנייה מדפיסה את internal=from-cli, כיוון ש-extra vars נמצאים בראש הסולם ושום דבר מתחתיהם לא יכול לדחוק אותם. זו גם הסיבה ש--e מתאים להרצה חד-פעמית אך שגוי בסקריפט שאתם מתחזקים: הוא עוקף בשקט כל החלטה שקולה במאגר שלכם.

כלל העבודה הוא: אם אתם רוצים שערך יהיה ניתן לשינוי, מקמו אותו ב-defaults/. הצבתו ב-vars/ מבהירה לכל משתמש עתידי של ה-role שה-inventory לא יוכל לשנות אותו. לעיתים זו הכוונה, אך בדרך כלל מדובר בטעות.

אימות שהתפקיד אידמפוטנטי: הרצה כפולה

הרצת 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 מדפיס את השורות המדויקות ש-template ישכתב. קראו את הפלט עם הסתייגות אחת: משימות shell ו-command מדלגות על ביצוע במצב check, לכן תוכנית שנראית נקייה עדיין עלולה להסתיר עבודה.

עמודה נוספת בסיכום זה דורשת תשומת לב דומה: מארח ש-Ansible לא הצליח להתחבר אליו נספר תחת unreachable ולא תחת failed, ואף אחת מהמשימות שלו לא רצה כלל. לכן החליטו מראש האם מארח אחד שלא ניתן להגיע אליו צריך לעצור את כל ההרצה לפני שתפנו תפקיד זה ליותר ממספר מצומצם של מכונות.

מדוע Ansible מדווח שהתפקיד (role) לא נמצא

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 בספרייה הנוכחית כאשר לספרייה זו יש הרשאות כתיבה לכל משתמש (world writable), שכן כל משתמש בשרת עלול להניח שם קובץ תצורה ולשנות את אופן הפעולה של ההרצה.

[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. בלעדיו, תקבלו את הגרסה שנמצאת ב-branch ברירת המחדל ביום שבו תריצו את הפקודה; כתוצאה מכך, פריסה שעבדה בחודש שעבר עלולה להיכשל ללא כל שינוי במאגר שלכם. כוונו את roles_path לספריית ההורדה, והשאירו ספרייה זו מחוץ ל-git:

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

תפקידים ב-roles/ לצד ה-playbook עדיין יזוהו, כיוון שנתיב זה נסרק תמיד בנוסף ל-roles_path. כך התפקידים שלכם נשארים ב-commit ועוברים סקירה, בעוד שתפקידי צד-שלישי הם הורדות הניתנות לשחזור ונעולות ל-tag ספציפי.

מתי תפקידים (roles) מפסיקים להיות הפתרון

תפקיד (role) הוא יחידת שימוש חוזר בתוך הרצה אחת של Ansible. הוא אינו יוצר שרתים או רשומות DNS אצל ספק התשתית שלכם, וניסיון לאלץ אותו לעשות זאת הוא הדרך שבה Playbooks הופכים למשהו שאף אחד לא רוצה לתחזק. כדאי לקרוא על חלוקת העבודה בין Ansible לבין Terraform לפני שמתחילים. תפקיד גם אינו מחליף תכנון של Inventory: ברגע שעוברים מספר קטן של מכונות, האופן שבו אתם מקבצים את השרתים וניגשים אליהם חשוב יותר מהאופן שבו המשימות מאורגנות בקבצים.

הקשחת האבטחה שמתקין התפקיד common הזה דורשת גם היא החלטות משל עצמה. ההגדרה המהירה לעיל קובעת שתי הנחיות בלבד ולא יותר, לכן קראו על אילו הגדרות SSH באמת כדאי לשנות ועל איך לגרום ל-Ubuntu להחיל עדכוני אבטחה באופן עצמאי לפני שתחליטו מה שייך לתפקיד עבור כל מארח שבבעלותכם.

FAQ

מתי כדאי להפוך Playbook של Ansible ל-Role?

כאשר יש להריץ את אותו בלוק משימות ב-play שני, או מול קבוצת מארחים נוספת. העתקת משימות בין Playbooks היא הסימן לכך, שכן מרגע זה כל תיקון יצטרך להתבצע פעמיים, ויום אחד הוא יבוצע רק פעם אחת. 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, שנמצא קרוב לתחתית סדר העדיפויות ומהווה את המקום הנכון לכל מה שקורא (caller) אמור להיות מסוגל לשנות. כדי לוודא שהסיבה היא אכן סדר העדיפויות ולא שגיאת כתיב, הריצו פעם אחת עם -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, שכן תיקיית עבודה שניתנת לכתיבה על ידי כולם (world writable) גורמת ל-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