多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Linux系统初始化Shell脚本实战:从裸机到可接业务服务器的自动化配置

Linux系统初始化Shell脚本实战:从裸机到可接业务服务器的自动化配置 刚接手一批新服务器的时候最烦的事情是什么不是业务部署而是把每台机器从默认状态调成自己想要的样子。装系统、设主机名、配时间同步、建用户、开防火墙……十几台机器一台一台手动敲命令敲到后来手都酸了而且很容易漏掉某个配置导致后面排查半天。所以我写了一个Linux系统初始化shell脚本把整个过程固化下来跑一遍就能完成90%的重复工作。这篇文章就把这个脚本的设计思路、核心模块、常见坑和排错经验完整分享出来适合刚接触linux运维的新手也适合想提升效率的资深工程师。这篇东西不是那种教科书式的概念堆砌而是我从实际运维中总结出来的可落地做法。脚本本身不算复杂但里面每一个函数、每一个参数我都是踩过坑后才确定的。如果你手头正好有批量初始化的需求可以直接拿我的思路去改如果你只是想了解linux系统初始化会涉及哪些东西读完这篇也能少走很多弯路。1. 初始化脚本的设计思路与核心功能拆解1.1 系统初始化到底要做什么很多人以为初始化就是把系统装好就完事了其实不是。做过运维的都清楚一台刚从镜像或虚拟机模板克隆出来的服务器默认状态往往不符合生产要求。比如主机名是一长串随机字符时区是UTCDNS指向的是虚拟化平台自带的地址root密码策略过于宽松甚至SSH默认开启了root密码登录……这些都是隐患。我理解的系统初始化是把一台“裸机”变成一台“可以接业务”的服务器它至少要完成以下几类事情基础配置主机名、时区、语言环境、DNS、hosts解析。软件源配置替换成国内镜像源或者公司内部源保证后续安装速度。用户与权限创建普通运维用户加入sudo组配置SSH密钥登录禁用root远程密码登录。安全基线防火墙规则、SELinux/AppArmor策略、关键文件权限、系统补丁更新。内核与资源调优文件描述符上限、TCP参数、内核模块加载。环境部署安装常用工具vim、git、curl、wget、jq等配置时间同步服务。这些工作如果用命令行一条条做一台机器至少需要20到30分钟而且非常容易漏。脚本化的价值就在于把每一步都变成可重复、可审计的操作执行完还能输出日志出了问题也知道是从哪一步挂的。1.2 脚本应该具备哪些模块我写初始化脚本时没有把它做成一个几百行的顺序执行文件而是拆成了“函数模块 主流程调度”。这样做的原因很简单不同环境的机器可能需要跳过某些模块。比如内网机器不需要替换软件源测试机器不需要做安全加固。如果你把逻辑写死每次都要改脚本那就失去了“自动化”的意义。我的脚本一般包含下面这些模块模块主要职责可配置项pre_check检查系统版本、网络、磁盘空间版本兼容性判断init_base主机名、时区、DNS、hosts主机名变量、DNS列表init_repo替换镜像源、安装epel源地址、是否启用init_user创建用户、sudo授权、SSH密钥用户名、密钥内容init_security防火墙、SELinux、更新补丁是否启用严格模式init_tune内核参数、limits、sysctl参数文件路径init_tools安装常用软件、时间同步软件列表、时间服务器init_log日志函数、状态输出日志级别、输出路径每个模块对应一个函数函数内部再拆成更小的步骤。主函数只负责按顺序调用模块并捕获每个阶段的返回码。这样脚本看起来清晰后续维护某个模块也不会影响其他部分。1.3 为什么用Shell而不是其他工具这个问题经常有人问。现在有Ansible、Puppet、SaltStack这些配置管理工具为什么还要写shell脚本我的理解是工具的定位不同。Ansible适合规模化、持续性的配置管理它有自己的抽象层学习成本也不低。但在以下场景shell脚本反而是最优解批量初始化一次性任务比如新采购了10台服务器需要快速完成基础配置用shell脚本跑一圈就结束不需要沉淀成playbook。环境受限有些内网环境没有python或ansible控制端但一定有/bin/sh。调试简单shell脚本可以直接加bash -x跟踪每一步出问题马上能看到是哪个命令返回非零。依赖最小不需要额外安装agent不需要配置SSH免密到目标机器脚本在本地执行即可。当然如果你的环境有成百上千台机器且需要持续变更管理那我建议你认真评估Ansible。但如果你是中小型集群或者刚入门linux运维shell脚本是最快见效的方案。我用shell脚本完成初始化后在生产环境中跑了两年稳定性一点问题都没有。2. 脚本编写前的准备工作环境分析与变量规划2.1 确认系统版本与包管理器动手写脚本之前先搞清楚目标系统的发行版和版本这是最重要的前提。CentOS 7用yumCentOS 8和Rocky/RHEL 8用dnf但yum命令仍然可用Ubuntu用apt。不同发行版的防火墙管理工具也不同CentOS 7默认firewalldUbuntu默认ufw。如果不做兼容判断脚本换个环境就废了。我通常会在脚本开头做一次系统识别用/etc/os-release文件来判断#!/bin/bash # 系统版本识别函数 detect_os() { if [ -f /etc/os-release ]; then . /etc/os-release OS_ID$ID OS_VERSION_ID$VERSION_ID else echo [ERROR] 无法识别操作系统 exit 1 fi case $OS_ID in centos|rhel|rocky|almalinux) PKG_MANAGERyum if command -v dnf /dev/null 21; then PKG_MANAGERdnf fi ;; ubuntu|debian) PKG_MANAGERapt-get ;; *) echo [ERROR] 不支持的系统: $OS_ID exit 1 ;; esac }这段代码中VERSION_ID也很关键比如CentOS 7和CentOS 8的防火墙配置命令有差异软件源仓库地址也不一样。我的做法是把OS_VERSION_ID也存下来后面每个函数内部再做具体判断。2.2 定义全局变量与日志函数脚本里到处出现硬编码不是一个好习惯。我会把所有需要自定义的内容都放到脚本开头的变量区域比如主机名、DNS服务器、要创建的用户名、SSH公钥内容、软件源地址、要安装的软件列表。这样别人拿到你的脚本只需要改头部变量就能用。日志函数是脚本可靠性的关键。我见过很多人写脚本不用日志出错以后只能靠echo猜。我的做法是定义一个统一的日志函数把时间、级别、消息都写到文件和终端LOG_FILE/var/log/sys_init_$(date %Y%m%d).log log_info() { echo [$(date %Y-%m-%d %H:%M:%S)] [INFO] $* | tee -a $LOG_FILE } log_error() { echo [$(date %Y-%m-%d %H:%M:%S)] [ERROR] $* | tee -a $LOG_FILE 2 } log_success() { echo [$(date %Y-%m-%d %H:%M:%S)] [OK] $* | tee -a $LOG_FILE }tee -a这个用法比较实用它把内容同时输出到终端和文件。log_error里加上2是为了让错误信息走标准错误输出这样在重定向日志时能区分正常信息与错误信息。脚本执行完以后我习惯再压缩一份日志存档方便后续审计。2.3 安全策略set -e、set -u、umask脚本执行过程中最怕的就是“命令执行失败但脚本继续跑”。比如yum install因为网络原因失败但脚本继续往下执行最后你得到的是一个残缺的配置排查起来特别痛苦。所以我在脚本开头一定会加上set -e # 任何命令返回非零状态立即退出 set -u # 使用未定义变量时退出 set -o pipefail # 管道中任一命令失败整个管道返回失败这三个选项组合在一起能让脚本变得非常“敏感”。但这里也有个坑有些命令会“正常地”返回非零状态比如grep没有匹配到内容时返回1。这时候如果加了set -e脚本会直接退出。解决办法是把这类命令放到if条件中判断或者临时用|| true来忽略# 正确写法grep放在if中 if grep -q sometext /etc/hosts; then echo found fi # 或者明确忽略失败 some_command || true另外脚本内创建临时文件时我会先设置umask 022确保新建文件权限不会太宽松。如果是包含敏感信息的临时文件可以单独设为600。3. 核心功能模块的实现细节与代码示例3.1 系统基础配置主机名、时区、DNS、yum/apt源先说说主机名。服务器的主机名不只是显示用有些软件比如数据库集群会依赖主机名做节点识别。我一般用变量HOSTNAME_VAR传入脚本执行时如果当前主机名和期望值不一样就用hostnamectl set-hostname修改。同时要更新/etc/hosts把新的主机名对应到内网IP否则有些服务解析不了。时区和时间同步是很多人容易忽略的。国内服务器一般要设为Asia/Shanghai但有些海外节点需要保持UTC。所以我把时区做成变量不写死set_timezone() { timedatectl set-timezone $TIMEZONE # 同步时间 if [ $OS_ID ubuntu ]; then apt-get install -y chrony /dev/null 21 else yum install -y chrony /dev/null 21 fi systemctl enable --now chronyd chronyc makestep /dev/null 21 log_success 时区已设置为 $TIMEZONE时间同步已开启 }DNS配置方面我会把默认的DNS追加到/etc/resolv.conf末尾。这里有个经验不要直接覆盖整个文件因为DHCP或NetworkManager可能会重写它。我的习惯是先备份再追加并加上注释标记方便回滚。软件源是必须处理的。新装的CentOS默认源在国外下载速度惨不忍睹。我的init_repo函数会判断系统版本然后替换成阿里云或清华的镜像源。以CentOS 7为例init_repo() { case $OS_ID-$OS_VERSION_ID in centos-7) mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo yum clean all yum makecache ;; ubuntu-20.04) sed -i s|archive.ubuntu.com|mirrors.aliyun.com|g /etc/apt/sources.list apt-get update ;; esac }这里我故意只写了两个分支实际脚本里会写得更完整。注意替换源之前先备份原文件这是基本的职业素养。3.2 用户与权限管理创建用户、sudo授权、SSH密钥系统初始化后禁止直接用root操作这是我的安全底线。所以创建普通用户并配置免密登录是脚本里最重要的一环。我的做法是init_user() { # 检查用户是否已存在 if id $NEW_USER /dev/null 21; then log_warn 用户 $NEW_USER 已存在跳过创建 else useradd -m -s /bin/bash $NEW_USER echo $NEW_USER ALL(ALL) NOPASSWD:ALL /etc/sudoers.d/$NEW_USER chmod 440 /etc/sudoers.d/$NEW_USER log_success 用户 $NEW_USER 创建并加入sudo fi # 配置SSH密钥 mkdir -p /home/$NEW_USER/.ssh echo $SSH_PUBLIC_KEY /home/$NEW_USER/.ssh/authorized_keys chown -R $NEW_USER:$NEW_USER /home/$NEW_USER/.ssh chmod 700 /home/$NEW_USER/.ssh chmod 600 /home/$NEW_USER/.ssh/authorized_keys }这里有两个细节。第一sudo授权文件用NOPASSWD是为了避免每次sudo都输密码在自动化场景下非常必要但如果你对安全要求极高可以去掉NOPASSWD。第二SSH公钥我建议写成变量从外部传入而不要直接写在脚本里这样脚本可以被不同团队复用公钥泄露的风险也小。用户创建完之后我会在/etc/ssh/sshd_config里做几处修改关闭root密码登录PermitRootLogin no、启用密钥认证PubkeyAuthentication yes、禁止空密码PermitEmptyPasswords no。修改完必须重启sshd服务但这里有个大坑如果你正在用SSH连接服务器而配置写错了重启sshd会直接断掉你的连接甚至再也连不上。我的做法是先写一个临时允许窗口确认能登录后再关闭root或者用sshd -t先校验配置语法再systemctl restart sshd。安全起见推荐用下面这种方式/usr/sbin/sshd -t || { echo [ERROR] sshd配置语法错误; exit 1; } systemctl reload sshd用reload而不是restart可以保持现有连接不断开。这个细节非常重要我因为一时手快吃过不少亏。3.3 安全加固防火墙、SELinux、Fail2Ban、系统更新安全加固不能一刀切。比如SELinux虽然号称更安全但如果你的业务程序没有适配SELinux策略开起来反而会制造一堆诡异问题。我的原则是内网机器如果业务明确不需要就保持SELINUXdisabled并永久生效如果是面向公网的机器我会开启SELinux并做好上下文配置。在脚本里我用一个变量来切换init_security() { # 防火墙基础策略 systemctl enable --now firewalld firewall-cmd --set-default-zonepublic firewall-cmd --permanent --add-servicessh firewall-cmd --permanent --add-servicehttp firewall-cmd --permanent --add-servicehttps firewall-cmd --reload }如果系统是Ubuntu对应的命令是ufw allow OpenSSH、ufw enable。注意Ubuntu的ufw如果启用时没有先允许SSH会导致当前连接断开。所以我总是在放行SSH端口之后再执行ufw enable。系统更新是另一个争议点。有人喜欢全量升级有人觉得升级可能引入不兼容问题。我的建议是初始化阶段只做安全更新避免把所有软件包都更新到最新版本。因为业务依赖的版本一旦被更新可能会出现兼容性问题这是运维事故的高发区之一。使用yum的话可以执行yum update -y --securityUbuntu则是apt-get update apt-get upgrade -y --only-upgrade-securityFail2ban是我通常会在公网服务器上装的工具它能有效拦截暴力破解。但这里提醒一句Fail2ban和firewalld配合时要避免它自动封禁自己的IP。我一般会配置ignoreip包含运维出口IP防止误封。3.4 常用软件安装与内核参数调优初始化肯定要装一些基础工具。每个运维都有自己习惯的软件列表我的常用清单是BASIC_TOOLSvim git curl wget lrzsz net-tools tree jq telnet nc lsof bind-utils对于Ubuntu需要把net-tools替换成net-tools其实包名差不多。安装命令没什么技术含量但有个效率技巧把软件列表放进一个变量然后用一条命令安装而不是每个软件执行一次install。这样不仅日志干净速度也更快。内核参数调优是初始化脚本里比较进阶的部分。比如高并发服务器常见需要调整net.core.somaxconn、net.ipv4.tcp_tw_reuse、net.ipv4.ip_local_port_range、fs.file-max以及进程的ulimit限制。我会把这些参数写入/etc/sysctl.d/99-custom.conf然后执行sysctl -pcat /etc/sysctl.d/99-custom.conf EOF net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_timestamps 0 net.ipv4.ip_local_port_range 10000 65000 net.core.somaxconn 32768 net.core.netdev_max_backlog 16384 fs.file-max 6553560 vm.swappiness 10 EOF sysctl --system注意不要随便改net.ipv4.tcp_tw_recycle这个参数在NAT环境下会导致连接异常现在的新内核已经删掉了。还有vm.swappiness如果机器是DBA或者大数据节点的角色建议设得更保守。这个参数不是越高越好我一般设在10到30之间。文件描述符限制也经常被忽略。如果跑Java应用或高并发服务默认1024的nofile很容易触顶。我会在/etc/security/limits.d/20-nofile.conf里写入* soft nofile 655350 * hard nofile 655350修改完要重新登录才能生效。测试时可以用ulimit -n确认。如果还是1024大概率是PAM的pam_limits.so没有被加载需要检查/etc/pam.d/common-session。3.5 日志审计与运行状态检查脚本执行完以后你怎么确认结果我的做法是脚本结束前自动生成一份“初始化报告”包括每个模块的执行状态、系统关键参数、网络连通性、当前监听端口等。这样不用人工逐个去验证。实现方式很简单利用一个全局变量记录模块执行结果declare -a MODULE_STATUS() record_status() { if [ $? -eq 0 ]; then MODULE_STATUS($1: SUCCESS) else MODULE_STATUS($1: FAILED) fi } # 在每个模块结束后调用record_status init_base最后生成报告时遍历这个数组把结果写入文件。同时再跑几条常见检查# 检查关键端口 ss -tlnp | grep -E :(22|80|443)\b # 检查时间同步状态 timedatectl status # 检查系统负载 uptime这些检查命令的输出会帮助你在第一时间发现配置异常。比如时间同步如果状态为“no servers,说明chrony没起来端口监听如果少了一个就要回头查防火墙。4. 实操过程的完整记录与参数说明4.1 脚本整体框架与执行流程说了那么多模块我把一个简化版的脚本主体结构放出来。实际使用时你不需要照抄重点是理解它的骨架#!/bin/bash # 描述: Linux系统初始化脚本 # 用法: sudo bash init_system.sh -h hostname -u username -k pubkey set -euo pipefail # 全局变量 HOSTNAME_VAR NEW_USER SSH_PUBLIC_KEY TIMEZONEAsia/Shanghai BASE_LOG/var/log/sys_init # 解析参数 while getopts h:u:k: opt; do case $opt in h) HOSTNAME_VAR$OPTARG ;; u) NEW_USER$OPTARG ;; k) SSH_PUBLIC_KEY$OPTARG ;; *) echo 用法: $0 -h 主机名 -u 用户名 -k 公钥; exit 1 ;; esac done # 日志函数 LOG_FILE${BASE_LOG}_$(date %Y%m%d_%H%M%S).log log_info() { echo [$(date %Y-%m-%d %H:%M:%S)] [INFO] $* | tee -a $LOG_FILE; } log_error() { echo [$(date %Y-%m-%d %H:%M:%S)] [ERROR] $* | tee -a $LOG_FILE 2; } # 系统识别 detect_os() { ... } # 模块函数 init_base() { ...; } init_repo() { ...; } init_user() { ...; } init_security() { ...; } init_tune() { ...; } init_tools() { ...; } # 主流程 main() { detect_os init_base init_repo init_user init_security init_tune init_tools log_info 系统初始化全部完成 } main这个骨架有几个好处参数通过命令行传给脚本而不是改脚本内容每个模块独立可以单独注释日志统一管理。我实际运行时一般会加bash -x init_system.sh来跟踪每一步但正式批量执行时不会加以免日志刷屏。4.2 关键函数详解与调参心得具体到每个函数我挑几个最容易出问题的说说。主机名设置函数我踩过一个坑直接用hostnamectl set-hostname后需要重新打开一个shell才能看到新主机名某些旧脚本里依赖的/etc/hosts没有同步更新于是就一直用旧主机名解析。所以我会在设置主机名后同步更新hosts文件init_base() { if [ -n $HOSTNAME_VAR ]; then hostnamectl set-hostname $HOSTNAME_VAR grep -q $HOSTNAME_VAR /etc/hosts || echo 127.0.0.1 $HOSTNAME_VAR /etc/hosts fi }另外DNS服务器的写法也要注意。如果直接覆盖/etc/resolv.conf重启后NetworkManager可能把它拉回原样。我现在倾向于使用nmcli修改但不同系统版本差异较大所以脚本里加了个判断如果存在/etc/NetworkManager/NetworkManager.conf就修改它的dns配置否则才走resolv.conf的兜底方案。软件源替换函数我认为了解VERSION_ID是必须的。CentOS 7和8的仓库结构完全不同如果你直接拿7的repo文件放到8上执行yum makecache一定会报错。所以每个版本都要单独写repo内容。我习惯在脚本里维护一个case分支把每个分支下的repo配置写成heredoc比curl远程文件更可控因为curl拿到的文件内容是第三方维护的不排除哪天内容变化导致兼容问题。用户权限模块我建议在创建用户前先检查系统是否已经存在同名用户。如果用useradd会直接报错但有了set -e脚本会退出。虽然这也是一种保护但更好的体验是跳过并记录警告。上面的代码已经体现了这个思路。还有一个细节是sudoers文件权限必须是440如果权限错误sudo会拒绝加载这个文件甚至导致当前用户无法工作。4.3 第一次执行脚本的经验与调整我第一次在实际环境跑这个脚本的时候是在一台CentOS 7机器上。当时脚本写得很粗糙没有set -e,没有日志函数结果在安装软件那一步因为网络源超时后面所有配置都没执行。我愣愣地看着屏幕上的一片红还不知道问题出在哪。从那以后我才明白自动化脚本必须具备“快速定位”能力日志、退出码、步骤标记一个都不能少。后来我把整个初始化流程梳理成三个阶段预检查阶段确认系统版本、网络连通性、磁盘空间、是否存在重要进程。预检查不通过就退出避免执行到一半才发现环境不满足。配置执行阶段按模块顺序执行每个模块执行前打点执行后记录结果。结果校验阶段复用ss、curl、timedatectl等命令做最终验证并输出摘要。这里特别说一下预检查。很多脚本新手喜欢拿到机器直接开始配置但生产环境常有各种“意外”磁盘满了、/usr分区只读、系统正处于半安装状态。我在脚本开头加了一段pre_check() { # 检查当前用户是否为root [ $(id -u) 0 ] || { echo 请使用root执行; exit 1; } # 检查磁盘空间 ROOT_FREE$(df / | awk NR2 {print $4}) if [ $ROOT_FREE -lt 1048576 ]; then echo 根分区剩余空间小于1GB拒绝执行 exit 1 fi # 检查网络连接 ping -c 2 -W 2 mirrors.aliyun.com /dev/null 21 || echo [WARN] 外网不可达软件源模块将跳过 }这些预检查看起来简单却能在关键时刻救你一命。比如内网机器没有外网访问权限预检查发现外网不通就跳过源替换和软件安装但继续执行其他模块。这个灵活性比硬编码重要得多。5. 常见问题与排错技巧实录5.1 脚本运行中断但找不到错误原因很多人遇到脚本中途退出第一反应是用echo在每行之间打点然后重新跑。这种方法太原始了而且浪费时间。我一般这样排查先看日志文件日志里每个[ERROR]都能对应到具体函数。用bash -x重新执行看退出前最后几条命令是什么。如果脚本里用了set -e检查最近几条命令是否返回非零尤其是grep、test、[ ]这类命令。分享一个实用技巧在关键函数开头增加一个trap捕获EXIT信号trap echo 出错行号: $LINENO, 函数: ${FUNCNAME:-main} | tee -a $LOG_FILE ERR这样不管脚本在哪一行失败日志里都会留下行号和函数名定位起来非常快。$LINENO是bash内置变量在单引号里会被trap展开注意别写成双引号否则取到的是trap定义时的值。5.2 在CentOS上编写的脚本在Ubuntu上报错跨发行版是shell脚本最头疼的问题我列几个最常见的坑包管理器命令不同yum还是apt-get必须分开判断。软件包名称不同比如bind-utils在Ubuntu里对应dnsutilsnet-tools包名虽然一样但可能需要单独启用universe源。防火墙命令不同firewalld vs ufw。服务管理差异有些老系统还在用service新系统都是systemctl。如果脚本里写死systemctl restart networkUbuntu上就报错。我的建议是尽量使用“抽象层”写法比如定义一个变量$PKG_CMD和$SVC_CMD在识别系统时赋值后面所有函数都调用变量而不是直接写命令名称。这样脚本的可移植性会大大提高。5.3 用户创建成功但无法登录用户创建了sudo也授权了SSH公钥放好了但登录时还是报“Permission denied”这个问题出现频率极高。90%的情况是权限不对SSH对~/.ssh目录权限非常敏感目录必须是700authorized_keys必须是600而且不能是root属主。还有一个坑是/home/$NEW_USER目录本身如果属于root用户就无法写入。可以用chown -R $NEW_USER:$NEW_USER /home/$NEW_USER解决。此外SELinux也可能拦截SSH读取authorized_keys。如果SELinux开启需要检查ls -Z /home/$NEW_USER/.ssh/authorized_keys确保上下文是ssh_home_t否则执行restorecon -R -v /home/$NEW_USER/.ssh修复。真正排查这类问题时建议先看/var/log/secure或journalctl -u sshd里的具体报错通常会有bad ownership or modes这样的关键提示。5.4 防火墙配置后SSH连接断开初始化脚本里启用防火墙是高风险操作。特别是你在远程执行脚本一旦防火墙规则没有允许SSH22端口执行完firewall-cmd --reload的瞬间连接就断了。防范措施在启用防火墙之前先把当前服务器的IP加入白名单MY_IP$(who am i | awk {print $NF} | sed s/[()]//g) firewall-cmd --permanent --add-rich-rulerule familyipv4 source address$MY_IP port port22 protocoltcp accept启用防火墙后延时5秒恢复原规则防止误操作把自己锁在外面。实现“延时恢复”可以用一个后台任务(sleep 5; if ! ssh -o ConnectTimeout2 -o StrictHostKeyCheckingno rootlocalhost true /dev/null 21; then firewall-cmd --reload fi) 但更稳妥的做法是在配置防火墙前手动在另一个终端保持一个登录会话以便随时回滚。运维操作的安全意识永远比命令本身重要。5.5 日志不输出或中文乱码日志不输出最常见的坑是重定向顺序错误。比如echo ... $LOG_FILE 21可能会把错误信息覆盖掉。更常见的是在函数内部使用管道时set -o pipefail导致日志文件无法写。我建议日志输出统一走函数不要自己到处用echo 。中文乱码问题多数是系统没有安装中文语言包或者locale环境不是en_US.UTF-8。初始化脚本里我建议把locale设置成与业务一致否则脚本里的中文日志会显示成乱码。如果实在需要中文输出先确保安装了glibc-langpack-zh或执行localectl set-locale LANGen_US.UTF-8。5.6 其他值得注意的坑set -u与${VAR:-default}搭配未定义变量在set -u下会直接退出养成写参数默认值的习惯。使用command -v判断程序是否存在比用which更可靠因为which在最小化安装中可能不存在。脚本里所有路径尽量使用绝对路径避免因PATH变化导致找不到命令。重启服务前先运行配置语法检查比如nginx -t、sshd -t、named-checkconf这是运维的基本素养。我在实际运维中还有一个体会初始化脚本不是写完就完了它需要跟随系统版本一起迭代。比如CentOS 7停止维护后镜像源要切换成vault源Ubuntu 22.04里iptables被nftables替换防火墙命令也要相应调整。所以每隔一段时间我都会把脚本放到一台新机器上测试一次确保它仍然能顺利跑完。最后再分享一个小技巧批量初始化多台机器时可以把脚本放到一台跳板机上用for循环加sshpass批量执行但注意密码管理要安全。也可以用pssh并行跑效率会高很多。不过无论用什么方式执行前一定要先在一台目标机器上试跑一遍确认无误后再批量。运维这个行当慢一点不可怕可怕的是自动化把错误加速放大。
返回列表