news 2026/10/22 7:45:47

多台 Ubuntu 服务器初始化脚本实战:从手动配置到一键交付

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多台 Ubuntu 服务器初始化脚本实战:从手动配置到一键交付

多台 Ubuntu 服务器初始化脚本实战:从手动配置到一键交付

如果你维护过三台以上的 Linux 服务器,大概率经历过这种场景:新机器拿到手,先改 root 密码、创建普通用户、配置 SSH 密钥登录、关掉密码登录、换阿里云源或清华源、装 Docker、调系统参数、配时间同步……一套流程下来快则半小时,慢则一上午。最尴尬的是,你以为自己已经"熟练得闭着眼都能配",结果到了第三台机器,还是漏了sysctl里的某个参数,或者忘了给新用户加sudo权限。

这个问题的本质不是"你不会配",而是手动重复操作必然导致不一致。服务器初始化看起来是运维的基本功,但它恰恰是出错率最高的环节之一。因为每一台机器的系统版本、网络环境、CPU 架构、磁盘分区都可能不同,而你在手动执行时,每一步都依赖"上一次的记忆"。

本文要解决的就是这个问题:把多台 Ubuntu 服务器的初始化过程,从"手动敲命令"变成"一条脚本跑完"。我会从初始化脚本的需求拆解讲起,给出一个模块化、可复用、带日志和校验的 Shell 脚本示例,并说明批量执行时最容易踩的坑。无论你是刚接手服务器的新人,还是正在搭建公司内部初始化规范的运维,这篇文章都值得收藏。

读完之后,你会得到三样东西:一套可以直接改改就用的 Ubuntu 初始化脚本模板;一套批量执行时的验证和安全检查思路;以及一份面对"脚本报错"时的排查路径。而不是"收藏了一堆命令,下次还是手动敲"。

1. 这篇文章真正要解决的问题

先想一个问题:服务器初始化的目标到底是什么?

有人说是"装好环境",有人说是"能用就行"。但从工程角度看,初始化的目标只有一个:让一批新机器达到一致的、可预期的、安全可控的初始状态。一致性代表每台机器的用户、目录、权限、软件版本相同;可预期代表你知道跑完脚本之后系统变成什么样;安全可控代表默认配置不是"能用"而是"够安全"。

围绕这个目标,手动操作有三个致命问题:

第一,遗漏成本高。比如你忘了关闭 root 密码登录,可能不会立刻出事,但服务器一旦暴露在公网,扫描器发现开放了 22 端口,密码爆破是分分钟的事。这类问题不会在当天暴露,但会在某次安全巡检时成为事故。

第二,版本漂移严重。第一台机器你装了 Docker 24.0,第二台机器一个月后装的 Docker 26.0,两台机器的容器运行行为可能就有差异。开发环境不明显,生产环境迁移时就是大问题。

第三,人肉操作不可审计。谁改了什么、什么时候改的、改了哪台机器,手动操作全靠群聊记录和个人记忆。出了问题,排查范围是整个机房。

所以,初始化脚本不是"懒得敲命令"的偷懒工具,而是把运维经验和安全基线固化成代码的手段。它让"初始化"这件事从依赖个人熟练度,变成依赖版本管理的工程行为。

什么样的读者最应该读这篇文章?

  • 刚接手多台 Ubuntu 服务器,每次都是手动配置的开发者或运维。
  • 团队正在从"一台台手动部署"转向"脚本化、自动化"的过渡阶段。
  • 需要为新员工或新环境准备一套统一初始化模板的技术负责人。

如果只是单台临时测试机,手动配一下问题不大;但只要超过三台、或者这些机器要长期运行,脚本化就很有必要。

2. Ubuntu 初始化的核心概念与常见误区

2.1 什么是初始化脚本

初始化脚本并不是一个严谨的技术名词,它更像一个约定俗成的概念:服务器交付后、正式接入业务之前,需要执行的一系列配置动作的集合。这些动作包括但不限于:

  • 系统基础配置:主机名、时区、语言环境。
  • 用户与权限:创建管理用户、配置 sudo、设置 SSH 密钥登录。
  • 网络安全:防火墙策略、SSH 端口加固、禁止 root 登录。
  • 软件源与基础软件:更换 APT 镜像源、安装常用工具。
  • 内核与系统参数:文件描述符限制、TCP 参数、内核转发。
  • 运维工具链:Docker、Python、监控 Agent 等。

每一个项目看起来都简单,但组合在一起,就是一个完整的"系统基线"。

2.2 容易混淆的概念:初始化脚本 vs 配置管理工具

很多人会问:既然有 Ansible、SaltStack、Puppet,为什么还要写 Shell 初始化脚本?

这是个好问题。两者的边界其实很清晰:

对比维度Shell 初始化脚本配置管理工具(如 Ansible)
学习成本低,会 Linux 命令即可较高,需要理解 Playbook、Inventory、模块
适用规模几台到几十台几十台以上
幂等性需要自己设计天然具备
可维护性脚本越长越难维护结构化声明,易读
部署依赖只需 SSH 和 bash需要控制端安装 Ansible,节点支持 Python

我的判断是:如果你只有三五台机器、且初始化是一次性的,Shell 脚本的性价比最高。因为 Ansible 本身也要装、也要写 inventory,为了三台机器引入一套自动化体系,学习成本有点高。但如果你在管理几十台甚至上百台机器,或者需要持续调整配置,那直接上 Ansible 或同类工具是更稳的选择。

本文讲的 Shell 脚本,正好覆盖"少量机器、一次性初始化、不需要复杂编排"这个区间。

2.3 新手最容易误解的三个点

误解一:初始化脚本写一次就能通用。实际上 Ubuntu 20.04、22.04、24.04 的默认软件源格式、SSH 配置路径、systemd 行为都有差异。脚本必须考虑版本兼容或只在固定版本上运行。

误解二:跑完脚本没报错 = 配置成功。Shell 命令执行成功(退出码为 0)不代表效果正确。比如sed -i修改文件,如果正则没匹配上,命令照样返回 0,但文件根本没改。所以脚本需要主动校验结果。

误解三:把初始化脚本写得越长越好。实际上一个 500 行的单文件脚本,比 5 个 100 行的模块化脚本难维护得多。一旦某个函数出错,排查时要在几百行里找上下文。模块化、带日志、带函数封装,才是可维护的脚本。

3. 初始化脚本的模块化设计思路

在动手写脚本之前,先想清楚结构。我推荐把初始化脚本拆成四个逻辑阶段,对应不同类型的任务:前置检查、系统配置、安全加固、软件安装与收尾。

这种拆分的好处有两个:一是每一段脚本可以独立测试,出问题知道去哪个模块排查;二是将来可以按需裁剪,比如测试环境跳过安全加固,开发机不装 Docker。

3.1 前置检查:不要盲目执行

脚本的第一件事不是配置,而是确认"这台机器能不能按预期执行"。前置检查至少包含以下内容:

  • 当前用户是否为 root(非 root 需要切换到 sudo)。
  • 系统是否为 Ubuntu,以及版本号。
  • 网络是否连通(能否访问 APT 源)。
  • 关键目录和文件是否存在。
  • 是否已经执行过初始化(避免重复执行造成冲突)。

前置检查做得越充分,后面报错越少。不要在没确认网络的情况下就执行apt update,否则如果源不可达,整段脚本会在网络等待上卡很久。

3.2 系统配置:基础环境一致化

系统配置阶段负责让每台机器在基础信息上保持一致,包括:

  • 设置主机名。
  • 设置时区和时间同步。
  • 设置locale。
  • 写入基础环境变量。
  • 调整limits.conf中的文件描述符限制。

这个阶段看起来简单,但最容易出问题的是"主机名需要按机器不同而不同"。如果脚本里硬编码了一个主机名,那所有机器都会被改成同一个名字,这在生产环境是灾难。所以主机名应该作为脚本参数传入,而不是写死在脚本里。

3.3 安全加固:先关后测

安全加固是初始化里最不能省的部分,包括:

  • 创建普通管理用户,加入 sudo 组。
  • 生成或导入 SSH 公钥。
  • 修改 SSH 配置:禁止 root 直接登录、禁止密码登录、修改端口。
  • 配置 UFW 防火墙默认规则。
  • 启用fail2ban(可选,但强烈推荐)。

这里要特别强调一个顺序问题:先确保密钥登录可用,再禁止密码登录。如果你把密码登录关了,但公钥又没配好,唯一的管理入口就断了。脚本设计上要做"先写公钥,再测试登录,最后加固"的逻辑。

3.4 软件安装与收尾

最后阶段安装基础软件,并输出初始化报告:

  • 更新 APT 源并执行系统升级。
  • 安装常用软件:curl、wget、git、vim、htop、tree、ufw。
  • 安装 Docker(可选模块)。
  • 清理系统缓存和临时文件。
  • 输出每台机器的初始化结果汇总。

每一次安装都要考虑失败重试,因为 APT 源的网络波动很常见。简单的方式是设置安装命令的重试次数,比如同一个apt install最多执行三次。

4. Ubuntu 服务器初始化脚本完整示例

有了设计思路,下面直接给出一个可以实际运行的最小可用脚本。这个脚本针对 Ubuntu 20.04/22.04/24.04 的 x86_64 和 ARM64 架构都做了考虑,但为了控制篇幅,只保留最核心的模块。你可以把它作为模板,按自己的环境扩展。

4.1 脚本主体结构

#!/usr/bin/env bash # ============================================================ # 文件名: ubuntu_init.sh # 描述: Ubuntu 服务器初始化脚本(模块化版本) # 适用: Ubuntu 20.04 / 22.04 / 24.04 # 用法: sudo bash ubuntu_init.sh --hostname node01 --ssh-port 22 # 注意: 请先在脚本中配置你的 SSH 公钥,再在测试机验证 # ============================================================ set -Eeuo pipefail # ---------- 全局变量 ---------- LOG_FILE="/var/log/ubuntu_init.log" SSH_PUB_KEY="${SSH_PUB_KEY:-}" # 建议在这里填入你的公钥内容 NEW_USER="deploy" HOST_NAME="" SSH_PORT=22 APT_MIRROR="mirrors.aliyun.com" # 可按需修改为其他镜像站 ENABLE_DOCKER=0 # ---------- 工具函数 ---------- log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}" } log_err() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" | tee -a "${LOG_FILE}" >&2 } # ---------- 参数解析 ---------- while [[ $# -gt 0 ]]; do case "$1" in --hostname) HOST_NAME="$2" shift 2 ;; --ssh-port) SSH_PORT="$2" shift 2 ;; --enable-docker) ENABLE_DOCKER=1 shift ;; --new-user) NEW_USER="$2" shift 2 ;; *) log_err "未知参数: $1" exit 1 ;; esac done # ---------- 前置检查 ---------- pre_check() { log "开始前置检查..." if [[ "$(id -u)" -ne 0 ]]; then log_err "请使用 root 用户执行,或使用 sudo bash ubuntu_init.sh" exit 1 fi if [[ ! -f "/etc/os-release" ]]; then log_err "未找到 /etc/os-release,无法确认系统版本" exit 1 fi source "/etc/os-release" if [[ "${ID:-}" != "ubuntu" ]]; then log_err "当前系统不是 Ubuntu,请勿运行该脚本" exit 1 fi if [[ -z "${HOST_NAME}" ]]; then log_err "请使用 --hostname 参数指定主机名" exit 1 fi if ! ping -c 1 -W 2 "${APT_MIRROR}" >/dev/null 2>&1; then log_err "无法访问 APT 镜像源 ${APT_MIRROR},请检查网络" exit 1 fi log "前置检查通过。系统: Ubuntu ${VERSION_ID},主机名: ${HOST_NAME}" }

这个脚本的前置检查做了四件事:确认 root 权限、确认是 Ubuntu 系统、确认主机名参数、确认镜像源可达。任何一个不满足都直接退出,不进入后续流程。

4.2 系统基础配置模块

# ---------- 系统基础配置 ---------- configure_system() { log "===== 开始配置系统基础环境 =====" # 设置主机名 hostnamectl set-hostname "${HOST_NAME}" log "已设置主机名为 ${HOST_NAME}" # 设置时区 timedatectl set-timezone Asia/Shanghai log "已设置时区为 Asia/Shanghai" # 安装 chrony 并启用时间同步 apt install -y chrony >/dev/null 2>&1 || { log_err "chrony 安装失败,请检查系统源" return 1 } systemctl enable chrony --now >/dev/null 2>&1 log "已启用 chrony 时间同步" # 调整文件描述符限制 if ! grep -q "soft nofile" /etc/security/limits.conf 2>/dev/null; then cat >> /etc/security/limits.conf <<EOF * soft nofile 655350 * hard nofile 655350 root soft nofile 655350 root hard nofile 655350 EOF log "已追加 limits.conf 文件描述符限制" else log "limits.conf 已有配置,跳过" fi log "系统基础配置完成" }

这里有一个值得注意的细节:判断是否已经配置过,而不是无脑追加。如果脚本被重复执行,cat >>会把配置追加多次。虽然limits.conf里后写的生效,但会造成文件冗余。更规范的做法是先备份原文件,再写入新配置。

4.3 用户与 SSH 密钥配置模块

# ---------- 用户与 SSH 密钥配置 ---------- configure_user_ssh() { log "===== 开始配置用户与 SSH 密钥 =====" # 创建管理用户 if id "${NEW_USER}" >/dev/null 2>&1; then log "用户 ${NEW_USER} 已存在,跳过创建" else useradd -m -s /bin/bash "${NEW_USER}" echo "${NEW_USER}:$(date +%s%N | sha256sum | head -c 16)" | chpasswd usermod -aG sudo "${NEW_USER}" log "已创建用户 ${NEW_USER} 并加入 sudo 组" fi # 创建 .ssh 目录 USER_HOME="/home/${NEW_USER}" mkdir -p "${USER_HOME}/.ssh" chmod 700 "${USER_HOME}/.ssh" # 写入 SSH 公钥 if [[ -z "${SSH_PUB_KEY}" ]]; then log_err "未设置 SSH_PUB_KEY,跳过公钥配置" return 1 fi if ! grep -q "${SSH_PUB_KEY}" "${USER_HOME}/.ssh/authorized_keys" 2>/dev/null; then echo "${SSH_PUB_KEY}" >> "${USER_HOME}/.ssh/authorized_keys" log "已写入 SSH 公钥" else log "SSH 公钥已存在,跳过" fi chmod 600 "${USER_HOME}/.ssh/authorized_keys" chown -R "${NEW_USER}:${NEW_USER}" "${USER_HOME}/.ssh" log "用户 ${NEW_USER} 的 SSH 密钥配置完成" }

4.4 SSH 安全加固模块

# ---------- SSH 安全加固 ---------- harden_ssh() { log "===== 开始 SSH 安全加固 =====" local sshd_config="/etc/ssh/sshd_config" # 备份配置 cp "${sshd_config}" "${sshd_config}.bak.$(date +%Y%m%d%H%M%S)" # 应用安全配置 sed -i "s/^#\?Port.*/Port ${SSH_PORT}/" "${sshd_config}" sed -i "s/^#\?PermitRootLogin.*/PermitRootLogin no/" "${sshd_config}" sed -i "s/^#\?PubkeyAuthentication.*/PubkeyAuthentication yes/" "${sshd_config}" sed -i "s/^#\?PasswordAuthentication.*/PasswordAuthentication no/" "${sshd_config}" # 部分系统默认以 Include 方式包含配置,需要确保最终生效 if grep -q "^Include /etc/ssh/sshd_config.d/\*.conf" "${sshd_config}"; then cat > /etc/ssh/sshd_config.d/99-hardening.conf <<EOF Port ${SSH_PORT} PermitRootLogin no PubkeyAuthentication yes PasswordAuthentication no EOF log "已写入 /etc/ssh/sshd_config.d/99-hardening.conf" fi # 校验 SSH 配置语法 if ! sshd -t; then log_err "SSH 配置校验失败,请检查 sshd_config" return 1 fi # 重启 SSH 服务 systemctl restart ssh log "SSH 服务已重启,端口: ${SSH_PORT}" }

这里有个很关键的细节:Ubuntu 20.04 之后的 sshd_config 末尾通常有Include /etc/ssh/sshd_config.d/*.conf。如果这一段在文件后面,那么前面对sshd_config的sed修改可能会被后面的 drop-in 配置覆盖。所以我在脚本里同时写了一个独立的99-hardening.conf,确保最终生效。这是实际运维中非常典型的"命令执行成功但结果不对"的成因。

4.5 防火墙配置模块

# ---------- 防火墙配置 ---------- configure_firewall() { log "===== 开始配置 UFW 防火墙 =====" apt install -y ufw >/dev/null 2>&1 if command -v ufw >/dev/null 2>&1; then ufw default deny incoming >/dev/null ufw default allow outgoing >/dev/null ufw allow "${SSH_PORT}/tcp" >/dev/null # 按需放行其他端口,例如 HTTP/HTTPS # ufw allow 80/tcp # ufw allow 443/tcp echo "y" | ufw enable >/dev/null log "UFW 已启用,仅放行 SSH 端口 ${SSH_PORT}" else log_err "UFW 安装失败,请手动检查" return 1 fi }

4.6 软件源与基础软件安装模块

# ---------- 软件源替换与基础软件安装 ---------- configure_apt_and_packages() { log "===== 开始配置 APT 源与安装基础软件 =====" local os_version="${VERSION_ID}" # 备份源文件 if [[ -f "/etc/apt/sources.list" ]]; then cp "/etc/apt/sources.list" "/etc/apt/sources.list.bak.$(date +%Y%m%d%H%M%S)" fi # Ubuntu 24.04 使用 deb822 格式,旧的 sources.list 写法不生效 # 这里使用清华源示例,可按需替换 if [[ "${os_version}" == "24.04" ]]; then cat > /etc/apt/sources.list.d/ubuntu.sources <<EOF Types: deb URIs: https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ Suites: noble noble-updates noble-backports Components: main restricted universe multiverse Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg EOF log "已替换 Ubuntu 24.04 APT 源为清华源" else # Ubuntu 20.04 / 22.04 使用 sources.list cat > /etc/apt/sources.list <<EOF deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ${os_version} main restricted universe multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ${os_version}-updates main restricted universe multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ${os_version}-backports main restricted universe multiverse deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ ${os_version}-security main restricted universe multiverse EOF log "已替换 Ubuntu ${os_version} APT 源为清华源" fi # 更新索引 apt update -o Acquire::Retries=3 >/dev/null 2>&1 || { log_err "apt update 失败,请检查网络或源配置" return 1 } # 安装基础软件(带重试) local packages="curl wget git vim htop tree unzip ufw tzdata chrony" apt install -y ${packages} -o Acquire::Retries=3 >/dev/null 2>&1 || { log_err "基础软件安装失败" return 1 } log "基础软件安装完成: ${packages}" }

为什么单独把 Ubuntu 24.04 拎出来?因为24.04 开始 Ubuntu 将 APT 源配置迁移到了 deb822 格式,/etc/apt/sources.list不再是主要配置文件。如果照搬旧写法,apt update可能仍然能跑,但很多第三方源的格式要求会变化。这个版本差异,恰恰是初始化脚本最容易"跑着跑着莫名报错"的地方。

4.7 Docker 安装模块与主函数

# ---------- Docker 安装模块(可选) ---------- install_docker() { if [[ "${ENABLE_DOCKER}" -eq 0 ]]; then log "未启用 Docker 安装,跳过" return 0 fi log "===== 开始安装 Docker =====" if command -v docker >/dev/null 2>&1; then log "Docker 已安装,跳过" return 0 fi curl -fsSL "https://get.docker.com" -o get-docker.sh || { log_err "下载 Docker 安装脚本失败" return 1 } sh get-docker.sh >/dev/null 2>&1 || { log_err "Docker 安装失败" rm -f get-docker.sh return 1 } rm -f get-docker.sh # 让管理用户可直接使用 docker usermod -aG docker "${NEW_USER}" systemctl enable docker --now >/dev/null 2>&1 log "Docker 安装完成,并已允许用户 ${NEW_USER} 使用 Docker" } # ---------- 主执行流程 ---------- main() { log "========== Ubuntu 服务器初始化开始 ==========" pre_check configure_system configure_user_ssh harden_ssh configure_firewall configure_apt_and_packages if [[ "${ENABLE_DOCKER}" -eq 1 ]]; then install_docker fi log "========== Ubuntu 服务器初始化完成 ==========" log "请使用新端口 ${SSH_PORT} 登录,并测试用户 ${NEW_USER} 的密钥登录" } main "$@"

这段主函数把前面的模块按顺序串起来。注意我特意把configure_apt_and_packages放在harden_ssh之后,是因为更新 APT 源时不需要依赖之前的用户配置,而安全加固如果失败,后面的软件安装就没有意义。你可以根据自己的需求调整顺序,但原则是:核心安全配置优先,软件安装最后。

5. 脚本使用前的准备

脚本有了,但要真正跑起来,还需要做两件事:准备公钥、准备多机器批量执行方式。

5.1 准备 SSH 公钥

在本地机器(你自己的工作电脑或跳板机)生成密钥对。不要直接用 root 的密钥,也不要复用已经用于其他系统的密钥。每个环境单独密钥更清晰:

ssh-keygen -t ed25519 -C "ubuntu-init-deploy" -f ~/.ssh/ubuntu_init

然后查看公钥内容并粘贴到脚本的SSH_PUB_KEY变量中:

cat ~/.ssh/ubuntu_init.pub

或者直接复制到脚本里:

SSH_PUB_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINxxxxx ubuntu-init-deploy"

注意,这个公钥会配置到服务器的deploy用户下。本地的私钥文件~/.ssh/ubuntu_init要保存好,后续登录服务器都用它对应用户。

5.2 准备多台机器的目标清单

我的建议是先在一台测试机上跑通脚本,确认没有问题后,再批量执行。目标机器的清单可以写在文件里,也可以直接用命令行参数批量循环。

假设有三台机器,IP 如下:

192.168.10.11 192.168.10.12 192.168.10.13

批量执行前,必须在每台机器上手动创建工作目录并上传脚本:

for ip in 192.168.10.11 192.168.10.12 192.168.10.13; do ssh -o StrictHostKeyChecking=no root@${ip} "mkdir -p /opt/init" scp -o StrictHostKeyChecking=no ubuntu_init.sh root@${ip}:/opt/init/ ssh -o StrictHostKeyChecking=no root@${ip} "bash /opt/init/ubuntu_init.sh --hostname node-${ip##*.} --ssh-port 22 --enable-docker" done

这里的node-${ip##*.}会生成类似node-11、node-12、node-13的主机名,避免所有机器使用同一个主机名。--ssh-port 22表示本次不改 SSH 端口。如果你要改端口,比如改成 22022,那需要在防火墙模块和本地 SSH 配置中同步修改。

5.3 脚本执行的正确姿势

执行脚本时,不要直接bash ubuntu_init.sh,建议使用:

sudo bash ubuntu_init.sh --hostname node-test --ssh-port 22 --enable-docker

可以加tee将输出保留一份:

sudo bash ubuntu_init.sh --hostname node-test --ssh-port 22 | tee -a /tmp/init_console.log

脚本本身已经在/var/log/ubuntu_init.log写入了日志,控制台输出加上tee是为了你实时看到每步的执行状态。

6. 初始化脚本的运行结果与验证

脚本跑完后,不要急着认为"初始化完成"。你需要做一系列验证,确认脚本真的让系统达到了预期状态。

6.1 从日志验证

先看脚本自己的日志尾部:

tail -n 20 /var/log/ubuntu_init.log

预期输出类似:

[2025-01-01 10:00:01] ========== Ubuntu 服务器初始化开始 ========== [2025-01-01 10:00:01] 开始前置检查... [2025-01-01 10:00:02] 前置检查通过。系统: Ubuntu 22.04,主机名: node-11 ... [2025-01-01 10:00:30] ========== Ubuntu 服务器初始化完成 ==========

特别注意中间每一步都显示"完成",没有[ERROR]标记。如果出现了[ERROR],立即定位到对应步骤排查。

6.2 用命令验证关键配置

# 检查主机名 hostnamectl status # 检查时区 timedatectl # 检查 SSH 端口与配置 sshd -T | grep -E "port|passwordauthentication|permitrootlogin" # 检查防火墙状态 ufw status verbose # 检查用户与 sudo 权限 id deploy sudo -l -U deploy # 检查 Docker docker version

每种验证都要有明确的凭据:

  • hostnamectl status的 Static hostname 应为传入的主机名。
  • sshd -T输出的port应为配置的端口,passwordauthentication no,permitrootlogin no。
  • ufw status verbose应显示 Status: active,并只放行了 SSH 端口。
  • id deploy返回的组信息里应有sudo。

6.3 在本地执行登录测试

服务器端验证完,还要从你的本地机器用新用户和密钥登录:

ssh -i ~/.ssh/ubuntu_init deploy@192.168.10.11

如果登录成功,说明密钥配置有效。此时你还可以用新用户检查sudo su -是否能切换到 root,确认 sudo 权限无误。

6.4 验证失败时怎么办

下面的顺序是排查时必须遵守的:

  1. 检查脚本日志,看失败发生在哪个模块。
  2. 检查该模块操作的配置文件,确认是否被正确修改。
  3. 检查服务状态:systemctl status ssh、systemctl status docker。
  4. 检查端口监听:ss -tlnp | grep ssh。
  5. 如果 SSH 端口变了,确保安全组/防火墙里也放行了新端口。

最危险的情况是:你改了 SSH 端口或禁用了密码登录,但密钥还没生效。此时当前连接可能断开,而你还不知道新端口是否可登录。所以强烈建议,在批量执行前先在一台机器上验证,而且最好保留一个 root 会话不要断开,这样即使 SSH 配置出错,你还有另一条通道修复。

7. 多台服务器批量执行时的常见问题与排查思路

脚本在单台机器上跑通后,批量执行时依然可能遇到各类问题。下面是实际运维中最常见的几类,按排查优先级排列:

问题现象可能原因排查方式解决方案
某台机器 apt update 超时该机器到镜像源的网络不通手动在该机器执行curl -I https://mirrors.tuna.tsinghua.edu.cn该机器换用其他镜像源,或检查网络策略/防火墙
SSH 端口变更后本地无法连接新端口被防火墙拦截,或 SSH 服务未重启成功检查云安全组/防火墙规则,用原始 root 会话查看ss -tlnp放行新端口,或临时恢复旧端口
公钥配置了但登录仍需要密码authorized_keys权限不对,或sshd配置未生效服务器上执行chmod 600 ~/.ssh/authorized_keys,sshd -T查看状态检查/etc/ssh/sshd_config.d/是否有覆盖配置
脚本重复执行后配置混乱cat >>或sed -i未做幂等处理比较/etc/security/limits.conf是否有重复内容在写入前用grep -q判断是否已存在
主机名未生效hostnamectl set-hostname只影响当前会话,部分服务缓存执行hostname查看,重开 shell重新登录,或在脚本末尾注释提示
批量循环里某台主机名相同${ip##*.}写法在主机名尾号相同(如.1与.01)时冲突打印hostnamectl status对比主机名参数改为手动指定

7.1 一个真实的排查案例

假设你在批量执行时,第三台机器报错:sshd -t校验失败。脚本日志末尾写着SSH 配置校验失败。

此时第一步不是去看sshd_config,而是先看你写入的那个 drop-in 文件语法:

cat /etc/ssh/sshd_config.d/99-hardening.conf

常见原因是文件中写了空行、或者Port后面跟了注释符导致格式错乱。也可能是因为不同 Ubuntu 版本对sshd_config的Include顺序处理不同,导致你的配置里有冲突。

按顺序排查:

  1. sshd -t会提示具体哪一行出了问题。
  2. 直接把错误行注释掉,再sshd -t直到通过。
  3. 通过后systemctl restart ssh,重启 SSH 恢复连接。

这类问题在多台机器上的出现率不低,因为不同机器的/etc/ssh/sshd_config.d/目录下可能有系统自带的 drop-in 文件。如果你的99-hardening.conf和别人家自带的配置冲突,后果就是 SSH 无法启动。

7.2 为什么不能盲目依赖set -e

我在脚本开头用了set -Eeuo pipefail,这是很多初始化脚本的标准开头。它的作用是:任何命令返回非零退出码时,脚本立即退出。

但实际操作中,很多"命令失败"是可控的。比如apt install因为网络超时失败,但重试可能就成功。所以我给关键命令加了-o Acquire::Retries=3。另外,如果你在函数里用了return 0来跳过某些逻辑,set -e不会把它当成错误。相反,如果函数内部的某个命令失败且没有处理,整个脚本会直接中断,这是设计上需要接受的——要么脚本快速失败,要么你用条件判断最小化失败的影响。

对初始化脚本而言,我更推荐"快速失败"策略:宁可中断,也不要让脚本在错误状态下继续执行后面的步骤。因为错误状态下继续执行的副作用,远比"停止等人工介入"更难排查。

8. 初始化脚本的最佳实践与工程建议

脚本能跑通只是第一步,真正到生产环境后,还有很多工程层面的细节值得打磨。

8.1 日志是唯一的真相

初始化脚本必须做到每步有日志,失败有位置,结果有摘要。这是我和很多运维同事交流后的一致结论。不要只在脚本结束输出一个"成功",而是在每一步写入当前操作对象、操作结果、耗时。这样即使批量执行后发现问题,你也能从几十台机器的日志里快速定位"哪一台、哪一步、为什么"。

推荐日志格式:

[时间] [模块名] [动作] [成功/失败] [目标文件/资源] [备注]

例如:

[2025-01-01 10:00:05] [harden_ssh] [修改配置] [成功] [/etc/ssh/sshd_config.d/99-hardening.conf] [Port 22022]

8.2 对所有配置文件做备份

脚本中修改的任何系统配置文件,执行前都应该先备份。备份的命名要带时间戳,不要只用一个.bak覆盖。

cp "${file}" "${file}.bak.$(date +%Y%m%d%H%M%S)"

这样如果你发现脚本某个sed写错了,可以直接从备份文件恢复。没有备份的初始化脚本,等于让运维在手术台上裸奔。

8.3 使用变量,不要硬编码

脚本里最容易埋雷的地方就是硬编码:

  • 硬编码用户名、密码、端口。
  • 硬编码 IP、主机名。
  • 硬编码路径、版本号。

正确的做法是:把需要变化的量全部提取成脚本参数或环境变量,并在脚本开头集中定义。比如本文例子里的--hostname、--ssh-port、--new-user都是参数。你甚至可以做成一个config.env文件,批量执行时每台机器加载不同的配置。

8.4 分阶段灰度,先测试后铺开

永远不要在同一时间对所有机器执行新脚本。建议顺序:

  1. 一台测试机,验证脚本能跑通。
  2. 一台生产前置机,验证不影响现有业务。
  3. 按业务单元分批执行。
  4. 每批执行完后,至少观察一段时间再继续。

如果你的机器数量很多,建议在批量脚本里加入"当前执行批次"的标记,方便后续回滚。

8.5 幂等性:宁可写 10 行判断,也不要让人手动清理

初始化脚本最怕重复执行。为了幂等,常见的做法:

  • 文件内容用grep -q判断是否存在再写入。
  • 用户存在则跳过创建。
  • 软件包已安装则跳过安装。
  • 配置文件先备份,再全量覆盖(而不是追加)。

幂等不是一次写出来的,而是反复执行脚本、观察差异、修补判断逻辑的过程。建议在测试机上重复执行脚本三次,确保结果一致,再进入批量阶段。

8.6 安全边界:不要在脚本里保存敏感明文

很多人会在脚本里写死初始密码、密钥、Token。这是极其危险的习惯。初始化脚本通常会被复制到多台机器、上传到内部 Git、甚至被同事同步,一旦泄露,所有机器都暴露。

推荐的做法:

  • 初始密码用随机生成,并强制用户首次登录修改。
  • SSH 私钥永远不要进脚本,只放公钥。
  • APT 源的访问如果需要认证,从环境变量读取。
  • 脚本执行权限设置为仅 root 可读可执行。

8.7 版本管理同样重要

不要以为 Shell 脚本不需要版本管理。初始化脚本会随系统版本、业务需求、安全基线变化而持续迭代。建议:

  • 每个脚本文件头部写明适用系统版本和最近变更记录。
  • 用 Git 管理脚本变更,提交信息写清楚"为什么改"。
  • 对生产环境的初始化脚本,执行前对比 Git 中最近几次提交,确认改动符合预期。

9. 总结与后续学习方向

这篇文章从一个日常运维痛点出发:多台 Ubuntu 服务器的手动初始化,既慢又容易出错,还没有一致性可言。然后我给出了一个模块化的初始化脚本,把前置检查、系统配置、用户与 SSH 加固、防火墙、软件源与基础软件、Docker 安装六大步骤串成一条可执行流程,并对 Ubuntu 20.04/22.04/24.04 的差异做了针对性处理。最后补充了批量执行、日志验证、常见问题排查和工程级最佳实践。

真正需要你动手去做的是三件事:

  1. 找一台全新测试机,把脚本里的SSH_PUB_KEY换成你自己的公钥,跑一遍。
  2. 逐个模块理解脚本干了什么,删掉不需要的功能,调整参数和源。
  3. 在测试机上重复执行脚本,验证幂等性,确认没有副作用后,再考虑批量。

如果你想继续深入,这几个方向值得探索:

  • 配置管理工具:当机器数量增长到几十台以上,Ansible 的 Playbook 可以在不编写复杂 Shell 的情况下实现类似目标,而且天然具备幂等特性。
  • 云厂商初始化机制:阿里云、腾讯云、AWS 都支持用户数据脚本(User Data / cloud-init),可以在实例创建时自动执行初始化,和这里脚本化的思路完全兼容。
  • 镜像与黄金镜像:把初始化好的系统做成自定义镜像,后续直接基于镜像创建实例,可以进一步缩短交付时间。这是比脚本更进一步的思路,但前提是初始化脚本本身足够稳定。

服务器初始化没有一劳永逸的方案。它更像一份不断更新迭代的运维资产:系统版本在升级,安全基线在收紧,业务需求在变化。你能做的,是把每一步配置都变成可执行、可验证、可回溯的代码,然后让这套代码成为团队的基础设施。希望这篇文章能帮你把这第一步走稳。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 7:45:37

中配硬件跑原生推理模型,构建有自身推理能力的AI智能体

“会调用工具的AI”和“有自身推理能力的智能体”&#xff0c;差别到底在哪&#xff1f;这是很多AI工程师在做过一两个Demo后都会产生的疑问。如果你已经试过用LangChain或各类Agent框架拼接提示词&#xff0c;大概会有同感&#xff1a;表面看流程跑通了&#xff0c;但模型一旦…

作者头像 李华
网站建设 2026/10/22 7:45:28

光伏功率预测午后误差大?MLP-SVR组合模型MATLAB实战

简介&#xff1a;这份资源面向具备机器学习基础与MATLAB编程经验的科研人员、能源系统工程师及电力数据分析人员&#xff0c;提供一套基于MLP-SVR混合模型的光伏功率预测完整项目实例。项目以多层感知机完成深层非线性特征提取&#xff0c;再结合支持向量回归实现稳健回归预测&…

作者头像 李华
网站建设 2026/10/22 7:45:27

从数字化到智能化制造:数据闭环与边缘计算落地路线图

简介&#xff1a;这份PDF资料围绕“中国制造2025”战略&#xff0c;系统梳理制造业从数字化走向智能化的转型路径&#xff0c;面向制造业从业者、企业管理者及工业软件学习者&#xff0c;帮助理解工业4.0背景下的价值链整合与数字化双胞胎技术。内容涵盖工业1.0到4.0的历史演变…

作者头像 李华
网站建设 2026/10/22 7:44:45

不会写代码也能上线网站:Claude Code建站+三大免费托管实战

“AI 能帮你写代码”这句话&#xff0c;大家已经听腻了。真正让大多数不会写代码的人卡住的&#xff0c;从来不是“生成代码”&#xff0c;而是 生成完之后怎么办 。 我见过太多朋友在 ChatGPT 里要到一个很漂亮的网站源码&#xff0c;复制粘贴到本地&#xff0c;双击打开没…

作者头像 李华
网站建设 2026/10/22 7:43:26

6G太赫兹信道建模:从实测仿真到动态簇模型的工程落地

简介&#xff1a;本资源是一份面向通信工程高年级本科生、研究生及6G前沿研究者的专业技术综述文档&#xff0c;聚焦6G无线信道建模这一核心基础问题&#xff0c;系统梳理全频谱&#xff08;毫米波/太赫兹/光波段&#xff09;、全场景&#xff08;卫星、无人机、海洋、水声、高…

作者头像 李华
网站建设 2026/10/22 7:43:23

浅析C++模板类型中的原样转发和可变参数的实现

前言 「原样转发」是中文技术圈对 perfect forwarding 的常见叫法&#xff0c;标准术语是完美转发。它要解决的问题是&#xff1a;一个中间层函数收到实参后&#xff0c;把实参原封不动地传给下一层&#xff0c;既不丢失 const 限定&#xff0c;也不把左值变成右值&#xff0c;…

作者头像 李华