三主节点高可用
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.214、192.168.3.215、192.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.215 和 192.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.215 和 192.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 远程关另外两台,可以先关 master2、master3,最后关自己:
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 路由