这次我们来看一个多台 Ubuntu 服务器初始化脚本的实战方案。场景非常常见:新到一批 Ubuntu 服务器,要统一做基础配置——设置时区、安装常用工具、装 Docker、配置 Zabbix Agent、加固 SSH。如果一台一台登录去敲命令,几台还能接受,几十台就非常浪费时间,而且每台的执行结果对不对也没有统一记录。更麻烦的是,同一套步骤在不同机器上执行,经常会因为漏了一条命令产生差异,后面排查起来很头疼。
这个项目的核心不是框架,而是两个 bash 脚本:一个控制端脚本run-remote-init.sh,读取hosts.ini里的主机清单,通过 scp 把初始化脚本上传到目标机器,再通过 ssh 批量执行;另一个是目标端脚本ubuntu-node-init.sh,负责在 Ubuntu 上完成分阶段初始化。从设计上重点解决四个问题:主机清单驱动、幂等可重跑、并发可控、日志与摘要完整。
硬件门槛很低。控制端一台能跑 bash 的 Linux 或 macOS 主机就行,一般 2 核 4G 足够;目标主机建议 Ubuntu 18.04 或 20.04 以上,开启 SSH 服务,并且有一个具备 sudo 权限的账号。磁盘上脚本只占几 MB,真正的开销主要在 ssh、scp 连接和 apt 更新时目标主机的 CPU、磁盘 IO。
这篇文章会给出完整可复制的脚本代码,按“环境准备 → SSH 免密配置 → 编写脚本 → 单机验证 → 批量执行 → 并发控制 → 结果复核 → 问题排查”的顺序展开。运维、DevOps、实验室管理员和手里有多台 Ubuntu 机器需要批量初始化的人,都可以直接照做。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | 基于 bash + ssh + scp 自建初始化脚本,无外部框架依赖 |
| 目标系统 | Ubuntu 18.04 / 20.04 / 22.04,建议 20.04 以上 |
| 控制端要求 | Linux 或 macOS,bash 4.3 以上,能访问目标主机 22 端口 |
| 目标主机要求 | 开启 SSH 服务,具备 root 或 sudo 权限,磁盘剩余空间建议 5GB 以上 |
| 执行方式 | 控制端读取主机清单,scp 上传脚本,ssh 远程执行 |
| 主机清单 | hosts.ini 每行一个user@host,支持注释和空行 |
| 并发控制 | 通过环境变量CONCURRENCY设置,默认 4,避免一次性压垮网络 |
| 幂等重跑 | 脚本内每个任务函数都做存在性判断,重复执行不会重复添加配置 |
| 日志与摘要 | 每台主机独立日志文件,执行结束输出 OK/FAIL 汇总清单 |
| 接口 API | 不提供 HTTP API,批量任务由脚本直接驱动;如需 Web 调用可自行封装 |
| 适用场景 | 新机器批量初始化、统一安全基线、批量安装 Docker/监控 Agent |
这个项目的核心价值在于“可维护、可重跑、可验证”。初始化不是一次性操作,后续很多机器会补货,或者某台机器初始化到一半失败,需要重新跑。如果脚本不具备幂等性,重跑就会产生重复配置、重复安装、配置漂移,所以设计时把幂等放在第一位。
2. 适用场景与使用边界
这个方案最适合三类场景。第一类是一次性批量初始化大量同规格 Ubuntu 主机,比如云上新开 30 台机器,需要统一装基础软件、统一时区、统一用户名;第二类是构建统一安全基线,例如关闭 SSH 密码登录、开启 UFW、配置自动安全更新;第三类是实验室或内部环境,没有现成的 Ansible/SaltStack 体系,临时需要一套轻量批量执行手段。
不适合的场景也很明确。如果团队已经在用 Ansible、SaltStack、Puppet 这类配置管理工具,并且有完善的 inventory 和 playbook,就没必要用这个脚本替代,直接用配置管理工具才是正确选择。另外,如果目标主机异构程度很高,有 Ubuntu、CentOS、Debian 混用,这套脚本还需要按发行版拆分,不建议在一个脚本里硬塞太多分支条件,否则后期维护成本会上升。
安全边界需要特别强调。脚本会在目标主机上用 root 权限执行,所有代码必须经过审查才能进入生产环境。涉及 SSH 加固的步骤,一定要确认所有管理入口都已经配置好公钥,否则关闭密码登录后可能把自己锁在外面。涉及防火墙开启的步骤,先放行当前管理网段,再执行ufw enable。如果脚本里包含服务器 IP、密钥、账号等敏感信息,不要明文写在仓库里,建议用变量注入或单独管理。任何时候都不要在脚本中硬编码真实生产环境的密码。
另外,目标主机可能不是完全空白的系统。如果机器上已经运行了业务,跑初始化脚本前必须确认脚本不会覆盖现有配置,比如不会重置/etc/ssh/sshd_config、不会卸载已有软件、不会修改正在使用的网络配置。建议先在测试机跑通,再灰度到生产。
3. 环境准备与前置条件
3.1 控制端检查清单
控制端不需要安装额外软件,系统自带 OpenSSH 客户端即可。先确认本机 ssh 可用,并记录 bash 版本:
which ssh which scp bash --version | head -n 1建议 bash 4.3 以上。后面批量并发控制里会用到wait -n,这个参数要求 bash 4.3+。如果控制端 bash 版本偏低,要么升级 bash,要么把并发控制改成轮询jobs -r的方式。
确认控制端能够访问目标主机的 22 端口。不要等到批量执行时才发现网络不通,提前用一个简单的 ssh 连通性测试扫一遍所有主机:
while read -r host; do [ -z "$host" ] && continue [[ "$host" == \#* ]] && continue if ssh -o BatchMode=yes -o ConnectTimeout=5 "$host" "echo ok" >/dev/null 2>&1; then echo "$host 连通" else echo "$host 不通" fi done < hosts.ini这个命令要求 hosts.ini 已经存在,并且已经配置好免密登录。如果还没配,先不要批量测试,逐台执行也可以。
3.2 目标主机检查清单
目标主机需要满足几个条件:系统是 Ubuntu,SSH 服务已启动,当前登录用户有 sudo 权限,磁盘剩余空间足够 apt 更新和软件安装。逐台登录确认的耗时比较长,可以在初始化脚本里加一个前置检查任务,自动判断环境是否满足要求。
下面是目标主机常见的预检查命令:
# 查看系统版本 cat /etc/os-release # 查看磁盘空间 df -h / # 确认 sudo 权限 sudo -n true && echo "sudo ok" # 确认 SSH 服务状态 systemctl is-active ssh || systemctl is-active sshd3.3 网络与端口要求
如果目标主机在云环境,安全组或防火墙需要放行控制端来源 IP 的 22 端口访问。如果目标主机在机房,要确认机房网络策略允许控制端主动建立 SSH 连接。整个初始化过程是控制端主动连目标主机,不需要在目标主机上开放额外入站端口,除非你选择在目标主机上开启 HTTP 回调等自定义功能。
初始化脚本本身会在目标主机上执行 apt 更新、下载软件包,因此目标主机还需要能访问外网的 apt 源。对于无法直连外网的内网环境,需要提前配置好内网 apt 源,或者把离线 deb 包准备好,再调整脚本中的安装逻辑。
4. SSH 免密登录与主机清单准备
4.1 生成密钥并推送公钥
批量执行的前提是控制端到目标主机实现 SSH 免密登录。如果还没有密钥,先生成一对:
ssh-keygen -t ed25519 -C "ubuntu-init-control" -f ~/.ssh/id_ed25519生成后不要直接对几十台机器逐台输入密码,先用ssh-copy-id把公钥推送到第一批测试机上:
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@192.168.1.101如果目标主机只允许普通用户登录,先推给这个普通用户,后续通过 sudo 执行初始化:
ssh-copy-id -i ~/.ssh/id_ed25519.pub ops@192.168.1.101推送完成后,验证一条命令能免密执行:
ssh ops@192.168.1.101 "sudo -n true && echo ok"这一条通过后,后续所有批量执行都基于同一个认证通道,不会再出现交互式密码提示。
4.2 hosts.ini 主机清单格式
主机清单文件是控制端脚本的输入。推荐每行一个目标主机,格式为user@host,支持注释和空行。这样scp和ssh命令不需要额外解析字段,直接用整行作为连接目标。
# hosts.ini # 每行一个目标主机,格式为 user@host # 先用 # 注释掉暂时不想执行的主机 ops@192.168.1.101 ops@192.168.1.102 ops@192.168.1.103 root@node-04.example.com如果目标主机的 SSH 端口不是默认 22,建议不要直接把端口写进user@host,而是通过~/.ssh/config配置别名。例如在控制端~/.ssh/config中写:
Host node-01 HostName 192.168.1.101 Port 2222 User ops IdentityFile ~/.ssh/id_ed25519然后 hosts.ini 里直接写node-01,控制端脚本就无需处理端口解析逻辑,更加简洁可靠。批量初始化这种场景,统一用默认 22 端口最简单,特殊端口的主机单独走 SSH config 别名。
5. 控制端批量执行脚本 run-remote-init.sh
控制端脚本负责三件事:读取主机清单、上传目标端脚本、远程执行并收集结果。下面是完整实现。
#!/usr/bin/env bash # run-remote-init.sh # 多台 Ubuntu 服务器批量初始化控制端脚本 set -uo pipefail HOST_FILE="${HOST_FILE:-hosts.ini}" REMOTE_SCRIPT="${REMOTE_SCRIPT:-ubuntu-node-init.sh}" REMOTE_PATH="/tmp/ubuntu-node-init.sh" # 并发数,环境变量可覆盖,默认 4 CONCURRENCY="${CONCURRENCY:-4}" LOGS_DIR="logs" SUMMARY_DIR="summary" mkdir -p "$LOGS_DIR" "$SUMMARY_DIR" summary_file="$SUMMARY_DIR/summary-$(date +%Y%m%d-%H%M%S).txt" : > "$summary_file" running=0 run_one_host() { local host="$1" local tag tag=$(echo "$host" | tr -c 'a-zA-Z0-9._-' '_') local log_file="$LOGS_DIR/${tag}.log" echo "[$(date '+%F %T')] 开始 $host" | tee -a "$log_file" if scp -q "$REMOTE_SCRIPT" "$host:$REMOTE_PATH" >>"$log_file" 2>&1; then if ssh -o ConnectTimeout=10 -o BatchMode=yes "$host" "sudo bash $REMOTE_PATH" >>"$log_file" 2>&1; then echo "[$(date '+%F %T')] OK $host" | tee -a "$log_file" echo "OK $host" >> "$summary_file" else echo "[$(date '+%F %T')] FAIL $host" | tee -a "$log_file" echo "FAIL $host" >> "$summary_file" fi else echo "[$(date '+%F %T')] SCP_FAIL $host" | tee -a "$log_file" echo "FAIL $host" >> "$summary_file" fi } while IFS= read -r host; do [[ -z "$host" ]] && continue [[ "$host" == \#* ]] && continue run_one_host "$host" & running=$((running + 1)) if [ "$running" -ge "$CONCURRENCY" ]; then wait -n running=$((running - 1)) fi done < "$HOST_FILE" wait echo "==================== 批量初始化汇总 ====================" cat "$summary_file" echo "========================================================"脚本逻辑说明如下。
第一,set -uo pipefail开启未定义变量检查和管道失败检测,但不开启set -e。原因在于批量执行时一台主机失败不应该中断整个脚本,失败信息要进入日志和汇总文件,而不是直接退出。
第二,run_one_host函数在子 shell 中后台执行,因此 OK/FAIL 结果写入$summary_file而不是通过变量返回,避免丢失子进程状态。
第三,SSH 参数使用-o BatchMode=yes,如果某台主机免密没配好,会直接失败而不是卡在密码交互界面。这个设计很重要,批量执行时不希望有任何交互等待。
第四,wait -n是并发控制的核心。后台任务数达到CONCURRENCY后,等待任意一个任务结束再继续拉起下一个,从而把同时执行的 SSH 进程数量限制在设定值内。如果控制端 bash 低于 4.3,可以把这段替换成:
if [ "$running" -ge "$CONCURRENCY" ]; then while [ "$(jobs -r | wc -l)" -ge "$CONCURRENCY" ]; do sleep 1 done running=$((running - 1)) fi两种写法都能限制并发,实际效果差不多,wait -n更接近事件驱动,轮询方式兼容性更好。
6. 目标端初始化脚本 ubuntu-node-init.sh
目标端脚本是整个方案的核心。它被 scp 上传到/tmp后,由 sudo 执行。脚本采用“任务函数 + 分阶段运行”的结构,每个任务函数都必须是幂等的。
#!/usr/bin/env bash # ubuntu-node-init.sh # 在目标 Ubuntu 主机上执行的初始化脚本 set -uo pipefail LOG_FILE="/var/log/ubuntu-node-init.log" exec > >(tee -a "$LOG_FILE") 2>&1 # 阶段执行器 run_stage() { local stage="$1" shift echo "===== [$stage] 开始 =====" for task_func in "$@"; do echo "----- 任务: $task_func -----" if "$task_func"; then echo "任务成功: $task_func" else echo "任务失败: $task_func" fi done echo "===== [$stage] 结束 =====" }这种设计的好处是新增一个任务只需要写一个函数,然后在对应阶段里加上函数名。某个阶段失败不会中断后续阶段,最终可以通过日志看出哪一个任务失败。
6.1 阶段一:基础配置
基础配置阶段处理时区、系统更新、时间同步。时区设置先判断当前值再修改,避免脚本重复执行时每次都会触发timedatectl。
task_set_timezone() { local current current=$(timedatectl show -p Timezone --value 2>/dev/null || echo "UTC") if [ "$current" != "Asia/Shanghai" ]; then timedatectl set-timezone Asia/Shanghai fi echo "当前时区: $(timedatectl show -p Timezone --value)" } task_update_apt() { export DEBIAN_FRONTEND=noninteractive apt-get update -y apt-get upgrade -y --no-install-recommends || true } task_enable_timesync() { timedatectl set-ntp true systemctl enable --now systemd-timesyncd || true }task_update_apt中apt-get upgrade加|| true,是因为某些环境有包被 hold 或者内核升级需要重启,如果因为单个包失败导致整个脚本中断,后续阶段全部无法执行。初始化阶段的目标是尽量推进到可交付状态,而不是追求单次升级零失败。
6.2 阶段二:基础软件安装
基础软件安装要逐个判断包是否已经存在。dpkg -s返回 0 表示包已安装,此时跳过安装,这就是幂等性的基础。
task_install_packages() { local packages=(curl wget git vim htop tree jq net-tools unzip ca-certificates gnupg lsb-release) export DEBIAN_FRONTEND=noninteractive for pkg in "${packages[@]}"; do if dpkg -s "$pkg" >/dev/null 2>&1; then echo "包 $pkg 已存在,跳过" else echo "安装 $pkg" apt-get install -y "$pkg" fi done }有些环境里 unzip 未安装会导致后续脚本解压失败,因此基础工具包的清单里尽量覆盖常见运维工具。读者可以按自己团队的软件标准增删。
创建统一运维用户是批量初始化的常见需求。下面这个函数在 root 用户下执行时会创建一个ops用户并加入 sudo 组,同时把 root 的authorized_keys复制过去,确保运维可以通过同一把公钥登录。
task_create_ops_user() { local user="${OPS_USER:-ops}" if id "$user" >/dev/null 2>&1; then echo "用户 $user 已存在,跳过" else useradd -m -s /bin/bash "$user" usermod -aG sudo "$user" fi mkdir -p "/home/$user/.ssh" cp /root/.ssh/authorized_keys "/home/$user/.ssh/authorized_keys" 2>/dev/null || true chown -R "$user":"$user" "/home/$user/.ssh" chmod 700 "/home/$user/.ssh" chmod 600 "/home/$user/.ssh/authorized_keys" }6.3 阶段三:Docker 安装
Docker 安装需要先判断是否已经安装。已经安装的机器直接返回,避免重复拉取仓库和安装包。安装流程参考 Docker 官方 Ubuntu 安装文档,使用/etc/os-release中的版本代号动态生成 apt 源,适配不同 Ubuntu 大版本。
task_install_docker() { if command -v docker >/dev/null 2>&1; then echo "Docker 已安装: $(docker --version)" return 0 fi export DEBIAN_FRONTEND=noninteractive # 清理可能存在的旧版本 for pkg in docker docker-engine docker.io containerd runc; do apt-get remove -y "$pkg" 2>/dev/null || true done install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc source /etc/os-release echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $VERSION_CODENAME stable" > /etc/apt/sources.list.d/docker.list apt-get update apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin systemctl enable --now docker }这里有一个细节需要关注:Docker 官方安装文档在不同时期的 GPG key 路径写法有变化,较早版本用的是/etc/apt/keyrings/docker.gpg,新版可能用/etc/apt/keyrings/docker.asc。实际部署时以目标系统对应的 Docker 官方文档为准,如果出现 GPG 校验失败,可以先检查 keyring 文件是否存在,再确认 apt 源里的signed-by路径是否匹配。
6.4 阶段四:SSH 加固与防火墙
SSH 加固是整个初始化脚本里风险最高的步骤。执行前必须确保公钥登录已经验证过。下面的函数用sed修改sshd_config,而不是直接覆盖整个文件,这样不会影响目标主机原有的其他 SSH 配置。
task_harden_ssh() { local sshd_config="/etc/ssh/sshd_config" # PermitRootLogin 设置为 prohibit-password if grep -qE '^PermitRootLogin' "$sshd_config"; then sed -i 's/^PermitRootLogin .*/PermitRootLogin prohibit-password/' "$sshd_config" else echo "PermitRootLogin prohibit-password" >> "$sshd_config" fi # 关闭密码登录 if grep -qE '^PasswordAuthentication' "$sshd_config"; then sed -i 's/^PasswordAuthentication .*/PasswordAuthentication no/' "$sshd_config" else echo "PasswordAuthentication no" >> "$sshd_config" fi systemctl restart ssh }注意,如果目标主机只有密码登录一种方式,关闭PasswordAuthentication之后当前登录会话虽然没有断开,但下次登录会失败。因此这个函数必须放在公钥免密配置验证之后,或者先用ops用户的公钥登录测试通过再执行加固。
防火墙函数同样要注意不要把管理通道断掉。下面示例只放行 22 端口,实际使用时把管理网段先加白名单,再放行业务端口。
task_setup_ufw() { if ! command -v ufw >/dev/null 2>&1; then echo "ufw 未安装,跳过" return 0 fi # 先放行管理网段,不要直接 enable # ufw allow from 203.0.113.0/24 to any port 22 ufw allow 22/tcp # ufw allow 80,443/tcp ufw --force enable }ufw --force enable用于非交互式启用,不加--force时 UFW 会询问是否确认,批量执行时会卡住。
6.5 阶段五:Zabbix Agent 扩展模板
Zabbix Agent 不是每台机器都需要的,因此这里只给出函数模板。不同 Zabbix 大版本的官方源地址差异较大,不做统一硬编码,需要按实际版本替换仓库地址。
task_install_zabbix_agent() { if command -v zabbix_agentd >/dev/null 2>&1; then echo "Zabbix Agent 已安装,跳过" return 0 fi # 按 Zabbix 官方文档添加对应 Ubuntu 版本的仓库 # 示例:先下载并安装 zabbix-release 包 # 再 apt-get install -y zabbix-agent # 最后修改 /etc/zabbix/zabbix_agentd.conf 中的 Server 和 ServerActive apt-get install -y zabbix-agent || { echo "Zabbix Agent 安装失败,请检查官方源" return 1 } systemctl enable --now zabbix-agent }Zabbix Agent 的仓库配置涉及具体版本号,建议先在一台测试机上手动完成安装,把确认可用的仓库地址写进脚本,再批量推广。
6.6 主函数组装
主函数按阶段调用,便于单独跳过或重新执行某个阶段。
main() { run_stage "阶段1 基础配置" \ task_set_timezone \ task_update_apt \ task_enable_timesync run_stage "阶段2 基础软件" \ task_install_packages \ task_create_ops_user run_stage "阶段3 Docker 安装" \ task_install_docker run_stage "阶段4 安全加固" \ task_harden_ssh \ task_setup_ufw # 按需启用 # run_stage "阶段5 Zabbix Agent" \ # task_install_zabbix_agent } main需要说明的是,这里的主函数编排只是一个示例顺序。实际部署时建议把安全加固放到靠后阶段,等确认基础环境全部可用后再执行。这样即使某个基础软件安装失败,也不会因为 SSH 被加固后无法远程登录而陷入被动。
7. 单台验证与批量执行
7.1 先用一台机器走通
批量执行前必须先用单台机器验证。把 hosts.ini 临时改成只剩一台测试机,并发数设为 1:
CONCURRENCY=1 ./run-remote-init.sh执行过程中可以实时看日志:
tail -f logs/ops_192.168.1.101.log日志会同时输出到终端和/var/log/ubuntu-node-init.log的远程日志,因为远程脚本里做了tee输出。第一次执行时重点看三个东西:脚本有没有出现交互式卡住、apt 更新是否成功、SSH 加固后当前连接是否还能保持。
7.2 单机结果验证
脚本执行完成后,在控制端直接通过 SSH 检查目标主机状态:
ssh ops@192.168.1.101 " timedatectl | grep 'Time zone' id ops docker --version systemctl is-active docker ufw status | head -n 5 "判断标准如下:
- 时区显示
Asia/Shanghai,说明基础配置生效。 id ops能显示用户信息,说明运维用户创建成功。docker --version能输出版本号,systemctl is-active docker输出active,说明 Docker 安装正常。ufw status显示Status: active,说明防火墙已启用。
7.3 检查幂等性
单机验证完成后,再执行一次同样的脚本,确认第二次执行不会产生异常。幂等判断的要点包括:软件包不会重复安装、配置文件不会重复追加、SSH 配置不会重复修改、Docker 不会重复安装。第二次执行如果日志里出现大量“已存在,跳过”字样,说明幂等设计在起作用。
下面用命令快速检查是否有重复配置:
ssh ops@192.168.1.101 "grep -c 'PasswordAuthentication no' /etc/ssh/sshd_config"这个命令输出应该是1,如果输出大于 1 说明脚本追加逻辑有问题,需要检查是否缺少 grep 判断。
7.4 批量执行与并发调整
单机验证通过后,把 hosts.ini 还原成完整主机清单,设置一个合理的并发数。初次批量执行建议用CONCURRENCY=4,执行命令如下:
CONCURRENCY=4 ./run-remote-init.sh批量执行期间不用一直盯着终端,可以隔一段时间查看summary/目录下的汇总文件:
while true; do clear cat summary/summary-*.txt sleep 5 done汇总文件里会不断追加 OK 或 FAIL 行。全部执行结束后,重点看 FAIL 主机的日志,定位失败原因。
对于失败主机,可以在确认问题修复后单独重新执行。因为脚本是幂等的,失败主机重跑不会影响已经完成的部分,这是这类脚本相比手工逐台操作的核心优势。
8. 资源占用与性能观察
8.1 控制端资源占用
控制端本身不会占用太多资源。每个并发任务对应一个 scp 进程和一个 ssh 进程,一般 2 核 4G 的机器跑并发 8 到 10 台也没有压力。真正的瓶颈通常不在控制端 CPU,而在网络带宽和控制端到目标主机之间的 SSH 握手速率。
如果 hosts.ini 里包含大量主机,建议先分成小批次执行,例如每批 10 台,执行完一批确认汇总结果后再跑下一批。一次跑 50 台虽然也能工作,但网络抖动导致中间某几台失败时,日志排查会稍微麻烦。
8.2 目标主机资源占用
目标主机的资源峰值主要出现在 apt update 和 apt upgrade 阶段。apt 会启动多个 dpkg 进程,磁盘 IO 和 CPU 占用会明显上升。如果目标主机是生产环境,初始化脚本应该避开业务高峰,或者在 apt upgrade 时手动控制并发数。
Docker 安装阶段需要下载较多软件包,网络延迟高时单台机器可能耗时几分钟。批量并发时控制端不等待某台完成再执行下一台,而是同时发起多台,因此如果 20 台机器同时 apt update,总带宽消耗会比较大。对于网络环境一般的机房,CONCURRENCY=2或CONCURRENCY=4是更稳妥的选择。
8.3 日志与磁盘占用
远程初始化脚本会把日志写入/var/log/ubuntu-node-init.log,apt upgrade 的输出非常长,一台机器全量更新后日志可能达到几 MB。批量几十台的情况下,控制端logs/目录累积的日志量也需要关注。建议定期清理旧日志,或者在上传脚本时加入简单的日志压缩逻辑。
控制端日志命名已经避免了特殊字符冲突。如果同一台机器分多次执行,日志文件会被覆盖,所以建议在关键时间点执行完批量任务后,把整个logs/和summary/目录打包归档:
tar -czf init-$(date +%Y%m%d-%H%M%S).tar.gz logs summary hosts.ini归档文件里包含主机清单、每台机器的日志和最终汇总,后续排查问题时可以直接对照。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| ssh 连接超时 | 目标主机 22 端口不通、安全组未放行 | ssh -v user@host查看连接日志 | 检查安全组、路由和 sshd 服务状态 |
| SSH 连接被拒绝 | 目标主机 sshd 未启动或端口配置变化 | systemctl status ssh或ss -lntp | 启动 sshd 服务 |
| 免密登录未生效 | 公钥未写入目标主机 authorized_keys | 手动执行 ssh 观察是否要密码 | 重新执行 ssh-copy-id |
| scp 上传失败 | 目标主机 /tmp 目录权限异常或磁盘满 | df -h /tmp | 清理临时文件 |
| sudo 执行失败 | 登录用户不在 sudo 组,或 sudoers 配置受限 | 执行sudo -n true | 先用 root 用户执行,或调整 sudoers |
| apt 锁冲突 | 目标主机有 apt 进程残留 | `ps aux | grep apt` |
使用wait -n报错 | 控制端 bash 版本过低 | bash --version | 升级 bash,或替换成 jobs 轮询并发控制 |
| 关闭密码登录后无法登录 | 加固步骤执行时公钥未配置好 | 检查 authorized_keys 是否存在 | 通过云控制台或带外管理恢复登录 |
| Docker GPG 校验失败 | keyring 路径或源地址不匹配 | ls -l /etc/apt/keyrings/ | 按 Docker 官方文档重新配置 |
| 脚本重复执行后配置重复 | 任务函数缺少 grep 判断 | 检查 sshd_config 等配置文件行数 | 修改函数,增加存在性判断 |
| 某台主机初始化中断 | 网络抖动或 apt 下载失败 | 查看该主机独立日志 | 修复后重跑脚本,幂等逻辑会自动跳过已完成任务 |
| 汇总文件里出现 FAIL | 阶段中某个任务函数返回非 0 | 查看对应日志中“任务失败”之前的内容 | 定位具体任务函数并修复 |
最常遇到的其实是两类问题:一类是免密登录没有真正打通,导致 ssh 批量执行时直接失败;另一类是 apt 源问题,比如目标主机内网环境无法访问外网,或 Docker 官方源被网络策略阻断。这两类问题建议在环境准备阶段提前验证,不要等到批量跑的时候才发现。
10. 最佳实践与使用建议
批量初始化脚本要成功落地,而不是写完就扔,建议从以下几个方面约束使用方式。
第一,先小后大。第一次使用无论如何都要先在一台测试机跑通,然后把 hosts.ini 扩展为 3 到 5 台小批量验证,确认无异常后再扩大到全量。不要一上来就 50 台并发执行,否则一旦某个函数存在缺陷,所有机器都会受影响。
第二,分阶段灰度。如果初始化脚本包含安全加固和 Docker 安装,可以先把主函数里的安全加固阶段注释掉,跑完基础配置后再手动放开。尤其是 SSH 加固,必须确认公钥登录已经验证过,再执行这一阶段。
第三,保留最小可运行配置。脚本目录里放一份 hosts.ini.example 和 README,记录清楚每个环境变量、每个阶段的作用,避免三个月后自己都记不清当时的脚本逻辑。脚本里的变量尽量用环境变量注入,比如OPS_USER、CONCURRENCY,不要在脚本内部写死。
第四,批量任务必须有日志和重跑机制。本文的脚本已经具备每台独立日志和汇总输出,实际使用中如果发现某台失败,修复后单独重跑即可。不要删除已有日志重新全量执行,否则会丢失历史记录。
第五,权限和敏感信息管理。脚本本身不写密码,所有认证依赖 SSH 密钥。密钥的私钥文件要严格保护,公钥内容要确保只分发到授权的目标主机。涉及生产环境的服务器 IP、账号名,建议通过变量文件单独管理,不要提交到公共仓库。
第六,效果复核不能省。初始化脚本执行完不等于交付完成。建议在汇总结果全部 OK 后,抽样几台机器检查时区、用户、Docker、防火墙、SSH 登录状态。批量操作越顺利,越要警惕“看起来都成功但实际配置不一致”的情况。
第七,版权与合规提醒。如果脚本里涉及从第三方源下载软件包,比如 Docker、Zabbix,请确认这些软件的使用范围和授权合规。目标主机上的数据、账号、密钥等都属于敏感信息,操作前应确保符合所在团队或机构的运维规范。
11. 总结与下一步
这套多台 Ubuntu 服务器初始化脚本,从架构上看就是一个控制端加一个目标端,中间用 SSH 串联。真正值得借鉴的地方不是某个安装命令,而是“任务函数化、阶段可裁剪、过程有日志、结果有汇总、重跑不重复”的设计方式。把这套思路复制到 CentOS 初始化、Windows 服务器初始化甚至网络设备配置备份上,同样适用。
最优先要验证的功能优先级是:先把 SSH 免密登录打通,再用单台机器跑通基础配置和 Docker 安装,最后才放行安全加固和全量并发。最容易踩的坑也是这两处:免密登录没有真正生效,以及 SSH 加固后无法再次登录。前者可以通过BatchMode=yes提前暴露,后者需要依赖云控制台或带外管理作为兜底。
如果后续要继续扩展,可以考虑把这个脚本封装成带 Web 界面的执行器,或者接入现有的配置管理平台,把每台主机的初始化状态登记到 CMDB。现阶段先用这版脚本,把批量初始化流程跑起来,把日志和汇总机制沉淀下来,后续无论迁移到 Ansible 还是其他自动化工具,这套“执行前有检查、执行中有日志、执行后有复核”的运维习惯都能直接复用。建议收藏备用,下次新机器到了直接打开这篇文章照跑一遍。