用 Ansible playbook 批量初始化服务器:从手忙脚乱到一条命令 用 Ansible playbook 批量初始化服务器:从手忙脚乱到一条命令刚拿到十台新机器,要装 Docker、建部署账号、配 SSH 密钥、关 root 密码登录、装监控 agent……如果你还在一台台 SSH 上去敲命令,不但慢,而且第三台漏了一步、第七台版本装错了都很正常。这篇用一个真实的「服务器初始化」场景,把 Ansible 从零跑通,重点讲那些文档里一笔带过、实际会绊住你的坑。为什么是 Ansible 而不是一坨 shell 脚本先说清楚 Ansible 到底解决了什么。你完全可以写个init.sh然后for host in ...; do ssh $host bash init.sh; done。但 shell 脚本有个致命问题:不幂等。脚本重复跑一遍,useradd deploy第二次就报错退出,echo xxx /etc/hosts会追加两遍。你不敢重复执行,也就没法「补跑失败的那几台」。Ansible 的核心价值就是幂等:每个模块声明的是「最终状态」而不是「执行动作」。user模块说「deploy 用户应该存在」,已经存在就跳过,不存在才创建。同一个 playbook 跑一百遍,结果一致,失败了随时重跑。Ansible 还不需要在目标机装 agent,走 SSH 就行,这也是它比 SaltStack/Puppet 上手快的原因。第一步:装 Ansible 和写 inventory控制端(你的笔记本或跳板机)装 Ansible,目标机什么都不用装,只要有 Python 和 SSH:# Ubuntu/Debian 控制端sudoaptupdatesudoaptinstall-yansible ansible--versioninventory.ini描述你要管哪些机器,按角色分组:[web] web1 ansible_host10.0.1.11 web2 ansible_host10.0.1.12 [db] db1 ansible_host10.0.1.21 # 组的组:all_servers 包含 web 和 db 两组 [all_servers:children] web db # 给所有机器统一配连接参数 [all_servers:vars] ansible_userroot ansible_ssh_private_key_file~/.ssh/id_rsa ansible_python_interpreter/usr/bin/python3先验证能连通,ping模块不是 ICMP,是「能不能 SSH 上去并跑 Python」:ansible all_servers-iinventory.ini-mping看到每台返回ping: pong就通了。这一步跑不通,后面全免谈,先解决连接问题(常见是密钥没免密、或 root 登录被禁)。第二步:写初始化 playbook新建init.yml。这里做一件完整的初始化:建部署用户、下发 SSH 公钥、装基础软件、装 Docker、做一点安全加固。----name:初始化服务器hosts:all_serversbecome:true# 以 sudo 提权执行vars:deploy_user:deploybase_packages:-curl-vim-git-htoptasks:-name:创建部署用户并加入 sudo 组ansible.builtin.user:name:{{ deploy_user }}groups:sudoshell:/bin/bashstate:present-name:下发部署用户的 SSH 公钥ansible.posix.authorized_key:user:{{ deploy_user }}state:present# lookup 从控制端本地读文件内容,不是从目标机读key:{{ lookup(file, ~/.ssh/id_rsa.pub) }}-name:安装基础软件包ansible.builtin.apt:name:{{ base_packages }}state:presentupdate_cache:true# 相当于先 apt update-name:安装 Docker(官方便捷脚本)ansible.builtin.shell:curl-fsSL https://get.docker.com|shargs:# 关键:已经装了就别再跑,手动补幂等creates:/usr/bin/docker-name:把部署用户加入 docker 组(免 sudo 用 docker)ansible.builtin.user:name:{{ deploy_user }}groups:dockerappend:true# append 很重要,否则会覆盖掉上面加的 sudo 组运行:ansible-playbook-iinventory.ini init.yml输出里每个 task 会显示ok(已是目标态,没动)或changed(做了修改)。再跑一遍,你会看到几乎全是ok——这就是幂等的样子。坑一:shell/command 模块默认不幂等,必须自己加 creates上面装 Docker 用了shell模块。shell和command是 Ansible 里少数不幂等的模块——它只管把命令跑一遍,不判断状态。你重复跑 playbook,curl | sh就会又下又装一遍。解决办法是加creates(或removes)参数:creates: /usr/bin/docker的意思是「如果这个文件已存在,就跳过这个 task」。这样第二次跑就不会重装。凡是用shell/command,都要问自己:重复跑会不会出问题?会就加creates/removes,或者换成有幂等语义的专用模块。坑二:append 不写,groups 会互相覆盖注意上面把 deploy 用户先加进sudo组,后面又加进docker组。user模块的groups默认是覆盖语义——第二次设置groups: docker不加append: true的话,会把用户从 sudo 组里踢出去,只留 docker。加了append: true才是「追加到现有组」。这个坑很隐蔽,因为它不报错,只是权限悄悄变了。坑三:安全加固要放最后,别把自己锁在门外初始化常包含「禁用 root 密码登录」这类加固。顺序很重要:必须先确认 deploy 用户能免密登录、能 sudo,再去禁 root,否则你可能把自己锁在门外。加固 task 用lineinfile精确改配置,并在改完后重启 sshd:-name:禁用 SSH 密码登录(仅允许密钥)ansible.builtin.lineinfile:path:/etc/ssh/sshd_configregexp:^#?PasswordAuthenticationline:PasswordAuthentication novalidate:sshd -t -f %s# 改完先校验语法,错了不落盘,防止 sshd 起不来notify:restart sshd# 触发下面的 handlerhandlers:-name:restart sshdansible.builtin.service:name:sshstate:restarted这里两个关键点:validate让 Ansible 在写入前先用sshd -t检查配置语法,语法错了直接失败、不破坏原文件;notifyhandlers是 Ansible 的经典模式——只有配置真的改变了才触发重启,没改动就不重启,避免每次跑 playbook 都无谓重启 sshd。只对部分机器跑 / 只跑某些步骤日常运维你常常只想操作一部分机器或一部分步骤。给 task 打 tag:-name:安装 Dockeransible.builtin.shell:curl-fsSL https://get.docker.com|shargs:creates:/usr/bin/dockertags:[docker]然后:# 只对 web 组跑ansible-playbook-iinventory.ini init.yml--limitweb# 只跑打了 docker 标签的 taskansible-playbook-iinventory.ini init.yml--tagsdocker# 先看看会改什么,但不真改(彩排模式)ansible-playbook-iinventory.ini init.yml--check--diff--check --diff尤其有用:上生产前先彩排一遍,看清楚每台会发生什么改动,再真正执行。小结Ansible 相比 shell 脚本的核心优势是幂等——声明最终状态而非动作,同一 playbook 跑一百遍结果一致,失败可随时重跑补齐。目标机零依赖(只需 Python SSH),先用-m ping确认连通再干活。shell/command是少数不幂等的模块,重复执行有副作用的一定要加creates/removes,或换成专用幂等模块。user模块改groups记得append: true,否则组会被覆盖,权限悄悄变错还不报错。安全加固放最后、改 sshd 用validate校验语法 notifyhandler 按需重启;上生产前用--check --diff彩排。一句话记忆点:Ansible 管的是「机器该是什么样」,不是「你要跑什么命令」——想清楚最终态,幂等自然就有了。