# 安装之前，先让客户的机器说真话

> 一个只读 bash 脚本把每项检查评为 PASS、WARN 或 FAIL，只有 FAIL 能叫停安装。

原文: https://fdetimes.net/zh/guides/preflight-script-pass-warn-fail/

Elasticsearch 处于生产模式时，只要有一项引导检查（bootstrap check）失败，它就拒绝启动。在开发模式下，同样的失败只会在日志里显示为一条警告。即使从没替客户装过 Elasticsearch，这条规则也值得带到每一次部署中。

设想在客户现场安装：安装程序跑到一半报错，这时才发现 swap 开着、某个内核参数没设，或者防火墙挡住了某个端口。预检脚本把这些发现全部提前到安装开始之前。它还定下清楚的规则：哪些发现可以叫停安装，哪些只需记录在案。

本指南演示如何用 bash 写这样一个脚本。动手尝试需要一台有 sudo 权限的 Linux 机器或虚拟机。读取内存、swap 和网络端口的命令都是**简化版，并假定运行在 Linux 上**：真正使用之前，要在客户所用的具体发行版上重新测试。

## 严重程度要靠规则，不能凭感觉

每项结果都用一个问题来分级：如果忽略它，待安装组件是会拒绝运行，还是只是运行得不好？拒绝运行就是 FAIL。运行不佳，或者拿不准，就是 WARN。

Kubernetes 文档给出了一个明确的 FAIL：默认情况下，kubelet 检测到节点上有 swap 就不会启动。相比之下，一个从没和你的产品一起测试过的 OS 版本不一定有问题，所以只值得一个 WARN。

注意这个问题里的“待安装组件”。swap 之所以是 FAIL，是因为 kubelet，而不是对所有软件都如此。每项检查只有在对应组件出现在安装清单上时才有意义，下面的脚本按组件启用检查。

## 第 1 步：所有结果都交给一个 report 函数

每项检查都经过同一个函数。这个函数负责应用运行模式和豁免列表。

```bash
#!/usr/bin/env bash
# preflight.sh — 只读，不改动客户机器上的任何东西
MODE="${MODE:-production}"   # production | development（仅限测试虚拟机）
WAIVE="${WAIVE:-}"           # 从 FAIL 降级为 WARN 的检查项，例如 "swap,net_443"
FAILS=0; WARNS=0

if [ "$WAIVE" = all ]; then
  echo "不接受 WAIVE=all：请按名称逐项列出检查"; exit 2
fi

report() {  # report <id> <PASS|WARN|FAIL> <message>
  local id="$1" level="$2" msg="$3"
  if [ "$level" = FAIL ]; then
    if [ "$MODE" = development ] || [[ ",$WAIVE," == *",$id,"* ]]; then
      level=WARN; msg="$msg（已降级为 WARN）"
    fi
  fi
  case "$level" in FAIL) FAILS=$((FAILS+1));; WARN) WARNS=$((WARNS+1));; esac
  printf '%-5s %-14s %s\n' "$level" "$id" "$msg"
}
```

`development` 模式模仿 Elasticsearch 把错误降级为警告的做法。`WAIVE` 借鉴自 kubeadm：升级前运行的预检阶段有一个 `--ignore-preflight-errors` 参数，它接受一组检查项，这些检查的错误只显示为警告。这个脚本有意拒绝 `all` 这个值，让每一项豁免都必须写明检查名称。

这里有个陷阱：`MODE=development` 会把所有 FAIL 降级为 WARN，实际上也等于忽略一切。因此这个模式只留给自己的测试虚拟机，绝不在客户的生产机器上运行。在客户机器上只用 `production`，再加上按名称逐项列出的 `WAIVE`。

**验证：**调用 `report demo FAIL "test"`，设置 `WAIVE=demo` 后再运行一次。这次应打印 WARN，并带有“已降级为 WARN”的注明。

## 第 2 步：检查资源，在信息里写明阈值

```bash
MIN_RAM_KB="${MIN_RAM_KB:-2097152}"   # 假定的阈值；请换成你的产品推荐值
check_ram() {
  local kb; kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
  if [ "$kb" -ge "$MIN_RAM_KB" ]; then report ram PASS "MemTotal=${kb}kB"
  else report ram WARN "MemTotal=${kb}kB，建议至少 ${MIN_RAM_KB}kB"; fi
}
check_swap() {
  local kb; kb=$(awk '/^SwapTotal:/ {print $2}' /proc/meminfo)
  if [ "$kb" -eq 0 ]; then report swap PASS "未启用 swap"
  else report swap FAIL "swap=${kb}kB，kubelet 默认不会启动"; fi
}
check_max_map_count() {
  local v; v=$(sysctl -n vm.max_map_count 2>/dev/null || echo 0)
  if [ "$v" -ge 262144 ]; then report max_map_count PASS "vm.max_map_count=$v"
  else report max_map_count FAIL "vm.max_map_count=$v，需要 >= 262144，用 sysctl 修复"; fi
}
```

每个阈值都属于某个产品。swap 是 Kubernetes 的事，因为 swap 开着时 kubelet 不会运行。通过 `sysctl` 把 `vm.max_map_count` 设为至少 262144，是 Elasticsearch 的要求，所以只装 Kubernetes 的客户没有理由被这项检查拦住。

内存评为 WARN，是因为内存不足通常让系统运行不佳，而不是拒绝运行。示例中的 `MIN_RAM_KB` 阈值是一个假定值：请从所安装产品的文档中取数，并在 README 中注明出处。

还有个小坑：机器报告的 `MemTotal` 可能低于标称内存，所以一台“2 GB”的虚拟机也可能低于阈值，平白得到一个 WARN。定下数字之前，先在客户的真实机器上跑一遍，看看它们报告的值。

**验证：**在开启 swap 的虚拟机上设置 `COMPONENTS=kubernetes`，`swap` 这一行应显示 FAIL。

## 第 3 步：版本、网络和权限

```bash
check_os() {
  . /etc/os-release
  case "$ID-$VERSION_ID" in
    rhel-8*|rhel-9*) report os PASS "$PRETTY_NAME" ;;   # 示例列表
    *) report os WARN "$PRETTY_NAME 不在已测试列表中" ;;
  esac
}
check_port() {  # check_port <host> <port> — 从运行脚本的机器发起出站 TCP 连接
  if timeout 3 bash -c "</dev/tcp/$1/$2" 2>/dev/null; then
    report "net_$2" PASS "已连接 $1:$2"
  else report "net_$2" FAIL "无法建立到 $1:$2 的连接"; fi
}
check_root() {
  if [ "$(id -u)" -eq 0 ]; then report root PASS "以 root 身份运行"
  else report root WARN "非 root，修复步骤需要 sudo"; fi
}
```

示例中的 OS 列表是假定的。请换成你实际和产品一起测试过的列表，陌生的 OS 一律保持 WARN。

`check_port` 只检查**出站**方向：这台机器能否与某个端点（比如镜像仓库）建立 TCP 连接。要检查入站方向，比如 Kubernetes API server 的 6443 端口，需要在客户网络中的另一台机器上运行同一个函数。Kubernetes 文档也建议用 netcat 做这项检查。

通过 `/dev/tcp` 连接是一种捷径。它能告诉你连接能否建立，但不能代替向客户询问代理和防火墙的情况。如果客户用同一个模板创建多台虚拟机，还要比对各节点的 MAC 地址和 product_uuid：kubeadm 文档提醒，重复的值可能导致安装失败。

## 第 4 步：按组件选择检查，再返回退出码

```bash
COMPONENTS="${COMPONENTS:-kubernetes}"   # 例如 "kubernetes,elasticsearch"
has() { [[ ",$COMPONENTS," == *",$1,"* ]]; }

check_os; check_root; check_ram
has kubernetes && check_swap
has elasticsearch && check_max_map_count
check_port registry.example.com 443   # 换成真实的端点
echo "合计：$FAILS 个 FAIL，$WARNS 个 WARN"
[ "$FAILS" -eq 0 ]
```

`COMPONENTS` 变量让每项检查各归其位：只有涉及 kubelet 时 swap 才会阻断，只有涉及 Elasticsearch 时 `vm.max_map_count` 才会阻断。最后一行把结果转成退出码，这样安装程序、Ansible 或 CI 无需有人看输出就会停下。

在一台开着 swap 的测试虚拟机上以普通用户运行，输出大致如下。这是**示意输出**：OS 名称和 kB 数值都是假定的，你的机器会报告不同的数字。

```text
PASS  os             Red Hat Enterprise Linux 9
WARN  root           非 root，修复步骤需要 sudo
WARN  ram            MemTotal=1863000kB，建议至少 2097152kB
FAIL  swap           swap=2097148kB，kubelet 默认不会启动
PASS  net_443        已连接 registry.example.com:443
合计：1 个 FAIL，2 个 WARN
```

**验证：**运行 `bash preflight.sh; echo $?`。只要还有 FAIL，结果就是非零。在测试虚拟机上，`MODE=development bash preflight.sh; echo $?` 应返回 0，即使机器有问题也一样。

**要点:** 好的预检只读取客户的机器，从不悄悄动手修复。

## 让预检失去公信力的几个错误

最严重的错误是让脚本自己修机器，比如调用 `sysctl -w`。kubeadm init 一开始会运行一系列预检，在做任何改动之前验证系统状态。`kubeadm upgrade apply` 的预检阶段还有 `--dry-run` 模式，只打印将要执行的操作，不改变状态。

预检的价值就在于什么都还没动；修复是客户 IT 团队的事，按他们自己的流程来。

下一个错误是信息含糊。要记住的规则是：**每条 FAIL 都必须带上实测值、要求的阈值和修复方法**，就像第 2 步里 `max_map_count` 那一行。

RHEL 升级工具 Leapp 是值得学习的范例。它把阻断性的发现称为 *inhibitor*（阻断项），明确指出哪些问题阻碍了升级，比如某个已被移除的内核驱动，并把报告写入 `/var/log/leapp` 下的文件。

据 Microsoft Learn 介绍，阻断项必须先解决，升级才能继续，所以它们正是你的 FAIL；把输出保存到文件，比如通过 `tee`，方便客户查看。

还有两个错误。把所有项都标成 FAIL，包括客户根本不装的产品的检查，会让客户习惯性地忽略它，没多久人人都想全部豁免。退出码永远是 0，则会让流水线若无其事地继续往下跑。

## 在客户现场，脚本是对话的开端

最有效的做法是在安装前几天把脚本发给客户的 IT 团队，请他们用 `production` 模式运行并把输出贴回来。每条 FAIL 变成一张有负责人的工单。每项通过 `WAIVE` 的豁免都记下批准人的名字。到了安装日，你已经知道哪些机器还没准备好。

对想转型做前线部署工程师（FDE）的人来说，这是一个小而精的作品集项目，恰好展示雇主想看的能力。把脚本放进代码仓库，附上一份示例输出，以及一张解释每项检查为何是 WARN 或 FAIL 的表格。

在简历上，把它描述为降低客户基础设施部署风险的工具。面试时，准备好解释为什么 swap 是 FAIL 而内存不足只是 WARN：这个回答能表明你会权衡风险，而不只是会写命令。

客户往往记住的不是安装程序跑得多快，而是你在任何东西出问题之前，就指出了哪些机器还没准备好。

**本周动手试试:**

- 按本指南的思路写出 preflight.sh，在开启 swap 的虚拟机上以 COMPONENTS=kubernetes 运行，确认脚本报告 FAIL 并返回非零退出码
- 为你部署的产品加一项专属检查，把它绑定到 COMPONENTS 中对应的组件，并在信息里写明阈值和修复方法
- 把脚本和一份示例输出放进作品集仓库，附上 README，说明每项检查为何是 WARN 或 FAIL

## 信源

- [Bootstrap checks | Elastic Documentation](https://www.elastic.co/docs/deploy-manage/deploy/self-managed/bootstrap-checks)

- [Maximum map count check | Elasticsearch Guide 8.19](https://www.elastic.co/guide/en/elasticsearch/reference/8.19/bootstrap-checks-max-map-count.html)

- [kubeadm upgrade apply phase preflight | Kubernetes v1.34](https://v1-34.docs.kubernetes.io/docs/reference/setup-tools/kubeadm/generated/kubeadm_upgrade/kubeadm_upgrade_apply_phase_preflight)

- [kubeadm init | Kubernetes](https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-init/)

- [Installing kubeadm | Kubernetes](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/)

- [Running Leapp pre-upgrade analysis: take unknowns out of your next RHEL upgrade](https://www.redhat.com/en/blog/running-leapp-pre-upgrade-analysis-take-unknowns-out-your-next-rhel-upgrade)

- [Upgrade RHEL pay-as-you-go virtual machines using Leapp - Virtual Machines | Microsoft Learn](https://learn.microsoft.com/en-us/troubleshoot/azure/virtual-machines/linux/leapp-upgrade-process-rhel-7-and-8)
