三主节点高可用

3 台机器全部做 control-plane
3 个 etcd 组成 stacked etcd
HAProxy + Keepalived 提供 API Server 虚拟 IP
containerd 作为容器运行时

规划:

192.168.3.214  k8s-master1
192.168.3.215  k8s-master2
192.168.3.216  k8s-master3
192.168.3.217  k8s-vip

API 入口用:

192.168.3.217:8443

不用 6443 是因为 HAProxy 和 kube-apiserver 都在同一批机器上,避免端口冲突。


快捷部署(脚本版)

如果你想先快速把三主节点 HA 集群跑起来,可以直接用我整理好的脚本。脚本放在仓库的 static/k8s 目录,站点发布后访问路径是 /k8s/脚本名

脚本默认使用这组地址:

192.168.3.214  k8s-master1
192.168.3.215  k8s-master2
192.168.3.216  k8s-master3
192.168.3.217  k8s-vip
Kubernetes: v1.36
API 入口: 192.168.3.217:8443

如果 IP、VIP、Kubernetes 版本不一样,先改 k8s_env.sh,或者执行脚本时通过环境变量覆盖:

MASTER1_IP=192.168.3.214 \
MASTER2_IP=192.168.3.215 \
MASTER3_IP=192.168.3.216 \
VIP_IP=192.168.3.217 \
K8S_VERSION=v1.36 \
bash k8s_prepare.sh

1. 下载脚本

如果你在这份文档仓库里,可以直接进入脚本目录:

cd static/k8s
chmod +x *.sh

如果是在三台 Ubuntu 机器上从网站下载:

mkdir -p ~/k8s-ha
cd ~/k8s-ha

for file in \
  k8s_env.sh \
  k8s_reset.sh \
  k8s_prepare.sh \
  k8s_init_master1.sh \
  k8s_join_control_plane.sh \
  k8s_post_install.sh \
  k8s_check_ha.sh \
  k8s_shutdown_all.sh
do
  curl -fsSLO "https://doc.jihw.top/k8s/${file}"
done

chmod +x *.sh

2. 可选:清理旧集群

如果这三台机器之前已经跑过 kubeadm 集群,三台都执行:

YES=yes bash k8s_reset.sh

这一步会删除本机的 Kubernetes、etcd、kubelet、CNI 和 ~/.kube 配置。新机器可以跳过。

3. 三台机器都执行准备脚本

192.168.3.214192.168.3.215192.168.3.216 三台都执行:

bash k8s_prepare.sh

这个脚本会自动做这些事:

安装基础工具、containerd、kubeadm、kubelet、kubectl
关闭 swap
配置内核模块和 sysctl
写入 hosts
按本机 IP 设置 hostname
配置 HAProxy
配置 Keepalived

如果脚本没有识别出本机 IP,手动指定:

NODE_IP=192.168.3.214 bash k8s_prepare.sh

如果默认网卡不是脚本自动识别的网卡,也可以指定:

IFACE=ens33 bash k8s_prepare.sh

4. 初始化第一个 control-plane

只在 192.168.3.214 上执行:

bash k8s_init_master1.sh

它会执行 kubeadm init,配置当前用户的 kubectl,安装 Flannel,并生成:

~/k8s-control-plane-join.sh

这个 join 脚本用于把另外两台机器加入 control-plane。

5. 加入另外两个 control-plane

192.168.3.214 上生成的 ~/k8s-control-plane-join.sh 拷到 192.168.3.215192.168.3.216,然后分别执行:

bash ~/k8s-control-plane-join.sh

如果不想拷贝脚本,也可以在另外两台机器上用 JOIN_COMMAND 传入完整 join 命令:

JOIN_COMMAND='sudo kubeadm join 192.168.3.217:8443 --token xxxxxx.xxxxxxxxxxxxxxxx --discovery-token-ca-cert-hash sha256:xxxx --control-plane --certificate-key xxxx' \
bash k8s_join_control_plane.sh

6. 部署测试应用并检查 HA

在任意一台 master 上执行:

bash k8s_post_install.sh
bash k8s_check_ha.sh

k8s_post_install.sh 会允许 control-plane 跑业务 Pod,并部署一个 nginx NodePort 服务。
k8s_check_ha.sh 会检查节点、系统 Pod、etcd、VIP 和 API Server 健康状态。

确认没问题后,可以手工关掉当前持有 VIP 的机器,再在其他 master 上执行:

kubectl get nodes
ip addr | grep 192.168.3.217

如果 kubectl get nodes 仍然能返回,并且 VIP 漂移到其他机器,说明 API Server HA 生效。


一、先理解 HA 架构

高可用 K8s 至少要解决三件事:

1. 多个 kube-apiserver
2. 多个 etcd,且保持多数派
3. 一个稳定的 API 入口

你这 3 台机器会变成:

k8s-master1: apiserver + scheduler + controller-manager + etcd + haproxy + keepalived
k8s-master2: apiserver + scheduler + controller-manager + etcd + haproxy + keepalived
k8s-master3: apiserver + scheduler + controller-manager + etcd + haproxy + keepalived

3 个 etcd 可以容忍 宕机 1 台
如果 3 台里宕 2 台,etcd 没有多数派,集群就不可用了。

如果这 3 台虚拟机都在同一台物理机上,那只是“学习 HA”,不是物理层真正 HA。


二、如果昨天已经初始化过集群,先清理

三台都执行。注意:这会清掉现有 K8s 集群配置。

sudo kubeadm reset -f

sudo rm -rf /etc/kubernetes
sudo rm -rf /var/lib/etcd
sudo rm -rf /var/lib/kubelet
sudo rm -rf /etc/cni/net.d
sudo rm -rf /var/lib/cni
sudo rm -rf ~/.kube

sudo systemctl restart containerd
sudo systemctl restart kubelet || true

三、三台机器都执行基础配置

sudo apt update
sudo apt install -y apt-transport-https ca-certificates curl gpg vim haproxy keepalived

关闭 swap:

sudo swapoff -a
sudo sed -i.bak '/ swap / s/^/#/' /etc/fstab

配置内核模块:

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

sudo modprobe overlay
sudo modprobe br_netfilter

配置内核参数:

cat <<EOF | sudo 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
net.ipv4.ip_nonlocal_bind = 1
EOF

sudo sysctl --system

配置 hosts:

cat <<EOF | sudo tee -a /etc/hosts
192.168.3.214 k8s-master1
192.168.3.215 k8s-master2
192.168.3.216 k8s-master3
192.168.3.217 k8s-vip
EOF

分别设置 hostname:

192.168.3.214

sudo hostnamectl set-hostname k8s-master1

192.168.3.215

sudo hostnamectl set-hostname k8s-master2

192.168.3.216

sudo hostnamectl set-hostname k8s-master3

四、三台机器安装 containerd

sudo apt install -y containerd

sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null

sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

sudo systemctl restart containerd
sudo systemctl enable containerd

五、三台机器安装 kubeadm/kubelet/kubectl

这里用 Kubernetes v1.36

sudo mkdir -p -m 755 /etc/apt/keyrings

curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key \
  | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.36/deb/ /' \
  | sudo tee /etc/apt/sources.list.d/kubernetes.list

sudo apt update
sudo apt install -y kubelet kubeadm kubectl 
sudo apt-mark hold kubelet kubeadm kubectl

sudo systemctl enable --now kubelet

配置镜像加速

在 K8s + containerd 里,不要改 Docker 的 /etc/docker/daemon.json,因为节点上不是 Docker 在拉镜像,而是 containerd 在拉。

你有两种方式。

方式一:推荐,给 containerd 配 Docker Hub 镜像加速

这要在 每个 K8s 节点 都执行:

# 创建 containerd 的 registry 配置目录
sudo mkdir -p /etc/containerd/certs.d/docker.io

# 写入 Docker Hub 镜像加速配置
cat <<EOF | sudo tee /etc/containerd/certs.d/docker.io/hosts.toml
server = "https://registry-1.docker.io"

[host."https://docker.m.daocloud.io"]
  capabilities = ["pull", "resolve"]
EOF

然后确认 containerd 启用了 config_path

# 目前测试不能识别多目录,将默认的docker目录去除才有用
sudo sed -i "s|config_path = '/etc/containerd/certs.d:/etc/docker/certs.d'|config_path = '/etc/containerd/certs.d'|" /etc/containerd/config.toml
sudo grep -n "config_path" /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl restart kubelet

如果没有 crictl,可以先装:

sudo apt install -y cri-tools

消除警告

cat <<EOF | sudo tee /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 10
debug: false
EOF

测试拉取:

sudo crictl -D pull nginx:latest
crictl images
#查看镜像日志
sudo journalctl -u containerd -f

六、三台机器配置 HAProxy

三台都写同样配置:

sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.bak

cat <<EOF | sudo tee /etc/haproxy/haproxy.cfg
global
    log /dev/log local0
    log /dev/log local1 notice
    daemon
    maxconn 4096

defaults
    log global
    mode tcp
    option tcplog
    option dontlognull
    timeout connect 5s
    timeout client  50s
    timeout server  50s

frontend kubernetes-apiserver
    bind *:8443
    mode tcp
    default_backend kubernetes-apiserver

backend kubernetes-apiserver
    mode tcp
    balance roundrobin
    option tcp-check
    server k8s-master1 192.168.3.214:6443 check
    server k8s-master2 192.168.3.215:6443 check
    server k8s-master3 192.168.3.216:6443 check
EOF

sudo systemctl restart haproxy
sudo systemctl enable haproxy

七、三台机器配置 Keepalived

先确认网卡名:

ip -o -4 route show to default | awk '{print $5}'

假设输出是:

ens33

如果你的不是 ens33,下面配置里的 ens33 要替换。

192.168.3.214 上执行

cat <<EOF | sudo tee /etc/keepalived/keepalived.conf
vrrp_script chk_haproxy {
    script "killall -0 haproxy"
    interval 2
    weight -20
}

vrrp_instance VI_1 {
    state MASTER
    interface ens33
    virtual_router_id 51
    priority 101
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass k8sha
    }

    unicast_src_ip 192.168.3.214
    unicast_peer {
        192.168.3.215
        192.168.3.216
    }

    virtual_ipaddress {
        192.168.3.217/24
    }

    track_script {
        chk_haproxy
    }
}
EOF

sudo systemctl restart keepalived
sudo systemctl enable keepalived

192.168.3.215 上执行

cat <<EOF | sudo tee /etc/keepalived/keepalived.conf
vrrp_script chk_haproxy {
    script "killall -0 haproxy"
    interval 2
    weight -20
}

vrrp_instance VI_1 {
    state BACKUP
    interface ens33
    virtual_router_id 51
    priority 100
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass k8sha
    }

    unicast_src_ip 192.168.3.215
    unicast_peer {
        192.168.3.214
        192.168.3.216
    }

    virtual_ipaddress {
        192.168.3.217/24
    }

    track_script {
        chk_haproxy
    }
}
EOF

sudo systemctl restart keepalived
sudo systemctl enable keepalived

192.168.3.216 上执行

cat <<EOF | sudo tee /etc/keepalived/keepalived.conf
vrrp_script chk_haproxy {
    script "killall -0 haproxy"
    interval 2
    weight -20
}

vrrp_instance VI_1 {
    state BACKUP
    interface ens33
    virtual_router_id 51
    priority 99
    advert_int 1

    authentication {
        auth_type PASS
        auth_pass k8sha
    }

    unicast_src_ip 192.168.3.216
    unicast_peer {
        192.168.3.214
        192.168.3.215
    }

    virtual_ipaddress {
        192.168.3.217/24
    }

    track_script {
        chk_haproxy
    }
}
EOF

sudo systemctl restart keepalived
sudo systemctl enable keepalived

检查 VIP 是否出现:

ip addr | grep 192.168.3.217

只要三台里有一台显示 192.168.3.217,就说明 VIP 正常。


八、初始化第一个 control-plane

只在 192.168.3.214 执行:

sudo kubeadm init \
  --control-plane-endpoint "192.168.3.217:8443" \
  --upload-certs \
  --apiserver-advertise-address=192.168.3.214 \
  --pod-network-cidr=10.244.0.0/16 \
  --cri-socket=unix:///run/containerd/containerd.sock

初始化成功后,保存输出里的两类 join 命令:

worker join 命令
control-plane join 命令

你需要的是带这个参数的:

--control-plane --certificate-key ...

配置 kubectl: 如果也想在其他上面使用 kubectl,再分别在它们上执行这三行

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

安装 Flannel,只用在一个节点执行:

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

九、加入另外两个 control-plane

192.168.3.215192.168.3.216 上执行 master 初始化输出的命令。

格式类似这样:

sudo kubeadm join 192.168.3.217:8443 \
  --token xxxxxx.xxxxxxxxxxxxxxxx \
  --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
  --control-plane \
  --certificate-key xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
  --cri-socket=unix:///run/containerd/containerd.sock

如果 join 命令丢了,在 192.168.3.214 重新生成:

kubeadm token create --print-join-command

重新生成 certificate key:

sudo kubeadm init phase upload-certs --upload-certs

然后把两者组合起来。


十、检查集群

在任意 master 上执行:

kubectl get nodes -o wide

你应该看到:

k8s-master1   Ready   control-plane
k8s-master2   Ready   control-plane
k8s-master3   Ready   control-plane

查看系统 Pod:

kubectl get pods -A -o wide

查看 etcd:

kubectl get pods -n kube-system -o wide | grep etcd

应该有 3 个 etcd:

etcd-k8s-master1
etcd-k8s-master2
etcd-k8s-master3

十一、允许 control-plane 也跑业务 Pod

因为你这 3 台都是 control-plane,没有普通 worker。学习环境可以允许它们跑业务: 一次性移除所有 Master(Control Plane)节点上的 NoSchedule污点,从而允许普通 Pod 调度到 Master 节点上运行。

kubectl taint nodes --all node-role.kubernetes.io/control-plane-

十二、测试部署 nginx

kubectl create deployment nginx --image=nginx:latest
kubectl expose deployment nginx --port=80 --type=NodePort

kubectl get pods -o wide
kubectl get svc nginx

假设 NodePort 是 31234,就可以访问:

http://192.168.3.214:31234
http://192.168.3.215:31234
http://192.168.3.216:31234

十三、测试高可用

先确认 kubectl 用的是 VIP:

kubectl config view --minify | grep server

应该类似:

server: https://192.168.3.217:8443

然后可以关掉当前持有 VIP 的机器,例如 192.168.3.214

sudo poweroff

等几十秒,在其他机器上执行:

kubectl get nodes

如果还能正常返回,说明 API Server HA 生效。

检查 VIP 是否漂移:

ip addr | grep 192.168.3.217

十四、虚拟机集群的日常关机和启动

如果 K8s 集群部署在家用服务器或台式机里的虚拟机上,为了省电,晚上可以把虚拟机关掉,第二天再启动。关键点是:不要直接强制断电,尽量让每台虚拟机正常关机

对于这套三主节点集群:

192.168.3.214  k8s-master1
192.168.3.215  k8s-master2
192.168.3.216  k8s-master3
192.168.3.217  k8s-vip

如果后面又加入了 worker,例如:

192.168.3.218  k8s-worker1
192.168.3.219  k8s-worker2
192.168.3.220  k8s-worker3

那么日常关机建议按这个顺序:

先关业务入口和 worker
再关 control-plane/master

启动时反过来:

先启动 control-plane/master
等 API Server、etcd、CNI 正常
再启动 worker
最后检查业务 Pod

1. 关机前检查集群状态

在任意一台 master 上执行:

kubectl get nodes -o wide
kubectl get pods -A -o wide

如果安装了 Ingress、Longhorn、GitLab、SonarQube 这类有状态或入口组件,也先看一下它们是否健康:

kubectl get pods -n ingress-nginx -o wide
kubectl get pods -n longhorn-system -o wide
kubectl get pods -n gitlab -o wide
kubectl get pods -n sonarqube -o wide

如果只是学习环境,通常不用把所有 Pod 都手动删掉。Kubernetes 会在节点重新启动后由 kubelet 拉起静态 Pod,并恢复 Deployment、StatefulSet、DaemonSet 管理的工作负载。

如果正在写入数据库、GitLab、Longhorn 卷,最好先停掉外部访问,等写入结束后再关机。不要在备份、迁移、镜像拉取、卷重建时直接断电。

2. 不建议对整套集群逐台 drain

kubectl drain 适合“只维护一台节点,其他节点继续运行”的场景。比如只重启一个 worker,可以这样:

kubectl drain k8s-worker1 --ignore-daemonsets --delete-emptydir-data
sudo reboot
kubectl uncordon k8s-worker1

但是如果晚上要把整套虚拟机集群全部关闭,就不建议把所有节点都 drain 一遍。因为所有节点都会停,Pod 没有地方重新调度,很多 eviction 会卡住,反而增加混乱。

整套集群关机时,核心原则是:

让业务停止写入
让每台虚拟机正常执行 shutdown/poweroff
不要强制断电

3. 关闭 worker 节点

如果有 worker,先在每台 worker 上执行:

sudo shutdown -h now

也可以从 master 上通过 SSH 远程执行:

ssh root@192.168.3.218 "shutdown -h now"
ssh root@192.168.3.219 "shutdown -h now"
ssh root@192.168.3.220 "shutdown -h now"

等待虚拟机状态变成 powered off。

4. 使用一键关闭脚本

如果不想每次手动 SSH 到每台机器,可以使用脚本:

static/k8s/k8s_shutdown_all.sh

如果是在网站上下载:

mkdir -p ~/k8s-ha
cd ~/k8s-ha

curl -fsSLO https://doc.jihw.top/k8s/k8s_env.sh
curl -fsSLO https://doc.jihw.top/k8s/k8s_shutdown_all.sh
chmod +x k8s_env.sh k8s_shutdown_all.sh

先看一遍关机计划,不会真正关机:

bash k8s_shutdown_all.sh --dry-run

确认没问题后,在任意一台 master 上执行:

YES=yes bash k8s_shutdown_all.sh

脚本默认会按这个顺序执行:

1. 关闭 worker: 192.168.3.218、192.168.3.219、192.168.3.220
2. 关闭远端 master
3. 最后关闭当前执行脚本的 master

默认 SSH 用户是 root。如果不是 root,可以指定:

SSH_USER=ubuntu YES=yes bash k8s_shutdown_all.sh

如果 SSH key 不是默认位置,可以指定:

SSH_KEY=~/.ssh/id_rsa YES=yes bash k8s_shutdown_all.sh

如果还没有 worker,或者只想关闭三台 master:

YES=yes bash k8s_shutdown_all.sh --no-workers

如果 worker IP 不是默认这三个,用 WORKER_IPS 覆盖:

WORKER_IPS="192.168.3.221 192.168.3.222" YES=yes bash k8s_shutdown_all.sh

脚本执行前会打印当前节点状态和即将关闭的机器。没有加 YES=yes 时,需要手动输入 SHUTDOWN 才会继续。

5. 手动关闭 master 节点

worker 关闭后,再关闭三台 master:

sudo shutdown -h now

如果从 k8s-master1 远程关另外两台,可以先关 master2master3,最后关自己:

ssh root@192.168.3.215 "shutdown -h now"
ssh root@192.168.3.216 "shutdown -h now"
sudo shutdown -h now

三台 master 都关闭后,etcd 会整体停止。只要是正常关机,第二天全部启动后可以恢复。

6. 第二天启动 master

先启动三台 master 虚拟机:

k8s-master1
k8s-master2
k8s-master3

等系统起来后,在任意 master 上检查基础服务:

systemctl status containerd --no-pager
systemctl status kubelet --no-pager
systemctl status haproxy --no-pager
systemctl status keepalived --no-pager

再检查 VIP:

ip addr | grep 192.168.3.217

如果当前这台没有 VIP,不代表异常。只要三台 master 里有一台持有 192.168.3.217 即可。

检查 API Server:

kubectl get nodes -o wide
kubectl get pods -n kube-system -o wide

刚启动的前几分钟,节点可能短暂显示 NotReady,部分 Pod 可能处于 ContainerCreating。等 CNI、kube-proxy、CoreDNS 都恢复后再继续。

7. 启动 worker

master 正常后,再启动 worker 虚拟机:

k8s-worker1
k8s-worker2
k8s-worker3

然后检查所有节点:

kubectl get nodes -o wide

期望看到所有节点都是 Ready

k8s-master1   Ready
k8s-master2   Ready
k8s-master3   Ready
k8s-worker1   Ready
k8s-worker2   Ready
k8s-worker3   Ready

8. 启动后检查业务

查看所有 Pod:

kubectl get pods -A -o wide

如果有 Pod 一直不是 Running,先看事件:

kubectl get events -A --sort-by=.lastTimestamp | tail -50

再看具体 Pod:

kubectl describe pod -n 命名空间 Pod名
kubectl logs -n 命名空间 Pod名 --tail=100

如果安装了 Longhorn,重点确认卷和副本恢复:

kubectl get pods -n longhorn-system -o wide
kubectl get volumes.longhorn.io -n longhorn-system

如果安装了 ingress-nginx,确认入口 IP 还在:

kubectl get svc -n ingress-nginx

如果业务域名访问不了,先检查 EXTERNAL-IP 是否仍然是原来的地址,比如:

192.168.3.230

9. 常见问题

如果 kubectl get nodes 连接不上,先确认 VIP 和 HAProxy:

ip addr | grep 192.168.3.217
systemctl status haproxy --no-pager
systemctl status keepalived --no-pager

如果节点是 NotReady,看 kubelet 和容器运行时:

systemctl status kubelet --no-pager
systemctl status containerd --no-pager
journalctl -u kubelet -n 100 --no-pager

如果 CoreDNS、应用 Pod 一直起不来,检查 CNI:

kubectl get pods -n kube-flannel -o wide
kubectl get pods -n kube-system -o wide

如果只有某一台 master 没起来,不要急着执行 kubeadm reset。先看服务状态和日志。三节点 etcd 可以容忍一台 master 暂时不可用;但如果三台里只有一台启动,etcd 多数派不足,集群可能暂时不可用。把三台 master 都启动后再判断。


十五、MetalLB、Ingress

先说明三个组件的作用:

MetalLB:
给裸机/家用 K8s 提供 LoadBalancer 能力。云厂商有云负载均衡,家里没有,所以用 MetalLB 从你的局域网里分配一个 IP。

ingress-nginx:
K8s 里的 HTTP/HTTPS 入口控制器。它根据域名、路径,把请求转发到不同 Service。

Ingress:
一条路由规则。例如 nginx.k8s.local/* -> nginx-demo:80。

0. 先确认集群状态

在任意 master 上执行:

kubectl get nodes -o wide
kubectl get pods -A -o wide

如果你是三台 master 都跑业务 Pod,确认 taint 已移除:

kubectl taint nodes --all node-role.kubernetes.io/control-plane- || true

如果有资源,不用急着删,下面的 kubectl apply 可以重复执行。

1. 规划 MetalLB 地址池

你家里 K8s 节点是:

192.168.3.214
192.168.3.215
192.168.3.216

建议预留:

192.168.3.230-192.168.3.240

注意:最好去主路由 DHCP 设置里确认这段不会分配给普通设备,避免 IP 冲突。

2. 安装 MetalLB

执行:

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.16.1/config/manifests/metallb-native.yaml

等待组件启动:

kubectl get pods -n metallb-system -o wide

正常会看到:

controller
speaker

其中:

controller:负责分配 LoadBalancer IP
speaker:负责在局域网里宣告这个 IP,让其他设备能访问

等待 controller 可用:

kubectl wait --namespace metallb-system \
  --for=condition=available deployment/controller \
  --timeout=180s

3. 配置 MetalLB 地址池

创建地址池和二层广播配置: 一定等上面的组件启动成功后才能执行

cat <<'EOF' | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: home-lan-pool
  namespace: metallb-system
spec:
  addresses:
    - 192.168.3.230-192.168.3.240
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: home-lan-l2
  namespace: metallb-system
spec:
  ipAddressPools:
    - home-lan-pool
EOF

解释:

IPAddressPool:
告诉 MetalLB 可以使用哪些 IP。

L2Advertisement:
让 MetalLB 用 ARP/NDP 在局域网里声明这些 IP 属于某个节点。

检查:

kubectl get ipaddresspool -n metallb-system
kubectl get l2advertisement -n metallb-system

4. 先单独测试 MetalLB

创建一个测试 nginx LoadBalancer:

kubectl create deployment lb-test-nginx --image=nginx:latest
kubectl expose deployment lb-test-nginx --port=80 --type=LoadBalancer

查看外部 IP:

kubectl get svc lb-test-nginx -w

集群内部访问 https://192.168.3.230 是通的 已运行 3 条命令 关键点找到了:三台节点都有这个 label:

node.kubernetes.io/exclude-from-external-load-balancers=

你应该看到类似:

EXTERNAL-IP: 192.168.3.230

如果一直是:

<pending>

说明 MetalLB 地址池还没生效。

测试访问: 目前虚拟机方式部署的k8s无法使用该地址访问

curl http://192.168.3.230

如果在 Windows 浏览器里访问也可以:

http://192.168.3.230

成功后可以删除这个临时测试:

kubectl delete deployment lb-test-nginx
kubectl delete svc lb-test-nginx

5. 安装 Helm

后面部署很多 K8s 应用都会用 Helm。可以理解为:

Helm = K8s 的包管理器

安装:

curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

检查:

helm version

6. 安装 ingress-nginx

添加仓库:

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

安装 ingress-nginx,并让它创建 LoadBalancer 类型 Service:

helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.service.type=LoadBalancer \
  --set controller.ingressClassResource.default=true

解释:

controller.service.type=LoadBalancer:
让 MetalLB 给 ingress-nginx 分配一个局域网 IP。

controller.ingressClassResource.default=true:
让默认 IngressClass 使用 nginx。

等待启动:

kubectl wait --namespace ingress-nginx \
  --for=condition=ready pod \
  --selector=app.kubernetes.io/component=controller \
  --timeout=180s

查看入口 IP:

kubectl get svc -n ingress-nginx

你应该看到:

ingress-nginx-controller   LoadBalancer   ...   192.168.3.230/231...

记住这个 EXTERNAL-IP,后面假设它是:

192.168.3.230

你实际以命令输出为准。

7. 创建 nginx Ingress 测试应用

创建 namespace:

kubectl create namespace demo

创建 nginx Deployment:

kubectl create deployment nginx-demo \
  --image=nginx:latest \
  --port=80 \
  -n demo

创建 ClusterIP Service:

kubectl expose deployment nginx-demo \
  --port=80 \
  --target-port=80 \
  -n demo

创建 Ingress:

cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-demo
  namespace: demo
spec:
  ingressClassName: nginx
  rules:
    - host: nginx.k8s.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-demo
                port:
                  number: 80
EOF

解释:

nginx-demo Deployment:
真正运行 nginx 容器。

nginx-demo Service:
给 Pod 提供一个稳定的集群内访问入口。

nginx-demo Ingress:
告诉 ingress-nginx:访问 nginx.k8s.local 时转发到 nginx-demo Service。

8. 测试 Ingress

先查 ingress-nginx 的外部 IP:

kubectl get svc -n ingress-nginx ingress-nginx-controller

假设是:

192.168.3.230

用 curl 测试,不需要改 DNS:

curl -H "Host: nginx.k8s.local" http://192.168.3.230

如果返回 nginx 欢迎页 HTML,说明成功。

如果想浏览器访问,在 Windows 的 hosts 文件里加:

192.168.3.230 nginx.k8s.local

Windows hosts 路径:

C:\Windows\System32\drivers\etc\hosts

然后浏览器打开:

http://nginx.k8s.local

9. 排查命令

如果 EXTERNAL-IP<pending>

kubectl get pods -n metallb-system
kubectl describe svc -n ingress-nginx ingress-nginx-controller
kubectl get ipaddresspool -n metallb-system
kubectl get l2advertisement -n metallb-system

如果 ingress-nginx Pod 没起来:

kubectl get pods -n ingress-nginx -o wide
kubectl describe pod -n ingress-nginx -l app.kubernetes.io/component=controller
kubectl logs -n ingress-nginx -l app.kubernetes.io/component=controller

如果 curl 返回 404:

kubectl get ingress -n demo
kubectl describe ingress nginx-demo -n demo

通常是 Host 不匹配,确认你用了:

curl -H "Host: nginx.k8s.local" http://入口IP

如果返回 502/503:

kubectl get pods -n demo -o wide
kubectl get svc -n demo
kubectl get endpoints -n demo

通常是 Service 没选中 Pod。

10. 成功后的状态应该是

kubectl get svc -n ingress-nginx

看到:

ingress-nginx-controller   LoadBalancer   192.168.3.xxx
kubectl get ingress -n demo

看到:

nginx-demo   nginx   nginx.k8s.local
curl -H "Host: nginx.k8s.local" http://192.168.3.xxx

返回 nginx 页面。

到这里,你就完成了:

MetalLB + ingress-nginx + Ingress 路由