- Published on
k3s 隔离网络安装完全指南 — 从镜像 tarball 带入到私有仓库、Agent 加入
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 — 用三个通过审批的文件把集群立起来
- 带入对象的清点 — 在连通网络区段里下载什么
- Server 节点安装 — 镜像放置与 SKIP_DOWNLOAD
- Agent 加入与内嵌 etcd 高可用组建
- 私有仓库路径 — registries.yaml 与自签名 CA
- 就是这里卡住 — 隔离网络 k3s 的失败模式
- 版本漂移与安装后的立即验证
- 结语 — 带入清单本身就是设计文档
- 参考资料
引言 — 用三个通过审批的文件把集群立起来
隔离网络安装之所以难,不是因为 Kubernetes 难。而是因为大部分安装工具都是按"需要的时候再下载"来设计的,而隔离网络里根本不存在那个"需要的时候"。直接执行 get.k3s.io 的脚本,第一行 curl 就会超时然后结束。
所以隔离网络的安装顺序是反过来的。先确定哪些东西必须带进去并把清单定死,在连通网络的区段里把这份清单准确填满,附上校验和送去审批,等介质进到内侧之后才开始安装。清单里漏掉任何一项,在内侧都没有补救的办法,只能等下一次带入审批,短则几天长则几周。
这篇文章就是针对 k3s,把那份清单和流程按命令逐条写下来的文档。验证所依据的版本如下。
| 项目 | 值 | 确认时点 | 确认的来源 |
|---|---|---|---|
| k3s 最新稳定版 | v1.36.2+k3s1(2026-06-24 发布) | 2026-07-31 | k3s 发布页面 |
| 官方文档示例 | v1.33.3+k3s1 | 2026-07-31 | k3s Air-Gap Install |
| 条件式导入 | v1.33.1+k3s1 以上 | 2026-07-31 | k3s Air-Gap Install |
| 证书自动续期 | 距到期 120 天以内(重启时) | 2026-07-31 | k3s Certificate |
下面的命令全部以 v1.36.2+k3s1 为基准编写。使用别的版本时只需替换版本字符串即可,但归档包与二进制文件的版本必须一致。原因后面会讲。
带入对象的清点 — 在连通网络区段里下载什么
k3s 隔离网络安装所需的文件最少三个。在此之上再加上工作负载镜像与部署清单。
#!/usr/bin/env bash
# collect-k3s.sh — 在能上网的暂存机上执行
set -euo pipefail
K3S_VERSION="v1.36.2+k3s1"
ARCH="amd64"
# URL 路径中的 + 必须编码为 %2B
URLVER="${K3S_VERSION/+/%2B}"
BASE="https://github.com/k3s-io/k3s/releases/download/${URLVER}"
OUT="./k3s-airgap-${K3S_VERSION}"
mkdir -p "${OUT}"
cd "${OUT}"
# 1) 系统镜像归档包(推荐 zstd,节省介质容量)
curl -fL -o "k3s-airgap-images-${ARCH}.tar.zst" \
"${BASE}/k3s-airgap-images-${ARCH}.tar.zst"
# 2) k3s 二进制文件
curl -fL -o k3s "${BASE}/k3s"
# 3) 安装脚本(与 SKIP_DOWNLOAD 搭配使用,让执行时不动用网络)
curl -fL -o install.sh https://get.k3s.io
# 4) 校验和文件(发布页面有提供的情况下)
curl -fL -o "sha256sum-${ARCH}.txt" "${BASE}/sha256sum-${ARCH}.txt" || \
echo "WARN: 发布页面没有 sha256sum-${ARCH}.txt。请用自行计算的值代替。"
ls -la
关于 sha256sum-amd64.txt 这个资产,本次确认时点无法断定它是否存在于发布页面的资产列表中。有的话就用它,没有的话,像下面这样把带入负责人自行计算的清单当作正本更安全。反正隔离网络的审批里,本来就需要一份能证明"是谁、何时、在哪里取得的文件"的自制清单。
# 生成自制校验和清单(连通网络区段)
cd "./k3s-airgap-v1.36.2+k3s1"
sha256sum k3s k3s-airgap-images-amd64.tar.zst install.sh > MANIFEST.sha256
cat MANIFEST.sha256
如果是启用了 SELinux 的 RHEL 系节点,还必须一并带入 k3s-selinux RPM。官方文档明确要求,在 SELinux 启用的节点上安装 k3s 之前要手动安装这个 RPM。漏掉这个文件的话安装本身能通过,但会以容器读不到卷的形式在很久以后才爆出来。
带入物清单
| 文件 | 是否必需 | 漏掉会发生什么 |
|---|---|---|
| k3s(二进制文件) | 必需 | 安装脚本尝试下载然后超时 |
| k3s-airgap-images-amd64.tar.zst | 必需 | 系统 Pod 全部 ErrImagePull |
| install.sh | 必需 | 得自己手工创建 systemd 单元与符号链接 |
| MANIFEST.sha256 | 事实上必需 | 无法在内侧判断介质是否损坏 |
| k3s-selinux RPM | 条件性 | SELinux 启用的节点上卷访问失败 |
| 自有工作负载镜像归档包 | 必需 | 只有应用 ErrImagePull — 最常见的事故 |
| Helm chart / 部署清单 | 必需 | 到部署阶段又得重新等带入审批 |
最后两行是这张表的核心。k3s 的归档包里只装了 k3s 把自己拉起来所需要的镜像。差不多就是 CoreDNS、Traefik、local-path-provisioner、metrics-server、pause 这些。内部应用的镜像当然没有,Prometheus 没有,替换用的 Ingress 控制器也没有。这一点后面的失败模式一节会再提一次。
介质带入与完整性验证
介质进到内侧之后,安装前务必先从验证开始。压缩归档包如果是悄悄损坏着进来的,k3s 会把导入失败当成一行日志放过去然后照样启动,那样你要等到几分钟后看 Pod 状态时才会发现。
# 在隔离网络节点上
cd /opt/staging/k3s-airgap-v1.36.2+k3s1
# 1) 校验和验证 — 这一步失败就不再往下走
sha256sum -c MANIFEST.sha256
# 2) 确认归档包本身能打开(需要 zstd)
zstd -t k3s-airgap-images-amd64.tar.zst && echo "archive OK"
# 3) 确认归档包里装着的镜像列表
zstd -dc k3s-airgap-images-amd64.tar.zst | tar -tf - | grep -E 'manifest|repositories' | head
节点上可能没有 zstd 二进制文件。隔离网络的 Linux 镜像大多是最小化安装,而且有些发行版里 zstd 不是默认软件包。这种情况下有两个选择。要么先从 OS 软件包镜像源安装 zstd,要么干脆在连通网络那边就取 .tar.gz 或者未压缩的 .tar 资产。发布页面上 k3s-airgap-images-amd64.tar、.tar.gz、.tar.zst 三种形态都有提供。哪怕觉得容量可惜,首次安装用 .tar.gz 也能少一个失败点。
Server 节点安装 — 镜像放置与 SKIP_DOWNLOAD
现在进入实际安装。顺序很重要。先放置镜像,然后再执行安装脚本。反过来的话,k3s 启动时找不到镜像,会进入重试循环。
#!/usr/bin/env bash
# install-k3s-server.sh — 隔离网络 Server 节点
set -euo pipefail
STAGE=/opt/staging/k3s-airgap-v1.36.2+k3s1
# 1) 放置系统镜像归档包
sudo mkdir -p /var/lib/rancher/k3s/agent/images/
sudo cp "${STAGE}/k3s-airgap-images-amd64.tar.zst" /var/lib/rancher/k3s/agent/images/
# 2) 启用条件式导入缓存(v1.33.1+k3s1 以上)
# 有这个文件的话,只要归档包没变,重启时就会跳过重新导入
sudo touch /var/lib/rancher/k3s/agent/images/.cache.json
# 3) 放置二进制文件
sudo cp "${STAGE}/k3s" /usr/local/bin/k3s
sudo chmod +x /usr/local/bin/k3s
# 4) 执行安装脚本 — 指示它跳过下载
sudo chmod +x "${STAGE}/install.sh"
sudo INSTALL_K3S_SKIP_DOWNLOAD=true "${STAGE}/install.sh"
说 INSTALL_K3S_SKIP_DOWNLOAD=true 就是这套流程的全部也不为过。没有这个值,脚本就会尝试连接发布服务器。想同时传服务器选项的话,用 INSTALL_K3S_EXEC。
sudo INSTALL_K3S_SKIP_DOWNLOAD=true \
INSTALL_K3S_EXEC="server --cluster-init --tls-san 10.10.20.10 --tls-san k8s-api.internal.example --write-kubeconfig-mode 0644 --disable traefik" \
/opt/staging/k3s-airgap-v1.36.2+k3s1/install.sh
比起把选项一长串排在命令行上,用配置文件来管理更好。隔离网络里重装和节点扩容都很频繁,每次都会有人漏掉某一个选项。
# /etc/rancher/k3s/config.yaml
cluster-init: true
tls-san:
- 10.10.20.10
- k8s-api.internal.example
write-kubeconfig-mode: '0644'
disable:
- traefik
node-label:
- topology.kubernetes.io/zone=dc-a
启动确认如下进行。
sudo systemctl status k3s --no-pager
sudo k3s kubectl get nodes -o wide
sudo k3s kubectl -n kube-system get pods
# 直接在 containerd 存储里确认镜像导入是否真的发生了
sudo k3s ctr images ls | awk '{print $1}' | sort -u | head -20
k3s ctr images ls 如果是空的,说明导入失败了。这时候该看的不是 journalctl -u k3s -n 200,而是 containerd 的日志。路径是 /var/lib/rancher/k3s/agent/containerd/containerd.log。
Agent 加入与内嵌 etcd 高可用组建
Agent 节点也是同样的顺序。放置好镜像归档包与二进制文件之后,把加入信息用环境变量传进去执行安装脚本。
# 在 Server 节点上确认 token
sudo cat /var/lib/rancher/k3s/server/node-token
#!/usr/bin/env bash
# install-k3s-agent.sh — 隔离网络 Agent 节点
set -euo pipefail
STAGE=/opt/staging/k3s-airgap-v1.36.2+k3s1
SERVER_URL="https://10.10.20.10:6443"
JOIN_TOKEN="K10xxxxxxxx::server:xxxxxxxx"
sudo mkdir -p /var/lib/rancher/k3s/agent/images/
sudo cp "${STAGE}/k3s-airgap-images-amd64.tar.zst" /var/lib/rancher/k3s/agent/images/
sudo touch /var/lib/rancher/k3s/agent/images/.cache.json
sudo cp "${STAGE}/k3s" /usr/local/bin/k3s
sudo chmod +x /usr/local/bin/k3s
sudo INSTALL_K3S_SKIP_DOWNLOAD=true \
K3S_URL="${SERVER_URL}" \
K3S_TOKEN="${JOIN_TOKEN}" \
"${STAGE}/install.sh"
如果需要高可用,数据存储的选择必须一开始就做。k3s 的默认数据存储是 SQLite,而 SQLite 无法增加 Server 节点。要走内嵌 etcd 的话,第一台 Server 用 --cluster-init 拉起来,其余的 Server 用 --server 接上去。
# Server 1 — 集群初始化(在 config.yaml 里写 cluster-init: true 也一样)
sudo INSTALL_K3S_SKIP_DOWNLOAD=true \
K3S_TOKEN="共享密钥" \
INSTALL_K3S_EXEC="server --cluster-init --tls-san 10.10.20.9" \
./install.sh
# Server 2、3 — 加入既有的 Server
sudo INSTALL_K3S_SKIP_DOWNLOAD=true \
K3S_TOKEN="共享密钥" \
INSTALL_K3S_EXEC="server --server https://10.10.20.10:6443 --tls-san 10.10.20.9" \
./install.sh
官方文档明确写着,内嵌 etcd 集群要维持法定人数,Server 节点数必须是奇数。n 台 Server 的法定人数是 (n/2)+1。两台的组成与一台的组成故障容忍能力相同,却只让运维复杂度上升,所以没有意义。另外 --cluster-dns、--cluster-domain、--cluster-cidr、--service-cidr 这类网络类的 flag,在所有 Server 节点上都必须相同。在隔离网络里一台一台扩容节点的过程中,这些值很容易错开。
即便已经用 SQLite 把单台 Server 立起来了,也还是有办法。按照官方文档,把既有的 Server 带上 --cluster-init flag 重启,就会切换成 etcd。不过这不是那种可以在生产环境里不做任何备份就去尝试的作业。
私有仓库路径 — registries.yaml 与自签名 CA
在隔离网络里分发镜像的正规做法,是架一个内部镜像仓库并让节点只看向那里。k3s 用 /etc/rancher/k3s/registries.yaml 来配置这件事。
# /etc/rancher/k3s/registries.yaml
mirrors:
docker.io:
endpoint:
- 'https://registry.internal.example:5000'
registry.k8s.io:
endpoint:
- 'https://registry.internal.example:5000'
ghcr.io:
endpoint:
- 'https://registry.internal.example:5000'
rewrite:
'^(.*)': 'mirror/ghcr/$1'
configs:
'registry.internal.example:5000':
auth:
username: k3s-puller
password: '带入时替换'
tls:
ca_file: /etc/rancher/k3s/certs/internal-ca.crt
想把所有仓库都塞进一个端点的话,就用通配符条目。官方文档明确指出,mirrors 和 configs 两边都可以把星号条目当作默认配置来用,而且星号必须用引号括起来。
# 把所有仓库都指向内部镜像仓库(通配符)
mirrors:
'*':
endpoint:
- 'https://registry.internal.example:5000'
configs:
'registry.internal.example:5000':
tls:
ca_file: /etc/rancher/k3s/certs/internal-ca.crt
rewrite 会改写路径的前半部分。在 Harbor 那种以项目为单位强制命名空间的仓库上,没有这个就没法原样使用原来的路径。上面的例子会把 ghcr.io/foo/bar 送到 registry.internal.example:5000/mirror/ghcr/foo/bar。
如果用自签名 CA,信任配置需要两处。有一半左右的人在这里绊倒。
# 1) 给 containerd 用 — registries.yaml 里 ca_file 指向的位置
sudo mkdir -p /etc/rancher/k3s/certs
sudo cp /opt/staging/internal-ca.crt /etc/rancher/k3s/certs/internal-ca.crt
sudo chmod 644 /etc/rancher/k3s/certs/internal-ca.crt
# 2) OS 信任存储 — 给 helm、skopeo、crictl、curl 等其他客户端用
# RHEL 系
sudo cp /opt/staging/internal-ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract
# Debian 系
sudo cp /opt/staging/internal-ca.crt /usr/local/share/ca-certificates/internal-ca.crt
sudo update-ca-certificates
# 3) registries.yaml 的变更必须重启才会生效 — 在所有节点上
sudo systemctl restart k3s # Server 节点
sudo systemctl restart k3s-agent # Agent 节点
第三步是官方文档明确强调的。原话是"配置变更要生效,必须在每个节点上重启 k3s"。而且这个文件必须分发到所有会 pull 镜像的节点上。只放进 Server 而忘了 Agent 是很常见的失误。
验证这样做。
# 确认仓库配置是否已反映到 containerd
sudo k3s ctr --namespace k8s.io images pull \
registry.internal.example:5000/library/busybox:1.36
# 失败的话就看 containerd 日志(而不是 kubelet 的消息)
sudo tail -n 100 /var/lib/rancher/k3s/agent/containerd/containerd.log
节点间的镜像再分发 — 内嵌仓库镜像源
k3s 里有基于 Spegel 的内嵌分布式仓库镜像源。它能让某个节点上已经有的镜像,被别的节点在没有外部仓库的情况下取走。在隔离网络里还没架起内部镜像仓库的阶段,或者边缘节点连内部镜像仓库都够不到的情况下很有用。
# /etc/rancher/k3s/config.yaml(所有 Server 节点)
embedded-registry: true
# /etc/rancher/k3s/registries.yaml(所有节点)
mirrors:
'*':
按官方文档,节点之间必须能用内部 IP 互相到达 TCP 5001(共享可用镜像列表的 P2P 网络)和 6443(各节点自己托管的本地 OCI 仓库)。在防火墙很严密的隔离网络里 5001 常常是被堵住的,所以要事先打开。
有一个误解需要澄清。这个功能做的只是再分发已经存在于某个节点上的镜像。它没有把镜像推进去的 push 功能。最初的投入依然得靠 airgap 归档包或者 k3s ctr images import 来做。
就是这里卡住 — 隔离网络 k3s 的失败模式
只写幸福路径的文档在隔离网络里没有用。这里整理实际会被卡住的地方。
| 症状 | 真正的原因 | 确认方法 |
|---|---|---|
| 系统 Pod 起来了但只有我的应用 ErrImagePull | airgap 归档包里没有我的镜像 | 确认 k3s ctr images ls 里是否没有该镜像 |
| ErrImagePull 的消息指向 docker.io | 只暴露出了默认仓库端点回退的结果 | 在 containerd.log 里确认实际的首次尝试对象 |
| 改了 registries.yaml 却没变化 | 没重启,或者没分发到 Agent | systemctl restart 之后重试,确认所有节点上文件都在 |
| helm 通得过但只有 containerd TLS 失败 | 只配了 ca_file 而漏了 OS 信任存储(或者反过来) | 分别尝试用 curl 连接仓库和 ctr images pull |
| 外部域名查询每次卡 5 秒 | CoreDNS 的上游无法到达 | 确认 CoreDNS ConfigMap 的 forward 对象与节点的 resolv.conf |
| 想加 Server 却加入不了 | 数据存储是 SQLite | 确认 k3s kubectl get nodes 与 Server 启动 flag |
| 重启后系统 Pod 去尝试 pull | 归档包版本与二进制文件版本不一致 | 比较 k3s -v 与归档包文件名里的版本 |
| 容器读不到卷(RHEL 系) | 未安装 k3s-selinux RPM | getenforce 与 rpm -q k3s-selinux |
失败模式 1 — 归档包里没有的镜像
最常见也最让人无力的失败。k3s 的 airgap 归档包只装了 k3s 自身需要的镜像。内部应用、换掉的 Ingress 控制器、监控栈,全都得另外带入。
如果内部镜像仓库还没有,可以临时直接导入到节点上。
# 在连通网络区段:把需要的镜像打成一个归档包
docker pull myapp/api:1.4.2
docker pull myapp/worker:1.4.2
docker save -o myapp-images.tar myapp/api:1.4.2 myapp/worker:1.4.2
sha256sum myapp-images.tar >> MANIFEST.sha256
# 在隔离网络节点上:方法 A — 放到镜像目录里然后重启
sudo cp myapp-images.tar /var/lib/rancher/k3s/agent/images/
sudo systemctl restart k3s
# 方法 B — 不重启,立刻导入
sudo k3s ctr --namespace k8s.io images import myapp-images.tar
sudo k3s ctr --namespace k8s.io images ls | grep myapp
用方法 B 放进去的镜像时,请确认 Pod spec 的 imagePullPolicy。标签如果是 latest,默认策略就变成 Always,于是节点上明明有镜像却还要去尝试 pull 然后失败。在隔离网络里标签要始终写成明确的版本,必要时把 imagePullPolicy: IfNotPresent 钉死。
失败模式 2 — 默认端点回退制造的假线索
containerd 有一种行为:无关 registries.yaml 的镜像源配置,作为最后一次尝试去连接原本的仓库。在隔离网络里这最后一次尝试必然失败,而且kubelet 给用户看的错误就是这最后一次尝试的结果。所以你分不清到底是镜像源配错了、镜像源里没有那个镜像、还是压根没走镜像源。
应对有两种。
# 应对 A — 真正的原因在 containerd 日志里
sudo grep -iE 'failed|error' /var/lib/rancher/k3s/agent/containerd/containerd.log | tail -40
# 应对 B — /etc/rancher/k3s/config.yaml
# 对已配置镜像源的仓库,关掉默认端点回退
disable-default-registry-endpoint: true
官方文档把这个选项解释为"当该仓库配置了镜像源时,禁用 containerd 的默认仓库端点回退"。它是在 2024 年 1 月的发布中作为实验性功能引入的,并且只适用于在 registries.yaml 里有镜像源条目的仓库。没有配置镜像源的仓库依然会回退。打开这个选项之后,错误消息就会指向实际的失败地点,调试时间会大幅减少。
失败模式 3 — CoreDNS 什么都找不到
症状是集群内部的名字(kubernetes.default.svc.cluster.local)解析得好好的,但内部域名或外部域名的查询会卡 5 秒然后失败。CoreDNS 默认的 Corefile 会把集群域之外的查询转发(forward)给节点的解析器,而如果那个解析器指向的是隔离网络中无法到达的公网 DNS(比如 8.8.8.8),那就每次查询都得等一次超时。
先确认实际的配置。不要猜,直接从集群里读出来。
# 确认 CoreDNS 把什么当作 forward 对象
sudo k3s kubectl -n kube-system get configmap coredns -o yaml
# 确认节点的解析器
cat /etc/resolv.conf
sudo resolvectl status 2>/dev/null | head -30
# 从节点上直接确认内部 DNS 是否真的会应答
dig @10.10.10.53 registry.internal.example +short
应对分两条路。第一,把节点的 /etc/resolv.conf 整理成只指向内部 DNS。如果是 systemd-resolved 管理的节点,因为符号链接的关系,直接改文件也会被改回去,所以必须改 resolved 的配置。第二,如果不方便动节点上的文件,就给 k3s 另外指定一个 kubelet 用的解析器文件。
# 单独放一份隔离网络专用的解析器文件
sudo tee /etc/rancher/k3s/resolv.conf > /dev/null <<'EOF'
nameserver 10.10.10.53
nameserver 10.10.10.54
search internal.example
options timeout:1 attempts:2
EOF
# /etc/rancher/k3s/config.yaml
resolv-conf: /etc/rancher/k3s/resolv.conf
--resolv-conf flag 在官方 CLI 文档里记载为 "Kubelet resolv.conf file",也可以用环境变量 K3S_RESOLV_CONF 来指定。如果是内部 DNS 本身就完全没有的环境,那还不如在 CoreDNS 的 Corefile 里把 forward 对象固定到内部解析器,或者只把需要的名字用 hosts 插件钉进去。options timeout:1 至少能把那些仍然漏出去的查询的延迟压到 1 秒。
版本漂移与安装后的立即验证
归档包与二进制文件错开的时候
在隔离网络里由多个人跨越几个月做带入,这个问题必然会出现。上个月带进来的 v1.35.6 归档包还留在镜像目录里,而这次带进来的二进制文件是 v1.36.2。
这时候会发生的事情是这样。k3s v1.36.2 要求与它自己匹配的 CoreDNS、pause、local-path-provisioner 标签,但节点上导入的镜像是 v1.35.6 用的标签。containerd 存储里没有那个标签,于是去尝试 pull,而这是隔离网络,所以失败。日志里只留下"找不到镜像",版本不一致这条线索哪里都没有。
诊断与清理这样做。
# 1) 二进制文件版本
k3s -v
# 2) 镜像目录里堆了些什么
ls -la /var/lib/rancher/k3s/agent/images/
# 3) containerd 存储里的系统镜像标签
sudo k3s ctr --namespace k8s.io images ls | grep -E 'coredns|pause|local-path|metrics-server'
# 清理 — 删掉旧归档包,只留下新归档包,然后重启
sudo systemctl stop k3s
sudo rm -f /var/lib/rancher/k3s/agent/images/k3s-airgap-images-amd64-v1.35.6.tar.zst
sudo rm -f /var/lib/rancher/k3s/agent/images/.cache.json
sudo cp /opt/staging/k3s-airgap-images-amd64.tar.zst /var/lib/rancher/k3s/agent/images/
sudo systemctl start k3s
要把 .cache.json 一起删掉的理由是,在 v1.33.1+k3s1 以上,这个缓存文件被用来判断"这个归档包已经导入过了"。归档包文件换掉了而缓存还留着的话,就可能跳过导入。官方文档的升级流程也明确写着要放入新归档包并删除既有归档包。
从根上防止漂移的方法,是把带入的单位按版本捆起来。
# 在带入目录名与文件名两边都刻上版本
/opt/staging/k3s-v1.36.2+k3s1/
├── MANIFEST.sha256
├── VERSION # v1.36.2+k3s1 一行
├── install.sh
├── k3s
└── k3s-airgap-images-amd64.tar.zst
# 在安装脚本里放一道版本闸门
EXPECTED="$(cat "${STAGE}/VERSION")"
ACTUAL="$(/usr/local/bin/k3s -v | awk '/^k3s version/{print $3}')"
if [ "${ACTUAL}" != "${EXPECTED}" ]; then
echo "FATAL: 二进制文件 ${ACTUAL} 与带入包 ${EXPECTED} 不一致" >&2
exit 1
fi
安装后的立即验证脚本
安装结束不等于结束。在隔离网络里,"看上去总算是起来了"的状态和"实际上能用"的状态之间差距很大。把下面的脚本在安装后立刻跑一遍,几天后的事故会大幅减少。
#!/usr/bin/env bash
# verify-airgap-k3s.sh
set -uo pipefail
ERR=0
K="sudo k3s kubectl"
echo "== 1. 节点状态"
${K} get nodes -o wide
NR=$(${K} get nodes --no-headers | grep -cv " Ready ") || true
[ "${NR}" -gt 0 ] && { echo "FAIL: NotReady 节点 ${NR} 台"; ERR=$((ERR+1)); }
echo "== 2. 系统 Pod"
BAD=$(${K} -n kube-system get pods --no-headers | grep -cvE "Running|Completed") || true
[ "${BAD}" -gt 0 ] && { ${K} -n kube-system get pods | grep -vE "Running|Completed"; ERR=$((ERR+1)); }
echo "== 3. 是否有往外部仓库跑的尝试"
if sudo grep -qiE 'docker\.io|registry\.k8s\.io|ghcr\.io' \
/var/lib/rancher/k3s/agent/containerd/containerd.log 2>/dev/null; then
echo "WARN: containerd 日志里有连接外部仓库的痕迹"
echo " 请确认 registries.yaml 的镜像源配置与 disable-default-registry-endpoint"
fi
echo "== 4. DNS"
${K} run dnscheck --rm -i --restart=Never --image=registry.internal.example:5000/library/busybox:1.36 -- \
nslookup kubernetes.default.svc.cluster.local || { echo "FAIL: 集群 DNS"; ERR=$((ERR+1)); }
echo "== 5. 证书到期"
sudo k3s certificate check --output table
echo "== 6. 数据存储"
if [ -d /var/lib/rancher/k3s/server/db/etcd ]; then
echo "datastore: embedded etcd"
ls -la /var/lib/rancher/k3s/server/db/snapshots/ 2>/dev/null | tail -5
else
echo "datastore: sqlite(无法扩容 Server 节点)"
fi
echo "== 结果: 错误 ${ERR} 件"
exit "${ERR}"
第 5 项的 k3s certificate check --output table 在隔离网络运维里格外重要。k3s 的客户端与服务端证书自签发日起有效 365 天,服务重启时如果距到期在 120 天以内就会自动续期。但没有重启就没有续期。几个月里谁都不去碰的隔离网络集群因证书到期而死掉的路径,正是这里。详细的应对在 Day 2 运维篇里讲。
结语 — 带入清单本身就是设计文档
隔离网络 k3s 安装里真正的作业不是命令而是清单。归档包、二进制文件、安装脚本、校验和、CA、工作负载镜像、chart、SELinux RPM,把内侧会需要的每一个字节都在外侧预先数清楚,这才是工作。命令本身连十行都不到。
而且那份清单不是做一次就完事的。版本上去了,归档包和二进制文件就得一起上去,而两者只有一边动了的那一刻,集群就会悄悄地变得不对劲。在带入单位上刻上版本、在安装脚本里放一道闸门,这五行能帮你挡下等待下一次带入审批的三周。