1. 公网K8S搭建的整体设计与思路拆解
把一套 Kubernetes 集群暴露到公网,和在内网里搭个玩具集群完全是两码事。内网集群你随便折腾,节点之间裸奔通信也没人管;一旦挂到公网,每天扫描 6443、2379、10250 这些端口的机器人流量能把你日志刷爆。我前后搭过几套面向公网提供服务的集群,踩过的坑足够写一本小册子,这里把整套思路和落地细节摊开讲。
先说清楚这套东西是什么、解决什么问题。公网K8S搭建,指的是在云主机或独立服务器上,从零构建一套 Kubernetes 集群,并让它能够安全、稳定地对外提供 HTTP/HTTPS 服务。它解决的核心问题是:如何让集群的控制面和节点在不可信网络环境下依然保持可用、可控、可观测。适合谁来参考?有一定 Linux 基础、想自己掌控一套生产级集群的开发者、运维人员,以及需要把内部服务对外发布的小团队。如果你只是想学 K8S 的基本概念,Minikube 或 Kind 更合适;但如果你要让真实流量打进来,那这套流程就是绕不开的。
整体设计上,我倾向于把集群拆成三个层次来考虑:控制面层、工作负载层、入口层。控制面层负责调度和状态管理,工作负载层跑业务 Pod,入口层负责把公网流量安全地引进来。这三层在公网环境下的安全要求完全不同,必须分开设计。控制面要尽量少暴露端口,工作负载层要隔离网络,入口层才是真正对外的那道门。
方案选型上,我最终采用的是kubeadm 引导 + 容器运行时 containerd + CNI 选 Calico + 入口用 Ingress-Nginx + 证书用 cert-manager这套组合。为什么这么选?kubeadm 是官方维护的引导工具,升级路径清晰,比二进制手工搭建省心太多,也比各种封装发行版更透明——出了问题你能直接查到 K8S 原生文档。containerd 相比 Docker 更轻,K8S 从 1.24 起就移除了 dockershim,直接用 containerd 少一层抽象,资源占用和故障面都更小。Calico 的 BGP 模式和 NetworkPolicy 支持成熟,公网环境下网络策略是刚需,能防止某个被攻破的 Pod 横向扫描整个集群。Ingress-Nginx 生态最全,注解丰富,和 cert-manager 配合自动签发续期证书几乎是标配。
这里有个关键取舍要讲清楚:控制面到底要不要暴露公网。我的做法是控制面 API Server 只对固定的运维出口 IP 开放,通过安全组白名单限制,绝不 0.0.0.0/0 全开。节点加入集群时走的是内网地址,公网只留入口层的 80/443。这样即使 API Server 有未公开漏洞,攻击面也被压到最小。很多人图省事把 6443 全开,结果被扫到弱口令或者未授权访问,集群直接被人拿去挖矿,这种案例我见过不止一次。
还有一个容易被忽略的设计点:etcd 的部署形态。kubeadm 默认把 etcd 以静态 Pod 形式跑在控制面节点上,三节点控制面就是三个 etcd 成员。公网环境下 etcd 的 2379/2380 端口绝对不能对外,节点间通信必须走内网或者加密通道。如果你的云主机跨可用区,内网延迟要提前测,etcd 对延迟敏感,跨区延迟超过 10ms 就可能出现选主抖动。
2. 核心细节解析与实操要点
2.1 主机规划与系统预配置
动手之前先把机器规划定死,后面所有步骤都依赖这个基础。我一般用三台控制面加若干工作节点的结构,控制面奇数台保证 etcd 仲裁。假设用某云厂商的三台 4C8G 机器做控制面,两台 8C16G 做工作节点,操作系统统一用较新的 LTS 版本,内核版本不低于 5.4,因为 Calico 的 eBPF 特性和一些网络优化需要较新内核。
系统预配置这块,每一步都有它的道理,不是照抄命令就完事。关闭 swap 是因为 kubelet 的调度假设内存是可预测的,swap 会让 Pod 的内存限制失效,导致 OOM 行为不可控。关闭防火墙或者放行特定端口,是因为 K8S 组件之间通信端口众多,用 firewalld 逐条放行容易漏,公网环境下更推荐用云安全组在外部做第一道过滤,主机内部用 iptables 规则精细控制。设置内核参数net.bridge.bridge-nf-call-iptables=1是为了让桥接流量经过 iptables,否则 Service 的 DNAT 规则对跨桥流量不生效,这是新手最常踩的坑之一。
时间同步必须做,而且要用可靠的时间源。etcd 和证书校验都依赖时间,节点间时间偏差超过几秒就可能出现证书验证失败或者选主异常。我习惯装 chrony 并指向内网时间服务器,公网 NTP 源在云环境里经常被限速。
主机名规划也有讲究。kubeadm 初始化时会把主机名注册进集群,如果主机名重复或者含特殊字符,后续节点加入会出问题。我统一用k8s-master-01、k8s-worker-01这种规范命名,并在/etc/hosts里把内网 IP 和主机名对应写好,避免依赖 DNS 解析带来的不确定性。
注意:所有节点的主机名必须唯一,且初始化后不要再改。改主机名会导致 kubelet 注册的 Node 对象和证书里的 CN 不匹配,节点直接 NotReady。
2.2 容器运行时与 kubelet 安装
containerd 的安装我倾向用官方二进制包而不是发行版仓库里的版本,仓库版本往往偏旧,和较新 K8S 的 CRI 接口可能有兼容问题。安装后要生成默认配置并调整两个关键点:一是SystemdCgroup = true,让 containerd 用 systemd 管理 cgroup,和 kubelet 的 cgroup 驱动保持一致,否则会出现 Pod 启动后资源限制不生效的诡异现象;二是配置镜像加速或者私有仓库地址,公网拉取镜像慢会拖垮整个部署节奏。
kubelet、kubeadm、kubectl 三个组件的版本必须严格一致,小版本都不能差。我见过有人 kubeadm 是 1.28.2 而 kubelet 是 1.28.0,初始化时直接报版本偏差错误。安装时用包管理器锁定版本,避免后续apt upgrade把版本升乱。kubelet 安装后先别急着启动,等 kubeadm init 跑完再让它起来,否则它会反复重启刷日志。
这里补充一个实操细节:kubelet 的--node-ip参数在双网卡机器上必须显式指定。云主机经常有内网网卡和公网网卡两块,kubelet 默认可能选错 IP 注册到集群,导致其他节点连不上它。我在 kubeadm 的配置里通过nodeRegistration.kubeletExtraArgs把node-ip固定为内网地址,这个坑踩过一次就忘不了。
2.3 集群初始化与网络插件部署
kubeadm 的初始化配置文件是整个搭建过程的核心,我习惯写一个kubeadm-config.yaml而不是用命令行参数,因为配置可版本化、可复用、可审计。配置文件里几个关键字段:controlPlaneEndpoint指向一个内网负载均衡地址或者第一台控制面的内网 IP 加端口,podSubnet和serviceSubnet要和后续 CNI 的网段规划一致,imageRepository指向可访问的镜像仓库。
初始化命令跑完后,按提示配置 kubectl 的 kubeconfig,然后立刻部署 CNI。Calico 我用的是 operator 方式或者 manifest 方式,取决于集群规模。小集群直接 apply manifest 最快,注意把CALICO_IPV4POOL_CIDR改成和podSubnet一致,否则 Pod 拿不到 IP。Calico 的IP_AUTODETECTION_METHOD要设成can-reach或者指定网卡名,确保它选对内网网卡做 BGP 宣告,公网网卡上跑 BGP 是灾难。
网络插件起来后,用kubectl get nodes确认所有节点 Ready,用kubectl get pods -n kube-system确认 CoreDNS 和 Calico 的 Pod 全部 Running。如果 CoreDNS 一直 Pending,八成是 CNI 没装好或者网段冲突;如果 Calico 的 Pod CrashLoop,去看日志里是不是网卡选错了或者内核模块没加载。
提示:初始化前先用
kubeadm config images pull把镜像拉下来,能提前暴露镜像仓库访问问题,避免 init 跑到一半卡住。
3. 实操过程与核心环节实现
3.1 控制面高可用与安全加固落地
单控制面集群在公网环境里没有容错能力,机器一挂整个集群失联。我推荐至少三控制面。kubeadm 支持 stacked etcd 模式,三台控制面各自跑一个 etcd 成员,前面挂一个内网负载均衡把 6443 端口转发到三台机器。负载均衡用云厂商的内网 CLB 或者自己用 keepalived 加 haproxy 都行,关键是健康检查要探 API Server 的/healthz端点。
初始化第一台控制面时用--upload-certs把证书加密上传,后续两台用kubeadm join带--control-plane和--certificate-key加入。加入顺序有讲究,一台一台来,每台加入后确认 etcd 成员健康再操作下一台。三台都加入后,用kubectl get pods -n kube-system -l component=etcd确认三个 etcd Pod 都在各自节点上 Running,再用etcdctl endpoint health逐个检查。
安全加固这块,我做了几件事。第一,API Server 的--authorization-mode确保包含Node,RBAC,默认就是,但有人改配置时容易漏。第二,给 kubelet 开启--anonymous-auth=false和--authorization-mode=Webhook,防止未认证请求访问 kubelet 的 10250 端口,这个端口在公网暴露且未加固的话,攻击者可以直接在节点上执行命令。第三,用 NetworkPolicy 默认拒绝所有入站流量,再按需放行,Calico 支持这个能力,一条默认 deny 策略能挡掉大量横向渗透。
审计日志也要开。API Server 的--audit-log-path和--audit-policy-file配置好,记录谁在什么时候对什么资源做了什么操作。公网集群出问题时,审计日志是排查的第一手材料。日志轮转用--audit-log-maxbackup和--audit-log-maxsize控制,别让日志把磁盘写满。
3.2 入口层与证书自动化配置
入口层是公网流量的唯一入口,这块配好了,后面的服务发布就是流水线作业。Ingress-Nginx 我用 Helm 安装,values 里重点调几个参数:controller.service.type设为 LoadBalancer 或者 NodePort 配合外部负载均衡,controller.config.proxy-body-size按业务调整默认 1m 往往不够,controller.metrics.enabled打开监控,controller.podAntiAffinity让多个副本分散到不同节点。
cert-manager 负责证书全生命周期。装完后创建 ClusterIssuer,用 ACME 的 HTTP-01 或者 DNS-01 验证。HTTP-01 要求域名解析到入口 IP 且 80 端口可达,适合简单场景;DNS-01 适合泛域名或者入口不直接暴露 80 的情况,需要配置 DNS 服务商的 API 凭证。我一般用 DNS-01 加泛域名证书,一张证书覆盖所有子域名,省去每个服务单独签发的麻烦。
证书自动续期要验证。cert-manager 默认在证书到期前 30 天续期,但续期失败不会主动告警。我加了一条 Prometheus 告警规则,监控certmanager_certificate_expiration_timestamp_seconds减去当前时间小于 14 天就触发,给自己留足排查时间。实测下来,DNS-01 续期偶尔会因为 DNS 服务商 API 限流失败,有告警才能及时发现。
注意:Ingress 的 TLS secret 和 cert-manager 的 Certificate 资源要对应好,域名变更时记得同步改 Ingress 的
tls.hosts,否则证书签了但没被引用,浏览器照样报不安全。
3.3 工作节点加入与污点调度策略
工作节点加入集群用kubeadm join带 token 和 CA 证书 hash。token 默认 24 小时过期,大批量加节点时记得用kubeadm token create --ttl 0生成长期 token,加完再清理。加入后给节点打标签,比如node-role.kubernetes.io/worker=,方便后续用 nodeSelector 或者 affinity 调度。
控制面节点默认带node-role.kubernetes.io/control-plane:NoSchedule污点,业务 Pod 不会调度上去。如果集群资源紧张想让控制面也跑业务,可以去掉污点,但我不建议——控制面节点跑业务会争抢资源,etcd 对 IO 和 CPU 敏感,业务高峰时可能拖垮控制面。真要利用资源,单独加一台工作节点更稳妥。
节点维护时用kubectl drain驱逐 Pod,加--ignore-daemonsets和--delete-emptydir-data,维护完kubectl uncordon恢复。公网集群里节点重启后,kubelet 会自动重连,但如果有 Pod 用了 hostNetwork 或者本地存储,重启后状态可能不一致,维护窗口内要重点检查这类 Pod。
4. 常见问题与排查技巧实录
公网集群的问题排查和内网差别很大,很多问题是被外部流量触发的。我把踩过的典型问题整理成速查表,附上排查思路。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 节点 NotReady,kubelet 日志报证书错误 | 节点时间偏差大或证书过期 | 检查 chrony 同步状态,kubeadm certs check-expiration查看证书有效期 |
| Pod 无法解析域名 | CoreDNS Pod 异常或 CNI 网段冲突 | kubectl logs看 CoreDNS,检查 podSubnet 与节点网段是否重叠 |
| Ingress 返回 502 | 后端 Service 无可用 Endpoint 或端口不匹配 | kubectl get endpoints确认后端 Pod 就绪,核对 Service targetPort |
| 证书签发一直 Pending | ACME 验证失败,DNS 未生效或 80 端口不通 | kubectl describe certificate看事件,用dig确认解析,检查安全组 |
| API Server 偶发超时 | etcd 跨区延迟高或磁盘 IO 瓶颈 | 测节点间延迟,etcdctl check perf看 etcd 性能,换 SSD 云盘 |
| 节点被扫描端口告警 | 安全组规则过宽或 kubelet 未加固 | 收紧安全组,确认 kubeletanonymous-auth=false |
除了表格里的,还有几个独家避坑技巧值得单独说。第一,公网集群一定要配 Pod 的出口 IP 伪装。Calico 默认对出集群流量做 SNAT,但如果配置不当,Pod 访问外网时源 IP 是 Pod IP,外部服务可能拒绝或者日志里全是内网地址。检查calicoctl get ippool的natOutgoing是否为 true。
第二,Ingress 的 real_ip 配置。流量经过云负载均衡后,后端看到的源 IP 是负载均衡的地址,业务日志里全是同一个 IP。要在 Ingress-Nginx 的 ConfigMap 里配use-forwarded-headers: "true"和compute-full-forwarded-for: "true",并设置proxy-real-ip-cidr为负载均衡的网段,这样X-Forwarded-For才能正确传递真实客户端 IP。
第三,etcd 备份必须做且要验证恢复。公网集群被攻击或者误操作的概率比内网高,etcd 快照是最后的救命稻草。我写了个定时任务每小时etcdctl snapshot save到对象存储,每周做一次恢复演练到临时集群验证快照可用。只备份不验证等于没备份,这个道理在真出事的时候体会最深。
第四,监控告警要覆盖控制面组件。kube-scheduler、kube-controller-manager、etcd 的指标默认只监听 127.0.0.1,Prometheus 抓不到。要么改监听地址加认证,要么用 kube-state-metrics 加 node-exporter 的组合间接监控。我倾向后者,改动小、风险低。
我个人在实际操作中的体会是,公网 K8S 搭建的难点从来不在“搭起来”,而在“搭起来之后怎么守住”。初始化那半小时的顺利程度,和后面几个月的稳定运行没有必然关系。真正决定成败的是安全组规则有没有收紧、证书续期有没有告警、etcd 备份有没有验证、审计日志有没有人看。这几件事做到位,集群才能安心对外提供服务。最后再分享一个小技巧:每次变更集群配置前,先用kubeadm config view导出当前配置存档,出问题时能快速对比差异,比凭记忆回滚靠谱得多。