想把K8s单机二进制部署跑通听起来像是老教程里的操作但这套流程直到今天都有不可替代的价值。kubeadm一条 init 就能拉起集群可是拉起之后你连 apiserver 是怎么监听端口的、etcd 证书是给谁用的、kubelet 的 kubeconfig 里到底放了谁的凭证都说不清楚。换成二进制部署每一步都得自己亲手来一遍组件之间的关系会非常直观。这篇博文会带你走完一套完整的 K8s 单机二进制部署流程。目标机器只要一台 Linux 服务器2 核 4G 内存就够步骤我尽量压缩成“复制粘贴就能执行”的命令块。脚本里的 IP、主机名需要改成你自己机器的值其余配置都是可以直接用的。跟着做完你会得到一个 Node 状态 Ready 的单节点集群包含 etcd、kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy、containerd、Flannel 和 CoreDNS。适合刚入门 K8s 想搞清楚原理的人也适合内网环境不方便走 kubeadm 镜像拉取的人。1. 部署方案是怎么定的1.1 为什么不用 kubeadm非要二进制kubeadm 确实快但它帮用户做了太多隐式决定。证书自动签发、镜像版本匹配、组件以静态 Pod 形式托管在容器里这些设计对生产环境很友好却把学习路径挡住了。你写了一个 Deployment想知道从创建到 Pod 运行之间发生了哪些调用用 kubeadm 装好之后第一反应是看日志但组件挂在容器里看日志前得先找容器排查链路明显变长。二进制部署则不同。每个组件都是 /usr/local/bin 下的独立可执行文件由 systemd 管理启停、看日志、改参数都非常直接。你甚至能用journalctl -u kube-apiserver一步步看到 apiserver 是如何因为证书 SAN 不匹配而拒绝连接的这种排障能力在容器化部署里很难获得。另一个实际场景是离线内网部署。kubeadm 默认要拉一堆镜像控制面组件镜像、pause 镜像、DNS 镜像每个都是几百 MB。二进制方案只需要把几个 tar 包拷进去后续网络插件再单独处理镜像即可资源占用通常也更低。所以我把这次部署定位成“学习 轻量部署”双用途。1.2 单机拓扑与组件划分单机部署不等于把多节点配置改一改而是要把所有角色压到一台机器上。这张表我建议先看一遍后面每一步都会对应到这里组件进程名作用配置文件位置数据库etcd保存集群全部状态/etc/etcd/etcd.yml控制面kube-apiserver提供 API是所有操作的入口/etc/kubernetes/pki 证书控制面kube-controller-manager负责控制器循环/etc/kubernetes/controller-manager.conf控制面kube-scheduler负责 Pod 调度决策/etc/kubernetes/scheduler.conf节点kubelet管理节点上的 Pod 生命周期/etc/kubernetes/kubelet.conf网络kube-proxy维护 Service 转发规则/etc/kubernetes/kube-proxy.conf运行时containerd真正创建容器的组件/etc/containerd/config.toml网络插件flannel提供 Pod 网络由 Kubernetes 资源部署控制面四个组件加 etcd再加节点侧的 kubelet 和 kube-proxy全部在一台机器上。互相之间的通信走本地回环地址只有需要外部访问的端口才绑定机器 IP这样既能减少暴露面也符合“单机实验”的定位。1.3 版本清单与下载说明版本不能随便乱配不同版本的 K8s 对 API 有兼容性要求etcd 和 containerd 也有各自支持的版本区间。我用的这套组合是经过大量验证的稳定组合软件版本说明Kubernetesv1.28.2当前比较稳定的主线版本etcdv3.5.9K8s 官方推荐配套版本containerdv1.7.11兼容 CRI 标准CNI pluginsv1.3.0kubelet 调用网络插件的基础Flannelv0.22.1简单稳定的 Pod 网络方案CoreDNSv1.10.1集群 DNS 服务cri-toolsv1.28.0提供 crictl 命令下载的时候有一个通用建议不要直接在服务器上用 curl 硬拉很多服务器访问外网很慢或者干脆拉不下来。本地电脑下载好后 scp 到服务器是最省事的方式。后面的命令里我会给直链但你要做好下载可能需要一段时间的准备。2. 前置准备与环境初始化2.1 机器要求与系统约定一台 amd64 架构的 Linux 服务器2 核 CPU、4G 内存、20G 磁盘就够了。系统我用 Ubuntu 22.04 做示例Debian 12 也能用CentOS/RHEL 系需要把包管理命令换成 yum其余命令基本一致。开始前强制检查两件事内存不能低于 2G内核版本不能太老。内存太小会导致 etcd 和编译型组件一起跑起来后 OOM内核版本太老则可能不支持某些网络特性。建议内核 5.4 以上直接用较新的发行版基本没问题。系统的 hostname 必须是合法的 DNS 名称不能带下划线否则 kubelet 注册节点时会报错。我这里统一用 node1。2.2 系统参数与目录初始化把这些命令整体复制到服务器执行。注意开头的两个 export 变量是整个部署过程中所有脚本共用的IP 一定改成你自己的内网 IP。# 定义全局变量后面所有脚本都会引用 export NODE_IP192.168.1.100 export HOST_NAMEnode1 # 设置主机名 hostnamectl set-hostname ${HOST_NAME} # 配置 hosts确保本地解析不出问题 cat /etc/hosts EOF ${NODE_IP} ${HOST_NAME} EOF # 关闭 swapk8s 默认要求必须关闭 swapoff -a sed -i / swap / s/^/#/ /etc/fstab # 加载内核模块 cat EOF | tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilter # 配置内核参数 cat EOF | tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sysctl --system # 关闭防火墙实验环境直接关避免干扰 systemctl stop ufw || true systemctl disable ufw || true内核参数里最容易被忽略的是net.ipv4.ip_forward。Flannel 的 VXLAN 转发、kube-proxy 的 iptables DNAT都依赖这个开关。不打开的话 Pod 之间互相 ping 不通Service 访问也会异常。关 swap 则是硬性要求kubelet 默认启动时会检查打开 swap 会直接报错虽然新版 kubelet 有 failSwapOn 参数可以绕过但实验环境没必要开启 swap 引入额外变量。2.3 下载并安装二进制文件这里把 Kubernetes 主组件、etcd、containerd、CNI 插件一次装好。所有 tar 包解压后都是独立可执行文件不需要编译这也是二进制部署最大的便利。export K8S_VERSIONv1.28.2 export ETCD_VERv3.5.9 export CONTAINERD_VER1.7.11 export CNI_VERv1.3.0 export CRICTL_VERv1.28.0 mkdir -p /opt/k8s-download cd /opt/k8s-download # 下载 kubernetes 核心组件 curl -L -O https://dl.k8s.io/${K8S_VERSION}/kubernetes-server-linux-amd64.tar.gz tar -xzf kubernetes-server-linux-amd64.tar.gz cp kubernetes/server/bin/kube-apiserver /usr/local/bin/ cp kubernetes/server/bin/kube-controller-manager /usr/local/bin/ cp kubernetes/server/bin/kube-scheduler /usr/local/bin/ cp kubernetes/server/bin/kubelet /usr/local/bin/ cp kubernetes/server/bin/kube-proxy /usr/local/bin/ cp kubernetes/server/bin/kubectl /usr/local/bin/ # 下载 etcd curl -L -O https://github.com/etcd-io/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz tar -xzf etcd-${ETCD_VER}-linux-amd64.tar.gz cp etcd-${ETCD_VER}-linux-amd64/etcd /usr/local/bin/ cp etcd-${ETCD_VER}-linux-amd64/etcdctl /usr/local/bin/ # 下载 containerd curl -L -O https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VER}/containerd-${CONTAINERD_VER}-linux-amd64.tar.gz tar -C /usr/local -xzf containerd-${CONTAINERD_VER}-linux-amd64.tar.gz # 下载 CNI 插件 mkdir -p /opt/cni/bin curl -L -O https://github.com/containernetworking/plugins/releases/download/${CNI_VER}/cni-plugins-linux-amd64-${CNI_VER}.tgz tar -C /opt/cni/bin -xzf cni-plugins-linux-amd64-${CNI_VER}.tgz # 下载 crictl curl -L -O https://github.com/kubernetes-sigs/cri-tools/releases/download/${CRICTL_VER}/crictl-${CRICTL_VER}-linux-amd64.tar.gz tar -C /usr/local/bin -xzf crictl-${CRICTL_VER}-linux-amd64.tar.gz # 验证版本 kube-apiserver --version etcd --version containerd --version crictl version如果服务器下载 k8s 这个 1.2 字节的完整 server 包觉得太大也可以只下载 single 模式的二进制但完整包里带了 kubectl后续管理集群要用所以一个包全搞定。下载完之后/opt/k8s-download目录可以留着也能删掉不影响运行。3. 证书体系与 etcd 数据库3.1 证书规划Kubernetes 内部通信几乎全部走 TLS所以证书是第一步也是最容易出问题的一步。这里用一套自签 CA给以下对象签发证书证书文件CN组织用途ca.crt / ca.keykubernetes-ca无根 CA签发所有证书apiserver.crt / apiserver.keykube-apiserver无apiserver 对外服务的服务端证书admin.crt / admin.keyadminsystem:masterskubectl 管理员访问证书etcd-client.crt / etcd-client.keyetcd-clientetcdapiserver 访问 etcd 使用kubelet.crt / kubelet.keysystem:node:node1system:nodeskubelet 访问 apiserver 的身份凭证kube-proxy.crt / kube-proxy.keysystem:kube-proxysystem:node-proxierkube-proxy 访问 apiserver 的身份凭证apiserver 证书必须把机器的内网 IP、127.0.0.1、Service 网段第一个 IP 写进 SAN否则外部访问 API 时会提示证书校验失败。这个 SAN 问题是最常见的坑后面排查章节会再展开。3.2 openssl 生成全部证书使用 openssl 的好处是不需要额外安装 cfssl 工具几乎所有 Linux 发行版自带。把所有证书生成命令放在一个脚本里逐个执行即可。export NODE_IP192.168.1.100 export HOST_NAMEnode1 SSLDIR/etc/kubernetes/pki mkdir -p ${SSLDIR} cd ${SSLDIR} # 1. 生成根 CA openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -subj /CNkubernetes-ca -days 3650 -out ca.crt # 2. 生成 kube-apiserver 证书 cat apiserver.cnf EOF [req] distinguished_name dn [dn] [ext] keyUsage keyEncipherment,digitalSignature extendedKeyUsage serverAuth,clientAuth subjectAltName alt [alt] IP.1 127.0.0.1 IP.2 ${NODE_IP} IP.3 10.96.0.1 DNS.1 kubernetes.default.svc DNS.2 kubernetes.default.svc.cluster.local DNS.3 ${HOST_NAME} EOF openssl genrsa -out apiserver.key 2048 openssl req -new -key apiserver.key -subj /CNkube-apiserver -config apiserver.cnf -out apiserver.csr openssl x509 -req -in apiserver.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -extensions ext -extfile apiserver.cnf -out apiserver.crt # 3. 生成 admin 证书 openssl genrsa -out admin.key 2048 openssl req -new -key admin.key -subj /CNadmin/Osystem:masters -out admin.csr openssl x509 -req -in admin.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -out admin.crt # 4. 生成 etcd-client 证书 openssl genrsa -out etcd-client.key 2048 openssl req -new -key etcd-client.key -subj /CNetcd-client/Oetcd -out etcd-client.csr openssl x509 -req -in etcd-client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -out etcd-client.crt # 5. 生成 kubelet 证书 openssl genrsa -out kubelet.key 2048 openssl req -new -key kubelet.key -subj /CNsystem:node:${HOST_NAME}/Osystem:nodes -out kubelet.csr openssl x509 -req -in kubelet.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -out kubelet.crt # 6. 生成 kube-proxy 证书 openssl genrsa -out kube-proxy.key 2048 openssl req -new -key kube-proxy.key -subj /CNsystem:kube-proxy/Osystem:node-proxier -out kube-proxy.csr openssl x509 -req -in kube-proxy.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -out kube-proxy.crt # 7. 生成 Service Account 签名密钥对 openssl genrsa -out sa.key 2048 openssl rsa -in sa.key -pubout -out sa.pub生成完检查一下文件列表共 20 个左右。如果中途有证书生成失败直接删掉对应的 key/csr/crt 重新执行不用怕。有一点要特别注意kubelet 证书的 CN 必须是system:node:主机名格式组织必须是system:nodes这是 Kubernetes 节点认证器的硬性识别规则。写错的话 kubelet 注册节点时会被 apiserver 拒绝。3.3 etcd 配置与启动etcd 是集群状态存储必须在其他组件之前启动。单机环境只需要一个 etcd 实例但配置里仍然要有initial-cluster成员信息这是 etcd 启动协议的固定要求。# 创建 etcd 用户和目录 useradd -s /usr/sbin/nologin etcd || true mkdir -p /etc/etcd /var/lib/etcd chown -R etcd:etcd /var/lib/etcd chown -R etcd:etcd /etc/etcd # 写入 etcd 配置文件 cat /etc/etcd/etcd.yml EOF name: ${HOST_NAME}>cat /etc/systemd/system/etcd.service EOF [Unit] DescriptionEtcd Server Afternetwork.target [Service] Typenotify Useretcd Groupetcd ExecStart/usr/local/bin/etcd --config-file/etc/etcd/etcd.yml Restartalways RestartSec10 LimitNOFILE65536 [Install] WantedBymulti-user.target EOF systemctl daemon-reload systemctl enable etcd --now启动后看状态systemctl status etcd再用 etcdctl 做健康检查。注意ETCDCTL_API3必须带上etcdctl 默认可能走 v2 协议。export ETCDCTL_API3 etcdctl --endpointshttps://127.0.0.1:2379 \ --cacert/etc/kubernetes/pki/ca.crt \ --cert/etc/kubernetes/pki/etcd-client.crt \ --key/etc/kubernetes/pki/etcd-client.key \ endpoint health看到healthy就代表数据库正常。如果这里失败后面所有组件都不用继续往下走apiserver 起来也会反复报连接 etcd 超时。4. 控制面组件部署4.1 统一的 kubeconfig 生成Kubernetes 组件之间不靠 IP 白名单认证而是靠 kubeconfig 文件里的客户端证书。这里我先生成所有组件需要的 kubeconfig它们结构相似只是引用的证书不同。export NODE_IP192.168.1.100 KUBECONFIG_DIR/etc/kubernetes # 生成 admin kubeconfig同时作为 kubectl 的管理凭证 cat ${KUBECONFIG_DIR}/admin.conf EOF apiVersion: v1 kind: Config preferences: {} clusters: - cluster: certificate-authority: /etc/kubernetes/pki/ca.crt server: https://${NODE_IP}:6443 name: cluster.local contexts: - context: cluster: cluster.local user: admin name: admincluster.local current-context: admincluster.local users: - name: admin user: client-certificate: /etc/kubernetes/pki/admin.crt client-key: /etc/kubernetes/pki/admin.key EOF # controller-manager 和 scheduler 共用 admin 证书实验环境简化处理 cp ${KUBECONFIG_DIR}/admin.conf ${KUBECONFIG_DIR}/controller-manager.conf cp ${KUBECONFIG_DIR}/admin.conf ${KUBECONFIG_DIR}/scheduler.conf # kubelet 专用 kubeconfig cat ${KUBECONFIG_DIR}/kubelet.conf EOF apiVersion: v1 kind: Config preferences: {} clusters: - cluster: certificate-authority: /etc/kubernetes/pki/ca.crt server: https://${NODE_IP}:6443 name: cluster.local contexts: - context: cluster: cluster.local user: kubelet name: kubeletcluster.local current-context: kubeletcluster.local users: - name: kubelet user: client-certificate: /etc/kubernetes/pki/kubelet.crt client-key: /etc/kubernetes/pki/kubelet.key EOF # kube-proxy 专用 kubeconfig cat ${KUBECONFIG_DIR}/kube-proxy.conf EOF apiVersion: v1 kind: Config preferences: {} clusters: - cluster: certificate-authority: /etc/kubernetes/pki/ca.crt server: https://${NODE_IP}:6443 name: cluster.local contexts: - context: cluster: cluster.local user: kube-proxy name: kube-proxycluster.local current-context: kube-proxycluster.local users: - name: kube-proxy user: client-certificate: /etc/kubernetes/pki/kube-proxy.crt client-key: /etc/kubernetes/pki/kube-proxy.key EOF ls -l ${KUBECONFIG_DIR}/*.conf这里用了 admin.conf 直接复制给 controller-manager 和 scheduler实验环境没问题生产环境不要这样。正式场景应该为每个组件签发独立的客户端证书最小化权限。单机部署的目标是先跑起来权限细化可以作为后续加固的练习。4.2 kube-apiserver 部署apiserver 是整个集群的总入口它同时处理 REST API、认证授权、与 etcd 的通信。参数非常长我把可变参数集中在/etc/kubernetes/env文件里方便统一修改。export NODE_IP192.168.1.100 export HOST_NAMEnode1 cat /etc/kubernetes/env EOF NODE_IP${NODE_IP} HOST_NAME${HOST_NAME} EOF服务单元文件里通过${NODE_IP}引用环境变量这样之后想换 IP 只需要改 env 文件。cat /etc/systemd/system/kube-apiserver.service EOF [Unit] DescriptionKubernetes API Server Afteretcd.service Wantsetcd.service [Service] EnvironmentFile/etc/kubernetes/env ExecStart/usr/local/bin/kube-apiserver \\ --advertise-address\${NODE_IP} \\ --bind-address0.0.0.0 \\ --secure-port6443 \\ --service-cluster-ip-range10.96.0.0/12 \\ --service-node-port-range30000-32767 \\ --etcd-servershttps://127.0.0.1:2379 \\ --etcd-cafile/etc/kubernetes/pki/ca.crt \\ --etcd-certfile/etc/kubernetes/pki/etcd-client.crt \\ --etcd-keyfile/etc/kubernetes/pki/etcd-client.key \\ --client-ca-file/etc/kubernetes/pki/ca.crt \\ --tls-cert-file/etc/kubernetes/pki/apiserver.crt \\ --tls-private-key-file/etc/kubernetes/pki/apiserver.key \\ --service-account-key-file/etc/kubernetes/pki/sa.pub \\ --service-account-signing-key-file/etc/kubernetes/pki/sa.key \\ --service-account-issuerhttps://kubernetes.default.svc.cluster.local \\ --authorization-modeNode,RBAC \\ --enable-bootstrap-token-authtrue \\ --allow-privilegedtrue [Install] WantedBymulti-user.target EOF systemctl daemon-reload systemctl enable kube-apiserver --now几个关键参数解释一下--service-cluster-ip-range是 Service 网段10.96.0.0/12 是 K8s 社区默认值后面的 CoreDNS Service IP 10.96.0.10 必须在这个网段内。--client-ca-file指向 CA 证书apiserver 会用它验证所有客户端证书包括 kubelet、kube-proxy、kubectl。--authorization-modeNode,RBAC开启了 Node 授权器和 RBAC这是标准安全配置。--service-account-key-file和--service-account-signing-key-file负责 ServiceAccount token 的签发和校验没有这几个参数Pod 创建时默认 ServiceAccount 会报错。启动后验证curl --cacert /etc/kubernetes/pki/ca.crt https://${NODE_IP}:6443/healthz返回ok就代表 apiserver 对 HTTPS 服务和 etcd 连接都正常。如果这里卡住优先看journalctl -u kube-apiserver -f的日志etcd 连接失败会直接看到connection refused证书问题会看到x509: certificate相关报错。4.3 controller-manager 与 scheduler 部署这两个组件都是控制面常驻进程一个负责调节集群状态到期望状态一个负责把 Pod 调度到合适节点。它们的启动参数相对简单只要有可用的 kubeconfig 就能连上 apiserver。cat /etc/systemd/system/kube-controller-manager.service EOF [Unit] DescriptionKubernetes Controller Manager Afterkube-apiserver.service Wantskube-apiserver.service [Service] EnvironmentFile/etc/kubernetes/env ExecStart/usr/local/bin/kube-controller-manager \\ --kubeconfig/etc/kubernetes/controller-manager.conf \\ --service-account-private-key-file/etc/kubernetes/pki/sa.key \\ --cluster-cidr10.244.0.0/16 \\ --allocate-node-cidrstrue \\ --service-cluster-ip-range10.96.0.0/12 \\ --use-service-account-credentialstrue \\ --leader-electfalse \\ --bind-address0.0.0.0 [Install] WantedBymulti-user.target EOF cat /etc/systemd/system/kube-scheduler.service EOF [Unit] DescriptionKubernetes Scheduler Afterkube-apiserver.service Wantskube-apiserver.service [Service] EnvironmentFile/etc/kubernetes/env ExecStart/usr/local/bin/kube-scheduler \\ --kubeconfig/etc/kubernetes/scheduler.conf \\ --leader-electfalse [Install] WantedBymulti-user.target EOF systemctl daemon-reload systemctl enable kube-controller-manager kube-scheduler --now systemctl status kube-controller-manager kube-scheduler--allocate-node-cidrstrue让 controller-manager 自动给每个节点分配 Pod 网段Flannel 启动后会用 node 的 podCIDR 生成本机网络配置。--leader-electfalse是因为单节点集群不需要选主省掉 keepalive 的心跳开销。如果你以后扩成多节点记得把这两个组件改成leader-electtrue否则多个副本会同时抢写状态。5. 节点侧组件containerd、kubelet、kube-proxy5.1 容器运行时 containerd 配置kubelet 不直接创建容器它通过 CRI 调用容器运行时。这里用 containerd相比 docker 少了 dockerd 和 shim 之间的代理层资源占用更小也是社区当前的主流方向。# 生成默认配置文件 mkdir -p /etc/containerd containerd config default | tee /etc/containerd/config.toml # 启用 SystemdCgroup并修改 pause 镜像地址 sed -i s#SystemdCgroup false#SystemdCgroup true# /etc/containerd/config.toml sed -i s#sandbox_image registry.k8s.io/pause:.*#sandbox_image docker.io/rancher/mirrored-pause:3.9# /etc/containerd/config.toml cat /etc/systemd/system/containerd.service EOF [Unit] Descriptioncontainerd container runtime Afternetwork.target [Service] ExecStart/usr/local/bin/containerd Restartalways RestartSec10 [Install] WantedBymulti-user.target EOF systemctl daemon-reload systemctl enable containerd --now systemctl status containerd crictl infoSystemdCgroup true这行是重点。kubelet 默认的 cgroup 驱动是 systemdcontainerd 如果还用 cgroupfs两边驱动不一致Pod 起一个就会被杀一个节点状态反复横跳。这是新手踩得最多的大坑。修改 pause 镜像地址是因为 registry.k8s.io 在很多环境拉不动换成 Docker Hub 上的镜像加速替代源功能完全一致只是仓库路径不同。如果crictl info报错先看 containerd 日志绝大多数情况是/etc/containerd/config.toml权限或格式问题。检查无异常后用crictl pull docker.io/library/nginx:latest验证一下运行时拉镜像是否可用。5.2 kubelet 部署kubelet 是节点上最核心的代理它要做的事包括向 apiserver 注册节点、监听 Pod 任务、管理容器生命周期、上报节点状态。配置我拆成两部分命令行参数放在 systemd unit 里身份认证和网络相关配置放在 YAML 文件里。export NODE_IP192.168.1.100 export HOST_NAMEnode1 # 写入 kubelet 主配置 cat /etc/kubernetes/kubelet-config.yaml EOF apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration address: 0.0.0.0 port: 10250 authentication: x509: clientCAFile: /etc/kubernetes/pki/ca.crt authorization: mode: Webhook clusterDNS: - 10.96.0.10 clusterDomain: cluster.local cgroupDriver: systemd failSwapOn: false maxPods: 110 EOF # 写入 systemd 单元 cat /etc/systemd/system/kubelet.service EOF [Unit] DescriptionKubernetes Kubelet Aftercontainerd.service Wantscontainerd.service [Service] EnvironmentFile/etc/kubernetes/env ExecStart/usr/local/bin/kubelet \\ --config/etc/kubernetes/kubelet-config.yaml \\ --kubeconfig/etc/kubernetes/kubelet.conf \\ --hostname-override\${HOST_NAME} \\ --node-ip\${NODE_IP} \\ --network-plugincni \\ --v2 [Install] WantedBymulti-user.target EOF systemctl daemon-reload systemctl enable kubelet --now--hostname-override指定节点名称必须和证书里的 CN 后缀保持一致即node1。--node-ip指定节点通信 IP这一点多网卡机器尤其重要不指定的话 kubelet 可能选错网卡导致节点 IP 变成一个无法访问的内网地址。clusterDNS指向 10.96.0.10这是稍后部署的 CoreDNS Service 地址如果不配置Pod 里无法解析集群内服务名。启动以后先别急着看节点状态因为还没有网络插件节点会一直显示 NotReady这是正常现象。先确认 kubelet 进程没有退出systemctl status kubelet journalctl -u kubelet -f --no-pager | tail -50日志里出现Failed to connect to apiserver说明 kubeconfig 或 apiserver 地址有问题出现Error getting node则不一定代表失败因为 kubelet 刚启动还没来得及注册。只要没有反复重启就继续往下走。5.3 kube-proxy 部署kube-proxy 负责把 Service 的虚拟 IP 翻译成实际 Pod IP常用模式是 iptables。它本身不是 Pod所以直接用 systemd 管理。先给 kube-proxy 创建 RBAC 权限因为 kube-proxy 的证书用户system:kube-proxy需要被授予操作 Service 和 Endpoint 的权限。用之前生成的 admin kubectl 配置来提交export KUBECONFIG/etc/kubernetes/admin.conf kubectl apply -f - EOF apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: kube-proxy roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: system:node-proxier subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: system:kube-proxy EOF这一步容易漏。如果你忘了给 kube-proxy 做 RBAC 绑定它能连接 apiserver但后续同步 Service 规则时会因为权限不足静默失败表面看进程活着实际 Service 完全不可访问。然后写 systemd 单元cat /etc/systemd/system/kube-proxy.service EOF [Unit] DescriptionKubernetes Kube Proxy Afterkube-apiserver.service Wantskube-apiserver.service [Service] EnvironmentFile/etc/kubernetes/env ExecStart/usr/local/bin/kube-proxy \\ --kubeconfig/etc/kubernetes/kube-proxy.conf \\ --cluster-cidr10.244.0.0/16 \\ --proxy-modeiptables \\ --bind-address0.0.0.0 [Install] WantedBymulti-user.target EOF systemctl daemon-reload systemctl enable kube-proxy --now systemctl status kube-proxy除了 RBAC还有一个容易踩的坑--cluster-cidr参数必须与前面 controller-manager 里配置的 Pod 网段一致都是 10.244.0.0/16。如果不一致kube-proxy 在设置流量转发规则时会把目标 Pod 网段当成外部网段处理Service 转发就到不了 Pod。6. 网络插件与集群可用性验证6.1 为什么选 Flannel单机网络插件我推荐 Flannel 而不是 Calico。Flannel 的 VXLAN 后端原理简单配置量少对单机网络性能要求低。Calico 功能强支持 NetworkPolicy但组件更多规则更复杂新手排障时容易被 BGP 和 Felix 的概念绕晕。Flannel 的安装方式通过 Kubernetes 资源对象部署它会以 DaemonSet 形式在每个节点上跑一个 Pod把 Pod 网络从主机网络里切分出来。安装前 kubelet 是 NotReady 状态Flannel 装完节点网络就绪kubelet 才能把 Node 状态改为 Ready。6.2 应用 Flannelexport KUBECONFIG/etc/kubernetes/admin.conf kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/v0.22.1/Documentation/kube-flannel.yml如果服务器拉取这个 YAML 文件困难可以先在本地下载好再 scp 上去。Flannel 镜像用的是flannel/flannel:v0.22.1YAML 应用后需要等待镜像拉取成功。检查部署状态kubectl -n kube-flannel get pods -o wide观察 Flannel Pod 日志kubectl -n kube-flannel logs -l appflannel --tail50如果 Flannel Pod 一直 CrashLoopBackOff最常见的原因是节点没有分配 PodCIDR。检查一下节点kubectl get node ${HOST_NAME} -o jsonpath{.spec.podCIDR}如果字段为空说明 controller-manager 没有成功分配检查它的启动参数里有没有--allocate-node-cidrstrue和--cluster-cidr10.244.0.0/16。分配完成以后Flannel 会在节点上生成/run/flannel/subnet.env并写好 CNI 配置/etc/cni/net.d/10-flannel.conflistkubelet 的 CNI 插件就会从这里读取网络配置。等 Flannel Pod 变成 Running再确认节点状态kubectl get nodes -o wide这时候节点应该变成 Ready 了。如果还是 NotReady多半是 CNI 插件二进制没装好或者 kubelet 日志里出现Failed to get subnet。检查/opt/cni/bin下是否有一堆 flannel 相关的可执行文件没有就把之前下载的 CNI 插件重新解压。6.3 CoreDNS 与测试负载K8s 集群没有 CoreDNS 就无法用服务名解析很多测试都无法进行。这里部署一个最小化的 CoreDNS配置足够单机实验使用。kubectl apply -f - EOF apiVersion: v1 kind: ServiceAccount metadata: name: coredns namespace: kube-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: system:coredns rules: - apiGroups: [] resources: [endpoints,services,pods,namespaces] verbs: [list,watch] - apiGroups: [discovery.k8s.io] resources: [endpointslices] verbs: [list,watch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: system:coredns roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: system:coredns subjects: - kind: ServiceAccount name: coredns namespace: kube-system --- apiVersion: v1 kind: ConfigMap metadata: name: coredns namespace: kube-system data: Corefile: | .:53 { errors health kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure fallthrough in-addr.arpa ip6.arpa } forward . /etc/resolv.conf cache 30 loop reload loadbalance } --- apiVersion: apps/v1 kind: Deployment metadata: name: coredns namespace: kube-system spec: replicas: 1 selector: matchLabels: k8s-app: kube-dns template: metadata: labels: k8s-app: kube-dns spec: serviceAccountName: coredns containers: - name: coredns image: coredns/coredns:1.10.1 args: [ -conf, /etc/coredns/Corefile ] ports: - containerPort: 53 name: dns protocol: UDP volumeMounts: - name: config-volume mountPath: /etc/coredns volumes: - name: config-volume configMap: name: coredns items: - key: Corefile path: Corefile --- apiVersion: v1 kind: Service metadata: name: kube-dns namespace: kube-system labels: k8s-app: kube-dns spec: selector: k8s-app: kube-dns clusterIP: 10.96.0.10 ports: - name: dns port: 53 protocol: UDP - name: dns-tcp port: 53 protocol: TCP EOF然后等 CoreDNS Pod Runningkubectl -n kube-system get pods -o wide最后跑一个 nginx 验证端到端联通性kubectl create deployment nginx --imagenginx kubectl expose deployment nginx --port80 --typeClusterIP kubectl get svc nginx到这一步如果你的EXTERNAL-IP还是 pending没关系ClusterIP 类型的 Service 不需要外部 IP。在集群里随便跑一个临时 Pod 测试kubectl run busybox --imagebusybox --rm -it --restartNever -- wget -qO- nginx看到 nginx 欢迎页输出说明 Pod 网络、Service 转发、kube-proxy、CoreDNS 全部正常。单机集群已经真正可用了。7. 问题排查与避坑笔记这套流程看起来顺实际执行时必然会遇到几个坑。我把最常见的现象、原因和解决方法整理成一个速查表按出现概率排序。现象根本原因排查与解决kube-apiserver 启动失败日志报 etcd 连接超时etcd 还没就绪或证书错误先确认 etcd 健康检查通过检查 apiserver 的 etcd 证书路径是否存在浏览器/curl 访问 API 报证书验证失败apiserver 证书没有把访问 IP 加入 SAN重新生成 apiserver.crt确认 SAN 里包含 127.0.0.1、节点 IP、10.96.0.1kubelet 反复重启日志出现 cgroup driver 不一致containerd 的 SystemdCgroup 还是 false修改 config.toml 后重启 containerdNode 一直 NotReady没有可用的 CNI 配置或网络插件镜像拉不下来查看 Flannel Pod 日志检查 /etc/cni/net.d 和 /opt/cni/bin 目录Pod 创建后一直 ContainerCreatingpause 镜像拉取失败修改 containerd 的 sandbox_image 后重启 containerdkubectl 能连 apiserver 但 exec 进 Pod 失败kubelet 没有配置认证给 kubelet 的 KubeletConfiguration 添加 authentication.x509.clientCAFilekube-proxy 没日志错误但 Service 不通RBAC 绑定缺失创建 ClusterRoleBinding给 system:kube-proxy 绑定 system:node-proxier节点 Ready 后过几分钟变 NotReady网络插件 Pod 被驱逐检查节点资源Flannel 和 CoreDNS 内存占用偏高时扩容或限制资源除了表格里的问题再分享几个实操经验。第一不要跳过任何一步去“精简配置”。比如省掉--service-account-key-file你创建 Deployment 时会发现 Pod 一直 ImagePullBackOff 或者 CreateContainerConfigError日志里全是 ServiceAccount 相关报错。这些参数看着没用其实在每一层都起着作用。第二证书有效期建议直接给 3650 天。二进制部署的证书体系不像 kubeadm 那样有自动更新机制过期以后所有组件中断临时续签很麻烦。实验环境没有必要模拟一年有效期到期这件事。第三单机二进制部署是绝佳的学习工具但也只适合学习和轻量实验。生产环境还是建议走 kubeadm 或者托管集群因为高可用、证书轮换、版本升级这些能力二进制裸跑都很难覆盖。等你理解了组件关系之后再回来看 kubeadm 生成的配置会看得非常通透。我个人在实际操作中的体会是一次完整的二进制部署顶得上十篇架构分析文章。当你亲手解决了 apiserver 证书 SAN 不匹配、cgroup 驱动冲突、CNI 配置缺失这三个问题之后K8s 在你眼里就不再是一个“黑盒容器平台”了。遇到生产环境的诡异问题你也会多一分底气去回答“这个组件之间到底是怎么连的”。希望这份步骤能让你少走些弯路。