SSD Nodes Learn
指南 Matt Connor作者: Matt Connor · 已更新 2026-07-24

Ansible 入門教學:如何在 VPS 上撰寫第一個 Playbook

本指南教您在 Ubuntu 24.04 使用 pipx 安裝 Ansible,並透過第一個 Playbook 強化 VPS 安全性。內容包含撰寫 inventory、解決 Permission denied 與 sudo 錯誤,並示範如何將 SSH key、fail2ban 與防火牆設定自動化。

建置目標

您將建置一台已安裝 Ansible 的控制機,以及一台或多台僅含原廠映像檔的全新 Ubuntu 24.04 VPS。完成後,您將擁有一個定義伺服器名稱的 inventory 檔案、一個可驗證端對端驗證是否成功的 ad-hoc ping 指令,以及一個將全新 VPS 檢查清單轉換為程式碼的 playbook:包含包含您的 SSH key 的部署使用者、強化的 sshd、fail2ban、unattended upgrades,以及在拒絕所有連線前先允許 OpenSSH 的防火牆。此流程適用於單台或二十台伺服器。執行兩次後,第二次執行不會產生任何變更 —— 這正是自動化的核心意義。

在進行了十五年的 VPS 配置經驗後,我可以告訴您一個真實的模式:每個人都會手動設定前五台伺服器,接著在第六台時浪費掉整個週末,因為沒人記得前五台做了什麼。本指南深化了 管理多台 Linux 伺服器 的內容 —— 當您發現自己正在三個終端機中輸入相同的 apt install 時,請閱讀本指南。

Ansible 的定義

Ansible 無需安裝 Agent。受控伺服器不需要安裝任何 daemon:控制端透過標準 SSH 連線,將小型 Python 模組複製到目標主機,執行該模組,讀取其輸出的 JSON,最後將模組刪除。目標主機僅需具備 python3,而所有標準的 Ubuntu 映像檔皆已內建此組件。關鍵概念在於「冪等性 (idempotent)」,其含義非常明確:任務描述的是「狀態」而非「動作」。針對套件的 state: present 代表「確保此套件已安裝」,而非「執行安裝程式」。若目標狀態已符合要求,Ansible 不會進行任何更動,並回報 ok 而非 changed。此特性即是該產品的核心——它讓重複執行 playbook 變得安全,而安全的重複執行正是將 shell script 轉化為基礎設施的關鍵。

前置作業與注意事項

  • 控制端機器:您的筆記型電腦或小型 VPS。預設環境為 Ubuntu 24.04;若已透過 Homebrew 安裝 pipx,macOS 的操作方式相同。
  • 一台或多台目標 VPS:運行於 KVM 上的 Ubuntu 24.04,且須具備 root 存取權限。目標機器不會安裝任何軟體。
  • 所有目標機器皆須使用 SSH key 驗證。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 視窗。我曾協助客戶修復的所有鎖定事件,皆是因為客戶為了「進行乾淨測試」而關閉了最後一個連線視窗。

Step 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;該 flag 的命名已說明其用途。正確的做法是使用 pipx,它會為 Ansible 提供獨立的 virtualenv,並將 binaries 加入 PATH:

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

執行完 pipx ensurepath 後請開啟新的 shell 以套用 PATH 變更。--include-deps 並非裝飾:ansible 套件本身不包含任何 console scripts — ansibleansible-playbook 與其他項目皆為其 ansible-core 依賴項的 entry points — 因此若未加上該 flag,pipx 會因 No apps associated with package ansible or its dependencies 而拒絕安裝。請安裝 ansible 套件而非單純的 ansible-core — 完整套件包含 community collections,而本 playbook 使用了其中兩個套件的 modules (ansible.posixcommunity.general)。

ansible --version

正確的結果會以類似 ansible [core 2.19.x] 的行開頭,並顯示其運行的 Python 版本;目前的任何核心版本皆可滿足需求。ansible: command not found 表示 ~/.local/bin 尚未加入您的 PATH — 請開啟新 shell 或使用 source ~/.bashrc

安裝流程至此結束。目標主機不會安裝任何內容。

Step 2: 透過 SSH key 存取所有目標主機

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 會在執行過程中彈出互動式提示,這看起來會非常像程式當機。

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=root

web1 是您自訂的別名,會顯示在輸出結果中,也是您使用 --limit web1 時指定的目標。ansible_host 是實際的位址。[vps] 是群組,[vps:vars] 用於設定該群組內所有主機的變數;ansible_user 是 Ansible 登入時使用的使用者。旁邊的 ansible.cfg 可讓您不必重複輸入 -i

[defaults]
inventory = inventory.ini

Ansible 會從目前目錄讀取 ansible.cfg。當每台主機擁有多個變數時,您會更偏好使用 YAML 格式的相同 Inventory —— 請將其儲存為 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 — 驗證結果的關鍵指標

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 中。

Step 5: 第一個 Playbook — 將新 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 解析器會讀取錯誤。lookup('file', ...) 會在執行時從 control 主機讀取你的公鑰,因此 Playbook 本身不包含任何金鑰資產。

The looploop: "{{ baseline_services }}" 會針對每個項目執行一次服務任務,且輸出結果會將每個項目分行顯示。請注意,apt 任務是一次處理整個套件清單 — 單次 apt 交易速度較快,也是安裝套件的首選模式;loop 適用於每次僅處理單一物件的模組。

The handler 是需要掌握的核心概念。notify: Restart ssh 並不代表「立即重啟 sshd」。它會將 handler 放入隊列,並在 Play 結束時,且 僅在 通知任務確實回報 changed 時才執行。明天再次執行 Playbook 時:drop-in 檔案已是正確狀態,copy 任務會回報 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 cloud images 已在該目錄中提供 60-cloudimg-settings.conf,且透過 cloud-init 啟用密碼登入的供應商會新增一個帶有 PasswordAuthentication yes50-cloud-init.conf;我們將檔案命名為 00-hardening.conf,使其排序最前並覆蓋兩者。

Task order 是防火牆的安全保障Allow OpenSSH 會在帶有 deny 策略的 Enable ufw 之前執行 — Ansible 會嚴格依照列出的順序執行任務,因此在牆建立前,漏洞會先存在。fail2ban 在此處無需配置即可發揮作用;其 Ubuntu 預設值會直接監控 sshd,至於 jail 的實際運作方式以及如何調整,請參閱 fail2ban on Ubuntu 24.04 guide

Step 6: 使用 --check 進行測試執行,然後正式執行

ansible-playbook site.yml --check

Check mode 會建立連線並計算預計執行的動作,但不會進行任何變更。請查看底部 PLAY RECAP 中的 changed= 數量,這代表每個主機預計會被修改的任務數量。一個注意事項:若後續任務依賴於先前任務的變更,check mode 會受到結構性限制。Ubuntu 標準伺服器映像檔預裝了 ufw,因此此 playbook 的測試執行結果會很乾淨;但在沒有預裝 ufw 的最小化映像檔上,ufw 任務在 check mode 下會失敗,因為 check mode 從未實際安裝該套件,導致模組無法呼叫。這是測試執行的限制,而非 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 可能與我的有 1 或 2 的差異:Ubuntu 標準映像檔預裝了 ufwunattended-upgrades,且 fail2ban 在 apt 安裝後會立即啟動,因此任務在第一次執行時可能會回報 ok —— 即該狀態已處於目標狀態。unreachablefailed 必須為 0。關於 become: true 的說明:這僅是您以 root 身分連線時的程序,但一旦將 ansible_user 改為 deploy,sudo 就會生效 —— 而此 playbook 安裝的 NOPASSWD sudoers 檔案,正是避免 -K 出現在命令列的原因。若缺少此設定,則會出現 Missing sudo password,詳見下文。

Step 7: 執行兩次 — 冪等性 (idempotence) 的表現

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

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

changed=0ok 的數值減少了 1,因為未觸發的 handler 從未執行。系統未重新安裝任何套件、sshd 未重啟、ufw 也未被更動。這使得此 playbook 同時具備「配置」與「稽核」的功能:下個月將 web3 加入 inventory 並重新執行,新主機會被建立,舊主機則會通過驗證。若未更動過的機器出現非零的 changed,即代表配置漂移 (drift),這表示有人手動修改了本應透過 playbook 修改的內容。

此模式會持續擴展。下一個值得撰寫的 playbook 是在 同一個 VPS 上部署 WireGuard VPN 並強化 ufw 規則,使 SSH 僅能在隧道中回應;接著是為每台應用伺服器安裝 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 傳輸在執行任何 module 前即失敗:ansible_user 錯誤、金鑰未複製至該主機,或提供了錯誤的金鑰。請使用 plain ssh root@10.0.0.10 重現問題,接著使用 ssh -v 查看已提供的金鑰。若 password 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. 您在 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

幾乎都是縮排問題:key 的層級錯誤,或冒號後缺少空格。報告的行號僅指向錯誤「附近」,而非錯誤本身 —— 請同時檢查上一行。其相關錯誤 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 映像檔中較罕見,但在 minimal 或 netboot 映像檔中很常見:module 執行失敗是因為目標主機沒有 Python。請使用 raw module 進行 bootstrap,這是唯一一個在遠端不需要任何預置條件的 module:使用 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)"?

若出現包含 Permission denied (publickey)UNREACHABLE! 錯誤,代表 Ansible 執行前 SSH 驗證已失敗。請檢查 inventory 中的 ansible_user 是否與您設定的帳戶一致、是否已對該主機執行 ssh-copy-id,並確認直接使用 ssh user@host 無需密碼即可登入。任何能修復原始 ssh 指令的問題都能修復 Ansible,因為兩者使用相同的傳輸機制。

Ansible 中的冪等性(idempotent)是什麼意思?

任務(task)宣告的是「期望狀態」——例如「此套件已安裝」或「此檔案包含此行內容」——而非執行動作。若狀態已符合,Ansible 將不執行任何動作,並回傳 ok 而非 changed。這就是為什麼第二次執行 playbook 時會顯示 changed=0,且重新執行是安全的稽核行為而非具風險的重新安裝。

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

請使用 pipx。Ubuntu 24.04 將系統 Python 標記為外部管理(externally managed),因此 pip install ansible 會因設計原因導致 error: externally-managed-environment 錯誤。pipx install --include-deps ansible 會將 Ansible 放入隔離的 virtualenv 中,並將 ansibleansible-playbook 及其他指令整潔地加入您的 PATH。

ansible 與 ansible-core 套件有何差異?

ansible-core 是引擎加上僅有的 ansible.builtin 模組。ansible 套件則將核心模組與精選的社群集合(community collections)打包在一起——包含本指南使用的 ansible.posixauthorized_key 模組)與 community.generalufw 模組)。建議從完整套件開始使用;僅在有特定需求時,才縮減為核心加上手選的集合。