0%

在 k8s 环境中使用 Ingress

这篇文章我们继续在之前部署的 Kind k8s 环境 中使用 Ingress,以学习熟悉 k8s 的 Ingress 特性。

什么是 Ingress

k8s Ingress 本质上是一个 API 对象,是一份 声明文件,用于定义 L7(七层,例如 HTTP/HTTPS)的 路由规则配置,定义 哪个域名/路径去哪家 Service。Ingress 通过 能够感知 7 层协议(Protocol-Aware)的配置机制(能够理解 URI、主机名、Path 等概念),将你的 HTTP(或 HTTPS)网络服务暴露出来。Ingress 允许你通过 Kubernetes API 定义规则,并根据这些规则将流量映射到不同的后端服务。

通过 Ingress,你可以实现如下功能:

  • 为服务提供外部可访问 URL
  • 负载均衡流量
  • 终止 SSL/TLS
  • 实现基于域名的虚拟主机

下图展示了如何通过 Ingress 配置的规则将流量发送到不同的后端服务:

需要注意,Ingress 本身只是一种配置资源,必须部署 Ingress Controller 才能让 Ingress 规则生效,仅创建 Ingress 资源无任何作用。Ingress Controller 才是配置规则的执行者 (网关实体):

  • 一个部署在集群内的 Pod 应用(如 Nginx、Traefik、HAProxy),是 Ingress 规范的具体实现。
  • 监听 Kubernetes API Server,获取所有 Ingress 资源的配置
  • 根据这些配置,动态生成自己的配置文件(例如,Nginx Controller 会生成 nginx.conf)
  • 作为集群的真正入口,接收所有外部请求,并严格按照 Ingress 定义的规则将请求负载均衡到后端的 Pod

如下展示了在使用 k8s Ingress 之后,流量进入集群的过程:

  • 用户发起请求:访问 https://xxx/api
  • 到达 Ingress Controller:它是集群流量的入口
  • 解析 Ingress 规则:Controller 查找 Ingress 定义的规则,发现 /api 对应的是名为 backend-serviceService
  • 将流量转发到对应的后端 Service,并最终达到业务 Pod

所以虽然使用了 Ingress/Ingress Controller,仍然还是需要有 Service,Ingress 只是在入口处做决策,Service 维护着 Pod 的状态映射(Endpoints),Ingress 必须通过 Service 才能找到这一组 Pod:

  • Service 负责把后端的 Pod 聚合成一个整体
  • Ingress Controller 负责根据 Ingress 配置规则将流量分发到 Service 上
  • 在生产环境下,为了让外部流量能访问到你的 Ingress Controller(即那个负责转发流量的 Nginx/Traefik Pod),你通常需要为这个 Controller 创建一个 type: LoadBalancer 的 Service

那什么时候需要使用 Ingress 呢,什么时候直接使用 Service 呢?Ingress 是绝大多数 Web 应用的首选,因为它工作在七层(HTTP/HTTPS),能够提供多域名/多路径路由、统一 SSL/TLS 管理、负载均衡、高级流量控制(灰度发布、黑白名单限制等等)等功能。额外解释下,通过 Ingress 也可以减少对云负载均衡器的依赖(对于 HTTP/HTTPS 应用):

  • 没有 Ingress 时:如果你有 10 个 Web 微服务,为了让外界能访问,你可能需要创建 10 个 type: LoadBalancer 的 Service
  • 有了 Ingress 后:你只需要创建一个 Ingress Controller,并定义一个 Ingress 规则,把这 10 个微服务都挂在这一个公网 IP 下,通过域名(如 a.com, b.com)或路径(如 /service1, /service2)来区分

Ingress 主要是为 HTTP/HTTPS 设计的,如果你要暴露的是数据库(MySQL, MongoDB)、邮件服务器(SMTP)、游戏后端(TCP/UDP 自定义协议)等,你应该直接使用 Service。另外,Ingress Controller 本身是一个代理层(Nginx/Envoy),会带来微小的性能损耗。

接下来我们就通过一些实际例子,来学习如何在 k8s 环境中使用 Ingress。

Ingress 示例环境部署

这里我们将使用 ingress-nginx 来作为 Ingress Controller 进行部署,除此之外还有非常多的 Ingress Controller 实现,具体可以参考这个文档

部署业务应用

首先我们创建部署业务应用,它使用 echo 作为示例业务应用,通过 echo 服务返回的内容我们知道请求是否被不同的 Service 处理了:

  • 后端业务应用 Service 配置文件 ingress/apps/echo-a.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 第 1 部分: Deployment (创建 Pod)
apiVersion: apps/v1
kind: Deployment
metadata:
name: echo-a
labels:
app: echo-a
spec:
replicas: 1
selector:
matchLabels:
app: echo-a
template:
metadata:
labels:
app: echo-a
spec:
containers:
- name: echo-server
image: ealen/echo-server:0.9.2
ports:
- containerPort: 80
env:
- name: ECHO_SERVER_ID
value: "app-a"
- name: ECHO_SERVER_RESPONSE_TEXT
value: "Hello from App-A"
---
# 第 2 部分: Service (暴露 Pod)
apiVersion: v1
kind: Service
metadata:
name: echo-a
spec:
type: ClusterIP
selector:
app: echo-a
ports:
- port: 80
targetPort: 80
  • 类似地,我们再创建 echo-b.yaml,配置文件,配置内容是类似的,只不过返回的响应内容不同,这样方便我们知道是不是对应的 Service 被访问了

  • 应用配置文件

1
2
kubectl apply -f ingress/apps/echo-a.yaml
kubectl apply -f ingress/apps/echo-b.yaml
  • 查看应用
1
2
3
4
# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
echo-a ClusterIP 10.101.235.189 <none> 80/TCP 6h22m
echo-b ClusterIP 10.103.24.153 <none> 80/TCP 6h22m

部署 ingress-nginx Controller

  • 使用官方的 Manifest 来部署 ingress-nginx
1
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.15.1/deploy/static/provider/baremetal/deploy.yaml

Manifest 包含的主要内容:

资源 作用 为什么需要
Namespace 隔离空间 Controller 独立运行
ServiceAccount 身份标识 Controller 访问 API 的身份
ClusterRole 集群级权限 监听所有 namespace 的 Ingress
Role namespace 权限 操作 ingress-nginx namespace 资源
ClusterRoleBinding 绑定集群权限 让 ServiceAccount 有 ClusterRole 权限
RoleBinding 绑定 namespace 权限 让 ServiceAccount 有 Role 权限
ConfigMap NGINX 配置 存储全局配置
Deployment Controller Pod 实际运行的 Controller
Service NodePort 暴露 Controller (默认随机端口)
IngressClass Controller 标识 区分不同 Controller
ValidatingWebhook 验证 Ingress 创建 Ingress 时校验配置
  • 等待 Controller Ready
1
kubectl rollout status deployment/ingress-nginx-controller -n ingress-nginx
  • 配置 Ingress Resource (路由规则)

按照如下配置文件内容,创建 Ingress 路由规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# ingress/nginx/04-ingress-path.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-path-routing
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: api.test
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: echo-a
port:
number: 80
- path: /v2
pathType: Prefix
backend:
service:
name: echo-b
port:
number: 80
字段 解释
ingressClassName nginx 使用 NGINX Controller 处理这个 Ingress
host api.test 域名规则,请求必须 Host header 匹配
path /v1 URL 路径匹配规则
pathType Prefix 前缀匹配,/v1 匹配 /v1/v1/xxx
backend.service.name echo-a 后端 Service 名称,必须已存在
backend.service.port.number 80 Service 端口

这里 rewrite-target 的作用:

1
2
3
请求:     /v1/users
rewrite: /users (去掉 /v1 前缀)
发送到后端: /users
  • 应用该 Ingress 规则
1
kubectl apply -f ingress/nginx/04-ingress-path.yaml
  • 修改 ingress-nginx-controller 的 NodePort:默认部署的 ingress-nginx-controller Service 是个 NodePort 类型的 Service,这一点我们可以通过 ingress-nginxmanifest 得知:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
app.kubernetes.io/version: 1.15.1
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- appProtocol: http
name: http
port: 80
protocol: TCP
targetPort: http
- appProtocol: https
name: https
port: 443
protocol: TCP
targetPort: https
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
type: NodePort

由于其并没有显式指定 NodePort,因此会随机分配 NodePort,而我们的 Kind 环境 只预留了端口映射 30000-30002,这样就无法通过宿主机来访问随机分配的 NodePort 了。为了解决这个问题,我们对 ingress-nginx-controller Service 进行 patch 操作,让 Service 使用固定 NodePort(30001/30002),这样就能通过宿主机的端口来访问了:

1
2
3
4
5
6
7
8
kubectl patch svc ingress-nginx-controller -n ingress-nginx -p '{
"spec": {
"ports": [
{"name": "http", "port": 80, "targetPort": 80, "nodePort": 30001, "protocol": "TCP"},
{"name": "https", "port": 443, "targetPort": 443, "nodePort": 30002, "protocol": "TCP"}
]
}
}'

检查 ingress-nginx

查看 ingress-nginx 命名空间(因为是系统组件,一般会运行在单独的命名空间下)的所有资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
# kubectl get all -n ingress-nginx
NAME READY STATUS RESTARTS AGE
pod/ingress-nginx-controller-f85ff6d7d-dzmh6 1/1 Running 0 4h21m

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/ingress-nginx-controller NodePort 10.103.87.147 <none> 80:30001/TCP,443:30002/TCP 4h21m
service/ingress-nginx-controller-admission ClusterIP 10.103.248.223 <none> 443/TCP 4h21m

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/ingress-nginx-controller 1/1 1 1 4h21m

NAME DESIRED CURRENT READY AGE
replicaset.apps/ingress-nginx-controller-f85ff6d7d 1 1 1 4h21m
  • 之前说过,ingress-nginx Controller 本质上就是一个跑在 Pod 里的 Nginx 服务(运行在 ingress-nginx 命名空间中),并通过一个 Service 暴露给外界
  • service/ingress-nginx-controller 就是核心的业务流量入口,它是 NodePort 类型的 Service,因此是通过节点 IP:NodePort 来访问的,这里 NodePort 是我们所指定的 30001/30002
  • service/ingress-nginx-controller-admission 这是给 K8s 内部使用的(Validating Webhook),用来校验 Ingress 配置的正确性

接下来查看 Ingress 路由规则(业务应用,default 命名空间):

1
2
3
#  kubectl get ingress -o wide
NAME CLASS HOSTS ADDRESS PORTS AGE
nginx-path-routing nginx api.test 172.19.0.4 80 4h41m
  • NAME:Ingress 名称
  • CLASS:IngressClass 名称,这里就是 nginx
  • HOSTS:域名规则
  • ADDRESS:LoadBalancer IP(NodePort 模式显示节点 IP)
  • PORTS:端口
  • AGE:创建时间

注意这里尤其需要注意 ADDRESS,这是 NodePort 模式的特性,Ingress ADDRESS 显示的是:

  • 如果是 LoadBalancer,显示 LB IP
  • 如果是 NodePort,显示 Ingress Pod 运行节点的 IP(或所有节点 IP)
1
2
3
4
5
6
7
8
9
# kubectl get pod -n ingress-nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ingress-nginx-controller-f85ff6d7d-dzmh6 1/1 Running 0 4h31m 10.244.1.11 k8s-cluster-worker <none> <none>

s# kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-cluster-control-plane Ready control-plane 2d5h v1.35.0 172.19.0.2 <none> Debian GNU/Linux 12 (bookworm) 5.15.0-40-generic containerd://2.2.0
k8s-cluster-worker Ready <none> 2d5h v1.35.0 172.19.0.4 <none> Debian GNU/Linux 12 (bookworm) 5.15.0-40-generic containerd://2.2.0
k8s-cluster-worker2 Ready <none> 2d5h v1.35.0 172.19.0.3 <none> Debian GNU/Linux 12 (bookworm) 5.15.0-40-generic containerd://2.2.0

测试 Service 访问

我们可以直接通过节点的 NodePort 来访问所暴露的 http 服务:

1
2
3
4
5
6
7
8
9
10
# curl --noproxy "*" -s http://172.19.0.3:30001/v1 -H "Host: api.test" | jq '.environment.ECHO_SERVER_ID'
"app-a"
# curl --noproxy "*" -s http://172.19.0.3:30001/v2 -H "Host: api.test" | jq '.environment.ECHO_SERVER_ID'
"app-b"

# curl --noproxy "*" -s http://172.19.0.4:30001/v1 -H "Host: api.test" | jq '.environment.ECHO_SERVER_ID'
"app-a"

# curl --noproxy "*" -s http://172.19.0.4:30001/v2 -H "Host: api.test" | jq '.environment.ECHO_SERVER_ID'
"app-b"
  • 对于 NodePort 类型的 Service,我们可以直接访问节点 IP:NodePort 来访问服务,这里可以看到针对不同的 URL,访问不通的后端 Service(app-a/app-b)

  • 对于 externalTrafficPolicy 为 Cluster(默认)的 NodePort 类型的 Service,流量达到任意的集群节点上,都可以正确地转发到实际运行的 Pod 上,因此我们访问 172.19.0.3:30001172.19.0.4:30001,流量最终都能正确转发,并不一定说 ingress-nginx-controller 运行在 172.19.0.4 上,就只能通过 172.19.0.4:30001 来访问

由于 NodePort 的端口 30001 和 30002 已经是映射后的端口(参见 Kind 的 extraPortMappings 配置),因此我们也可以直接通过宿主机本身的 IP 来访问:

1
2
3
4
5
$ curl --noproxy "*" -s http://10.9.33.133:30001/v1 -H "Host: api.test" | jq '.environment.ECHO_SERVER_ID'
"app-a"

$ curl --noproxy "*" -s http://10.9.33.133:30001/v2 -H "Host: api.test" | jq '.environment.ECHO_SERVER_ID'
"app-b"

使用 LoadBalancer 的 Ingress 部署

接下来我们将使用 LoadBalancer 来暴露 Ingress Controller,以和 NodePort 的 Ingress Controller 模式进行对比:

1
2
NodePort 模式:  外部 → 基于 Node IP:NodePort → Service → Pod
LoadBalancer 模式: 外部 → 基于 LB IP → Service → Pod

上一篇文章我们已经学习过基于 MetalLB 来部署 LoadBalancer 类型的 Service,这里我们直接使用 MetalLB 来部署 Ingress Controller Service。

  • 创建配置文件 ingress/nginx/03-service-lb.yaml,实现 LoadBalancer 类型的 Ingress Controller Service:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Service
metadata:
name: ingress-nginx-controller-lb
namespace: ingress-nginx
spec:
type: LoadBalancer
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
- name: https
port: 443
targetPort: 443
protocol: TCP
字段 说明
type LoadBalancer MetalLB 会分配外部 IP
selector app.kubernetes.io/name=ingress-nginx 与 Deployment Pod 标签匹配
port 80/443 Service 端口
targetPort 80/443 Pod 端口

注意:这个 Service 使用相同的 selector 来指向同一个 Deployment(ingress-nginx-controller)。这样 NodePort 和 LoadBalancer 两个 Service 都可以暴露同一个 Controller。

  • 应用配置:
1
# kubectl apply -f ingress/nginx/03-service-lb.yaml
  • 确认 Service 状态
1
2
3
4
5
6
# kubectl get svc -n ingress-nginx

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller NodePort 10.103.87.147 <none> 80:30001/TCP,443:30002/TCP 5h9m
ingress-nginx-controller-admission ClusterIP 10.103.248.223 <none> 443/TCP 5h9m
ingress-nginx-controller-lb LoadBalancer 10.102.225.215 172.19.0.101 80:30448/TCP,443:30527/TCP 0s

可以看到,新增了 ingress-nginx-controller-lb 这个 类型为 LoadBalancer 的 Service,并且 MetalLB 分配了外部 IP 172.19.0.101

  • 修改 ingress-nginx-controller deployment 的启动参数,通过 --publish-service 参数来指定 Ingress Controller 需要监听哪个 Service 来获取 ADDRESS 信息。它去查看你指定的那个 Service(通常是 ingress-nginx-controller service 本身)的 External IP 或 Node IP,并将这个有效地址填入所有 Ingress 资源的 ADDRESS 栏。我们将其改成使用新创建的 ingress-nginx-controller-lb 这个 Service:
1
kubectl patch deployment ingress-nginx-controller -n ingress-nginx --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--publish-service=$(POD_NAMESPACE)/ingress-nginx-controller-lb"}]'
  • 等待 rollout 完成
1
# kubectl rollout status deployment/ingress-nginx-controller -n ingress-nginx --timeout=60s
  • 验证配置生效
1
2
# kubectl get deployment ingress-nginx-controller -n ingress-nginx -o yaml | grep 'publish-service'
- --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller-lb
  • 当 Controller 重启完成之后,查看 Ingress 可以看到:Ingress 资源的 ADDRESS 已经变成了我们 LoadBalancer Service 的外部 IP:
1
2
3
# kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
nginx-path-routing nginx api.test 172.19.0.101 80 5h55m
  • 基于 LoadBalancer 类型的 Ingress 来访问业务:
1
2
3
4
5
# curl --noproxy "*" -s http://172.19.0.101:80/v1 -H "Host: api.test" | jq '.environment.ECHO_SERVER_ID'
"app-a"

# curl --noproxy "*" -s http://172.19.0.101:80/v2 -H "Host: api.test" | jq '.environment.ECHO_SERVER_ID'
"app-b"

小结

这篇文章我们学习了如何 k8s 的基本概念,包括 Ingress、Ingress Controller、Service 之间的关联和差异,并实际部署了 NodePort 和 LoadBalancer 两种 Serviced 类型的 ingress-nginx-controller,并通过它们来访问不同的后端服务。