SSD Nodes Learn 🎉 VPS החל מ־$5.50/חודש
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-08-03

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

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

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, התחילו ב־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 מריץ את הקובץ הזה כאשר ה־role נקרא, וכל תיקייה אחרת היא אופציונלית.
  • defaults/main.yml מכילה את המשתנים שמצופה מהקורא לדרוס. זהו מקור המשתנים בעל העדיפות הנמוכה ביותר ב־Ansible, ולכן כמעט כל מקור אחר גובר עליו.
  • vars/main.yml מכילה משתנים שלא מצופה מהקורא לדרוס. העדיפות שלה גבוהה מזו של inventory, וזו קביעה מחייבת. השתמשו בה לעיתים רחוקות.
  • handlers/main.yml מכילה משימות שמופעלות על ידי notify. handler פועל בסוף ה־play, פעם אחת בלבד, בלי קשר למספר המשימות שהודיעו עליו.
  • files/ מכילה קבצים שמועתקים כפי שהם באמצעות המודול copy, ו־templates/ מכילה תבניות Jinja2 שעוברות עיבוד באמצעות המודול template. בתוך role מפנים לשניהם באמצעות שם קובץ בלבד, ללא נתיב, משום ש־Ansible מחפש תחילה בתיקיות של ה־role עצמו.
  • meta/main.yml מצהירה על תלויות ה־role ועל המטא־נתונים ש־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. Handler שמציין את היחידה השגויה נכשל רק כאשר משהו אכן משתנה בתבנית. לכן הבעיה מתגלה בדרך כלל רק כעבור כמה שבועות.

השורה 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 קורא ל־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 יכול לשרת שתי קבוצות של hosts:

  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 בזמן ניתוח הקובץ, והמשימות שלו הופכות לחלק מה־play. לכן ansible-playbook --list-tasks site.yml מציג אותן, ו־tag שמוגדר בייבוא חל על כל המשימות שבתוכו. ‏include_role הוא דינמי. שום דבר אינו נקרא עד שהמשימה רצה, ולכן אפשר לקבוע את שם ה־role מתוך משתנה או לולאה. החיסרון הוא שהמשימות האלה אינן גלויות ל־--list-tasks ול־--start-at-task.

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

איזו משתנה גוברת: ברירות מחדל, 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 לשנות אותו. לפעמים זו אכן הכוונה, אך בדרך כלל מדובר בטעות.

הוכיחו שה־role אידמפוטנטי: הריצו אותו פעמיים

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

מדוע 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/ אינם תואמים עוד. Ansible מציג גם את הנתיבים שבהם ניסה לחפש. שמרו את שני הקבצים באותה תיקייה. אפשר להפעיל את הפקודה מתיקיית אב, משום שהנתיב של ה־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. בלעדיו תקבלו את מה שנמצא ב־default branch ביום שבו תריצו את הפקודה. לכן deployment שעבד בחודש שעבר עלול להיכשל בלי שביצעתם שינוי כלשהו ב־repository שלכם. הגדירו את roles_path לתיקיית ההורדות, והחריגו את התיקייה הזו מ־git:

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

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

היכן התפקידים מפסיקים להיות הפתרון

תפקיד הוא יחידת שימוש חוזר בתוך הרצה אחת של Ansible. הוא אינו יוצר שרתים או רשומות DNS אצל ספק התשתית שלכם, וניסיון לגרום לו לעשות זאת הופך את ה־playbooks למשהו שאיש אינו רוצה לתחזק. כדאי לקרוא את חלוקת העבודה בין Ansible ל־Terraform לפני שמתחילים. תפקיד גם אינו מחליף תכנון inventory: לאחר שעוברים מספר קטן של מכונות, האופן שבו מקבצים את השרתים האלה וניגשים אליהם חשוב יותר מהאופן שבו מחלקים את המשימות לקבצים.

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

FAQ

מתי כדאי להפוך Ansible playbook ל־role?

כאשר אותו מקטע משימות צריך לפעול ב־play נוסף או מול קבוצת hosts נוספת. העתקת משימות בין 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, שנמצא סמוך לתחתית סדר הקדימויות ומתאים למשתנים שמפעיל חיצוני צריך להיות מסוגל לשנות. כדי לוודא שסדר הקדימויות הוא הגורם ולא שגיאת כתיב, הריצו פעם אחת עם -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