SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-01

Ansible 初學者教學:編寫第一個 VPS 設定 Playbook

在 Ubuntu 24.04 使用 pipx 安裝 Ansible。本指南教您編寫 inventory 與 playbook 強化 VPS,並解決 Permission denied 與 sudo 權限錯誤,確保伺服器設定自動化。

您將建置的內容

一台安裝了 Ansible 的控制機器,以及一台或多台僅安裝原始映像檔的全新 Ubuntu 24.04 VPS。完成後,您將擁有一個列出伺服器名稱的清單檔案、一個驗證端對端連線是否正常的 ad-hoc ping 指令,以及一個將全新 VPS 設定清單自動化的 playbook:包含帶有您 SSH 金鑰的部署使用者、強化後的 sshd、fail2ban、自動更新 (unattended upgrades),以及在拒絕所有連線前先允許 OpenSSH 的防火牆。您可以將其指向一台或二十台伺服器。執行兩次,第二次執行時不會有任何變更,這正是其核心目的。

在配置了十五年的 VPS 後,我可以告訴您一個真實的模式:每個人都會手動設定前五台伺服器,然後在第六台時浪費整個週末,因為沒人記得對前五台做了什麼。本指南深入探討了 管理多台 Linux 伺服器 的內容,當您發現自己需要在三個終端機輸入相同的 apt install 時,請參考本指南。

Ansible 的本質簡述

Ansible 採用無代理程式(agentless)架構。受管理的伺服器無需安裝任何常駐程式:控制端機器透過標準 SSH 進行連線,將小型 Python 模組複製至目標端並執行,讀取其輸出的 JSON 後隨即刪除。目標端僅需具備 python3,而所有標準 Ubuntu 映像檔皆已內建此元件。核心概念在於冪等性(idempotent),其意義明確:任務描述的是一種「狀態」而非「動作」。針對套件使用 state: present 代表「確保該套件已安裝」,而非「執行安裝程式」。若目標狀態已達成,Ansible 將不會進行任何變更,並回報為 ok 而非 changed。此特性即為本產品的核心,它確保了重複執行 playbook 的安全性,而這種安全的重複執行能力,正是將 Shell script 轉化為基礎架構的關鍵。

先決條件與注意事項

  • 控制端機器:您的筆記型電腦或小型 VPS。本文假設使用 Ubuntu 24.04;若透過 Homebrew 安裝 pipx,macOS 的操作方式完全相同。
  • 一台或多台執行 Ubuntu 24.04 的 KVM VPS 作為目標端,且需能以 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 字元皆會導致執行失敗。
  • 在 playbook 強化 sshd 設定期間,請保持每個目標端的 SSH 連線開啟。我協助客戶復原的每一次鎖定案例,皆是因為客戶為了「從乾淨狀態測試」而關閉了最後一個連線。

步驟 1:使用 pipx 而非 pip 在控制端安裝 Ansible

傳統直覺是使用 pip3 install ansible。在全新的 24.04 映像檔上,這會提早一步失敗,即 Command 'pip3' not found, but can be installed with: sudo apt install python3-pip,而安裝 pip 只會讓您撞上真正的牆:

pip3 install ansible
error: 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 提供獨立的 virtualenv,並將二進位檔案放入您的 PATH:

sudo apt update && sudo apt install -y pipx
pipx ensurepath
pipx install --include-deps ansible

執行 pipx ensurepath 後請開啟新的 shell,以便 PATH 變更生效。--include-deps 並非裝飾:ansible 套件本身不提供主控台指令碼,ansibleansible-playbook 以及其他指令碼皆為其 ansible-core 依賴項的進入點,因此若不加此旗標,pipx 會以 No apps associated with package ansible or its dependencies 拒絕安裝。此外,請安裝 ansible 套件而非單純的 ansible-core,完整套件會包含社群集合,而本劇本使用了其中兩個集合的模組(ansible.posixcommunity.general)。

ansible --version

正確的結果會以類似 ansible [core 2.19.x] 的行開頭,並標示其運作所在的 Python 版本;目前的任何核心版本皆適用於此處的所有操作。若出現 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:清單檔案,先用 INI,擴充時改用 YAML

清單檔案是一個文字檔,列出 Ansible 可能會存取的機器。在新的專案目錄中建立 inventory.ini

[vps]
web1 ansible_host=10.0.0.10
web2 ansible_host=10.0.0.20

[vps:vars]
ansible_user=root

web1 是您選擇的別名,它會顯示在輸出中,也是您使用 --limit web1 時指定的目標。ansible_host 是實際位址。[vps] 是一個群組,而 [vps:vars] 為其中的每個主機設定變數;ansible_user 是 Ansible 登入時使用的使用者名稱。在其旁邊加上 ansible.cfg,這樣您就不必再輸入 -i

[defaults]
inventory = inventory.ini

Ansible 會從目前目錄讀取 ansible.cfg。當主機包含多個變數時,您會傾向使用 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" --become

Ad-hoc 指令適用於一次性任務與檢查。任何需要執行兩次以上的操作,都應放入 Playbook 中。

步驟 5:第一個 Playbook,將新 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', ...) 會在執行階段從控制機器讀取您的公開金鑰,因此 Playbook 本身不會攜帶任何金鑰資料。

迴圈。 loop: "{{ baseline_services }}" 會針對每個項目執行一次服務任務,輸出結果會將每個項目顯示在獨立行中。請注意,apt 任務會一次處理整個套件清單;單次 apt 交易速度較快,是處理套件的首選模式。迴圈僅適用於確實一次只能處理單一項目的模組。

處理常式 (Handler) 是必須內化的概念。notify: Restart ssh 並不代表「立即重新啟動 ssh」。它會將處理常式加入佇列,該常式僅會在 Play 結束時執行一次,且僅限於通知任務確實回報 changed 時才會執行。明天重新執行此 Playbook:若插入式檔案已正確,複製任務會回報 ok,且 sshd 不會被重啟。validate: 行是觸發器的安全機制,sshd 會在取代舊檔案前檢查新檔案,因此若有語法錯誤,任務會失敗,而不會導致 Daemon 損毀。

刻意使用 PermitRootLogin prohibit-password,而非 no 此 Playbook 以 root 身分透過金鑰登入。prohibit-password 會關閉 root 密碼登入,同時保留您的存取權。一旦部署使用者驗證無誤(ssh deploy@10.0.0.10 sudo true,即純位址,因為 web1 僅是 Ansible 認得的別名),請在 Inventory 中切換 ansible_user=deploy,並在後續執行時將其收緊為 no。請依照不會讓自己受困的順序進行強化。

00- 前綴至關重要。 對於大多數關鍵字,sshd 會採用其解析到的第一個出現項目,而 Ubuntu 的 sshd_config 會依字母順序將 sshd_config.d/*.conf 包含在其主體之前。Ubuntu 24.04 雲端映像檔在該目錄中已內建 60-cloudimg-settings.conf,而透過 cloud-init 啟用密碼登入的供應商會新增一個包含 PasswordAuthentication yes50-cloud-init.conf;將我們的檔案命名為 00-hardening.conf 可確保其排序優先,並覆蓋上述兩者。

任務順序是防火牆的安全關鍵。 Allow OpenSSH 會在採用拒絕原則的 Enable ufw 之前執行。Ansible 會嚴格依照列出的順序執行任務,因此在牆築起之前,漏洞必須先存在。fail2ban 在此處無需額外設定即可發揮作用;其 Ubuntu 預設值開箱即用,會自動監控 sshd。關於 Jail 的實際運作方式及調校細節,請參閱 Ubuntu 24.04 上的 fail2ban 指南

步驟 6:使用 --check 進行預演,隨後執行正式部署

ansible-playbook site.yml --check

檢查模式會建立連線並計算「預計」執行的動作,但不會進行任何變更。請查看輸出底部 PLAY RECAP 中的 changed= 數值,這代表每個主機預計會被修改的任務數量。有一點需要注意:若後續任務依賴於先前任務的變更,檢查模式會受到結構性限制。Ubuntu 標準伺服器映像檔預先安裝了 ufw,因此該劇本在檢查模式下可順利執行;但在沒有該套件的精簡映像檔中,ufw 任務會在檢查模式下「失敗」,因為檢查模式並未實際安裝套件,導致模組無處呼叫。這是預演模式的限制,而非劇本的錯誤。當計畫確認無誤後:

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 包含事實收集、八個任務以及處理常式。您的 changed 數值與我相差一到兩個是正常的:Ubuntu 標準映像檔預先安裝了 ufwunattended-upgrades,且 fail2ban 在 apt 安裝完成後會立即啟動,因此任務在首次執行時回報 ok 是合理的,因為其宣告的狀態已達成。必須為零的數值是 unreachablefailed。關於 become: true 的說明:當您以 root 身分連線時,這僅是形式,但一旦將 ansible_user 切換為 deploy,sudo 即正式生效。本劇本安裝的 NOPASSWD sudoers 檔案正是為了讓您的指令列免於 -K。若缺少此檔案,您將會遇到下文所述的 Missing sudo password

步驟 7:執行兩次,觀察冪等性 (idempotence)

立即再次執行相同的指令:

web1 : ok=9  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

changed=0,且 ok 減少了 1,因為未收到通知的處理常式 (handler) 並未執行。沒有任何套件被重新安裝,sshd 未重啟,ufw 也未被更動。這就是為什麼 Playbook 既是配置工具,也是一種「稽核」手段:下個月將 web3 加入清單並重新執行,新伺服器會被建立,舊伺服器則會被驗證。若未更動過的伺服器出現非零的 changed,即代表發生了組態偏移 (drift),這表示有人手動修改了本應由 Playbook 管理的設定。

此模式可進一步疊加。下一個值得撰寫的 Playbook 可用於部署 同一台 VPS 上的 WireGuard VPN,並收緊 ufw 規則,使 SSH 僅能透過通道連線;之後,再編寫一個在每台應用程式伺服器上安裝 Docker 與 Compose 的 Playbook。當 site.yml 的長度超過三個螢幕畫面時,再將其拆分為角色 (roles),在此之前無需拆分。

失敗模式與對應訊息

無法連線且顯示 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

缺少 sudo 密碼。

web1 | FAILED! => {
    "msg": "Missing sudo password"
}

您設定了 become: true 並以非 root 使用者身分連線,且該使用者執行 sudo 時需要密碼。請在指令列加入 -K (--ask-become-pass),或是為該使用者設定 NOPASSWD 的 sudoers 權限;這正是為什麼 playbook 會在您切換使用者前,先為 deploy 安裝此設定的原因。

error: externally-managed-environment。 您在 Ubuntu 24.04 上對系統 Python 執行了 pip。請參考步驟 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 代表混入了定位字元 (tab);YAML 禁止使用 tab。請養成在每次執行前執行 ansible-playbook site.yml --syntax-check 的習慣,並將編輯器設定為 YAML 使用兩個空格縮排。

/usr/bin/python3: not found。 這在標準 Ubuntu 24.04 映像檔中很少見,但在精簡版或網路開機映像檔中很常見:目標主機沒有 Python 導致模組執行失敗。請使用 raw 模組進行引導 (bootstrap),這是唯一不需要遠端主機具備任何條件的模組:ansible all -m raw -a "apt-get update && apt-get install -y python3" --become,然後重新執行 playbook。

FAQ

我需要在受管理的伺服器上安裝 Ansible 嗎?

不需要。Ansible 是無代理程式(agentless)的:控制端機器會透過 SSH 推送小型 Python 模組、執行它們,然後將其移除。目標主機僅需具備 python3 與 SSH 存取權限,標準的 Ubuntu 映像檔皆已內建這些功能。本指南中唯一的安裝步驟僅發生在您的控制端機器上。

為什麼 Ansible 顯示「Permission denied (publickey)」?

出現 UNREACHABLE! 區塊並伴隨 Permission denied (publickey),表示 SSH 驗證在 Ansible 執行任何動作前即已失敗。請檢查 Inventory 中的 ansible_user 是否與您實際設定的帳號相符、您是否已對該主機執行 ssh-copy-id,以及單純執行 ssh user@host 是否能無需密碼登入。任何能修復標準 ssh 指令的方法都能解決 Ansible 的問題,因為兩者使用相同的傳輸機制。

在 Ansible 中「冪等性」(idempotent)是什麼意思?

任務宣告的是期望的狀態(例如「此套件必須存在」、「此行必須存在於此檔案中」),而非要執行的動作。若該狀態已達成,Ansible 將不會執行任何動作,並回報 ok 而非 changed。這就是為什麼執行兩次 Playbook 時,第二次會顯示 changed=0,且重新執行是安全的稽核動作,而非風險較高的重新安裝。

在 Ubuntu 24.04 上安裝 Ansible 時,應該使用 pip 還是 pipx?

請使用 pipx。Ubuntu 24.04 將系統 Python 標記為外部管理,因此 pip install ansible 會因設計機制而失敗並顯示 error: externally-managed-environmentpipx install --include-deps ansible 會將 Ansible 安裝在隔離的 virtualenv 中,並將 ansibleansible-playbook 以及其他指令整潔地加入您的 PATH。

ansible 與 ansible-core 套件有什麼區別?

ansible-core 是引擎本身,僅包含 ansible.builtin 模組。ansible 套件則將核心與精選的社群集合(collections)打包在一起,包含 ansible.posix(即 authorized_key 模組)與 community.general(即 ufw 模組),本指南皆有使用。請從完整套件開始使用;僅在有明確需求時,才精簡為核心套件並手動挑選所需的集合。