学习 Linux 运维时,我们通常先从一条条命令开始:安装软件包、修改配置文件、启动服务、检查端口。单台服务器上,这种方式足够直接;但当环境扩展到多台服务器,并且服务之间开始产生依赖,问题就不再只是“命令会不会写”,而是“部署过程能不能被重复执行”。
我的 linux_shell 项目,就是对这个问题的一次实践。
它以 Shell 脚本为基础,尝试部署一套包含负载均衡、Web 应用、数据库、缓存、共享存储和备份服务的完整环境。项目中没有复杂的平台,也没有成熟的编排引擎,核心方法很朴素:先梳理服务器职责,再把每个节点上的手工操作固化为脚本,最后按照依赖顺序完成整套环境部署。
这套项目后来也成为我编写 Ansible 自动化项目的重要基础。回头看,它最有价值的地方并不是节省了多少次键盘输入,而是让我第一次真正理解:自动化的本质,是把系统架构、依赖关系和操作顺序写进代码。
项目要解决什么问题
项目模拟的是一套常见的中小型 Web 业务架构,共有 8 台业务服务器:
| 节点 | 主要职责 |
|---|---|
| lb01、lb02 | Nginx 负载均衡与 Keepalived 高可用 |
| web01、web02 | Nginx、PHP-FPM 和 Web 业务 |
| nfs01 | NFS 共享存储与 Lsyncd 实时同步 |
| backup01 | Rsync 备份接收端 |
| db01 | MariaDB 数据库 |
| rd01 | Redis 缓存与 Session 存储 |
此外还有一台跳板机,用于统一登录和执行初始化操作。
如果完全手工部署,需要在不同服务器上反复完成以下工作:
- 创建用户与目录。
- 安装并配置 Nginx、PHP、MariaDB、Redis 等服务。
- 配置 NFS 挂载和文件权限。
- 部署 WordPress 等 Web 业务。
- 配置负载均衡与虚拟 IP。
- 建立 NFS 到备份服务器的同步链路。
- 启动服务并设置开机自启。
这些操作单独看并不复杂,真正麻烦的是它们不能随意调换顺序。
例如,Web 节点挂载共享目录之前,NFS 服务必须已经可用;应用连接数据库之前,数据库用户和权限必须创建完成;Lsyncd 启动之前,Rsync 服务端必须准备好认证信息和备份目录;Keepalived 接管虚拟 IP 之前,Nginx 必须能够正常提供服务。
因此,项目首先要解决的不是“如何写一个很长的脚本”,而是“如何描述整套系统的部署链路”。
先画出服务依赖关系
这套架构可以拆成四层:
访问入口
└─ lb01 / lb02
├─ Nginx 负载均衡
└─ Keepalived 虚拟 IP
业务层
└─ web01 / web02
├─ Nginx
├─ PHP-FPM
└─ WordPress 等 Web 业务
数据层
├─ db01:MariaDB
├─ rd01:Redis
└─ nfs01:共享业务文件
备份层
└─ backup01:接收 nfs01 的实时同步数据
从这个关系可以反推出部署顺序:
SSH 初始化
→ NFS、数据库、Redis
→ Web 节点
→ 负载均衡节点
→ 备份节点
→ 重启并验证同步链路
这一步非常重要。很多自动化脚本失败,并不是某条命令本身有问题,而是执行命令时,它依赖的服务还没有准备好。
如果没有先理清依赖,脚本只是把手工操作变快;一旦有了明确的依赖链,脚本才开始具备“部署系统”的能力。
第一步:统一 SSH 访问
多服务器自动化首先要解决的是远程连接。
项目中的 setup_ssh.sh 会遍历服务器列表,将跳板机公钥复制到各个目标节点,然后收集服务器的 SSH 主机指纹。它的基本结构可以简化为:
servers=(
"10.0.0.105"
"10.0.0.106"
"10.0.0.107"
"10.0.0.108"
)
for server in "${servers[@]}"; do
ssh-copy-id "root@$server"
done
服务器数组解决了两个问题:
- 节点清单集中维护,不需要重复修改多段命令。
- 可以使用循环对所有节点执行相同的初始化操作。
这是非常典型的 Shell 自动化思路:先把变化的数据提取出来,再用循环消除重复操作。
不过,项目最初为了减少首次连接时的交互,使用过 sshpass 和关闭严格主机指纹检查的方式。这在隔离的学习环境中比较方便,但不应该直接照搬到生产环境。
更稳妥的方案是:
- 不在脚本中保存明文密码。
- 首次初始化后立即停用密码登录。
- 保留
StrictHostKeyChecking,通过可信渠道提前维护known_hosts。 - 使用普通运维账号配合
sudo,避免长期直接使用 root。 - 为自动化账号限制可登录来源和权限范围。
自动化降低了操作成本,也会放大错误和凭据泄漏的影响。连接越方便,权限边界越需要被认真设计。
按节点职责拆分脚本
项目没有把所有命令塞进一个总脚本,而是按照服务器职责拆分:
setup_ssh.sh
lb01.sh
lb02.sh
web01.sh
nfs01.sh
backup01.sh
db01.sh
rd01.sh
这种拆分方式虽然简单,但已经具备了模块化的雏形。
db01.sh 只关心数据库安装、启动、账号和权限;rd01.sh 只负责 Redis;nfs01.sh 负责共享目录和实时同步;负载均衡脚本则聚焦 Nginx upstream、代理参数、Keepalived 和健康检查。
按职责拆分有三个直接好处:
- 某个节点部署失败时,可以单独排查和重跑。
- 阅读脚本时,能够快速定位某项服务的配置。
- 将来迁移到 Ansible Role 时,拆分边界已经基本形成。
它也说明一个很实用的经验:Shell 项目不一定要一开始就设计复杂框架,但至少应该避免把数据库、缓存、Web 和负载均衡混在同一个不可维护的大文件中。
Web 节点:一台服务器上的完整业务栈
Web 节点是项目中内容最多的部分。
脚本需要依次完成:
- 创建统一的
www用户和用户组。 - 安装并配置 Nginx。
- 安装 PHP-FPM 及扩展。
- 调整 PHP-FPM 运行用户和监听地址。
- 挂载 NFS 共享目录。
- 写入不同站点的 Nginx 配置。
- 下载并部署 WordPress。
- 设置文件权限。
- 连接远程 MariaDB 和 Redis。
这里最值得复盘的不是安装命令,而是服务之间的边界。
Nginx 负责接收 HTTP 请求,遇到 PHP 文件时交给 PHP-FPM;业务代码放在 NFS 共享目录中,让两台 Web 服务器访问相同文件;数据库独立运行在 db01;Session 保存到 Redis,避免请求在 web01 和 web02 之间切换时丢失登录状态。
也就是说,这些脚本不只是在“装软件”,而是在实现一套无状态 Web 节点架构:
用户请求
→ 负载均衡
→ 任意 Web 节点
→ NFS 读取共享代码和文件
→ MariaDB 读取业务数据
→ Redis 读取 Session
当 Web 节点不再独占业务数据时,新增节点和故障切换都会更容易。
项目中 web02 采用复制 web01 的方式快速生成。对于实验环境,这能节省部署时间;但从自动化设计上看,这也是一个明显的改进点。更理想的方式,是让 web01 和 web02 从同一份脚本独立部署,而不是依赖虚拟机复制。只有这样,脚本才能证明自己完整描述了节点状态。
负载均衡与虚拟 IP 漂移
入口层由两台负载均衡服务器组成。
Nginx upstream 将请求转发到两台 Web 节点:
upstream web_cluster {
server 10.0.0.107:80;
server 10.0.0.108:80;
}
server {
listen 80;
location / {
proxy_pass http://web_cluster;
}
}
只部署一台负载均衡服务器,会产生新的单点故障。因此项目又引入 Keepalived,在 lb01 和 lb02 之间维护一个虚拟 IP。
正常情况下,MASTER 节点持有虚拟 IP;当它发生故障时,BACKUP 节点接管地址,客户端仍然访问同一个入口。
项目还编写了 Nginx 健康检查脚本:
if ! pgrep -x nginx >/dev/null; then
systemctl restart nginx
sleep 1
if ! pgrep -x nginx >/dev/null; then
systemctl stop keepalived
exit 1
fi
fi
这段逻辑体现了两级处理:
- Nginx 进程消失时,先尝试自动恢复。
- 恢复失败时退出高可用集群,让备用节点接管入口。
比起只检查服务器是否在线,这种方式更接近真正的服务健康检查。服务器还活着,不代表 Nginx 一定能工作;高可用判断应该尽量靠近用户真正依赖的服务。
进一步改进时,还可以从“进程是否存在”升级为“HTTP 请求是否返回预期状态”,例如:
curl --fail --silent --max-time 2 http://127.0.0.1/health >/dev/null
这样可以识别进程存在但配置错误、端口不可用或后端异常等情况。
NFS、Lsyncd 与 Rsync 备份链路
共享存储和备份是项目中另一条完整链路。
nfs01 将业务目录通过 NFS 提供给 Web 节点,同时运行 Lsyncd 监听文件变化;当目录内容发生变化时,Lsyncd 调用 Rsync,将数据同步到 backup01。
web01 / web02
↓ 挂载
nfs01
↓ Lsyncd 监听变化
↓ Rsync 增量传输
backup01
这套组合的优点是实现成本低,而且非常适合理解 Linux 文件服务之间的协作方式:
- NFS 解决多台 Web 节点共享文件的问题。
- Lsyncd 负责监听目录变化。
- Rsync 负责高效传输变化的数据。
- backup01 将备份数据与生产共享目录分离。
但实时同步不等于完整备份。
如果源目录中的文件被误删,并且同步配置允许删除目标文件,那么错误也会被快速同步到 backup01。真正的生产备份还应该考虑版本保留、快照、异地副本、恢复演练和备份监控。
因此,这条链路更准确的名称应该是“实时副本”或“同步备份”。它解决了服务器损坏后的数据副本问题,但不能独立解决误操作、勒索软件或历史版本恢复问题。
Shell 自动化最容易遇到的三个问题
这套项目成功地把大量手工操作固化了下来,但它也暴露了 Shell 作为配置管理工具的边界。
1. 重复执行不一定安全
例如:
useradd www
mkdir /data
cat >> /etc/fstab << 'EOF'
...
EOF
第一次执行可能成功,第二次执行就可能因为用户已存在、目录已存在或配置重复追加而产生问题。
更稳妥的写法需要显式判断状态:
id www >/dev/null 2>&1 || useradd -M -s /sbin/nologin www
mkdir -p /data
grep -qF "$mount_entry" /etc/fstab || echo "$mount_entry" >> /etc/fstab
自动化脚本不能只考虑“从零开始第一次运行”,还应该考虑失败后重跑、局部修复和重复执行。
2. 错误可能被悄悄忽略
Shell 默认会继续执行后续命令。前面的软件安装或配置写入失败,脚本仍可能在最后打印“部署完成”。
一个基础改进是:
set -Eeuo pipefail
同时为关键步骤增加检查:
nginx -t
php-fpm -t
systemctl is-active --quiet nginx
curl --fail http://127.0.0.1/
“命令执行完”不等于“服务部署成功”。自动化必须定义自己的成功标准。
3. 配置、变量和敏感信息混在代码中
IP 地址、端口、用户名和密码如果直接散落在脚本里,修改环境时很容易漏改,也不适合提交到 Git 仓库。
至少可以先把普通变量集中到配置文件:
DB_HOST="10.0.0.151"
REDIS_HOST="10.0.0.161"
NFS_HOST="10.0.0.131"
敏感信息则应通过环境变量、权限受控的独立文件或专门的密钥管理服务注入:
: "${DB_PASSWORD:?DB_PASSWORD is required}"
代码仓库应该描述“需要一个数据库密码”,而不是保存密码本身。
从 Shell 项目到 Ansible 项目
完成这套 Shell 自动化后,再看 Ansible 的 Inventory、变量、Role、Handler 和 Playbook,会更容易理解它们为什么存在。
| Shell 项目中的做法 | 在 Ansible 中的演进 |
|---|---|
| 服务器 IP 数组 | Inventory 主机清单 |
| 脚本顶部变量 | group_vars、host_vars |
| 按节点拆分脚本 | Role |
| 按顺序手动执行 | Playbook 编排 |
systemctl restart |
Handler |
grep 后再修改 |
声明式模块的幂等性 |
| 明文敏感变量 | Ansible Vault |
| 手工查看输出 | Task 状态与执行结果 |
这并不意味着 Shell 脚本没有价值。
Shell 仍然非常适合单机初始化、小型维护任务、故障处理、系统检查和作为其他自动化工具的辅助脚本。问题只在于,当服务器数量、环境数量和服务依赖不断增加时,仅靠 Shell 手动维护执行顺序和状态判断,成本会越来越高。
这套项目可以看作我的自动化学习路径中的第一阶段:
手工命令
→ 单机 Shell 脚本
→ 多节点 Shell 部署链路
→ Ansible 角色化与统一编排
每一步都不是推翻前一步,而是在前一步暴露的问题上继续演进。
如果重新设计一次
如果今天重新整理这个项目,我会保留它清晰的节点职责,同时进行以下改造:
- 增加统一入口脚本,明确部署阶段和执行顺序。
- 所有脚本启用严格错误处理,并记录执行日志。
- 把 IP、端口和普通变量移到独立配置文件。
- 移除所有明文密码,改为运行时注入。
- 为用户、目录、挂载和配置追加操作增加幂等判断。
- 每个服务部署后执行配置检查、端口检查和业务探测。
- 不再复制虚拟机生成相同节点,而是让节点独立执行同一份脚本。
- 为备份增加快照、版本保留和恢复测试。
- 使用 ShellCheck 和测试环境提前发现脚本问题。
- 当环境规模继续增长时,将稳定逻辑迁移到 Ansible。
其中最重要的一项,是让脚本能够回答三个问题:
- 当前执行到了哪一步?
- 失败的具体原因是什么?
- 修复问题后能否安全重跑?
一个只能在全新系统上成功运行一次的脚本,更像安装记录;一个能够检查状态、处理错误并安全重跑的脚本,才真正接近工程化自动化。
结语
linux_shell 项目并不完美。它有学习阶段常见的硬编码、重复执行风险和安全边界问题,也包含一些依赖人工补充的步骤。
但它完成了一次非常重要的转变:把分散在 8 台服务器上的操作,整理成了有顺序、有职责、有架构关系的一套部署链路。
也正是通过这套脚本,我开始意识到,运维自动化不只是“把命令写进文件”。真正需要被自动化的,是系统状态,是服务器之间的关系,也是部署失败后可以重新恢复的过程。
Shell 让我把手工经验写成代码;Ansible 则让我进一步把这些代码组织成工程。
这两段实践连接起来,才构成了一条完整的自动化演进路径。
Discussion
评论