如何写一个系统服务安装脚本

wen 实用脚本 2

如何编写一个健壮的系统服务安装脚本(Systemd/Init.d 实战指南)

目录导读

  1. 为什么需要服务安装脚本? —— 运维自动化的基石
  2. 脚本的核心架构 —— 前置检查、安装、配置、启动、验证五段式
  3. 针对 Systemd 与 Init.d 的差异化处理
  4. 常见陷阱与规避策略(含权限、路径、日志轮转)
  5. 脚本健壮性增强:幂等性、回滚机制与日志记录
  6. 问答精华 —— 运维老手最关心的 5 个问题

手把手构建你的服务安装脚本

如何写一个系统服务安装脚本

为什么需要服务安装脚本?

当你在 50 台服务器上手动敲 systemctl start nginx 时,效率与一致性已经失败,一个系统服务安装脚本能实现:一键部署、环境统一、可重复执行,它不仅仅是把二进制文件拷贝到 /usr/local/bin,更是一个状态机——负责处理依赖、权限、端口冲突、开机自启等复杂逻辑。

脚本的核心架构:五段式设计

第一段:前置检查(Pre-flight Checks)

  • 检查是否 root 权限(关键!EUID 必须为 0)。
  • 检查操作系统发行版(/etc/os-release)以决定使用 systemctl 还是 service
  • 检查端口冲突(ss -lntp | grep :8080)。
  • 检查依赖包(command -v wgetdpkg -l)。

第二段:安装核心文件

  • 定义变量:SERVICE_NAME="myapp", INSTALL_DIR="/opt/myapp"
  • 使用 install -d -m 755 $INSTALL_DIR 创建目录。
  • 使用 install -m 644 config.yaml $INSTALL_DIR/ 拷贝文件(相比 cpinstall 能精确控制属主与权限)。

第三段:注册系统服务

  • Systemd 系统:写 /etc/systemd/system/$SERVICE_NAME.service 文件。
    
    [Unit]
    Description=My Custom Service
    After=network.target

[Service] Type=simple User=myapp ExecStart=/opt/myapp/run.sh Restart=on-failure RestartSec=5

[Install] WantedBy=multi-user.target

- **Init.d 系统**:写 `/etc/init.d/$SERVICE_NAME` 脚本,并包含 `start|stop|restart|status` 函数,然后执行 `update-rc.d $SERVICE_NAME defaults`。
**第四段:启动与验证**
- 先 `daemon-reload`(systemd),再 `enable` 开机自启,`start`。
- 验证:`systemctl is-active` 或 `pgrep -f yourapp`,或者 `curl -f http://localhost:8080/health`。
**第五段:收尾与优雅退出**
- 设置 `trap` 捕获错误,若启动失败,自动回滚(删除文件、恢复备份)。
### 3. 差异化处理:Systemd vs Init.d
**在脚本中检测 init 系统**:
```bash
if [ -d /run/systemd/system ]; then
    INIT_SYS="systemd"
else
    INIT_SYS="initd"
fi
  • Systemd:注意 Type 的选择。forking 用于启动后父进程退出的程序;simple 适用于前台运行。
  • Init.d:必须处理 pidfile 约定,并确保脚本开头有 #!/bin/sh 且具有执行权限。

常见陷阱与规避策略

  • 权限错误:服务运行用户尽量不使用 root,单独创建 useradd -r -s /sbin/nologin $SERVICE_USER
  • 路径硬编码:绝对路径与环境变量($JAVA_HOME)混用易出错,优先使用 readlink -f 解析脚本自身位置。
  • 日志处理:不要手动重定向输出到 /dev/null,应配置 StandardOutput=append:/var/log/myapp.log
  • 卸载残留:脚本应提供 uninstall 参数,删除服务文件并执行 daemon-reload

脚本健壮性增强:幂等性、回滚与日志

  • 幂等:每次执行前检查 if [ -f "$INSTALL_DIR/bin/app" ]; then echo "已安装,跳过安装步骤"; fi
  • 回滚:安装前备份 conf 文件为 .bak,若启动失败,cp .bak 恢复。
  • 全量日志:将整个脚本输出写入 $INSTALL_DIR/install.log,方便远程排查。

问答精华(你关心的 5 个问题)

Q1: 脚本中如何优雅地停止旧服务版本?

A: 使用 service $NAME stop || true 忽略错误,但更好的方式是先查 PID,kill -TERM,等待 3 秒后 kill -KILL,配合 trap 确保优雅退出。

Q2: 如何处理服务配置文件中的敏感信息(如数据库密码)?

A: 将密码写入 /etc/$SERVICE_NAME.conf 并设置 chmod 600,服务运行时通过 EnvironmentFile 读取,避免明文出现在进程参数中。

Q3: systemd 服务启动后立即退出(exit code 1),如何排查?

A: 不要只看 journalctl -u 的尾部错误,在脚本中捕获 变量,并在启动失败时自动输出 journalctl -xe --no-pager | tail -50 到安装日志。

Q4: 如何确保脚本跨 CentOS 7 与 Ubuntu 20.04 兼容?

A: 对于包管理器,使用 yum || apt 二选一;对于服务管理,抽象 systemctlservice 为函数变量:start_cmd="systemctl start $SERVICE"

Q5: 安装脚本本身需要加密保护吗?

A: 不要加密,采用 shfmt 规范格式,并用 shellcheck 静态检查,若包含敏感数据,可在脚本内调用外部加密的 secrets.env 文件。


一个好的服务安装脚本,是运维人品的直接体现,它不在于技巧炫技,而在于面对未知环境时的防御性思维,建议你写完后,在干净的虚拟机里反复执行三次,每次删除不同配置项,观察脚本的报错提示是否清晰,最终把脚本提交到内部 Git,用 CI 工具定时做冒烟测试,这才是工程化精神。

抱歉,评论功能暂时关闭!