最初發表於 kuryzhev.cloud
您的 Ansible 引導劇本在第一次對新建立的執行個體執行時失敗,然後在您三十秒後重新執行時就能完美運作。如果這個情境聽起來很熟悉,您正在處理間歇性的 ansible 引導劇本失敗問題——而且幾乎從來不是網路問題。我在 AWS EC2、Hetzner 和 DigitalOcean droplets 上遇到過完全相同的模式,每次修復方法都相同:不要將「執行個體正在執行」視為「執行個體已就緒」。
症狀
通常會出現三種失敗特徵,通常是組合出現,而且它們很令人抓狂,因為它們是非確定性的。第一種:UNREACHABLE! => Failed to connect to the host via ssh 發生在新執行個體上,第二次嘗試時完全消失。確切的錯誤是:
fatal: [10.0.1.42]: UNREACHABLE! => {
"changed": false,
"msg": "Failed to connect to the host via ssh: ssh: connect to host 10.0.1.42 port 22: Connection refused",
"unreachable": true
}
那個「Connection refused」——而不是「timed out」——就是關鍵。它表示網路層級的連接埠已開啟,但 sshd 尚未繫結到它。第二個症狀:become: true 任務以 {"msg": "Missing sudo password"} 崩潰,即使您可以手動 SSH 登入並執行 sudo whoami 而不需提示。第三個:劇本在「GATHERING FACTS」停留超過一分鐘然後逾時,或者下一個 apt 任務以 E: Could not get lock /var/lib/dpkg/lock-frontend 死亡。
個別看起來像是三個不相關的錯誤。合在一起,它們是一個問題:控制節點正在與 cloud-init 競速。
根本原因
Cloud-init 尚未完成設定 SSH 守護程式和使用者帳戶,Ansible 就嘗試第一次連線。在 API 看來,您的執行個體是「正在執行」,但實際上還不能使用——這是一個時序競速,完完全全不是防火牆或安全群組設定錯誤。
此外,ansible_python_interpreter 自動探索在仍有 python2 符號連結與 python3 一起提供的發行版上選擇錯誤的二進位檔案,這會靜默地破壞模組而不是大聲失敗。同時,沒有人為引導使用者配置 NOPASSWD sudoers 條目,所以 become 會失敗,除非設定 ansible_become_pass——而這通常沒有設定,因為建立基礎映像的人假設互動式 sudo 沒問題。
最後,雲端提供者的無人值守升級程序或 cloud-init 的初始 apt 執行會在您的劇本 apt: update_cache=yes 觸發時,恰好取得 /var/lib/dpkg/lock-frontend 的 dpkg 鎖。兩個程序,一把鎖,一個輸家。
修復 #1 — 在執行任務前等待 SSH 和 cloud-init 就緒
在主機證明它已就緒之前,不要收集 facts。使用 wait_for_connection 作為 play 中的第一個任務——它從 Ansible 2.3 開始就內建於 ansible.builtin,不需要安裝額外的 collection。
- name: Wait for SSH to become available
ansible.builtin.wait_for_connection:
delay: 10 # 給 cloud-init 一個先發優勢再探測
timeout: 300 # 某些提供者的冷啟動需要 2-4 分鐘
sleep: 5
然後檢查實際的 cloud-init 完成標記,而不是用 sleep() 來猜測:
- name: Wait for cloud-init to finish
ansible.builtin.wait_for:
path: /var/lib/cloud/instance/boot-finished
timeout: 180
注意事項: 如果您在 SSH 連線外掛程式中加入重試邏輯,但保留 play 的 gather_facts: true,Ansible 會在您的就緒任務執行前就嘗試收集 facts——整個 play 會在您的重試啟動前逾時。在 play 層級設定 gather_facts: false,並在就緒檢查通過後明確收集 facts。另外提高 ansible.cfg 的預設值——標準 [ssh_connection] retries = 3, timeout = 10 是針對溫啟動主機調整的,而不是冷啟動雲端執行個體。
修復 #2 — 使用適當的特權提升修復 become/sudo 失敗
最乾淨的修復發生在 Ansible 連線之前:在執行個體建立時將 sudoers 檔案烘焙到 cloud-init user-data 中。
# cloud-init user-data 片段
runcmd:
- echo "ansible ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ansible-bootstrap
- chmod 440 /etc/sudoers.d/ansible-bootstrap
在您信任它之前驗證它實際上是否正常運作:
ansible -m command -a "sudo -n true" host
# 結束代碼 0 = 無密碼 sudo 正常運作
# 結束代碼 1 = 設定錯誤,請先修復再繼續
在劇本中,使用清楚的訊息快速失敗,而不是讓後續每個任務都以隱晦的錯誤死亡:
- name: Verify passwordless sudo works
ansible.builtin.command: sudo -n true
register: sudo_check
changed_when: false
failed_when: false
- name: Fail with clear message if sudo not configured
ansible.builtin.fail:
msg: "NOPASSWD sudo not configured for {{ ansible_user }} — fix cloud-init user-data"
when: sudo_check.rc != 0
注意: 如果您的環境需要基於密碼的 sudo,請使用 ansible-vault encrypt_string 儲存 ansible_become_pass。我見過不止一個儲存庫在 group_vars/all.yml 中放置明文 sudo 密碼,已提交並推送。這是一個五分鐘的修復,可以防止非常糟糕的一天。另外——將 NOPASSWD 條目範圍限定在引導使用者身上,並在初始佈建完成後輪換或移除它。如果該帳戶以後遭到入侵,保留廣泛的 NOPASSWD 存取權限會造成特權提升風險。
修復 #3 — 固定 Python 直譯器並處理 apt 鎖競爭
停止依賴 auto 探索。特別是在 Debian 11/12 和 Ubuntu 22.04/24.04 映像中,明確地在 group_vars 中固定它,因為探索邏輯之前已經讓人們受傷:
ansible_python_interpreter: /usr/bin/python3
ansible_python_interpreter: auto_silent 只是隱藏警告——它不會修復錯誤的二進位檔案選擇。如果您不確定發生什麼事,請執行 ansible-playbook -vvv 並檢查 SSH 命令輸出中的確切直譯器路徑。這是確認不匹配的最快方法。
對於 dpkg 鎖,最乾淨的修復是 apt 模組上的原生 lock_timeout 參數(Ansible 2.10+),結合重試迴圈:
- name: Update apt cache with retry
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
lock_timeout: 60 # 原生鎖處理,不需要手動 fuser 迴圈
become: true
register: apt_result
retries: 5
delay: 10
until: apt_result is succeeded
如果您使用的是沒有 lock_timeout 的舊版 Ansible core,請改用手動等待——檢查兩個鎖路徑,因為不同 dpkg 版本使用不同的路徑:
- name: Wait for dpkg lock to release
ansible.builtin.shell: |
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do sleep 1; done
changed_when: false
become: true
這是完整的組合劇本,已經在針對 Ubuntu 22.04 和 Debian 12 的 Python 3.11 控制節點上的 Ansible core 2.16.3 上測試過:
# playbook: bootstrap.yml
---
- name: Bootstrap fresh cloud servers
hosts: new_servers
gather_facts: false
become: false
vars:
ansible_python_interpreter: /usr/bin/python3
tasks:
- name: Wait for SSH to become available
ansible.builtin.wait_for_connection:
delay: 10
timeout: 300
sleep: 5
- name: Wait for cloud-init to finish
ansible.builtin.wait_for:
path: /var/lib/cloud/instance/boot-finished
timeout: 180
- name: Now gather facts safely
ansible.builtin.setup:
- name: Verify passwordless sudo works
ansible.builtin.command: sudo -n true
register: sudo_check
changed_when: false
failed_when: false
- name: Fail with clear message if sudo not configured
ansible.builtin.fail:
msg: "NOPASSWD sudo not configured for {{ ansible_user }} — fix cloud-init user-data"
when: sudo_check.rc != 0
- name: Wait for dpkg lock to release
ansible.builtin.shell: |
while fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; do sleep 1; done
changed_when: false
become: true
- name: Update apt cache with retry
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
lock_timeout: 60
become: true
register: apt_result
retries: 5
delay: 10
until: apt_result is succeeded
以及相對應的 ansible.cfg——這是讓上述修復在整個機群中穩定運作,而不是只有一個幸運執行個體的關鍵:
# ansible.cfg — 針對冷啟動雲端執行個體調整
[defaults]
timeout = 30
host_key_checking = False
retry_files_enabled = False
[ssh_connection]
retries = 5
timeout = 30
pipelining = True
請注意,收集 facts 的逾時位於 [defaults] timeout 下,而不是某個獨立的 fact-caching 設定。我見過工程師在快取設定中鑽研二十分鐘,而真正的旋鈕就放在那裡。
預防
真正的修復根本不在劇本中——它在上游,在基礎映像或 user-data 指令碼中。將就緒狀態(SSH 設定、sudoers、套件管理員狀態)烘焙到映像中,這樣 Ansible 就不必一開始就與競速條件搏鬥。這是修補症狀與移除根本原因之間的差別。
下次使用的實用檢查清單:
- 在它觸及真實雲端 API 之前,透過 Molecule 使用 Docker 驅動程式(
molecule-plugins[docker]v23.x)在 CI 中執行引導劇本。 - 在您的 requirements 檔案中固定
ansible-core,並在requirements.yml中鎖定 collection 版本——直譯器和模組行為會在次要版本之間靜默地改變。 - 記錄每個主機的引導執行持續時間和失敗類型。如果 cloud-init 在映像更新後突然多花 90 秒,您需要一個顯示該情況的圖表,而不是凌晨 2 點的憤怒 Slack 訊息。
- 注意無限制的
wait_for_connection逾時——200 台主機清單中每台主機 600 秒以上可能會無聲地增加 30 分鐘以上,如果主機是緩慢失敗而不是快速失敗,會增加您的 CI 執行器費用。 - 如果
boot-finished永遠不會出現,請先檢查目標上的/var/log/cloud-init-output.log——這通常是實際 cloud-init 錯誤所在的地方。
我們在 kuryzhev.cloud 上涵蓋更多這些佈建邊緣案例和 CI 強化模式——如果您對自動擴展群組或 spot 機群執行 Ansible,而這個競速經常出現,值得一看。參考官方 wait_for_connection 模組文件 以取得完整參數清單,如果您需要確切了解特定提供者映像上何時寫入 boot-finished 標記,請參考 cloud-init 自己的 啟動階段文件。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.