מדריך Ansible: הקמת שרת VPS ראשון עם Playbook
למדו להתקין Ansible עם pipx ב-Ubuntu 24.04, להגדיר inventory ו-Playbook לאבטחת VPS חדש, ולפתור Permission denied ושגיאות sudo.
מה תבנו
מכונת בקרה אחת עם Ansible מותקן, ושרת VPS אחד או יותר עם Ubuntu 24.04 שהותקנה עליהם תמונת ברירת המחדל בלבד. בסוף התהליך יהיה ברשותכם קובץ inventory שמגדיר את השרתים, פקודת ping אד־הוק שמוכיחה שהאימות פועל מקצה לקצה, ו־playbook שמבצע כקוד את כל רשימת הבדיקות לשרת VPS חדש: משתמש פריסה עם מפתח ה־SSH שלכם, sshd מוקשח, fail2ban, עדכונים ללא השגחה, וחומת אש שמאפשרת OpenSSH לפני שהיא חוסמת כל דבר אחר. אפשר להפעיל אותו על שרת אחד או על עשרים. בהפעלה השנייה לא יחול שום שינוי. זו כל המטרה.
לאחר חמש־עשרה שנים של הקמת שרתי VPS, התבנית ברורה: כולם מגדירים ידנית את חמשת השרתים הראשונים, ואז מאבדים סוף שבוע על השרת השישי, משום שאיש אינו זוכר מה נעשה בחמשת הראשונים. המדריך הזה מעמיק את הסקירה בנושא ניהול שרתי Linux מרובים. חזרו אליו ברגע שאתם תופסים את עצמכם מקלידים את אותו apt install בשלושה מסופים.
מה Ansible באמת, בפסקה אחת
Ansible פועל ללא agent. אין צורך להתקין daemon בשרתים שהוא מנהל: מכונת הבקרה מתחברת באמצעות SSH רגיל, מעתיקה מודול Python קטן ליעד, מפעילה אותו, קוראת את ה־JSON שהוא מדפיס ומוחקת אותו. הדבר היחיד שהיעד זקוק לו הוא python3, שכבר קיים בכל image סטנדרטי של Ubuntu. המונח החשוב הוא idempotent, ומשמעותו פשוטה: משימה מתארת מצב, ולא פעולה. state: present עבור package פירושו "לוודא שהוא מותקן", ולא "להפעיל את תוכנית ההתקנה". אם המצב כבר מתקיים, Ansible אינו משנה דבר ומדווח על המשימה כ־ok במקום כ־changed. תכונה זו היא מהות המוצר: היא הופכת הפעלה חוזרת של playbook לבטוחה, והפעלות חוזרות בטוחות הן שהופכות shell script לתשתית.
דרישות מוקדמות, והמלכודות שכדאי להכיר מראש
- מכונת בקרה: המחשב הנייד שלכם או VPS קטן. ההנחה היא שאתם משתמשים ב־Ubuntu 24.04; macOS פועל באופן זהה לאחר התקנת
pipxבאמצעות Homebrew. - VPS יעד אחד או יותר, המריצים Ubuntu 24.04 על KVM, ונגישים באמצעות המשתמש
root. לא יותקן בהם דבר. - אימות באמצעות מפתח SSH מול כל יעד. Ansible משתמש בדיוק באותם פרטי אימות שבהם משתמשת הפקודה
ssh; אםssh root@hostמבקשת סיסמה, Ansible נכשל. - ב־Ubuntu 24.04,
pip install ansibleנכשלת עםerror: externally-managed-environment. זו מדיניות מכוונת של ההפצה, ולא תקלה. השתמשו ב־pipx. - רווחים ויישור ב־YAML הם חלק מהתחביר. הזחה שגויה גורמת ל־
mapping values are not allowed in this context, ותו Tab בכל מקום גורם לכשל. - השאירו חיבור SSH פעיל בכל יעד בזמן שה־playbook מקשיח את sshd. בכל אירוע נעילה שממנו סייעתי ללקוח להתאושש, החיבור האחרון נסגר כדי "לבדוק מחיבור נקי".
שלב 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 של המערכת ככזו שמנוהלת חיצונית (PEP 668), ולכן pip אינו יכול להתחרות ב־apt על אותם קבצים. אל תשתמשו ב־--break-system-packages; שם הדגל מתאר את פעולתו במדויק. הפתרון הנקי הוא pipx, שמספק ל־Ansible סביבה וירטואלית מבודדת ומוסיף את הקבצים הבינאריים ל־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 לבדה. החבילה המלאה כוללת את אוספי הקהילה, וה־playbook הזה משתמש במודולים משניים מהם (ansible.posix ו־community.general).
ansible --versionהתוצאה התקינה נפתחת בשורה כגון ansible [core 2.19.x] ומציינת את Python שבו Ansible פועל. כל גרסת core עדכנית מתאימה לכל הפעולות כאן. ansible: command not found פירושו ש־~/.local/bin עדיין אינו נמצא ב־PATH שלכם: פתחו shell חדש, או הפעילו את source ~/.bashrc.
זהו כל תהליך ההתקנה. במערכות היעד לא מותקן דבר.
שלב 2: גישה באמצעות מפתח SSH לכל יעד
ssh-keygen -t ed25519 -C "ansible control"
ssh-copy-id root@10.0.0.10
ssh-copy-id root@10.0.0.20לאחר מכן הוכיחו זאת, פעם אחת עבור כל מארח:
ssh root@10.0.0.10 true && echo okשורה זו מבצעת שתי פעולות: היא מאשרת שאימות באמצעות מפתח פועל ללא סיסמה, ומתעדת את מפתח המארח ב־known_hosts. בצעו זאת כעת, משום ש־Ansible מציג מפתח מארח שלא תועד כהנחיה אינטראקטיבית באמצע ההרצה. מצב כזה נראה בדיוק כמו תקיעה.
שלב 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 הוא כינוי שתבחרו. הוא יופיע בפלט, ובאמצעותו תבחרו את היעד של --limit web1. ansible_host היא הכתובת בפועל. [vps] היא קבוצה, ו־[vps:vars] מגדיר משתנים לכל השרתים שבה; 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 מתאים יותר לעשרים שרתים. בחרו פורמט אחד והמשיכו הלאה.
שלב 4: פקודות ad-hoc, ה־pong הירוק שמוכיח שהכול פועל
ansible all -m pingאין כאן שימוש ב־ICMP. המודול ping הוא חזרה מלאה לקראת ההרצה: התחברות ב־SSH, העתקת המודול, הרצת Python ביעד וניקוי. התוצאה התקינה היא ירוקה, עם בלוק אחד לכל מארח:
web1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}תוצאה ירוקה של SUCCESS פירושה שהאימות, מפרש Python והעברת הנתונים פועלים, ולכן גם ה־playbook יפעל. תוצאה אדומה של UNREACHABLE! פירושה שההעברה נכשלה לפני שהמודול הופעל; המחרוזת המדויקת והפתרון מופיעים בסעיף מצבי הכשל שבהמשך. כדאי להכיר גם שתי פקודות ad-hoc נוספות:
ansible all -a "uptime"
ansible all -m apt -a "update_cache=true upgrade=dist" --becomead-hoc מיועד לפעולות חד־פעמיות ולבדיקות. כל פעולה שתפעילו פעמיים צריכה להיכלל ב־playbook.
שלב 5: הפלייבוק הראשון, רשימת הבדיקה של VPS חדש כקוד
זה כל מה שהייתם מבצעים ידנית בעשר הדקות הראשונות בשרת חדש. שמרו זאת בשם 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אלה השורות שכדאי להבין במקום להעתיק:
משתנים נמצאים תחת vars:, ומפנים אליהם באמצעות "{{ deploy_user }}". יש לתחום במירכאות את הביטוי כולו כאשר ערך מתחיל בסוגר מסולסל, אחרת מנתח YAML עלול לפרש אותו באופן שגוי. lookup('file', ...) קורא בזמן הריצה את המפתח הציבורי מהמחשב ה־control, ולכן הפלייבוק אינו כולל חומר מפתח.
הלולאה. loop: "{{ baseline_services }}" מריץ את משימת השירות פעם אחת לכל פריט, והפלט מציג כל פריט בשורה נפרדת. שימו לב שמשימת apt מקבלת את רשימת החבילות כולה בפעולה אחת. עסקת apt אחת מהירה יותר, וזהו הדפוס המועדף להתקנת חבילות; לולאות מיועדות למודולים שבאמת פועלים בכל פעם על פריט אחד.
ה־handler הוא המושג שחשוב להפנים. notify: Restart ssh אינו אומר "להפעיל מחדש את ssh עכשיו". הוא מצרף את ה־handler לתור, והוא מופעל פעם אחת בסוף ה־play, ורק אם המשימה שהודיעה עליו דיווחה בפועל על changed. אם תריצו את הפלייבוק שוב מחר, קובץ ה־drop-in כבר יהיה תקין, משימת ההעתקה תדווח על ok, ו־sshd לא יופעל מחדש. השורה validate: מספקת את שכבת הבטיחות בטריגר: sshd בודק את הקובץ לפני שהוא מחליף את הקובץ הישן, ולכן שגיאת כתיב תכשיל את המשימה במקום להשבית את ה־daemon.
PermitRootLogin prohibit-password, ולא no, בכוונה. הפלייבוק הזה מתחבר כ־root באמצעות מפתח. prohibit-password משבית התחברויות root באמצעות סיסמה, תוך שמירה על החיבור שלכם פעיל. לאחר שתוודאו שמשתמש הפריסה פועל כראוי (ssh deploy@10.0.0.10 sudo true, הכתובת הפשוטה, משום ש־web1 הוא כינוי ש־Ansible בלבד מכיר), החליפו את ansible_user=deploy במלאי והקשיחו אותו ל־no בהרצה מאוחרת יותר. בצעו את ההקשחה בסדר שאינו עלול לנעול אתכם מחוץ לשרת.
הקידומת 00- חשובה. עבור רוב מילות המפתח, sshd מכבד את המופע הראשון שהוא מנתח, ו־sshd_config של Ubuntu כולל את sshd_config.d/*.conf בסדר לקסיקוגרפי, לפני גוף הקובץ שלו. תמונות הענן של Ubuntu 24.04 כבר כוללות 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.
שלב 6: הרצה מקדימה באמצעות --check, ולאחר מכן הרצה בפועל
ansible-playbook site.yml --checkמצב בדיקה מתחבר, מחשב מה הוא היה מבצע, ואינו משנה דבר. קראו את מונה ה־changed= ב־PLAY RECAP שבתחתית. זהו מספר המשימות שהיו משנות כל שרת. חשוב להכיר במגבלה אחת: למצב בדיקה יש מגבלה מבנית כאשר משימה מאוחרת תלויה בשינויים שביצעה משימה קודמת. תמונת השרת הסטנדרטית של Ubuntu כוללת מראש את ufw, ולכן הרצה מקדימה של ה־playbook הזה מסתיימת ללא שגיאות. עם זאת, בתמונה מינימלית שאינה כוללת אותו, משימות ufw נכשלות במצב בדיקה, משום שמצב זה אינו מתקין בפועל את החבילה, ולמודול אין דבר להפעיל. זו מגבלה של הרצות מקדימות, ולא באג ב־playbook. כאשר התוכנית נראית נכונה:
ansible-playbook site.ymlכל משימה מדפיסה שורה עבור כל שרת: changed בצהוב, ok בירוק, ותקציר ההרצה אמור להיראות כך:
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 כוללים איסוף עובדות, שמונה משימות ואת ה־handler. ה־changed שלכם יכול להיות שונה מזה שלי באחד או שניים. תמונת השרת הסטנדרטית של Ubuntu כוללת מראש את ufw ואת unattended-upgrades, ו־fail2ban מופעל מיד לאחר ש־apt מתקין אותו. לכן משימה יכולה לדווח באופן תקין על ok כבר בהרצה הראשונה, משום שהמצב שהיא מגדירה כבר התקיים. המונים שחייבים להיות אפס הם unreachable ו־failed. הערה לגבי become: true: זהו עניין פורמלי כל עוד אתם מתחברים כ־root. אולם ברגע שתשנו את ansible_user ל־deploy, sudo נכנס לשימוש בפועל, וקובץ ה־sudoers עם NOPASSWD שה־playbook הזה מתקין הוא שמונע את הופעת -K בשורת הפקודה. בלעדיו תקבלו Missing sudo password, כפי שמוסבר בהמשך.
שלב 7: להפעיל פעמיים ולראות כיצד נראית אידמפוטנטיות
הפעילו שוב מיד את אותה פקודה:
web1 : ok=9 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 ו־ok קטנו באחד, משום שה־handler שלא קיבל התראה לא הופעל. שום דבר לא הותקן מחדש, sshd לא הופעל מחדש, ו־ufw לא שונה. כך ה־playbook משמש גם כ־audit וגם ככלי להקצאת תצורה: הוסיפו בחודש הבא את web3 ל־inventory והפעילו שוב. השרת החדש יוגדר, והשרתים הקיימים יאומתו. ערך שאינו אפס של changed בשרת שלא שיניתם מצביע על drift, ומראה שמישהו ערך ידנית הגדרה שהיה צריך לערוך ב־playbook.
מכאן התבנית מתרחבת. ה־playbook הבא שכדאי לכתוב מתקין VPN של WireGuard באותו VPS ומחמיר את כלל ufw, כך ש־SSH יענה רק דרך המנהרה. לאחר מכן כדאי לכתוב playbook שמתקין Docker ו־Compose בכל שרת יישומים. כאשר site.yml עובר את גבול שלושה המסכים, פצלו אותו ל־roles, אך לא לפני כן.
מצבי כשל, עם המחרוזות שיופיעו
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) לשורת הפקודה, או העניקו למשתמש רשומת sudoers מסוג NOPASSWD. זו בדיוק הסיבה שה־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כמעט תמיד מדובר בהזחה: מפתח נמצא בעומק שגוי, או שחסר רווח אחרי נקודתיים. מספר השורה המדווח מצביע בסביבת השגיאה, ולא בהכרח עליה. בדקו גם את השורה שמעליה. קרובת המשפחה שלה, found character '\t' that cannot start any token, פירושה שנכנסה ללשונית; YAML אינו מתיר לשוניות. הפכו את ansible-playbook site.yml --syntax-check להרגל קבוע לפני כל הרצה, והגדירו בעורך הזחה של שני רווחים עבור YAML.
/usr/bin/python3: not found. מצב זה נדיר בתמונות Ubuntu 24.04 רגילות, אך נפוץ בתמונות מינימליות או בתמונות netboot: הפעלת המודול נכשלת משום שלמארח היעד אין Python. אתחלו אותו באמצעות המודול raw, המודול היחיד שאינו זקוק לשום דבר בצד המרוחק: ansible all -m raw -a "apt-get update && apt-get install -y python3" --become, ולאחר מכן הריצו מחדש את ה־playbook.
FAQ
האם עליי להתקין את Ansible בשרתים שהוא מנהל?
לא. Ansible פועל ללא סוכן: מכונת הבקרה מעבירה מודולי Python קטנים באמצעות SSH, מפעילה אותם ומסירה אותם. בשרת היעד נדרשים רק python3 וגישה באמצעות SSH, ושניהם כבר זמינים בתמונות Ubuntu רגילות. ההתקנה היחידה בכל המדריך הזה מתבצעת במכונת הבקרה.
מדוע Ansible מציג "Permission denied (publickey)"?
הבלוק UNREACHABLE! עם Permission denied (publickey) מציין שאימות ה־SSH נכשל לפני ש־Ansible הריץ פעולה כלשהי. בדקו ש־ansible_user במלאי תואם לחשבון שהגדרתם בפועל, שהרצתם ssh-copy-id מול השרת הזה, ושפקודת ssh user@host רגילה מתחברת ללא סיסמה. כל תיקון לפקודת ה־ssh הרגילה יתקן גם את Ansible, משום ששניהם משתמשים באותו ערוץ תעבורה.
מה פירוש המונח idempotent ב־Ansible?
משימה מצהירה על מצב רצוי, כגון "החבילה מותקנת" או "השורה נמצאת בקובץ", במקום להצהיר על פעולה שיש לבצע. אם המצב כבר קיים, Ansible אינו עושה דבר ומדווח ok במקום changed. לכן הרצה של playbook בפעם השנייה מציגה changed=0, ולכן הרצה חוזרת היא בדיקת מצב בטוחה ולא התקנה מחדש הכרוכה בסיכון.
האם עליי להשתמש ב־pip או ב־pipx כדי להתקין את Ansible ב־Ubuntu 24.04?
ב־pipx. Ubuntu 24.04 מסמן את Python של המערכת כמנוהל חיצונית, ולכן 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 כוללת את הליבה ואת אוספי הקהילה שנבחרו, ובהם ansible.posix (מודול authorized_key) ו־community.general (מודול ufw), ששניהם משמשים במדריך הזה. התחילו בחבילה המלאה. עברו ל־core ולאוספים שנבחרו ידנית רק כאשר יש לכך סיבה.