מדריך Ansible: כתיבת Playbook ראשון ב-VPS
התקנת Ansible באמצעות pipx ב-Ubuntu 24.04, יצירת inventory ו-playbook לגיבוי השרת, כולל פתרון לשגיאות Permission denied ובעיות sudo נפוצות.
מה אתם בונים
מכונת בקרה אחת עם Ansible מותקן, ומכשיר VPS אחד או יותר עם Ubuntu 24.04 חדש, המכיל אך ורק את ה-stock image המקורי. בסיום התהליך יהיה לכם קובץ inventory המפרט את השרתים שלכם, פקודת ad-hoc ping שמוכיחה שהאימות (authentication) עובד מקצה לקצה, ו-playbook שמריץ את כל רשימת הבדיקות (checklist) עבור VPS חדש כקוד: משתמש deploy עם מפתח ה-SSH שלכם, הגנה על sshd, fail2ban, unattended upgrades, וחומת אש (firewall) המאפשרת OpenSSH לפני שהיא חוסמת כל דבר אחר. ניתן להריץ זאת על שרת אחד או עשרים. הרצה שנייה לא תשנה דבר — וזה בדיוק המטרה.
לאחר חמש עשר שנים של הקמת VPS, אני יכול לומר לכם את התבנית המוכרת: כולם מגדירים את חמשת השרתים הראשונים באופן ידני, ואז מבזבזים סוף שבוע שלם על השרת השישי כי אף אחד לא זוכר מה עשה בחמשת הראשונים. מדריך זה מרחיב את הסקירה ב-ניהול מספר שרתי Linux — התחילו לקרוא אותו ברגע שאתם תופסים את עצמכם מקלידים את אותו apt install בשלושה טרמינלים שונים.
מהו Ansible בפסקה אחת
Ansible היא כלי ללא agent. אין צורך להתקין daemon בשרתים המנוהלים: מכונת הבקרה מתחברת באמצעות SSH רגיל, מעתיקה מודול Python קטן ליעד, מריצה אותו, קוראת את פלט ה-JSON ומסירה את המודול. הדבר היחיד שהיעד דורש הוא python3, הקיים בכל image סטנדרטי של Ubuntu. המושג החשוב ביותר הוא idempotent, ומשמעותו פשוטה: משימה מתארת מצב (state), ולא פעולה. state: present עבור חבילה פירושו "ודא שהיא מותקנת", ולא "הרץ את המתקין". אם המצב כבר קיים, Ansible לא משנה דבר ומדווח על כך כ-ok במקום changed. תכונה זו היא לב המוצר — היא זו שהופכת הרצה חוזרת של playbook לבטוחה, והרצות חוזרות בטוחות הן שהופכות shell script לתשתית (infrastructure).
דרישות קדם ומכשולים נפוצים
- מכונת בקרה: הלפטופ שלך או VPS קטן. אני מניח שאתה משתמש ב-Ubuntu 24.04; macOS יעבוד באותו אופן לאחר התקנת pipx באמצעות Homebrew.
- VPS אחד או יותר המריצים Ubuntu 24.04 על KVM, עם גישת root. לא מותקן עליהם דבר.
- אימות באמצעות מפתח SSH (key) לכל יעד. Ansible משתמש באותה רמת אימות כמו פקודת
sshשלך — אםssh root@hostמבקשת סיסמה, Ansible ייכשל. - ב-Ubuntu 24.04,
pip install ansibleנכשל עםerror: externally-managed-environment. זוהי מדיניות הפצה מכוונת ולא תקלה. השתמש ב-pipx. - ב-YAML, הרווחים (whitespace) הם חלק מהתחביר. הזחה (indent) שגויה תגרום ל-
mapping values are not allowed in this context, ותו tab בכל מקום יגרום לשגיאה קריטית. - השאר סשן SSH פעיל בכל יעד בזמן שה-playbook מבצע hardening ל-sshd. כל מקרה של נעילה שסייעתי ללקוח לשחזר נבע מסגירת הסשן האחרון "כדי לבדוק מתוך סביבה נקייה".
Step 1: התקנת Ansible במכונת הבקרה באמצעות pipx, לא pip
האינסטינקט הקלאסי הוא להשתמש ב-pip3 install ansible. בגרסת 24.04 חדשה לחלוטין שנתקלת בשגיאה בשלב מוקדם — Command 'pip3' not found, but can be installed with: sudo apt install python3-pip — והתקנת pip רק תוביל למבוי סתום:
pip3 install ansibleerror: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to
install.ב-Ubuntu 24.04 ה-Python של המערכת מוגדר כ-externally managed (PEP 668), ולכן pip לא יכול להתנגש עם apt על אותם קבצים. אל תשתמש ב---break-system-packages; שם הדגל הוא ישיר. הפתרון הנכון הוא pipx, אשר יוצר עבור Ansible סביבה וירטואלית (virtualenv) מבודדת ומעדכן את ה-PATH עם הקבצים הבינאריים:
sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansibleפתח shell חדש לאחר pipx ensurepath כדי שהשינוי ב-PATH ייכנס לתוקף. --include-deps אינו רק קישוט: לחבילת ansible אין console scripts משלה — ansible, ansible-playbook ושאר הפקודות הם entry points של התלות ansible-core — לכן ללא הדגל, pipx יסרב להתקין עם השגיאה No apps associated with package ansible or its dependencies. התקן את החבילה ansible ולא את ansible-core בלבד — החבילה המלאה כוללת את ה-community collections, ו-playbook זה משתמש במודולים משתיים מהן (ansible.posix ו-community.general).
ansible --versionהתוצאה התקינה מתחילה בשורה כמו ansible [core 2.19.x] ומציינת את גרסת ה-Python עליה היא רצה; כל גרסת ליבה עדכנית תתאים לכל הצעדים כאן. ansible: command not found פירושו ש-~/.local/bin עדיין לא נמצא ב-PATH שלך — פתח shell חדש, או השתמש ב-source ~/.bashrc.
זהו תהליך ההתקנה המלא. ה-targets לא מקבלים שום דבר.
Step 2: SSH key access to every target
ssh-keygen -t ed25519 -C "ansible control"
ssh-copy-id root@10.0.0.10
ssh-copy-id root@10.0.0.20לאחר מכן, בדקו זאת פעם אחת עבור כל host:
ssh root@10.0.0.10 true && echo okהפקודה הזו מבצעת שתי פעולות: היא מאשרת ש-key auth עובד ללא סיסמה, והיא רושמת את ה-host key ב-known_hosts. בצעו זאת כעת, כיוון ש-Ansible מציג host key לא רשום כ-interactive prompt באמצע הריצה, מה שנראה בדיוק כמו תקיעה.
Step 3: ה-inventory — קודם INI, ואז YAML כשהפרויקט גדל
ה-inventory הוא קובץ טקסט המפרט את המכונות ש-Ansible עשוי לנהל. צרו את inventory.ini בתוך ספריית פרויקט חדשה:
[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20
[vps:vars]
ansible_user=rootweb1 הוא כינוי (alias) שתבחרו — זהו השם שיופיע בפלט והיעד שבו תשתמשו עם --limit web1. ansible_host הוא הכתובת האמיתית. [vps] הוא קבוצה, ו-[vps:vars] מגדיר משתנים עבור כל מארח (host) בקבוצה; ansible_user הוא המשתמש שאיתו Ansible מתחבר. לצד זה, ניתן להוסיף ansible.cfg כדי שלא תצטרכו להקליד את -i שוב:
[defaults]
inventory = inventory.iniAnsible קורא את ansible.cfg מהספרייה הנוכחית. אותו inventory בפורמט YAML — שמורו אותו כ-inventory.yml והפנו את ansible.cfg לשם זה במקום — הוא הפורמט המועדף ברגע שלכל מארח יהיו משתנים רבים:
vps:
hosts:
web1:
ansible_host: 10.0.0.10
web2:
ansible_host: 10.0.0.20
vars:
ansible_user: rootהפורמטים זהים. INI קל יותר לבדיקה ויזואלית עבור שני שרתים; YAML מתאים טוב יותר לעבודה עם עשרים שרתים. בחרו אחד והפסיקו לחשוב על זה.
Step 4: ad-hoc commands — the green pong that proves everything
ansible all -m pingזה אינו ICMP. המודול ping הוא תרגול מלא: התחברות SSH, העתקת מודול, הרצת Python ביעד וניקוי. התוצאה התקינה היא ירוקה, בלוק אחד לכל host:
web1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}SUCCESS ירוק אומר שאימות الهוסט (authentication), מפרש ה-Python והתעבורה (transport) 모두 עובדים — גם ה-playbook יעבוד. UNREACHABLE! אדום אומר שהתעבורה נכשלה לפני שהמודול הופעל; המחרוזת המדויקת והפתרון מופיעים בסעיף failure modes להלן. שתי פקודות ad-hoc נוספות שחשוב להכיר:
ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --becomeפקודות ad-hoc מיועדות למשימות חד-פעמיות ובדיקות. כל דבר שצריך להריץ פעמיים צריך להיות בתוך playbook.
Step 5: ה-playbook הראשון — רשימת הבדיקות של new-VPS כקוד
זהו כל מה שניתן לבצע ידנית ב-10 הדקות הראשונות בשרת חדש. שמרו את הקובץ בשם site.yml:
---
- name: Baseline a fresh Ubuntu VPS
hosts: vps
become: true
vars:
deploy_user: deploy
deploy_pubkey: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
baseline_packages:
- fail2ban
- unattended-upgrades
- ufw
baseline_services:
- fail2ban
- unattended-upgrades
tasks:
- name: Create the deploy user
ansible.builtin.user:
name: "{{ deploy_user }}"
groups: sudo
append: true
shell: /bin/bash
- name: Install the deploy user's SSH key
ansible.posix.authorized_key:
user: "{{ deploy_user }}"
key: "{{ deploy_pubkey }}"
- name: Passwordless sudo for the deploy user
ansible.builtin.copy:
dest: /etc/sudoers.d/deploy
content: "{{ deploy_user }} ALL=(ALL) NOPASSWD:ALL\n"
mode: "0440"
validate: /usr/sbin/visudo -cf %s
- name: Install baseline packages
ansible.builtin.apt:
name: "{{ baseline_packages }}"
state: present
update_cache: true
- name: Enable and start baseline services
ansible.builtin.service:
name: "{{ item }}"
state: started
enabled: true
loop: "{{ baseline_services }}"
- name: Harden sshd with a drop-in
ansible.builtin.copy:
dest: /etc/ssh/sshd_config.d/00-hardening.conf
content: |
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
X11Forwarding no
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Restart ssh
- name: Allow OpenSSH through ufw
community.general.ufw:
rule: allow
name: OpenSSH
- name: Enable ufw with default deny
community.general.ufw:
state: enabled
policy: deny
handlers:
- name: Restart ssh
ansible.builtin.service:
name: ssh
state: restartedשורות שחשוב להבין ולא רק להעתיק:
Variables (משתנים) נמצאים תחת vars: ומתייחסים אליהם באמצעות "{{ deploy_user }}" — יש להוסיף גרשיים לביטוי המלא כאשר הערך מתחיל בסוגריים מסולסלים, אחרת ה-YAML parser יקרא אותו بشكل שגוי. ה-lookup('file', ...) קורא את המפתח הציבורי שלכם מהמכונה ה-control בזמן ריצה, לכן ה-playbook אינו מכיל מידע של מפתח.
The loop. ה-loop: "{{ baseline_services }}" מריץ את משימת ה-service פעם אחת עבור כל פריט, והפלט מציג כל פריט בשורה נפרדת. שימו לב שמשימת ה-apt מקבלת את כל רשימת החבילות בבת אחת — פעולת apt אחת מהירה יותר וזהו התבנית המועדפת עבור חבילות; לולאות מיועדות למודולים הפועלים על פריט אחד בכל פעם.
The handler הוא מושג שחשוב להבין. notify: Restart ssh לא אומר "הפעל מחדש את sshd עכשיו". הוא מכניס את ה-handler לתור, אשר רץ פעם אחת בסוף ה-play, ורק אם המשימה המדווחת (notifying task) דיווחה בפועל על changed. הריצו מחדש את ה-playbook מחר: קובץ ה-drop-in כבר יהיה תקין, משימת ה-copy תדווח על ok, ו-sshd לא יופעל מחדש. שורת ה-validate: היא מנגנון הבטיחות — sshd בודק את הקובץ לפני שהוא מחליף את הישן, כך ששגיאת הקלדה תכשיל את המשימה במקום לשבור את ה-daemon.
PermitRootLogin prohibit-password, לא no — בכוונה. ה-playbook הזה מתחבר כ-root באמצעות מפתח. prohibit-password חוסם כניסות root באמצעות סיסמה תוך שמירה על המפתח שלכם פעיל. לאחר שיוכח תקינות המשתמש ל-deploy (ssh deploy@10.0.0.10 sudo true — הכתובת הפשוטה, מכיוון ש-web1 הוא רק alias ש-Ansible מכיר), שנו את ansible_user=deploy ב-inventory והחמירו אותו ל-no בריצה מאוחרת יותר. בצעו hardening בסדר שלא יוכל לנתק אתכם מהשרת.
הקידומת 00- חשובה. עבור רוב המילות המפתח, sshd מכבד את המופע הראשון שהוא קורא, ו-sshd_config של Ubuntu כולל את sshd_config.d/*.conf בסדר לקסיקוגרפי לפני הגוף שלו. Ubuntu 24.04 cloud images כבר כוללים 60-cloudimg-settings.conf בתיקייה זו, וספקי ענן המאפשרים כניסה עם סיסמה דרך cloud-init מוסיפים 50-cloud-init.conf עם PasswordAuthentication yes; מתן השם 00-hardening.conf לקובץ שלנו גורם לו להופיע ראשון ולגבור על שניהם.
סדר המשימות הוא מנגנון הבטיחות של ה-firewall. Allow OpenSSH רץ לפני Enable ufw עם מדיניות deny — Ansible מבצע משימות בסדר הרשימה המדויק, לכן ה"חור" קיים לפני שה"קיר" נבנה. fail2ban אינו דורש הגדרה כדי להיות שימושי כאן; ברירת המחדל של Ubuntu מנטרת את sshd באופן אוטומטי, ומה שה-jails עושים בפועל — ומה כדאי לכוון — מוסבר ב-מדריך fail2ban ב-Ubuntu 24.04.
Step 6: dry run with --check, then run it for real
ansible-playbook site.yml --checkמצב בדיקה (check mode) מתחבר למערכת, מחשב מה הוא עושה בפועל, ואינו משנה דבר. בדקו את ספירת ה-changed= ב-PLAY RECAP בתחתית — זהו מספר המשימות שישנו כל מארח. הערה חשובה: למצב בדיקה יש מגבלה מבנית כאשר משימה מאוחרת תלויה בשינויים של משימה מוקדמת יותר. אימג' השרת הסטנדרטי של Ubuntu כולל מראש את ufw, ולכן ה-playbook מריץ dry-run ללא שגיאות — אך באימג' מינימלי ללא ufw, משימות ה-ufw ייכשלו במצב בדיקה, מכיוון שמצב בדיקה אינו מתקין את החבילה בפועל והמודול לא יוכל לפעול. זוהי מגבלה של הרצת dry-run, ולא באג ב-playbook שלכם. כאשר התוכנית נראית תקינה:
ansible-playbook site.ymlכל משימה מדפיסה שורה עבור כל מארח — changed בצהוב, ok בירוק — והסיכום (recap) אמור להיראות כך:
PLAY RECAP *********************************************************************
web1 : ok=10 changed=9 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
web2 : ok=10 changed=9 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0עשר ok הם איסוף נתונים (fact-gathering) בתוספת שמונה משימות וה-handler. ה-changed שלכם עשוי להיות שונה משלי בערך אחד או שניים: האימג' הסטנדרטי של Ubuntu כולל מראש את ufw ו-unattended-upgrades, ו-fail2ban מתחיל לפעול ברגע ש-apt מתקין אותו, לכן משימה יכולה לדווח באופן לגיטימי על ok בהרצה הראשונה שלה — המצב שהיא מצהירה שכבר קיים. המספרים חייבים להיות 0 עבור unreachable ו-failed. הערה לגבי become: true: זוהי פורמליות בזמן שאתם מתחברים כ-root, אך ברגע שתשנו את ansible_user ל-deploy, ה-sudo יהיה פעיל — וקובץ ה-sudoers עם NOPASSWD שה-playbook הזה מתקין הוא בדיוק מה שמונע את -K בשורת הפקודה. ללא כך תקבלו את Missing sudo password, המפורט בהמשך.
Step 7: הרץ אותו פעמיים — כך נראית אידמפוטנטיות (idempotence)
הרץ שוב את אותה פקודה מיד:
web1 : ok=9 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 ו-ok ירדו ב-1 מכיוון שה-handler שלא הודיע על שינוי מעולם לא רץ. דבר לא הותקן מחדש, ה-sshd לא הופעל מחדש, ו-ufw לא נגעו בו. זה מה שהופך את ה-playbook לכלי של audit (ביקורת) بقدر שהוא כלי של provisioner: הוסף את web3 ל-inventory בחודש הבא והרץ מחדש — השרת החדש יוקם, והשרתים הישנים יתוקפרו. ערך changed שונה מ-0 בשרת שלא נגעת בו מעיד על drift (סטייה), וזה מעיד על כך שמישהו ערך ידנית את מה שנועד להיערך בתוך ה-playbook.
מנקודה זו, התבנית מתרחבת. ה-playbook הבא שכדאי לכתוב מגדיר WireGuard VPN על אותו VPS ומחמיר את כלל ה-ufw כך ש-SSH יקבל מענה רק דרך ה-tunnel; לאחר מכן, כזה שמתקין Docker and Compose בכל שרת אפליקציה. כאשר site.yml עובר שלושה מסכים, חלק אותו ל-roles — אך לא לפני כן.
Failure modes, with the strings you will see
UNREACHABLE with Permission denied.
web1 | UNREACHABLE! => {
"changed": false,
"msg": "Failed to connect to the host via ssh: root@10.0.0.10: Permission denied (publickey).",
"unreachable": true
}התקשורת ב-SSH נכשלה לפני הרצת המודולים: ansible_user אינו נכון, המפתח לא הועתק למארח, או שמוצע מפתח שגוי. ניתן לשחזר זאת באמצעות ssh root@10.0.0.10 פשוט, ולאחר מכן ssh -v כדי לראות אילו מפתחות הוצעו. אם התחברות ב-SSH באמצעות סיסמה עובדת אך Ansible לא, סימן שדילגת על ssh-copy-id.
Missing sudo password.
web1 | FAILED! => {
"msg": "Missing sudo password"
}הגדרת את become: true, התחברת כמשתמש שאינו root, ולמשתמש זה נדרשת סיסמה עבור sudo. יש להוסיף את -K (--ask-become-pass) לשורת הפקודה, או לתת למשתמש הרשאת NOPASSWD ב-sudoers — וזו הסיבה המדויקת לכך שה-playbook מתקין הרשאה כזו עבור deploy לפני המעבר אליו.
error: externally-managed-environment. הרצת pip מול ה-Python של המערכת ב-Ubuntu 24.04. הנושא מוסבר בשלב 1: יש להשתמש ב-pipx ולא ב-pip, ולא ב---break-system-packages.
mapping values are not allowed in this context.
ERROR! Syntax Error while loading YAML.
mapping values are not allowed in this contextכמעט תמיד מדובר בהזחה (indentation): מפתח בעומק לא נכון, או חוסר ברווח לאחר נקודתיים. מספר השורה המדווח מצביע על אזור סמוך לטעות, ולא על הטעות עצמה — בדוק גם את השורה שמעליה. שגיאת found character '\t' that cannot start any token מצביעה על שימוש ב-tab; YAML אוסר על שימוש ב-tabs. הפוך את ה-ansible-playbook site.yml --syntax-check להרגל לפני כל הרצה, והגדר את העורך שלך להזחה של שני רווחים עבור YAML.
/usr/bin/python3: not found. נדיר בדימויים סטנדרטיים של Ubuntu 24.04, נפוץ בדימויי minimal או netboot: הרצת המודול נכשלה כי ביעד אין Python. התקן אותו באמצעות מודול raw, המודול היחיד שאינו דורש שום דבר בצד המרוחק: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become, ולאחר מכן הרץ מחדש את ה-playbook.
FAQ
האם עלי להתקין את Ansible על השרתים שהוא מנהל?
לא. Ansible היא מערכת ללא agent: מכונת הבקרה שולחת מודולי Python קטנים דרך SSH, מריצה אותם, ומסירה אותם. היעד זקוק רק ל-python3 ולגישת SSH, שניהם קיימים ב-Ubuntu images סטנדרטיים. ההתקנה היחידה לאורך המדריך כולו מתבצעת על מכונת הבקרה שלך.
מדוע Ansible מציגה את השגיאה "Permission denied (publickey)"?
הבלוק UNREACHABLE! עם Permission denied (publickey) מעיד על כך שאימות ה-SSH נכשל לפני ש-Ansible הריצה פעולה כלשהנה. ודא ש-ansible_user ב-inventory תואם לחשבון שהגדרת, שביצעת ssh-copy-id לאותו host, וכי פקודת ssh user@host פשוטה מתחברת ללא סיסמה. כל מה שמתקן את פקודת ה-ssh הרגילה יתקן את Ansible, כיוון שהן משתמשות באותו פרוטוקול תקשורת.
מה המשמעות של idempotent ב-Ansible?
משימה (task) מגדירה מצב רצוי — למשל "חבילה זו קיימת" או "שורה זו נמצאת בקובץ זה" — במקום פעולה לביצוע. אם המצב כבר קיים, Ansible לא מבצע דבר ומדווח ok במקום changed. זו הסיבה שריצה שנייה של playbook תציג changed=0 בפעם השנייה, וזו הסיבה שריצה חוזרת היא בדיקה בטוחה ולא התקנה מסוכנת.
האם עלי להשתמש ב-pip או ב-pipx כדי להתקין את Ansible ב-Ubuntu 24.04?
ב-pipx. ב-Ubuntu 24.04 ה-Python של המערכת מוגדר כ-externally managed, ולכן pip install ansible ייכשל עם error: externally-managed-environment מכוון. pipx install --include-deps ansible מציבה את Ansible בתוך virtualenv מבודד ומנגישה את ansible, ansible-playbook ושאר הפקודות ב-PATH שלך בצורה נקייה.
מה ההבדל בין החבילות ansible ל-ansible-core?
ansible-core היא המנוע יחד עם מודולי ה-ansible.builtin בלבד. החבילה ansible מאחדת את ה-core יחד עם אוספי הקהילה (community collections) — כולל ansible.posix (מודול authorized_key) ו-community.general (מודול ufw), שניהם משמשים במדריך זה. התחל עם החבילה המלאה; צמצם ל-core בתוספת אוספים נבחרים רק אם יש לכך סיבה.