最初发布于 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 符号链接的发行版上选择错误的二进制文件,导致模块静默中断而非大声失败。同时,没有人为引导用户配置 NOPASSWD sudoers 条目,因此除非设置 ansible_become_pass(通常未设置,因为基础镜像的创建者假定交互式 sudo 即可)。
最后,云提供商自己的无人值守升级进程或 cloud-init 的初始 apt 运行会在您的剧本 apt: update_cache=yes 触发时抢占 /var/lib/dpkg/lock-frontend 的 dpkg 锁。两个进程,一个锁,一个输家。
修复 #1 — 在运行任务前等待 SSH 和 cloud-init 就绪
在主机证明就绪前不要收集事实。将 wait_for_connection 作为 play 中的字面第一个任务使用——它自 Ansible 2.3 起包含在 ansible.builtin 中,无需额外安装集合。
- name: Wait for SSH to become available
ansible.builtin.wait_for_connection:
delay: 10 # give cloud-init a head start before probing
timeout: 300 # cold boots on some providers take 2-4 minutes
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 会在就绪任务运行前尝试收集事实——整个 play 在重试生效前超时。在 play 级别设置 gather_facts: false,并在就绪检查通过后显式收集事实。同时提高 ansible.cfg 的默认值——原有的 [ssh_connection] retries = 3, timeout = 10 是为温机调整的,不是冷启动的云实例。
修复 #2 — 使用正确的权限提升修复 become/sudo 失败
最干净的修复发生在 Ansible 连接之前:在实例创建时将 sudoers 文件烘焙到 cloud-init user-data 中。
# cloud-init user-data snippet
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
# exit code 0 = passwordless sudo works
# exit code 1 = misconfigured, fix before continuing
在剧本中,快速失败并显示清晰消息,而不是让每个后续任务都以隐晦错误失败:
- 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 发现。显式固定在 group_vars 中,尤其是在 Debian 11/12 和 Ubuntu 22.04/24.04 镜像上,发现逻辑之前已经让人们受苦:
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 # native lock handling, no manual fuser loop needed
become: true
register: apt_result
retries: 5
delay: 10
until: apt_result is succeeded
如果您使用的是没有 lock_timeout 的较旧 Ansible 核心,请回退到手动等待——检查两个锁路径,因为不同 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
以下是完整的组装剧本,已在 Ansible core 2.16.3、Python 3.11 控制节点上测试,针对 Ubuntu 22.04 和 Debian 12:
# 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 — tuned for cold-boot cloud instances
[defaults]
timeout = 30
host_key_checking = False
retry_files_enabled = False
[ssh_connection]
retries = 5
timeout = 30
pipelining = True
注意,事实收集超时位于 [defaults] timeout 下,而不是某个单独的事实缓存设置。我见过工程师在缓存配置中挖掘二十分钟,而真正的旋钮就在那里。
预防
真正的修复根本不在剧本中——而在上游,在基础镜像或 user-data 脚本中。将就绪状态(SSH 配置、sudoers、包管理器状态)烘焙到镜像中,这样 Ansible 就不必首先与竞争条件斗争。这就是修补症状和消除根本原因之间的区别。
下次使用的实用清单:
- 在触及真实云 API 之前,使用 Docker 驱动程序(
molecule-plugins[docker]v23.x)通过 Molecule 在 CI 中运行引导剧本。 - 在 requirements 文件中固定
ansible-core,并在requirements.yml中锁定集合版本——解释器和模块行为在次要版本之间会静默漂移。 - 记录每个主机的引导运行持续时间和失败类型。如果 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 强化模式——如果您在针对自动扩展组或现货舰队运行 Ansible(这种竞争经常出现),值得一看。参考官方 wait_for_connection 模块文档 获取完整参数列表,以及 cloud-init 自己的 引导阶段文档,如果您需要了解特定提供商镜像上何时写入 boot-finished 标记。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.