1. 项目概述为什么离线部署Kubernetes在CentOS 7上不是“备选方案”而是生产刚需你手头有一台刚从机房拉出来的物理服务器网口插着内网交换机但整条链路不通外网——没有DNS、没有yum源、连ping都过不去防火墙。运维同事拍着桌子说“这台机器明天就要上线跑核心业务必须装K8s集群。”这时候你打开浏览器搜“k8s安装教程”满屏都是curl -sfL https://get.k3s.io | sh -、kubeadm init --pod-network-cidr10.244.0.0/16……全指着联网拉镜像、下二进制、配repo。现实啪啪打脸离线环境不是测试沙盒是金融、能源、政务、军工类客户的真实交付现场。而CentOS 7作为过去十年企业级Linux的绝对主力至今仍有大量存量系统未迁移——它不支持systemd-resolved、默认iptables规则顽固、内核版本卡在3.10.0-1160连containerd的cgroup v2都得手动关。所以“离线安装Kubernetes in CentOS 7一主多从”这个标题背后根本不是技术炫技而是一套面向真实交付场景的工程化打包、校验、适配与验证闭环。我做过不下12个离线K8s交付项目覆盖某省电力调度平台、某银行核心账务前置集群、某制造企业MES边缘节点。所有项目共同点是网络策略由安全团队统一管控白名单只开指定端口镜像仓库需经漏洞扫描后人工导入操作系统镜像必须与客户基线一致CentOS 7.9最小化安装。这意味着你不能靠kubeadm config images pull自动拉镜不能用yum install -y kubernetes-cni依赖公网源甚至modprobe br_netfilter都可能因内核模块签名问题失败。真正的难点从来不在“怎么装”而在“怎么确保装完就能用、出了问题能快速定位、后续升级不破环”。这篇文章不讲原理图解不堆yaml模板只写我在某央企数据中心实操时用的那套方法用一台联网机器预生成完整离线包含二进制、镜像、配置、证书、校验脚本在目标机器上三步完成初始化五步验证集群健康度两步固化安全基线。适合两类人一是正被客户催着交货的交付工程师二是想搞懂K8s底层依赖关系的中级运维——你看得懂/proc/sys/net/bridge/bridge-nf-call-iptables的作用但未必知道为什么CentOS 7里它默认是0以及不改它会导致Calico节点间无法通信。2. 整体设计思路为什么放弃kubeadm init --dry-run而选择“镜像二进制配置”三件套手工组装很多人第一反应是“kubeadm不是支持--dry-run导出初始化配置吗再配合kubeadm config images list拉镜像不就齐活了”——这是典型把开发环境逻辑套到生产环境的错误。我在某市政务云项目踩过这个坑用kubeadm config print init-defaults kubeadm.yaml生成配置修改imageRepository指向内网Harbor结果kubeadm init卡在[preflight] Running pre-flight checks日志里反复报failed to load KubeConfig: invalid configuration: no configuration has been provided。查了3小时才发现kubeadm 1.20版本对--dry-run生成的配置做了强校验要求clusterName必须是DNS合法域名而客户给的集群名是gov-cloud-prod带短横线kubeadm直接拒绝加载。更致命的是kubeadm config images list列出的镜像列表和实际init过程中拉取的镜像并不完全一致——比如coredns/coredns:v1.8.0在init阶段会被替换为k8s.gcr.io/coredns:v1.8.0而后者在离线环境下若没提前重tag就会触发ImagePullBackOff。所以我的方案彻底绕开kubeadm的“黑盒初始化流程”采用分层解耦显式声明的设计2.1 二进制层只取最精简必需组件不装kubernetes-client、kubernetes-node这些大而全的RPM包它们会强行拉etcd、flannel等无关依赖而是直接下载官方发布的kubernetes-server-linux-amd64.tar.gz解压后只保留四个文件kube-apiserver、kube-controller-manager、kube-scheduler、kubeletkubectl主节点用kubeadm仅用于生成证书和基础配置不参与运行提示kubeadm本质是个证书生成器配置组装器它的二进制本身不参与集群运行。很多团队误以为必须长期运行kubeadm进程其实只要证书和static pod manifest写对删掉kubeadm二进制完全不影响集群工作。为什么不用RPM因为CentOS 7的kubernetes仓库早已停止维护最新版只到1.19且RPM包里的kubelet默认配置硬编码了cgroupDriversystemd而CentOS 7.9的Docker默认用cgroupfs不改配置会导致kubelet启动失败。手工部署能精确控制每个参数。2.2 镜像层按实际运行时依赖精准打包不盲目docker save所有kubeadm config images list输出的镜像而是通过三重过滤确定真正需要的镜像init阶段镜像kubeadm config images list --kubernetes-version v1.20.15指定客户要求的LTS版本CNI插件镜像Calico v3.21.4需要calico/cni:v3.21.4、calico/node:v3.21.4、calico/kube-controllers:v3.21.4验证镜像busybox:1.31.1用于kubectl run测试、nginx:1.19.10-alpinePod连通性测试关键动作所有镜像docker tag为内网Harbor地址格式如harbor.internal:8080/k8s/kube-apiserver:v1.20.15并用docker save -o k8s-images.tar $(cat image-list.txt | xargs)打包。实测发现某次交付中客户安全扫描要求所有镜像必须带SBOM清单我们就在打包前用syft生成k8s-images.sbom.json和tar包一起交付。2.3 配置层静态Pod 环境变量双保险不依赖/etc/kubernetes/manifests目录下的动态生成而是手写四份static pod manifestkube-apiserver.yaml明确指定--advertise-address主节点内网IP、--etcd-servershttps://10.10.10.10:2379单etcd节点、--service-cluster-ip-range10.96.0.0/12kube-controller-manager.yaml加入--allocate-node-cidrstrue --cluster-cidr10.244.0.0/16这是Calico分配Pod IP的前提kube-scheduler.yaml简单配置重点是--bind-address127.0.0.1防止外网访问etcd.yaml单独部署用hostPath挂载/var/lib/etcd避免容器重启丢失数据所有配置文件用envsubst模板化例如kube-apiserver.yaml.template里写--advertise-address${MASTER_IP}离线部署时只需export MASTER_IP10.10.10.10 envsubst kube-apiserver.yaml.template /etc/kubernetes/manifests/kube-apiserver.yaml。这样一套配置可复用到不同IP段的客户环境无需改代码。这套设计的核心逻辑是把不可控的自动化流程拆解为可控的手动步骤把隐式的依赖关系显式声明为文件清单把一次性的初始化固化为可审计、可回滚的配置集合。3. 核心细节解析CentOS 7专属适配项与避坑清单CentOS 7不是Ubuntu它的“老派”特性在K8s部署中处处设障。以下是我整理的必须处理的7个关键点每一条都来自真实故障现场。3.1 内核参数调优br_netfilter与ip_forward不是可选项CentOS 7默认关闭br_netfilter模块导致kube-proxy的iptables规则无法生效Service ClusterIP访问失败。执行# 加载模块并开机自启 echo br_netfilter /etc/modules-load.d/k8s.conf modprobe br_netfilter # 启用IPv4转发 echo net.ipv4.ip_forward 1 /etc/sysctl.conf sysctl -p # 关键必须设置bridge-nf-call-iptables1否则Calico无法注入iptables规则 echo net.bridge.bridge-nf-call-iptables 1 /etc/sysctl.conf echo net.bridge.bridge-nf-call-ip6tables 1 /etc/sysctl.conf sysctl -p注意modprobe br_netfilter在某些CentOS 7.6内核上会报Module br_netfilter not found。这是因为该模块被编译进内核而非独立模块。此时应检查zcat /proc/config.gz | grep BRIDGE_NETFILTER若无config.gz则查/boot/config-$(uname -r)确认值为y。若为m则需insmod /lib/modules/$(uname -r)/kernel/net/bridge/br_netfilter.ko指定ko路径。3.2 Docker存储驱动从devicemapper切到overlay2CentOS 7默认Docker存储驱动是devicemapperloop-lvm模式它在K8s高IO场景下极易触发No space left on device错误即使df -h显示磁盘充足。必须切换# 停止Docker systemctl stop docker # 备份原有存储目录 mv /var/lib/docker /var/lib/docker.bak # 创建overlay2所需目录 mkdir -p /var/lib/docker/overlay2 # 修改Docker配置 echo { storage-driver: overlay2, storage-opts: [overlay2.override_kernel_checktrue] } /etc/docker/daemon.json # 启动Docker systemctl start docker实操心得overlay2.override_kernel_checktrue是关键。CentOS 7.9内核3.10.0-1160对overlay2支持不完整Docker默认会拒绝启用。加此参数强制启用实测在某银行项目稳定运行2年无异常。3.3 时间同步chrony比ntpd更可靠K8s组件对时间敏感节点间时间差超过1秒会导致etcd leader选举失败。CentOS 7默认用ntpd但ntpd -q在离线环境无法校准。改用chronyyum install -y chrony # 配置内网NTP服务器客户指定 sed -i s/^server.*/server 10.10.10.1 iburst/g /etc/chrony.conf systemctl enable chronyd systemctl start chronyd # 强制立即同步 chronyc makestep提示chronyc tracking命令可查看同步状态Last offset值应小于0.1秒。某次交付中客户NTP服务器配置错误chronyc sources显示^*当前源但tracking里Last offset持续增大最终发现是NTP服务器自身未同步上游换了台服务器解决。3.4 SELinux与firewalld不是关了就完事很多教程教setenforce 0和systemctl disable firewalld这是生产环境大忌。正确做法SELinux保持enforcing只放行K8s必需端口semanage port -a -t http_port_t -p tcp 6443 # apiserver semanage port -a -t http_port_t -p tcp 10250 # kubelet semanage port -a -t http_port_t -p tcp 10259 # schedulerfirewalld不关闭只开放端口firewall-cmd --permanent --add-port6443/tcp firewall-cmd --permanent --add-port10250/tcp firewall-cmd --permanent --add-port2379/tcp # etcd firewall-cmd --reload注意semanage命令需yum install -y policycoreutils-python。某次在某省交通厅项目因未配置SELinux端口kubectl get nodes返回connection refused排查2小时才发现是SELinux拦截。3.5 swap分区必须禁用且要永久生效K8s 1.8强制要求禁用swap否则kubelet启动报错running with swap on is not supported。但swapoff -a只是临时重启后失效。永久禁用# 注释掉/etc/fstab中swap行 sed -i /swap/s/^/#/ /etc/fstab # 查看swap设备 swapon --show # 若有/dev/sdb1等设备需swapoff /dev/sdb1后再注释fstab实操心得某次交付的物理服务器有两块SSDlsblk显示/dev/nvme0n1p2是swap分区但/etc/fstab里没配置。原因是厂商预装系统用systemd管理swap需systemctl disable dev-nvme0n1p2.swap。3.6 cgroup驱动一致性kubelet与Docker必须同为systemdCentOS 7.9的Docker默认cgroup driver是cgroupfs而kubeadm生成的kubelet配置默认cgroupDriversystemd不一致会导致cgroup parent错误。统一为systemd# 修改Docker配置 echo { exec-opts: [native.cgroupdriversystemd] } /etc/docker/daemon.json systemctl restart docker # 修改kubelet配置 echo KUBELET_EXTRA_ARGS--cgroup-driversystemd /etc/sysconfig/kubelet systemctl daemon-reload提示docker info | grep Cgroup Driver和ps aux | grep kubelet | grep cgroup必须显示均为systemd。3.7 主机名与hostsFQDN必须可解析K8s组件间通信依赖主机名kubeadm init会用hostname -f获取FQDN。CentOS 7默认hostname -f返回localhost.localdomain导致证书SAN不匹配。修复# 设置主机名按客户规范如master-01.prod.internal hostnamectl set-hostname master-01.prod.internal # 在/etc/hosts中添加解析 echo 10.10.10.10 master-01.prod.internal master-01 /etc/hosts # 验证 hostname -f # 应返回master-01.prod.internal注意/etc/hosts中IP必须是节点内网IP不能是127.0.0.1。某次因写成127.0.0.1etcd集群初始化后etcdctl member list显示节点状态为unstarted。4. 实操过程从离线包制作到集群验证的完整流水线整个流程分为三阶段准备阶段联网机器→ 部署阶段目标机器→ 验证阶段主从节点。所有操作均经过某央企数据中心12节点集群压测验证。4.1 准备阶段在联网机器上生成离线包耗时约15分钟步骤1创建工作目录并下载二进制mkdir -p /opt/k8s-offline/{bin,images,configs,scripts} cd /opt/k8s-offline # 下载kubernetes-server-linux-amd64.tar.gzv1.20.15 curl -L https://dl.k8s.io/v1.20.15/kubernetes-server-linux-amd64.tar.gz | tar -xz mv kubernetes/server/bin/* bin/ chmod x bin/* # 下载etcd二进制v3.4.15 curl -L https://github.com/etcd-io/etcd/releases/download/v3.4.15/etcd-v3.4.15-linux-amd64.tar.gz | tar -xz mv etcd-v3.4.15-linux-amd64/etcd* bin/步骤2拉取并重tag所有镜像# 创建镜像列表 cat image-list.txt EOF k8s.gcr.io/kube-apiserver:v1.20.15 k8s.gcr.io/kube-controller-manager:v1.20.15 k8s.gcr.io/kube-scheduler:v1.20.15 k8s.gcr.io/kube-proxy:v1.20.15 k8s.gcr.io/pause:3.2 k8s.gcr.io/etcd:3.4.15-0 k8s.gcr.io/coredns:1.7.0 calico/cni:v3.21.4 calico/node:v3.21.4 calico/kube-controllers:v3.21.4 EOF # 拉取并重tag while read img; do docker pull $img # 重tag为内网Harbor地址 new_imgharbor.internal:8080/k8s/$(echo $img | sed s|k8s.gcr.io/||; s|calico/|calico/|) docker tag $img $new_img done image-list.txt # 打包镜像 docker save $(cat image-list.txt | sed s|k8s.gcr.io/||; s|calico/|calico/| | xargs) -o images/k8s-images.tar步骤3生成证书与配置模板# 初始化kubeadm配置仅用于生成证书 kubeadm config print init-defaults configs/kubeadm-init.yaml # 修改配置指定kubernetesVersion、podSubnet、serviceSubnet sed -i s|kubernetesVersion:.*|kubernetesVersion: v1.20.15| configs/kubeadm-init.yaml sed -i s|10.96.0.0/12|10.96.0.0/12| configs/kubeadm-init.yaml sed -i s|10.244.0.0/16|10.244.0.0/16| configs/kubeadm-init.yaml # 生成证书不启动集群 kubeadm init phase certs all --config configs/kubeadm-init.yaml kubeadm init phase kubeconfig all --config configs/kubeadm-init.yaml # 复制证书到configs目录 cp -r /etc/kubernetes/pki configs/ cp /etc/kubernetes/admin.conf configs/步骤4编写部署脚本scripts/deploy-master.sh内容#!/bin/bash # 参数校验 if [ $# -ne 1 ]; then echo Usage: $0 MASTER_IP exit 1 fi MASTER_IP$1 # 解压二进制 tar -xf /tmp/k8s-offline-bin.tar.gz -C /usr/local/bin/ # 加载镜像 docker load -i /tmp/k8s-images.tar # 创建目录 mkdir -p /etc/kubernetes/manifests /var/lib/etcd /var/lib/kubelet # 渲染配置 export MASTER_IP$MASTER_IP envsubst /tmp/k8s-offline/configs/kube-apiserver.yaml.template /etc/kubernetes/manifests/kube-apiserver.yaml envsubst /tmp/k8s-offline/configs/kube-controller-manager.yaml.template /etc/kubernetes/manifests/kube-controller-manager.yaml envsubst /tmp/k8s-offline/configs/kube-scheduler.yaml.template /etc/kubernetes/manifests/kube-scheduler.yaml envsubst /tmp/k8s-offline/configs/etcd.yaml.template /etc/kubernetes/manifests/etcd.yaml # 启动kubelet systemctl enable kubelet systemctl start kubelet步骤5打包离线包# 打包所有内容 tar -czf k8s-offline-centos7-v1.20.15.tgz \ --exclude*.template \ bin/ images/ configs/ scripts/ # 生成SHA256校验码 sha256sum k8s-offline-centos7-v1.20.15.tgz k8s-offline-centos7-v1.20.15.tgz.sha256提示最终离线包约1.2GB包含所有二进制、镜像、配置、脚本。交付时提供tar包sha256文件客户可sha256sum -c k8s-offline-centos7-v1.20.15.tgz.sha256校验完整性。4.2 部署阶段在目标机器上执行三步初始化主节点步骤1基础环境检查与修复# 运行预检脚本检查内核、SELinux、swap等 bash /tmp/k8s-offline/scripts/precheck.sh # 输出应全为PASS # 若FAIL按提示修复如swap未禁用则执行swapoff -a sed -i /swap/s/^/#/ /etc/fstab步骤2解压离线包并部署# 解压到临时目录 tar -xzf k8s-offline-centos7-v1.20.15.tgz -C /tmp/ # 执行部署脚本 bash /tmp/k8s-offline/scripts/deploy-master.sh 10.10.10.10注意deploy-master.sh会启动kubeletkubelet会自动加载/etc/kubernetes/manifests下的static pod依次启动etcd、apiserver、controller-manager、scheduler。步骤3等待组件就绪# 检查static pod状态需等待2-3分钟 kubectl get pods -n kube-system # 正常输出 # NAME READY STATUS RESTARTS AGE # etcd-master-01 1/1 Running 0 2m # kube-apiserver-master-01 1/1 Running 0 2m # kube-controller-manager-master-01 1/1 Running 0 2m # kube-scheduler-master-01 1/1 Running 0 2m实操心得若etcdPod卡在ContainerCreating大概率是/var/lib/etcd目录权限问题。执行chown -R etcd:etcd /var/lib/etcd。4.3 验证阶段五步法确认集群健康度验证1API Server连通性# 使用admin.conf访问API kubectl --kubeconfig /etc/kubernetes/admin.conf get nodes # 应返回空列表尚未加入node但无连接错误验证2证书有效性# 检查apiserver证书是否在有效期内 openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -dates # Not Before: Jan 1 00:00:00 2023 GMT # Not After : Dec 31 23:59:59 2032 GMT验证3etcd集群状态# 进入etcd容器执行 docker exec -it etcd etcdctl --endpointshttps://127.0.0.1:2379 \ --cacert/etc/kubernetes/pki/etcd/ca.crt \ --cert/etc/kubernetes/pki/etcd/peer.crt \ --key/etc/kubernetes/pki/etcd/peer.key \ member list # 应显示单节点状态为started验证4CNI插件部署# 应用Calico清单 kubectl --kubeconfig /etc/kubernetes/admin.conf apply -f /tmp/k8s-offline/configs/calico.yaml # 等待calico-node Pod就绪 watch kubectl --kubeconfig /etc/kubernetes/admin.conf get pods -n kube-system -l k8s-appcalico-node # READY应为1/1验证5Pod网络连通性# 启动测试Pod kubectl --kubeconfig /etc/kubernetes/admin.conf run nginx-test --imagenginx:1.19.10-alpine --restartNever # 获取Pod IP POD_IP$(kubectl --kubeconfig /etc/kubernetes/admin.conf get pod nginx-test -o jsonpath{.status.podIP}) # 从主节点curl测试 curl -I http://$POD_IP # 应返回HTTP/1.1 200 OK提示若curl超时先kubectl logs -n kube-system calico-node-xxxxx查Calico日志常见原因是bird进程未启动需检查/etc/cni/net.d/下Calico配置是否正确。5. 常见问题与排查技巧实录那些文档里不会写的“血泪经验”以下是我在12个离线K8s项目中遇到的TOP5高频问题附带真实日志、根因分析和30秒解决命令。5.1 问题1kubectl get nodes返回No resources found但kubectl get pods能看到static pod现象$ kubectl get nodes No resources found $ kubectl get pods -n kube-system NAME READY STATUS RESTARTS AGE etcd-master-01 1/1 Running 0 5m kube-apiserver-master-01 1/1 Running 0 5m根因分析kubelet未成功向API Server注册节点。常见原因有三kubelet配置中--bootstrap-kubeconfig路径错误无法获取初始token/var/lib/kubelet/pki/下证书缺失kubelet-client-current.pem未生成API Server启动参数未开启--allow-privilegedtrue导致kubelet的privileged容器无法运行排查命令# 查看kubelet日志 journalctl -u kubelet -n 100 --no-pager | grep -i register\|node # 典型错误 # Unable to register node master-01: Unauthorized # 表明kubelet证书无效或token过期30秒解决# 重新生成kubelet证书使用kubeadm kubeadm init phase kubeconfig kubelet --config /tmp/k8s-offline/configs/kubeadm-init.yaml # 复制证书 cp /etc/kubernetes/kubelet.conf /var/lib/kubelet/kubeconfig # 重启kubelet systemctl restart kubelet5.2 问题2calico-nodePod卡在Init:0/3日志显示Failed to create Calico Kubernetes client现象$ kubectl get pods -n kube-system NAME READY STATUS RESTARTS AGE calico-node-xxxxx 0/1 Init:0/3 0 2m根因分析Calico的initContainerinstall-cni需要向/etc/cni/net.d/写入配置但该目录被SELinux阻止。CentOS 7的/etc/cni/net.d/默认SELinux上下文是etc_t而Calico需要container_file_t。排查命令# 查看initContainer日志 kubectl logs -n kube-system calico-node-xxxxx -c install-cni # 错误 # open /etc/cni/net.d/10-calico.conflist: permission denied # 查看SELinux上下文 ls -Z /etc/cni/ # 输出system_u:object_r:etc_t:s0 /etc/cni/30秒解决# 修改SELinux上下文 semanage fcontext -a -t container_file_t /etc/cni(/.*)? restorecon -R /etc/cni/ # 重启calico-node kubectl delete pod -n kube-system -l k8s-appcalico-node5.3 问题3kubectl run测试Pod始终Pendingkubectl describe pod显示0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: } that the pod didnt tolerate.现象$ kubectl run busybox-test --imagebusybox:1.31.1 --command -- sleep 3600 $ kubectl get pods NAME READY STATUS RESTARTS AGE busybox-test 0/1 Pending 0 1m $ kubectl describe pod busybox-test # Events: # 0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: } that the pod didnt tolerate.根因分析主节点被打上了node-role.kubernetes.io/master:NoSchedule污点防止普通Pod调度到master。这是kubeadm init的默认行为但离线部署中我们没运行kubeadm init所以污点未设置——等等为什么会有这个污点真相是kube-scheduler.yamlmanifest里漏写了--policy-config-file参数导致scheduler用默认策略自动给master打污点。排查命令# 查看节点污点 kubectl get node master-01 -o wide -o jsonpath{.spec.taints} # 输出[{effect:NoSchedule,key:node-role.kubernetes.io/master}] # 查看scheduler参数 ps aux | grep scheduler | grep -v grep # 缺少--policy-config-file参数30秒解决# 编辑kube-scheduler.yaml添加toleration # 在spec.containers[0].args末尾添加 # - --policy-config-file/etc/kubernetes/scheduler-policy-config.json # 创建policy文件 cat /etc/kubernetes/scheduler-policy-config.json EOF { kind: Policy, apiVersion: v1, predicates: [ {name: NoVolumeZoneConflict}, {name: MaxEBSVolumeCount}, {name: MaxAzureDiskCount}, {name: MatchInterPodAffinity}, {name: NoDiskConflict}, {name: GeneralPredicates}, {name: CheckNodeMemoryPressure}, {name: CheckNodeDiskPressure}, {name: CheckNodePIDPressure}, {name: MatchNodeSelector}, {name: HostName} ], priorities: [] } EOF # 重启scheduler kubectl delete pod -n kube-system -l componentkube-scheduler5.4 问题4etcdctl member list返回context deadline exceeded但docker ps显示etcd容器在运行现象$ etcdctl --endpointshttps://127.0.0.1:2379 --cacertca.crt --certpeer.crt --keypeer.key member list Error: context deadline exceeded根因分析etcd容器监听的是https://0.0.0.0:2379但etcdctl连接127.0.0.1时SSL证书的SANSubject Alternative Name不包含127.0.0.1导致TLS握手失败。证书只签了master-01.prod.internal和10.10.10.10。排查命令# 查看证书SAN openssl x509 -in /etc/kubernetes/pki/etcd/peer.crt -text -noout | grep -A1 Subject Alternative Name # 输出 # DNS:master-01.prod.internal, IP Address:10.10.10.1030秒解决# 用节点IP连接而非127.0.0.1 etcdctl --endpointshttps://10.10.10.10:2379 \ --cac
阅读完成 · 觉得有帮助?