目录
- [微服务是什么 & 为什么需要 Service](#微服务是什么 & 为什么需要 Service)
- [Service 的四种类型总览](#Service 的四种类型总览)
- 前置环境说明
- [实验一:理解标签与 Service 中的 Endpoints](#实验一:理解标签与 Service 中的 Endpoints)
- [实验二:用 Service 暴露 Deployment 控制器](#实验二:用 Service 暴露 Deployment 控制器)
- [kube-proxy:iptables vs ipvs 模式](#kube-proxy:iptables vs ipvs 模式)
- [实验三:ClusterIP(含 Headless 无头服务)](#实验三:ClusterIP(含 Headless 无头服务))
- 实验四:NodePort(含端口范围破限)
- [实验五:LoadBalancer + MetalLB 裸金属环境](#实验五:LoadBalancer + MetalLB 裸金属环境)
- ExternalName:给集群内访问打个外部域名别名
- [实验六:Ingress 七层代理(部署 + 路径 + 域名 + TLS + 认证 + 重写)](#实验六:Ingress 七层代理(部署 + 路径 + 域名 + TLS + 认证 + 重写))
- 实验七:金丝雀(灰度)发布
一、微服务是什么 & 为什么需要 Service
上一节我们学了控制器(Deployment / ReplicaSet ...),它能保证有 N 个 Pod 在跑。但是 Pod 有一个天生的问题:IP 不固定。一个 Pod 被删除重建,IP 就会变;Deployment 扩缩容,新 Pod 又是新 IP。客户端如果直接连 Pod IP,服务端每次重建就要改客户端配置,根本没法用。
Service(微服务 / 服务)就是来解决这个问题的:
- 它是一组提供相同服务的 Pod 对外暴露的统一入口;
- 背后通过**标签选择器(label selector)**自动把符合条件的 Pod IP:Port 收集到一份 Endpoints 列表里;
- 客户端只需要记 Service 的名字(DNS)或者 VIP(ClusterIP),不用关心后端 Pod 怎么变;
- 默认只做 四层(TCP/UDP)负载均衡,要 HTTP 域名 / URL 路径分发要靠 Ingress(下一节)。

一句话总结:
- Service 通过
selector选中一批 Pod,把它们的IP:Port自动写进同名的Endpoints对象; - 访问方只需要记 Service 的名字(DNS)或 ClusterIP,Pod 重建 / 扩缩容后 Endpoints 自动更新;
- Service 默认只做四层(TCP/UDP)负载均衡,七层(HTTP 域名 / 路径)能力要交给 Ingress。
二、Service 的四种类型总览
spec.type 决定 Service 的暴露方式,一共有 4 种外加 1 个特殊变体。

| 类型 | 作用 | 典型访问方式 |
|---|---|---|
| ClusterIP(默认) | 集群内分配一个虚拟 IP(VIP),只能在集群内部访问;提供健康检测与自动发现 | ClusterIP:Port 或 服务名.命名空间.svc.cluster.local |
| NodePort | 在 ClusterIP 基础上,在每个节点的同一个端口上开洞,外部通过任意 NodeIP:nodePort 访问 |
NodeIP:30000-32767 |
| LoadBalancer | 在 NodePort 基础上,由云厂商(或裸机 MetalLB)分配一个外部 VIP 并做负载均衡 | 云厂商给的 EXTERNAL-IP |
| ExternalName | 不给 ClusterIP,而是用 DNS CNAME 把集群内请求转发到指定外部域名 | 服务名 → 外部域名 |
以上四种都是四层;七层(基于域名 / URL 路径 / TLS / 认证)由 Ingress 补齐(见第十一节)。
三、前置环境说明
为了方便对照命令,本文所有示例使用:
- 集群节点:
k8s-master(172.25.254.100)、k8s-node1、k8s-node2,ceph节点; - Pod 网段:
10.244.0.0/16(flannel); - Service 网段:
10.96.0.0/12(默认); - 测试镜像:
myapp:v1(Hello MyApp | Version: v1)、myapp:v2(Hello MyApp | Version: v2)、busyboxplus; - 私有镜像仓库:
reg.timinglee.org(本系列实验统一使用)。
如果你的环境网段 / 仓库域名不同,把命令里的 IP 与镜像名换掉即可,整体流程不变。
四、实验一:理解标签与 Service 中的 Endpoints
目标 :亲眼看到 Service 是怎么靠标签认亲的------标签对不上 Endpoints 是空的,打上正确标签 Endpoints 立刻出现。
1. 创建一个 Service
bash
# kubectl create service clusterip --tcp 80:80 <name> 自动生成 yaml 模板
[root@k8s-master services]# kubectl create service clusterip --tcp 80:80 testservice \
--dry-run=client -o yaml > testservice.yaml
[root@k8s-master services]# vim testservice.yaml
生成的 yaml 默认 selector: { app: webserver }(这是模板自带的,先不改它,留着当反面教材):
yaml
apiVersion: v1
kind: Service
metadata:
labels: { app: testservice }
name: testservice
spec:
ports:
- name: webport
port: 80
protocol: TCP
targetPort: 80
selector: { app: webserver } # ← 注意:选择器是 app=webserver
type: ClusterIP
应用:
bash
[root@k8s-master services]# kubectl apply -f testservice.yaml
service/testservice created
2. 起一个"标签对不上"的 Pod
bash
[root@k8s-master services]# kubectl run webserver --image myapp:v1 \
--dry-run=client -o yaml > webserver.yml
[root@k8s-master services]# cat webserver.yml
yaml
kind: Pod
metadata:
labels: { run: webserver } # ← 注意:Pod 的标签是 run=webserver
name: webserver
spec:
containers:
- image: myapp:v1
name: webserver
bash
[root@k8s-master services]# kubectl apply -f webserver.yml
pod/webserver created
3. 监控:Endpoints 是空的
bash
[root@k8s-master ~]# watch -n 1 \
"kubectl describe svc testservice ; kubectl get pods --show-labels"
text
Name: testservice
Namespace: default
Selector: app=webserver
Type: ClusterIP
IP: 10.100.62.227
Port: webport 80/TCP
TargetPort: 80/TCP
Endpoints: <none> # ← 关键:没有后端
Session Affinity: None
NAME READY STATUS RESTARTS AGE LABELS
webserver 1/1 Running 0 77s run=webserver
Endpoints: <none> 说明:Pod 在跑,但 Service 选不中它 ------因为 Service 要 app=webserver,Pod 给的是 run=webserver。
4. 给 Pod 补上正确标签
bash
[root@k8s-master services]# kubectl label pods webserver app=webserver
pod/webserver labeled
再回到监控窗口,立刻看到:
text
Endpoints: 10.244.2.7:80 # ← 自动加进来了
NAME READY STATUS RESTARTS AGE LABELS
webserver 1/1 Running 0 3m10s app=webserver,run=webserver
直接 curl Service 的 VIP 也通了:
bash
[root@k8s-master services]# curl 10.100.62.227
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>

经验法则 :Service 的
selector与 Pod 的label是匹配关系,少一个字段都算不匹配 。所以生产中约定一个统一的app=xxx标签,Service 就盯这一个标签去找 Pod 即可。
五、实验二:用 Service 暴露 Deployment 控制器
上一个实验只暴露了单个 Pod。生产中没人会这么干,因为 Pod 挂了就没了。我们要的是:Deployment 维护 N 个 Pod 副本,Service 自动把所有 Pod 加到 Endpoints,副本被删重建后 Service 自动跟上。
1. 一键生成 Deployment + Service 组合 yaml
bash
[root@k8s-master services]# kubectl create service clusterip --tcp 80:80 webservice \
--dry-run=client -o yaml >> webservice.yaml
[root@k8s-master services]# kubectl create deployment webcluster --image myapp:v1 \
--dry-run=client -o yaml >> webservice.yaml
[root@k8s-master services]# vim webservice.yaml
把 selector: app=webcluster 与 Deployment labels: app=webcluster 对齐,让 Service 选得到 Pod:
yaml
---
apiVersion: v1
kind: Service
metadata:
labels: { app: webservice }
name: webservice
spec:
ports:
- name: webport
port: 80
protocol: TCP
targetPort: 80
selector: { app: webcluster } # ← 对齐 Deployment
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels: { app: webcluster }
name: webcluster
spec:
replicas: 2
selector:
matchLabels: { app: webcluster }
template:
metadata:
labels: { app: webcluster }
spec:
containers:
- image: myapp:v1
name: myapp
2. 应用 & 验证
bash
[root@k8s-master services]# kubectl apply -f webservice.yaml
service/webservice created
deployment.apps/webcluster created
bash
# 持续观察(另一终端跑)
[root@k8s-master ~]# watch -n 1 \
"kubectl describe svc webservice ; \
kubectl get deploy --show-labels; \
kubectl get pods -o wide --show-labels"
text
Name: webservice
Selector: app=webcluster
Type: ClusterIP
IP: 10.98.31.55
Port: webport 80/TCP
TargetPort: 80/TCP
Endpoints: 10.244.2.8:80,10.244.5.95:80 # ← 2 个 Pod 都进来了
NAME READY UP-TO-DATE AVAILABLE AGE LABELS
webcluster 2/2 2 2 3m41s app=webcluster
NAME READY STATUS IP NODE
webcluster-77c87d9946-pz5nq 1/1 Running 10.244.5.95 k8s-node2
webcluster-77c87d9946-tlzxj 1/1 Running 10.244.2.8 ceph
3. 访问验证(负载均衡)
myapp 镜像有个 /hostname.html 接口,会返回当前 Pod 的主机名。多 curl 几次,看到两个 Pod 轮询被命中:
bash
[root@k8s-master services]# curl 10.98.31.55/hostname.html
webcluster-77c87d9946-tlzxj
[root@k8s-master services]# curl 10.98.31.55/hostname.html
webcluster-77c87d9946-pz5nq
[root@k8s-master services]# curl 10.98.31.55/hostname.html
webcluster-77c87d9946-pz5nq
六、kube-proxy:iptables vs ipvs 模式
Service 本身只是一段配置 ,真正负责"把请求转发到后端 Pod"的是每个节点上的 kube-proxy 组件。kube-proxy 一直在监听 apiserver,发现 Service / Endpoints 变化就刷新本机的转发规则。它有两种实现模式:

| 模式 | 实现方式 | 性能 | 适用场景 |
|---|---|---|---|
| iptables(老版本默认) | 在宿主机上写大量 iptables 规则(KUBE-SERVICES 链),顺序匹配 | 规则多时 O(n),CPU 消耗高 | 小规模集群(< 1000 Service) |
| ipvs(推荐) | 内核态哈希表 + 虚拟网卡 kube-ipvs0 承载所有 Service IP,O(1) 查找 |
万级 Pod 也能撑住 | 生产环境必选 |
切换 ipvs 的命令很简单,但改完配置必须重建 kube-proxy Pod 才会生效(它是 DaemonSet,会自动重建一个干净的):
bash
# 所有节点装工具,用于查看 ipvs 规则
[root@k8s-master ~]# for name in 100 10 20 200; do
> ssh -l root 172.25.254.$name "dnf install ipvsadm -y"
> done
# 改 ConfigMap
[root@k8s-master ~]# kubectl -n kube-system edit cm kube-proxy
# 把 mode: "" 改成 mode: "ipvs"
[root@k8s-master ~]# kubectl -n kube-system get pods | awk '/kube-proxy/{system("kubectl -n kube-system delete pods "$1)}'
切换成功后,每台节点都会多出一张虚拟网卡 kube-ipvs0,上面挂着所有 Service 的 VIP:
bash
[root@k8s-master ~]# ip a | tail
8: kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether fe:9f:c8:5d:a6:c8 brd ff:ff:ff:ff:ff:ff
inet 10.96.0.10/32 scope global kube-ipvs0
inet 10.96.0.1/32 scope global kube-ipvs0
inet 10.97.59.25/32 scope global kube-ipvs0 # ← 我们的 Service VIP
用 ipvsadm -Ln 能看到所有 Service 的转发规则和调度算法:
bash
[root@k8s-master ~]# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.96.0.1:443 rr
-> 172.25.254.100:6443 Masq 1 0 0
TCP 10.96.0.10:53 rr # kube-dns
-> 10.244.1.2:53 Masq 1 0 0
-> 10.244.1.3:53 Masq 1 0 0
TCP 10.98.31.55:80 rr # 我们的 webservice
-> 10.244.2.8:80 Masq 1 0 0
-> 10.244.5.95:80 Masq 1 0 0
ipvs 常用的调度算法:
| 缩写 | 全称 | 用途 |
|---|---|---|
rr |
Round Robin | 轮询(默认) |
lc |
Least Connection | 最少连接 |
dh |
Destination Hashing | 目标地址哈希(搭配 Ingress 持久化) |
sh |
Source Hashing | 源地址哈希(会话保持) |
sed |
Shortest Expected Delay | 最短期望延迟 |
nq |
Never Queue | 永不排队 |
七、实验三:ClusterIP(含 Headless 无头服务)
1. 普通 ClusterIP
最常见的用法,前面实验二其实就是 ClusterIP。再次确认一下基础 yaml:
yaml
apiVersion: v1
kind: Service
metadata:
name: webservice
spec:
selector: { app: webcluster }
ports:
- port: 80
protocol: TCP
targetPort: 80
type: ClusterIP # 默认就是 ClusterIP,不写也行
bash
[root@k8s-master services]# kubectl apply -f webservice.yaml
[root@k8s-master services]# kubectl describe svc webservice
text
Name: webservice
Type: ClusterIP
IP: 10.109.112.7 # ← vip,访问这个就能被调度到后端 Pod
Endpoints: 10.244.5.98:80,10.244.2.14:80
2. 集群内部用 DNS 解析
K8s 集群内置 coredns(Service 名是 kube-dns)做服务发现。ServiceName.<namespace>.svc.cluster.local 就是完整的 FQDN 域名:
bash
[root@k8s-master services]# kubectl -n kube-system get svc
NAME TYPE CLUSTER-IP PORT(S)
kube-dns ClusterIP 10.96.0.10 53/UDP,53/TCP,9153/TCP
bash
# dig <servicename>.<namespace>.<sourcetype>.cluster.local @<coredns>
[root@k8s-master services]# dig webservice.default.svc.cluster.local @10.96.0.10
text
;; ANSWER SECTION:
webservice.default.svc.cluster.local. 30 IN A 10.109.112.7
;; Query time: 1 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
经验 :集群内部通信用服务名而不是 IP,因为 IP 会变,名字不会。这也是微服务架构"服务发现"的基础。
3. Headless(无头)服务
Headless Service 是一种特殊的 ClusterIP:设置 clusterIP: None 之后:
- Service 不分配 VIP;
kube-proxy不处理它(也就没有负载均衡);- DNS 解析直接返回所有后端 Pod 的真实 IP(多条 A 记录),由客户端自己挑一个连。
适用场景:有状态服务 (MySQL 主从、Redis Cluster、Elasticsearch 等)需要客户端直连指定实例时,Headless 配合 StatefulSet 是标准方案。
yaml
apiVersion: v1
kind: Service
metadata:
name: webservice
spec:
selector: { app: webcluster }
ports:
- port: 80
protocol: TCP
targetPort: 80
type: ClusterIP
clusterIP: None # ← 关键:没有 VIP
bash
[root@k8s-master services]# kubectl apply -f webservice.yaml
[root@k8s-master services]# kubectl describe svc webservice
text
Name: webservice
Type: ClusterIP
IP: None # ← 没有 vip
IPs: None
Port: webport 80/TCP
Endpoints: 10.244.2.15:80,10.244.5.99:80
bash
[root@k8s-master services]# dig webservice.default.svc.cluster.local @10.96.0.10
text
;; ANSWER SECTION:
webservice.default.svc.cluster.local. 30 IN A 10.244.5.99
webservice.default.svc.cluster.local. 30 IN A 10.244.2.15
直接得到两个 Pod IP,没有负载均衡,客户端自己选。

4. 在 Pod 里验证 DNS 解析
bash
[root@k8s-master ~]# kubectl run test --image busyboxplus -it
/ # nslookup webservice
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: webservice
Address 1: 10.244.5.99
Address 2: 10.244.2.15
/ # curl webservice
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>
八、实验四:NodePort(含端口范围破限)
ClusterIP 只能在集群内部访问。如果想让集群外的用户 (开发、测试、对端服务)也能访问,最简单的办法是 NodePort ------在每台节点上同一个端口开洞,外部通过 任意节点IP:nodePort 都能访问到。
1. 创建 NodePort
yaml
apiVersion: v1
kind: Service
metadata:
name: webservice
spec:
ports:
- name: webport
port: 80
protocol: TCP
targetPort: 80
nodePort: 30951 # ← 手动指定;不写就在 30000-32767 随机一个
selector: { app: webcluster }
type: NodePort
bash
[root@k8s-master ~]# kubectl apply -f nodeport.yaml
2. 验证转发规则
bash
[root@k8s-master services]# ipvsadm -Ln
TCP 172.17.0.1:30951 rr # docker 网桥 IP + nodePort
-> 10.244.2.16:80 Masq 1 0 0
-> 10.244.5.100:80 Masq 1 0 0
TCP 172.25.254.100:30951 rr # 节点真实网卡 IP + nodePort
-> 10.244.2.16:80 Masq 1 0 0
-> 10.244.5.100:80 Masq 1 0 0
3. 外部访问
bash
# 在集群外任意一台机器(这里是被机主机的 Windows 终端)
[Administrator.DESKTOP-VJ307M3] ➤ curl 172.25.254.100:30951/hostname.html
webcluster-77c87d9946-ks6cd
[Administrator.DESKTOP-VJ307M3] ➤ curl 172.25.254.100:30951/hostname.html
webcluster-77c87d9946-rn6rh
即便请求打到的是
k8s-master节点,流量也会自动转发到真正运行 Pod 的节点(这里是 k8s-node2 / ceph),不需要 Pod 在哪台机器上就让用户连哪台。
4. 端口破限:30000-32767 不够用怎么办
yaml
- name: webport
port: 80
targetPort: 80
nodePort: 44444 # ← 超出范围,会报错
bash
[root@k8s-master services]# kubectl apply -f nodeport.yaml
The Service "webservice" is invalid:
spec.ports[0].nodePort: Invalid value: 44444:
provided port is not in the valid range. The range of valid ports is 30000-32767
破限方法:修改 apiserver 的启动参数(apiserver 是 static pod,改完会自动重建):
bash
[root@k8s-master services]# vim /etc/kubernetes/manifests/kube-apiserver.yaml
42 - --service-node-port-range=30000-50000
保存即生效(不需要手动重启 apiserver)。等 apiserver 重新 Ready 之后再 apply:
bash
[root@k8s-master services]# kubectl apply -f nodeport.yaml
service/webservice created
[root@k8s-master services]# kubectl describe svc webservice
NodePort: webport 44444/TCP

九、实验五:LoadBalancer + MetalLB 裸金属环境
NodePort 已经能让"外部访问"了,但每次让用户记"哪台节点 IP + 端口"很不优雅。LoadBalancer 的目标是给你一个单一稳定的外部 VIP,背后是云厂商的负载均衡器(AWS ALB / 阿里 SLB 等)。
1. 裸金属环境的痛点
把 type 改成 LoadBalancer 试试:
yaml
spec:
type: LoadBalancer
bash
[root@k8s-master services]# kubectl apply -f loadbalancer.yml
[root@k8s-master services]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 4d5h
webservice LoadBalancer 10.111.5.103 <pending> 80:31426/TCP 12s
# ^
# EXTERNAL-IP 一直是 pending
因为我们不是云厂商,没有 LB 资源 可分配。裸金属环境要自己解决 LB 问题,靠 MetalLB。
2. MetalLB 是什么
MetalLB 是为裸金属 / 自建机房 K8s 集群实现 LoadBalancer 类型的开源项目。它能做两件事:
- 从地址池分配 VIP (
IPAddressPool,支持二层 ARP / 三层 BGP 两种模式); - 用 ARP/NDP 让 VIP 在局域网内可达(speaker 是 DaemonSet,每个节点一个)。

3. 部署 MetalLB
Step 1:所有节点装 ipvsadm,并让 kube-proxy 支持 strictARP
bash
# 所有节点装 ipvsadm
[root@k8s-master ~]# for name in 100 10 20 200; do
> ssh -l root 172.25.254.$name "dnf install ipvsadm -y"
> done
bash
[root@k8s-master mateLLB]# kubectl edit configmap -n kube-system kube-proxy
yaml
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
strictARP: true # ← 必须开,speaker 才能用 ARP 宣告 VIP
bash
# 删掉所有 kube-proxy Pod 让它们重新加载配置
[root@k8s-master mateLLB]# kubectl -n kube-system delete pods kube-proxy-xxxx
Step 2:上传镜像到私有仓库
bash
# 离线环境:先 load 镜像包
[root@k8s-master metaLLB]# docker load -i metallb-v0.16.1.tar
[root@k8s-master metaLLB]# docker tag quay.io/metallb/controller:v0.16.1 reg.timinglee.org/metallb/controller:v0.16.1
[root@k8s-master metaLLB]# docker tag quay.io/metallb/speaker:v0.16.1 reg.timinglee.org/metallb/speaker:v0.16.1
[root@k8s-master metaLLB]# docker push reg.timinglee.org/metallb/controller:v0.16.1
[root@k8s-master metaLLB]# docker push reg.timinglee.org/metallb/speaker:v0.16.1
Step 3:改 deploy.yaml 镜像地址 + 应用
bash
# 把 controller / speaker 镜像地址改成自己仓库
[root@k8s-master metaLLB]# vim 1-metallb-native.yaml
2136: image: reg.timinglee.org/metallb/controller:v0.16.1
2233: image: reg.timinglee.org/metallb/speaker:v0.16.1
[root@k8s-master metaLLB]# kubectl apply -f 1-metallb-native.yaml
Step 4:配置地址池 + L2 通告
bash
[root@k8s-master metaLLB]# vim 2-metallb-confmap.yml
yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 172.25.254.50-172.25.254.99 # ← 改成你本机空闲网段
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-system
spec:
ipAddressPools:
- first-pool
bash
[root@k8s-master metaLLB]# kubectl apply -f 2-metallb-confmap.yml
4. 验证
bash
[root@k8s-master metaLLB]# kubectl -n metallb-system get pods
NAME READY STATUS RESTARTS AGE
controller-b7cdf5787-2hf62 1/1 Running 0 17m
speaker-4spld 1/1 Running 0 17m
speaker-h84jr 1/1 Running 0 17m
speaker-tsqns 1/1 Running 0 17m
speaker-xnb7g 1/1 Running 0 17m
重新创建一次 Service 让它从 MetalLB 拿 VIP:
bash
[root@k8s-master services]# kubectl delete -f loadbalancer.yml
[root@k8s-master services]# kubectl apply -f loadbalancer.yml
[root@k8s-master services]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP
webservice LoadBalancer 10.111.5.103 172.25.254.50 80:31426/TCP
# ^^^^^^^^^^^
# 拿到 MetalLB 分配的 VIP 了
集群外直接访问这个 VIP:
bash
[Administrator.DESKTOP-VJ307M3] ➤ curl 172.25.254.50
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>
⚠️ 地址池网段必须和宿主机网段一致 ,且这段 IP 不能再被其他机器占用,否则 ARP 会冲突。L2 模式简单但单点(一个 speaker 节点负责响应 ARP),需要高可用就上 BGP 模式。
十、ExternalName:给集群内访问打个外部域名别名
ExternalName 是一种特殊的 Service:
- 不分配 ClusterIP;
kube-proxy不处理它;- 集群内访问这个 Service 名时,CoreDNS 把它解析成 CNAME 记录 ,指向
spec.externalName指定的外部域名。
yaml
apiVersion: v1
kind: Service
metadata:
name: externalname-demo
spec:
selector: # 没有 selector,因为外部没有 Pod
app: externalname
type: ExternalName
externalName: www.timinglee.org
bash
[root@k8s-master ~]# kubectl apply -f extrnalname.yaml
[root@k8s-master services]# kubectl get svc externalname-demo
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
externalname-demo ExternalName <none> www.timinglee.org <none> 5s
典型用途:
- 业务迁移过渡 :外部老系统要被迁进 K8s,但短期内还需要让老调用方继续用原来的域名。先把外部域名映射成 Service 名,等 Pod 都进 K8s 了再切换到 ClusterIP,业务代码不用改。
- 跨集群访问:把外部服务的真实域名封装成一个 K8s 内部 Service 名,配置统一。
十一、实验六:Ingress 七层代理(部署 + 路径 + 域名 + TLS + 认证 + 重写)
Service + ClusterIP / NodePort / LoadBalancer 都是 四层(TCP/UDP)代理。如果想要:
- 一个 VIP 背后根据域名分发给不同业务;
- 根据 URL 路径 分发(
/v1给 A,/v2给 B); - HTTPS 终止(TLS 证书卸载);
- Basic Auth 用户认证;
- URL 重写 / 灰度发布;
就得靠 Ingress。Ingress 由两部分组成:
- Ingress Controller :实际干活的代理(nginx / envoy / traefik),本文用官方
ingress-nginx; - Ingress 资源对象 :你写的 yaml 规则(
host + path → backend service)。

1. 部署 ingress-nginx
Step 1:拉镜像 / 离线加载镜像
bash
[root@k8s-master ingress-v1.5.1]# docker load -i ingress-v1.15.1.tar
[root@k8s-master ingress-v1.5.1]# docker tag \
registry.k8s.io/ingress-nginx/controller:v1.15.1 \
reg.timinglee.org/ingress-nginx/controller:v1.15.1
[root@k8s-master ingress-v1.5.1]# docker tag \
registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.6.9 \
reg.timinglee.org/ingress-nginx/kube-webhook-certgen:v1.6.9
[root@k8s-master ingress-v1.5.1]# docker push reg.timinglee.org/ingress-nginx/controller:v1.15.1
[root@k8s-master ingress-v1.5.1]# docker push reg.timinglee.org/ingress-nginx/kube-webhook-certgen:v1.6.9
Step 2:下载 deploy.yaml 并改镜像
bash
wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.15.1/deploy/static/provider/baremetal/deploy.yaml
bash
[root@k8s-master ingress-v1.5.1]# vim deploy.yaml
# 改三处镜像地址为自己的私有仓库
444: image: reg.timinglee.org/ingress-nginx/controller:v1.15.1
547: image: reg.timinglee.org/ingress-nginx/kube-webhook-certgen:v1.6.9
603: image: reg.timinglee.org/ingress-nginx/kube-webhook-certgen:v1.6.9
# 把 service type 改成 LoadBalancer(裸金属环境走 MetalLB 拿 VIP)
365: type: LoadBalancer
Step 3:部署并验证
bash
[root@k8s-master ingress-v1.5.1]# kubectl apply -f deploy.yaml
[root@k8s-master ingress-v1.5.1]# kubectl -n ingress-nginx get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
ingress-nginx-controller LoadBalancer 10.103.86.192 172.25.254.50 80:42476/TCP,443:31023/TCP
ingress-nginx-controller-admission ClusterIP 10.103.198.161 <none> 443/TCP
NAME READY UP-TO-DATE AVAILABLE
deployment.apps/ingress-nginx-controller 1/1 1 1
EXTERNAL-IP: 172.25.254.50正是上一节 MetalLB 分配的地址------ingress-nginx-controller自己也声明了一个 LoadBalancer 类型 Service,从 MetalLB 拿到了 IP。
2. 基于路径的访问
业务准备:建两个版本的 Deployment + Service。
bash
# 业务 1:myapp:v1
[root@k8s-master services]# kubectl create deployment myappv1 --image myapp:v1 \
--replicas 2 --dry-run=client -o yaml > myappv1.yaml
[root@k8s-master services]# kubectl create service clusterip servicev1 --tcp 80:80 \
--dry-run=client -o yaml >> myappv1.yaml
# vim 把 spec.selector 改成 app: myappv1
[root@k8s-master services]# kubectl apply -f myappv1.yaml
# 业务 2:myapp:v2(用上面 yaml 当模板改一份即可)
# 关键差异:镜像 myapp:v2、label app=myappv2、service 名 servicev2
bash
[root@k8s-master services]# curl 10.97.35.20
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>
[root@k8s-master services]# curl 10.100.116.247
Hello MyApp | Version: v2 | <a href="hostname.html">Pod Name</a>
Ingress 规则:
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress1
annotations:
nginx.ingress.kubernetes.io/rewrite-target: / # 把 /v1、/v1/xxx 都重写为 / 再转发
spec:
ingressClassName: nginx
rules:
- host: www.timinglee.org
http:
paths:
- backend:
service: { name: servicev1, port: { number: 80 } }
path: /v1
pathType: Prefix # 前缀匹配
- backend:
service: { name: servicev2, port: { number: 80 } }
path: /v2
pathType: Prefix
bash
[root@k8s-master services]# kubectl apply -f 1-ingress.yml
测试:
bash
# /etc/hosts 加解析
[Administrator.DESKTOP-VJ307M3] ➤ vim /etc/hosts
172.25.254.50 www.timinglee.org
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/v1
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/v2
Hello MyApp | Version: v2 | <a href="hostname.html">Pod Name</a>
# rewrite-target 让 /v2/aaaa 也仍然能命中 servicev2
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/v2/aaaa
Hello MyApp | Version: v2 | <a href="hostname.html">Pod Name</a>
pathType 的三种取值(决定 URL 怎么匹配):
| 值 | 含义 |
|---|---|
Exact |
完全匹配,多一个字符都不行 |
Prefix |
前缀匹配,/v1 能匹配 /v1、/v1/abc(按 / 分段,不会 匹配 /v1abc) |
ImplementationSpecific |
由 Controller 自己决定;配 nginx.ingress.kubernetes.io/use-regex: "true" 可写正则 |
3. 基于域名的访问
一个 VIP 想给多个域名用,那就一个 host 一条 rule:
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress1
spec:
ingressClassName: nginx
rules:
- host: myapp1.timinglee.org
http:
paths:
- backend:
service: { name: servicev1, port: { number: 80 } }
path: /
pathType: Prefix
- host: myapp2.timinglee.org
http:
paths:
- backend:
service: { name: servicev2, port: { number: 80 } }
path: /
pathType: Prefix
bash
# /etc/hosts 把这两个域名都指向 VIP
172.25.254.50 www.timinglee.org myapp1.timinglee.org myapp2.timinglee.org
[Administrator.DESKTOP-VJ307M3] ➤ curl myapp1.timinglee.org
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>
[Administrator.DESKTOP-VJ307M3] ➤ curl myapp2.timinglee.org
Hello MyApp | Version: v2 | <a href="hostname.html">Pod Name</a>
4. 动静分离 / 正则路径
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress1
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /index.html
spec:
ingressClassName: nginx
rules:
- host: www.timinglee.org
http:
paths:
- backend:
service: { name: servicev1, port: { number: 80 } }
path: /php
pathType: ImplementationSpecific
- backend:
service: { name: servicev2, port: { number: 80 } }
path: /static
pathType: ImplementationSpecific
bash
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/php/
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/static/
Hello MyApp | Version: v2 | <a href="hostname.html">Pod Name</a>
5. TLS 加密访问
Step 1:生成自签证书
bash
[root@k8s-master ~]# openssl req -newkey rsa:2048 -nodes -keyout tls.key \
-x509 -days 365 -subj "/CN=nginxsvc/O=nginxsvc" -out tls.crt
Step 2:把证书塞进 K8s Secret
bash
[root@k8s-master ~]# kubectl create secret tls web-tls-secret --key tls.key --cert tls.crt
secret/web-tls-secret created
Step 3:在 Ingress 里引用
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress1
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true" # 访问 HTTP 强制跳 HTTPS
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
tls:
- hosts:
- myapp-tls.timinglee.org
secretName: web-tls-secret
ingressClassName: nginx
rules:
- host: myapp-tls.timinglee.org
http:
paths:
- backend:
service: { name: servicev1, port: { number: 80 } }
path: /
pathType: Prefix
bash
[Administrator.DESKTOP-VJ307M3] ➤ vim /etc/hosts
172.25.254.50 ... myapp-tls.timinglee.org
[Administrator.DESKTOP-VJ307M3] ➤ curl -k https://myapp-tls.timinglee.org
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>
生产中证书用 Let's Encrypt / cert-manager 自动签发,不要用自签。
Secret存的是 K8s 内部一种"配置注入"机制,并不是加密手段。
6. Basic Auth 用户认证
Step 1:生成认证文件
bash
[root@k8s-master ~]# dnf install httpd-tools -y
[root@k8s-master ~]# htpasswd -cm auth admin
# 输入两次密码
[root@k8s-master ~]# cat auth
admin:$apr1$k8PiN9hR$TWLIGse6Y9oGgGFZr96P00
Step 2:注入为 K8s Secret
bash
[root@k8s-master ~]# kubectl create secret generic web-auth --from-file auth
Step 3:Ingress 加认证注解
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress1
annotations:
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: web-auth
nginx.ingress.kubernetes.io/auth-realm: "Please input username and password"
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: www.timinglee.org
http:
paths:
- backend:
service: { name: servicev1, port: { number: 80 } }
path: /v1
pathType: Prefix
验证:
bash
# 不带用户名密码 → 401
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/v1
<html><head><title>401 Authorization Required</title></head>
<body><center><h1>401 Authorization Required</h1></center></body></html>
# 带用户名密码 → 正常返回
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org/v1 -uadmin:lee
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>
7. URL 重写与默认文件
场景 A:访问 / 直接跳到某个具体文件
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress1
annotations:
nginx.ingress.kubernetes.io/app-root: /hostname.html # 默认入口
spec:
ingressClassName: nginx
rules:
- host: www.timinglee.org
http:
paths:
- backend:
service: { name: servicev1, port: { number: 80 } }
path: /
pathType: Prefix
bash
[root@k8s-master services]# curl -I www.timinglee.org
HTTP/1.1 302 Moved Temporarily
Location: http://www.timinglee.org/hostname.html
[root@k8s-master services]# curl -L www.timinglee.org
webcluster-77c87d9946-xxxx # 跟到 hostname.html 去了
场景 B:正则重写,路径变着花样都能匹配
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress1
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2 # 第二个捕获组作为新路径
nginx.ingress.kubernetes.io/use-regex: "true"
spec:
ingressClassName: nginx
rules:
- host: www.timinglee.org
http:
paths:
- backend:
service: { name: servicev1, port: { number: 80 } }
path: /
pathType: Prefix
- backend:
service: { name: servicev2, port: { number: 80 } }
path: /lee(/.*|$)(.*) # 匹配 /lee, /lee/abc, /lee/a/b...
pathType: ImplementationSpecific
bash
[Administrator.DESKTOP-VJ307M3] ➤ curl -L www.timinglee.org
Hello MyApp | Version: v1
[Administrator.DESKTOP-VJ307M3] ➤ curl -L www.timinglee.org/lee
Hello MyApp | Version: v2
[Administrator.DESKTOP-VJ307M3] ➤ curl -L www.timinglee.org/lee/a
Hello MyApp | Version: v2
[Administrator.DESKTOP-VJ307M3] ➤ curl -L www.timinglee.org/lee/a/a
Hello MyApp | Version: v2
rewrite-target: /$2 意思是把正则里第 2 个括号捕获到的内容作为新路径 转发。/lee(/.*|$)(.*) 两个括号:第 1 个是 /xxx 部分的中间串,第 2 个是剩余内容 ------这样 /lee 后面接啥都能稳稳命中 v2。
十二、实验七:金丝雀(灰度)发布
金丝雀发布 (Canary / 灰度发布):新版本先放一小部分流量 上去验证,没问题再逐步放大,直到全量替换。本质是"在保证老版本可用 + Pod 总量不低于期望值的前提下先加后减"。
我们继续用 ingress-nginx 的 canary 注解来做,两种方式:

1. 基于请求头(精准控制)
适用场景:想让"特定测试用户 / 内部账号"先去新版本。
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress1 # 正式:v1
spec:
ingressClassName: nginx
rules:
- host: www.timinglee.org
http:
paths:
- backend:
service: { name: servicev1, port: { number: 80 } }
path: /
pathType: Prefix
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress2 # 灰度:v2
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-by-header: "version" # 自定义请求头名
nginx.ingress.kubernetes.io/canary-by-header-value: "2" # 该头等于 2 时走 v2
spec:
ingressClassName: nginx
rules:
- host: www.timinglee.org
http:
paths:
- backend:
service: { name: servicev2, port: { number: 80 } }
path: /
pathType: Prefix
bash
# 普通请求 → 走 v1
[Administrator.DESKTOP-VJ307M3] ➤ curl www.timinglee.org
Hello MyApp | Version: v1
# 带 version: 2 请求头 → 走 v2
[Administrator.DESKTOP-VJ307M3] ➤ curl -H "version:2" www.timinglee.org
Hello MyApp | Version: v2
2. 基于权重(按比例分流)
适用场景:想让"随机 10% 的真实用户"先去新版本。
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress2
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10" # 10% 流量到 v2
nginx.ingress.kubernetes.io/canary-weight-total: "100" # 总权重 100
spec:
ingressClassName: nginx
rules:
- host: www.timinglee.org
http:
paths:
- backend:
service: { name: servicev2, port: { number: 80 } }
path: /
pathType: Prefix
写个脚本跑 100 次统计比例:
bash
[root@k8s-master services]# vim check_cannary.sh
#!/bin/bash
v1=0
v2=0
for (( i=0; i<100; i++ ))
do
response=$(curl -s www.timinglee.org | grep -c v1)
v1=$(expr $v1 + $response)
v2=$(expr $v2 + 1 - $response)
done
echo "v1:$v1, v2:$v2"
bash
[root@k8s-master services]# sh check_cannary.sh
v1:95, v2:5 # 接近 10%,但因为是概率,结果会有小波动
灰度逐步放量流程:先 5% → 20% → 50% → 100%,每次观察监控 / 错误率,没问题再放量。一旦监控到异常,立刻把
canary-weight改成 0,相当于快速回滚。