资讯动态

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

发布时间:2026/10/22 7:45:55 来源:尧图企业网站定制
多台 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_USERdeploy HOST_NAME SSH_PORT22 APT_MIRRORmirrors.aliyun.com # 可按需修改为其他镜像站 ENABLE_DOCKER0 # ---------- 工具函数 ---------- 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_DOCKER1 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 21; 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 21 || { log_err chrony 安装失败请检查系统源 return 1 } systemctl enable chrony --now /dev/null 21 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 21; 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 21 if command -v ufw /dev/null 21; 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::Retries3 /dev/null 21 || { log_err apt update 失败请检查网络或源配置 return 1 } # 安装基础软件带重试 local packagescurl wget git vim htop tree unzip ufw tzdata chrony apt install -y ${packages} -o Acquire::Retries3 /dev/null 21 || { 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 21; 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 21 || { 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 21 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_KEYssh-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 StrictHostKeyCheckingno root${ip} mkdir -p /opt/init scp -o StrictHostKeyCheckingno ubuntu_init.sh root${ip}:/opt/init/ ssh -o StrictHostKeyCheckingno 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 nopermitrootlogin no。ufw status verbose应显示 Status: active并只放行了 SSH 端口。id deploy返回的组信息里应有sudo。6.3 在本地执行登录测试服务器端验证完还要从你的本地机器用新用户和密钥登录ssh -i ~/.ssh/ubuntu_init deploy192.168.10.11如果登录成功说明密钥配置有效。此时你还可以用新用户检查sudo su -是否能切换到 root确认 sudo 权限无误。6.4 验证失败时怎么办下面的顺序是排查时必须遵守的检查脚本日志看失败发生在哪个模块。检查该模块操作的配置文件确认是否被正确修改。检查服务状态systemctl status ssh、systemctl status docker。检查端口监听ss -tlnp | grep ssh。如果 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_keyssshd -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顺序处理不同导致你的配置里有冲突。按顺序排查sshd -t会提示具体哪一行出了问题。直接把错误行注释掉再sshd -t直到通过。通过后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::Retries3。另外如果你在函数里用了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 分阶段灰度先测试后铺开永远不要在同一时间对所有机器执行新脚本。建议顺序一台测试机验证脚本能跑通。一台生产前置机验证不影响现有业务。按业务单元分批执行。每批执行完后至少观察一段时间再继续。如果你的机器数量很多建议在批量脚本里加入当前执行批次的标记方便后续回滚。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 的差异做了针对性处理。最后补充了批量执行、日志验证、常见问题排查和工程级最佳实践。真正需要你动手去做的是三件事找一台全新测试机把脚本里的SSH_PUB_KEY换成你自己的公钥跑一遍。逐个模块理解脚本干了什么删掉不需要的功能调整参数和源。在测试机上重复执行脚本验证幂等性确认没有副作用后再考虑批量。如果你想继续深入这几个方向值得探索配置管理工具当机器数量增长到几十台以上Ansible 的 Playbook 可以在不编写复杂 Shell 的情况下实现类似目标而且天然具备幂等特性。云厂商初始化机制阿里云、腾讯云、AWS 都支持用户数据脚本User Data / cloud-init可以在实例创建时自动执行初始化和这里脚本化的思路完全兼容。镜像与黄金镜像把初始化好的系统做成自定义镜像后续直接基于镜像创建实例可以进一步缩短交付时间。这是比脚本更进一步的思路但前提是初始化脚本本身足够稳定。服务器初始化没有一劳永逸的方案。它更像一份不断更新迭代的运维资产系统版本在升级安全基线在收紧业务需求在变化。你能做的是把每一步配置都变成可执行、可验证、可回溯的代码然后让这套代码成为团队的基础设施。希望这篇文章能帮你把这第一步走稳。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑