3-15-1-ingress与服务发现
人们通常把Kubernetes集群内部的通信称为东西向流量 , 而把集群内外部的通信称为南北向流量。本章主要介绍Kubernetes为统一管理Ingress流量而特地设计的同名资源类型及其支撑组件Ingress控制 器(Ingress Nginx和Contour)相关的话题。
1. Ingress资源
Kubernetes上的NodePort和LoadBalancer类型的Service资源能够把集群内部服务暴露给集群外部客户端访问 , 但两个负载均衡跃点必然产生更大的网络延迟 , 且无疑会大大增加组织在使用云服务方面的费用开销。因此 , Kubernetes为这种需求提供了一种更为高级的流量管理约束方式 , 尤其是对HTTP/HTTPS协议的约束。 Kubernetes使用Ingress控制器作为统一的流量入口 , 管理内部各种必要的服务 , 并通过Ingress这一API资源来描述如何区分流量以及内部 的路由逻辑。有了Ingress和Ingress控制器 , 我们就可通过定义路由 流量的规则来完成服务发布 , 而无须创建一堆NodePort或LoadBalancer类型的Service , 而且流量也会由Ingress控制器直接到达Pod对象。
1.1. Ingress与Ingress控制器流量转发
从本质上来说 , Ingress资源基于HTTP虚拟主机或URL路径的流量转发规则 , 它把需要暴露给集群外部的每个Service对象 , 并映射为 Ingress控制器上的一个虚拟主机或某虚拟主机上的一个PATH路径 ( 例如/auth等 ) , 如下图所示。

然而 , Ingress资源自身并不能进行" 流量穿透" , 它仅是一组路由规则的集合 , 这些规则要真正发挥作用还需要其他功能的辅助 , 例如监听某套接字 , 然后根据这些规则的匹配机制真正完成流量路由等。实现该功能的组件就是Ingress控制器。
Ingress控制器是Kubernetes集群的一个重要附件 , 它更像是一个自定义控制器 , 但支撑着API Server内置的Ingress资源及相关功能的实现 , 该控制器需要在集群上单独部署。
Ingress控制器本身就是一类以代理HTTP/HTTPS协议为主要功能的代理程序 , 通常兼有传输层代理功能 , 甚至可能支持更多的应用层协议 , 如Redis协议等。它可以由任何具有反向代理 ( HTTP/HTTPS协议 ) 功能的服务程序实现 , 例如Nginx、Envoy、 HAProxy、Vulcand和Traefik等。Ingress控制器自身也是运行在Kubernetes集群上的Pod资源对象 , 通常能够与集群上被代理的服务的Pod直接通信 , 如下图所示 , 流量到达外部负载均衡器 ( externalLB ) 后 , 首先转发至Service资源Ingres-nginx上 , 然后通过Ingress控制器基于Ingress资源定义的规则将客户端请求流量直接转发至与Service对应的后端Pod资源之上。这种转发机制会绕过Service资源(app Service;api Service) , 从而省去了由kube-proxy实现的端口代理开销。Ingress规则需要由一个Service资源对象辅助识别相关的所有Pod资源。如下Ingress通过app service资源去匹配后端的pod1和pod2;这个app service只是起到一个辅助识别功能。

可用作Ingress控制器的Nginx、Envoy和Traefik等应用程序的配置文件格式不尽相同 , 甚至无法互相兼容。 因此 , Ingress控制器自身得能够识别标准的Ingress资源且能够自动完成匹配到自身的配置格式转化 , 还要能够支持配置信息的动态重载 , 而无须重新应用进程才能满足Kubernetes应用场景中的需求。像Nginx这类传统的应用层代理程序并不能直接用作Ingress控制器 , 但Envoy和Traefik项目是云原生时代的产品 , 它们原生支持动态配置等功能。
Kubernetes集群也支持同时部署多个不同类型的Ingress控制器 , 为了避免一个Ingress被多个控制器重复加载 , 或者有意限制它仅可由某个特定的控制器加载 , 我们通常还需要在Ingress资源上注明它要适配的Ingress控制器类型。
另一方面 , 使用Ingress资源进行流量分发时 , Ingress控制器可基于某Ingress资源定义的规则将客户端的请求流量直接转发至Service对应的后端Pod资源之上 , 这种转发机制会绕过Service资源 , 从而省去了由kube-proxy实现的端口代理开销。在上图中 , Ingress规则需要由一个Service资源对象辅助识别所有相关的Pod对象 , 但Ingress控制器可经由api.ikubernetes.io规则的定义直接将请求流量调度至Pod3或Pod4 , 而无须经由Service对象API的再次转发 , app相关规则的作用方式与此类似。
1.2. Ingress资源规范
Ingress定义在extensions群组中 , 隶属于名称空间级别 , 它通过规则 ( .spec.rules ) 来定义流量路由逻辑 , 每个规则定义了其服务的主机名称 , 以及该主机的每个路径所映射到的后端 , 从而实现将发往某主机的流量根据路径的不同路由到不同的后端端点之上。Kubernetes v1.19版本中 , Ingress资源规范终于从长达数年的v1beta1版本进化至稳定的v1版本。
Ingress可用字段说明如下 :
apiVersion: networking.k8s.io/v1 # 资源所属的API群组及版本
kind: Ingress # 资源类型标识符
metadata: # 元数据
name <string> # 资源名称
annotations <map[string]string> # 资源注解 , v1使用下面的注解来指定要解析资源的控制器类型
kubernetes.io/ingress.class: <string> # 适配的Ingress控制器类别
namespace <string> # 名称空间
spec:
rules <[]Object> # Ingress规则列表
- host <string> # 虚拟主机的FQDN , 支持" *" 前缀统配 , 不支持IP , 不支持指定端口
http <Object> #
paths <[]Object> # 虚拟主机PATH定义的列表 , 由path和backend组成
- path <string> # 流量匹配的HTTP PATH , 必须以/开头
pathType <string> # 支持Exact、Prefix和ImplementationSpecific , 必选字段
backend <Object> # 匹配流量要转发的目标后端
resource <Object> # 引用的同一名称空间下的资源 , 与下面连个字段互斥
service <Object> # 关联的后端Service对象
name <string> # 后端Service的名称
port <Object> # 后端Service上的端口对象
name <string> # 端口名称
number <integer> # 端口号
tls <[]Object> # TLS配置 , 用于指定上述rules中定义的哪些主机需要工作在HTTPS模式下
- hosts <[]string> # 使用同一组证书的主机名称列表
secretName <string> # 保存数字证书和私钥信息的Secret资源名称
defaultBackend <Object> # 默认backend的定义 , 可嵌套字段及使用格式与rules字段中的相同
ingressClassName <string> # ingress类名称 , 用于指定适配的控制器配置示例:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-demo
annotations:
kubernetes.io/ingress.class: "nginx"
namespace: dev
spec:
rules:
- host: www.ik8s.io # 第一个FQDN主机 , 所有流量都由同一个backend处理
http: # 空路由表示到该主机的所有流量
paths: # 相应后端的定义
- path: /
pathType: Prefix
backend:
service:
name: ik8s
port:
number: 80
- host: www.ilinux.io # 第二个FQDN主机 , 服务于两个不同的路径
http:
paths:
- path: /foo # 第一个路由规则
pathType: Prefix
backend:
service:
name: foo-svc
port:
number: 80
- path: /bar # 第二个路由规则
pathType: Prefix
backend:
service:
name: bar-svc
port:
number: 8080
tls: # TLS定义 , 用于指明哪个FQDN主机需要服务于HTTPS模式
- hosts:
- www.ik8s.io
secretName: tls-ik8s1.3. Ingress资源类型
基于HTTP协议暴露的每个Service资源均可以发布到一个独立的FQDN主机名之上 , 例如www.ik8s.io , 也可发布到某主机的URL路径上 , 从而将它们整合到同一个Web站点 , 例如www.ilinux.io/foo等。 至于是否需要发布为HTTPS类型的应用则取决于用户的业务需求。
1.3.1. 单Service资源型Ingress
暴露单个服务的方法有很多种 , 例如服务类型中的NodePort、 LoadBalancer等 , 不过一样可以考虑使用Ingress来暴露服务 , 此时只需要为Ingress指定默认后端即可。例如下面的示例 :
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demoapp-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: www.demoapp.io
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: demoapp
port:
number: 80Ingress控制器会为配置清单中的ingress/demoapp-ingress资源分配一个IP地址以接入请求流量 , 并将流量转发至示例中定义的demoapp-svc后端。
1.3.2. 基于URL路径进行流量分发
垂直拆分或微服务架构中 , 每个小的应用都有其专用的Service资源暴露服务 , 但在对外开放的站点上 , 它们可能是财经、新闻、电商、无线端或API接口等一类的独立应用 , 每个应用可通过主域名的URI路径 ( path ) 分别接入 , 例如www.ilinux.io/api、 www.ilinux.io/app便可用于发布集群内名为api和app的services资源。于是 , 对应地可创建一个类似如下的Ingress资源 , 将对 www.ilinux.io/api的请求统统转发至api Service资源 , 把对 www.ilinux.io/app的请求转发至app Service资源。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demoapp-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: www.ilinux.io # 明确指明了虚拟主机 , 则仅服务到达该FQDN的流量
http:
paths:
- path: /api # 通过URI路径进入的第一个应用 , 将到达该路径的流量转发给api后端
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- path: /app # 通过URI路径进入的第二个应用 , 将到达该路径的流量转发给app后端
pathType: Prefix
backend:
service:
name: app-service
port:
number: 80801.3.3. 基于FQDN的虚拟主机
根据上面类型2描述的需求 , 也可以把每个应用分别以独立的FQDN主机名进行输出 , 例如使用app.ik8s.io和api.ik8s.io分别发布集群内部的app与api这两个Service资源。这种实现方案其实就是Web站点部署中的" 基于主机名的虚拟主机" , 将多个FQDN解析至同一个IP地址 , 然后根据主机头信息进行转发。下面是以独立FQDN主机形式发布服务的Ingress资源示例。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demoapp-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: api.ik8s.io # 第一个FQDN虚拟主机名称
http:
paths:
- pathType: Prefix
backend: # 后端服务api , 该服务下的每个端点都是该虚拟主机的一个被代理目标
service:
name: api-service
port:
number: 80
- host: app.ik8s.io # 第二个FQDN虚拟主机名称
http:
paths:
- pathType: Prefix
backend:
service:
name: app-service
port:
number: 80801.3.4. TLS类型的Ingress资源
这种类型能以HTTPS协议发布Service资源 , 基于一个含有私钥和证书的Secret对象即可配置TLS协议的Ingress资源 , 目前来说 , Ingress资源仅支持单TLS端口 , 且会卸载TLS会话。在 Ingress资源中引用此Secret对象即可让Ingress控制器加载并配置为 HTTPS服务。下面是一个简单的TLS类型的Ingress资源示例。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: demoapp-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: api.ik8s.io # 第一个FQDN虚拟主机名称
http:
paths:
- pathType: Prefix
backend: # 后端服务api , 该服务下的每个端点都是该虚拟主机的一个被代理目标
service:
name: api-service
port:
number: 80
tls: # TLS定义 , 用于指明哪个FQDN主机需要服务于HTTPS模式
- hosts:
- api.ik8s.io
secretName: tls-ik8s2. Ingress控制器部署与应用
Ingress控制器自身也是运行在Pod中的容器应用 , 它们是一类具有代理及负载均衡功能的守护进程 , 而注册监视API Server上的Ingress资源 , 可根据这些资源上定义的流量路由规则生成相应应用程序专有格式的配置文件 , 并通过重载或重启守护进程而生效新配置。例如 , 对于Nginx来说 , Ingress规则需要转换为Nginx的配置信息。然而 , 同样运行为Pod资源的Ingress控制器进程如何接入外部的请求流量呢?常用的解决方案有两种。
以
Deployment控制器管理Ingress控制器的Pod资源 , 通过NodePort或LoadBalancer类型的Service对象或者通过拥有外部IP地址 (externalIP) 的Service对象为其接入集群外部的请求流量。这意味着 , 在生产环境中定义一个Ingress控制器时 , 必须在其前端定义一个负载均衡器 , 负载均衡器可以是LoadBalancer类型的Service, 或用户自行管理的边缘路由器 , 如下图所示
借助
DaemonSet控制器 ,Ingress控制器的各Pod分别以单一实例的方式运行在Kubernetes集群中所有或部分工作节点之上 , 并配置这类Pod对象以hostPort( 见下图中左图 ) 或hostNetwork( 见下图中右图 ) 的方式在当前节点接入外部流量。
下面示例中 , 这里部署Ingress Nginx时设置其Service同时支持ExternalIP和NodePort。以便在没有可用的边缘路由器时能通过指定的外部IP访问Ingress Nginx控制器上的服务。
2.1 Ingress Nginx 部署及测试
Ingress-Nginx控制器在其项目源码目录中分别为aws、kind和 baremetal等Kubernetes部署场景提供了各自适用的部署清单 , 这些部署位于项目的deploy/static/provider/目录中。这里以kubeadm部署的Kubernetes集群可归入baremetal场景 , 因而这里使用该子目录下的部署清单完成Ingress-Nginx控制器部署即可。
下载资源清单文件
# wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.2.0/deploy/static/provider/baremetal/deploy.yaml -O ingress-nginx-deploy.yaml修改该资源清单 , 添加
ExternalIP。 ( 可选 ) 这里测试添加 , 实际生产根据情况而定。# vim ingress-nginx-deploy.yaml --- 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.2.0 name: ingress-nginx-controller namespace: ingress-nginx spec: 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 externalIPs: # 这里添加这里该配置 - 192.168.102.101创建该资源 , 该资源部署在
ingress-nginx名称空间下 , 包括由Deployment控制器编排的ingress-nginx-controller Pod及用于配置应用的configmap/ingress-nginx-controller等。创建完成后 ,service/ingress-nginx-controller资源通过TCP端口80和443进行提供服务。# kubectl apply -f ingress-nginx-deploy.yaml # kubectl get service/ingress-nginx-controller -n ingress-nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE ingress-nginx-controller NodePort 10.96.25.44 192.168.102.101 80:30696/TCP,443:32340/TCP 22s待
ingress-nginx-controller的Pod资源转为正常运行状态后 , 我们便可以基于Pod发起服务发布测试。不过 , 这里示例中尚且缺少一个可用的边缘负载均衡器 , 如上面图中所示的externalLB, 因此 , 外部客户端对Ingress控制器的访问暂时还只能通过<NodeIP>: <NodePort>或 者<ExternalIP>:<ServicePort>进行。查看
Pod状态# kubectl get pods -n ingress-nginx NAME READY STATUS RESTARTS AGE ingress-nginx-admission-create-wwrqd 0/1 Completed 0 1m ingress-nginx-admission-patch-nmgf2 0/1 Completed 0 1m ingress-nginx-controller-84b85b6b66-7prdh 1/1 Running 0 1m访问测试 , 这里通过
<ExternalIP>:<ServicePort>方式。这里返回404, 说明我们的ingress-nginx就部署OK了 , 只是暂时没有可用的后端 , 所以返回的404。
2.2. Ingress Nginx 测试
上面已安装了一个Nginx Ingress Controller控制器 , 有了Ingress控制器后 , 我们就可以定义Ingress资源来实现七层负载转发了 , 大体上Ingress支持三种使用方式 : 1. 基于虚拟主机转发 , 2. 基于虚拟机主机URI转发 , 3. 支持TLS加密转发
2.2.1. Ingress 定义
这里为了测试 , 首先创建一个Deployment资源 ( 包含2个副本 ) 和并通过Service资源进行暴露。
编写资源清单文件
demoapp-deploy.yaml--- apiVersion: apps/v1 kind: Deployment metadata: name: demoapp-ingress-demo namespace: default spec: replicas: 2 selector: matchLabels: app: demoapp template: metadata: name: demoapp labels: app: demoapp spec: containers: - name: demoapp image: vvoo/demoapp:v1.0 imagePullPolicy: IfNotPresent --- apiVersion: v1 kind: Service metadata: name: demoapp-svc namespace: default spec: selector: app: demoapp type: ClusterIP ports: - name: http port: 80 targetPort: 80 protocol: TCP创建该资源并验证。
# kubectl apply -f demoapp-deploy.yaml deployment.apps/demoapp-ingress-demo created service/demoapp-svc created # kubectl get deploy/demoapp-ingress-demo svc/demoapp-svc NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/demoapp-ingress-demo 2/2 2 2 2m21s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/demoapp-svc ClusterIP 10.108.38.111 <none> 80/TCP 84s编写资源清单
demoapp-ingress-demo.yaml, 上述两个步骤已创建了一个service, 如下我们定义一个ingress对象将起转发至demoapp-svc这个service, 通过ingress.class指定控制器的类型为nginx--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: demoapp-ingress annotations: kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/ssl-redirect: "false" # 禁止重定向HTTP到HTTPS spec: rules: - host: www.demoapp.io http: paths: - path: / pathType: Prefix backend: service: name: demoapp-svc port: number: 80创建
ingress资源# kubectl apply -f demoapp-ingress-demo.yaml ingress.networking.k8s.io/demoapp-ingress created # kubectl get ingress demoapp-ingress NAME CLASS HOSTS ADDRESS PORTS AGE demoapp-ingress <none> www.demoapp.io 80 11s查看
ingress详情 , 可以在Rules规则中看到后端Pod的列表 , 自动发现和关联相关Pod。 ( 这里有一个error, 表示没有定义默认的backend, 该错误可忽略 )# kubectl describe ingress demoapp-ingress Name: demoapp-ingress Labels: <none> Namespace: default Address: Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>) Rules: Host Path Backends ---- ---- -------- www.demoapp.io / demoapp-svc:80 (10.244.1.34:80,10.244.2.27:80) Annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/ssl-redirect: false Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Sync 19s nginx-ingress-controller Scheduled for sync进行访问测试 , 通过结果可以看出 , 分别轮循到了各个后端
Pod节点。# curl -H "Host:www.demoapp.io" 192.168.102.101 vvoo demoapp v1.0 !! ClientIP: 10.244.1.33, ServerName: demoapp-ingress-demo-54ccfd59c4-prtlw, ServerIP: 10.244.1.34! # curl -H "Host:www.demoapp.io" 192.168.102.101 vvoo demoapp v1.0 !! ClientIP: 10.244.1.33, ServerName: demoapp-ingress-demo-54ccfd59c4-vdssq, ServerIP: 10.244.2.27!
2.2.2. Ingress 虚拟主机
这里后端应用创建一组demoapp应用和一组nginx应用
编写资源清单
vhost-backend-app.yaml, 创建后端应用。--- apiVersion: apps/v1 kind: Deployment metadata: name: demoapp namespace: test spec: replicas: 2 selector: matchLabels: app: demoapp template: metadata: name: demoapp labels: app: demoapp spec: containers: - name: demoapp image: vvoo/demoapp:v1.0 imagePullPolicy: IfNotPresent --- apiVersion: apps/v1 kind: Deployment metadata: name: nginx namespace: test spec: replicas: 2 selector: matchLabels: app: nginx template: metadata: name: nginx labels: app: nginx spec: containers: - name: nginx image: nginx:alpine imagePullPolicy: IfNotPresent # 定义demoapp应用的Service资源 --- apiVersion: v1 kind: Service metadata: name: demoapp namespace: test spec: selector: app: demoapp type: ClusterIP ports: - name: http port: 80 targetPort: 80 protocol: TCP # 定义nginx应用的Service资源 --- apiVersion: v1 kind: Service metadata: name: nginx namespace: test spec: selector: app: nginx type: ClusterIP ports: - name: http port: 80 targetPort: 80 protocol: TCP创建上面定义的资源对象并查看验证
# kubectl create namespace test namespace/test created # kubectl apply -f vhost-backend-app.yaml deployment.apps/demoapp created deployment.apps/nginx created service/demoapp created service/nginx created # kubectl get svc -n test NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE demoapp ClusterIP 10.100.132.179 <none> 80/TCP 12s nginx ClusterIP 10.110.47.11 <none> 80/TCP 12s # kubectl get pods -n test NAME READY STATUS RESTARTS AGE demoapp-54ccfd59c4-dhngb 1/1 Running 0 19s demoapp-54ccfd59c4-gb6l6 1/1 Running 0 19s nginx-5cbdd6459f-4qvfg 1/1 Running 0 19s nginx-5cbdd6459f-7nlvp 1/1 Running 0 19s编写创建
Ingress资源的清单文件ingress-vhost-demo.yaml--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: vhost-ingress namespace: test annotations: kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/ssl-redirect: "false" # 禁止重定向HTTP到HTTPS spec: rules: - host: demoapp.ikubernetes.io http: paths: - path: / pathType: Prefix backend: service: name: demoapp port: number: 80 - host: nginx.ikubernetes.io http: paths: - path: / pathType: Prefix backend: service: name: nginx port: number: 80创建
Ingress资源# kubectl apply -f ingress-vhost-demo.yaml ingress.networking.k8s.io/vhost-ingress created # kubectl get ingress -n test NAME CLASS HOSTS ADDRESS PORTS AGE vhost-ingress <none> demoapp.ikubernetes.io,nginx.ikubernetes.io 80 7s查看其
Ingress详细信息 , 可以看出一个ingress生成了两个host# kubectl describe ingress demoapp-ingress -n test Name: vhost-ingress Labels: <none> Namespace: test Address: 192.168.102.102 Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>) Rules: Host Path Backends ---- ---- -------- demoapp.ikubernetes.io / demoapp:80 (10.244.1.36:80,10.244.2.29:80) nginx.ikubernetes.io / nginx:80 (10.244.1.37:80,10.244.2.30:80) Annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/ssl-redirect: false Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Sync 5s (x2 over 62s) nginx-ingress-controller Scheduled for sync进行访问测试 , 这里在本地电脑配置一个
host解析 , 解析上面配置的ExternalIP进行访问验证。访问
demoapp应用 ,http://demoapp.ikubernetes.io
访问
nginx应用 ,http://nginx.ikubernetes.io
2.3. Ingress 路径转发
Ingress支持URI格式的转发方式 , 同时支持URL重写 , 这里使用上面创建的两个service进行测试 , 分别用http://demo.ikubernetes.io/nginx和http://demo.ikubernetes.io/demoapp转发到两个不同的service。
编写资源清单文件
ingress-uri-demo.yaml--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: uri-ingress namespace: test annotations: kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/ssl-redirect: "false" # 禁止重定向HTTP到HTTPS nginx.ingress.kubernetes.io/rewrite-target: / spec: rules: - host: demo.ikubernetes.io http: paths: - path: /demoapp pathType: Prefix backend: service: name: demoapp port: number: 80 - path: /nginx pathType: Prefix backend: service: name: nginx port: number: 80创建
Ingress资源# kubectl apply -f ingress-uri-demo.yaml ingress.networking.k8s.io/uri-ingress created # kubectl get ingress uri-ingress -n test NAME CLASS HOSTS ADDRESS PORTS AGE uri-ingress <none> demo.ikubernetes.io 80 10s查看其
Ingress详细信息 , 可以看出一个ingress生成了一个host, 且下面有两个不同的uri。# kubectl describe ingress uri-ingress -n test Name: uri-ingress Labels: <none> Namespace: test Address: Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>) Rules: Host Path Backends ---- ---- -------- demo.ikubernetes.io /demoapp demoapp:80 (10.244.1.36:80,10.244.2.29:80) /nginx nginx:80 (10.244.1.37:80,10.244.2.30:80) Annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/ssl-redirect: false Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Sync 19s nginx-ingress-controller Scheduled for sync同样添加
host进行访问测试验证。访问
http://demo.ikubernetes.io/nginx
访问
http://demo.ikubernetes.io/demoapp
2.4. Ingress TLS
四层的负载均衡无法支持https请求 , 当前大部分业务都要求以https方式接入 , Ingress能支持https的方式接入 , 通过Secrets存储证书+私钥 , 实现https接入 , 同时还能支持http跳转功能。对于用户的请求流量来说 , 客户端到ingress controller是https流量 , ingress controller到后端service则是http , 提高用户访问性能 , 如下介绍ingress TLS功能实现步骤。
这里已上面配置路径转发的demo.ikubernetes.io示例为基准进行修改测试。
生成自签名证书和私钥
# openssl genrsa -out demo.ikubernetes.io.key 2048 # openssl req -new -x509 -key demo.ikubernetes.io.key -out demo.ikubernetes.io.crt -subj /C=CN/ST=ShenZhen/L=ShenZhen/O=DevOps/CN=demo.ikubernetes.io -days 3650基于上面创建的证书和私钥创建
Secret# kubectl create secret tls tls-ingress-ikubernetes-io --cert=demo.ikubernetes.io.crt --key=demo.ikubernetes.io.key -n test secret/tls-ingress-ikubernetes-io created # kubectl get secret -n test NAME TYPE DATA AGE default-token-jkq7b kubernetes.io/service-account-token 3 31m tls-ingress-ikubernetes-io kubernetes.io/tls 2 12s编写带
TLS的Ingress资源清单ingress-uri-tls-demo.yaml--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: uri-tls-ingress namespace: test annotations: kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/ssl-redirect: "true" # 禁止重定向HTTP到HTTPS nginx.ingress.kubernetes.io/rewrite-target: / spec: rules: - host: demo.ikubernetes.io http: paths: - path: /demoapp pathType: Prefix backend: service: name: demoapp port: number: 80 - path: /nginx pathType: Prefix backend: service: name: nginx port: number: 80 tls: - hosts: - demo.ikubernetes.io secretName: tls-ingress-ikubernetes-io创建
Ingress资源# kubectl apply -f ingress-uri-tls-demo.yaml ingress.networking.k8s.io/uri-tls-ingress created # kubectl get ingress uri-tls-ingress -n test NAME CLASS HOSTS ADDRESS PORTS AGE uri-tls-ingress <none> demo.ikubernetes.io 80, 443 14s查看其
Ingress详细信息 , 可以看出TLS已启用 , 且所使用的Secret信息。# kubectl describe ingress uri-tls-ingress -n test Name: uri-tls-ingress Labels: <none> Namespace: test Address: Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>) TLS: tls-ingress-ikubernetes-io terminates demo.ikubernetes.io Rules: Host Path Backends ---- ---- -------- demo.ikubernetes.io /demoapp demoapp:80 (10.244.1.36:80,10.244.2.29:80) /nginx nginx:80 (10.244.1.37:80,10.244.2.30:80) Annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/ssl-redirect: true Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Sync 23s nginx-ingress-controller Scheduled for sync访问测试 , 这里通过访问
http进行测试 , 应该能通过http自动跳转至https。因为上面配置中添加了nginx.ingress.kubernetes.io/ssl-redirect: "true"

2.5 Ingress 资源案例
我们知道 , Kubernetes Dashboard基于默认的资源清单部署到 kubernetes-dashboard名称空间中 , 而出于安全方面的需要 , Dashboard必须通过HTTPS协议进行远程访问。在Kubernetes Dashboard项目官方提供的配置清单中 , 相应的service/kubernetes-dashboard需经由ClusterIP的443端口发布到集群内部 , 但经由浏览器访问Dashboard的请求几乎都来自Kubernetes集群外部。
而此前的方法中 , 将服务发布到集群外部的常用方式是将Service 资源的类型修改为NodePort或LoadBalancer , 但NodePort将会把端口映射为某个不知名端口 , 甚至是随机端口 , 而LoadBalancer则依赖于公有云环境中的LBaaS服务 , 二者均存在着诸多限制。所以在 Kubernetes上 , Ingress才是发布HTTP应用的更好的方式。下面的配置清单示例 ( ingress-kubernetes-dashboard.yaml ) 即可将基于默认清单部署的Kubernetes Dashboard发布到集群外部。
编写资源清单文件
ingress-kubernetes-dashboard.yaml--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: uri-ingress namespace: kubernetes-dashboard annotations: kubernetes.io/ingress.class: "nginx" ingress.kubernetes.io/ssl-passthrough: "true" # ssl透传 nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" # 后端使用TLS协议 nginx.ingress.kubernetes.io/rewrite-target: /$2 # URL重定向 spec: rules: - http: # 未限定主机 , 表示可附着于任何主机之上进行访问 paths: - path: /dashboard(/|$) (.*) # 正则表达式格式 pathType: Prefix backend: service: name: kubernetes-dashboard port: number: 443创建该
ingress资源 , 创建后会在kubernetes-dashboard名称空间中创建一个名为uri-ingress的Ingress资源对象。该资源直接将客户端的HTTPS请求透传到后端的Kubernetes Dashboard相关的Pod资源之上 , 而不会做SSL会话的卸载 , 这是因为后端的服务自身要求SSL通信的缘故。资源清单中的两个注解ingress.kubernetes.io/ssl-passthrough和nginx.ingress.kubernetes.io/backend-protocol也正是因为这个目的而特地添加的。# kubectl apply -f ingress-kubernetes-dashboard.yaml ingress.networking.k8s.io/uri-ingress created # kubectl get ingress uri-ingress -n kubernetes-dashboard NAME CLASS HOSTS ADDRESS PORTS AGE uri-ingress <none> * 192.168.102.102 80 33s # kubectl describe ingress uri-ingress -n kubernetes-dashboard Name: uri-ingress Labels: <none> Namespace: kubernetes-dashboard Address: 192.168.102.102 Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>) Rules: Host Path Backends ---- ---- -------- * /dashboard(/|$)(.*) kubernetes-dashboard:443 (10.244.2.31:8443) Annotations: ingress.kubernetes.io/ssl-passthrough: true kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/backend-protocol: HTTPS nginx.ingress.kubernetes.io/rewrite-target: /$2 Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Sync 7s (x3 over 10m) nginx-ingress-controller Scheduled for sync访问测试 , 这里直接通过
ingress-nginx的ExternalIP进行访问 , 跟上上面资源清单中定义的/dashboard。http://192.168.102.101/dashboard/。
3. Contour 控制器
Contour是Kubernetes Ingress控制器的另一款开源实现 , 它以高性能的Envoy代理程序作为数据平面 , 支持开箱即用的动态配置和多种高级路由机制 , 支持TCP代理 , 并且提供了自定义资源类型HTTPProxy资源以扩展Ingress API , 以更丰富的功能集部分解决了Ingress原始设计中的缺点 , 是Ingress控制器较为出色的实现之一。

3.1 Envoy数据平面
Envoy是专为大型现代SOA架构设计的应用代理和通信总线 , 使用C++语言编写 , 是高性能的HTTP/HTTPS协议代理 , 支持负载均衡、超时、重试、熔断、限流和回退等多种高级路由功能 , 支持HTTP/2 和gRPC , 而且能够作为HTTP/1.1和HTTP/2之间的双向透明代理机制。Envoy原生支持动态配置和服务发现机制 , 能够通过xDS API从控制平面 ( 管理服务器 ) 获取配置信息 , 同时提供了良好的可观测性 , 未来甚至会作为UPDA ( Universal Data Plane API ) 的标准实现之一。
作为代理服务 , Envoy使用" 侦听器" ( listener ) 维护与下游客户端的连接 , 使用集群管理器管理上游服务器集群 , 并根据侦听器上配置的代理层级和路由策略" 桥接" 客户端与后端服务器 , 如下图所示。它支持配置任意数量的侦听器和任意数量的上游集群 , 这些配置可使用静态方式指定 , 也可分别基于不同的服务发现API动态生成。

Envoy通过侦听器监听网络套接字以接收客户端请求 , 并且支持在一个进程上启用任意数量的侦听器 , 从而赋予用户按需配置Envoy监听一至多个套接字的能力。每个侦听器都应独立配置一些网络 ( L3/L4 ) 过滤器 , 在接收到请求后 , 侦听器负责实例化指定过滤器链中的各个过滤器 , 并由这些过滤器处理后续的相关事件。一般说来 , 用户可根据不同的过滤器链配置侦听器以完成不同代理任务 , 例如HTTP代理、TCP代理、TLS客户端认证、限速等。
HTTP是现代面向服务的架构中至关重要的组件 , 因此Envoy通过名为HTTP连接管理器的网络 ( L3/L4 ) 过滤器实现HTTP协议的应用层代理 , 内置支持HTTP/1.1、WebSockets和HTTP/2几类协议。另外 , Envoy在传输层和应用层分别提供了对gRPC协议的原生支持 , gRPC是来自Google的RPC框架 , 它基于HTTP/2构建和传递请求/响应报文。Envoy内置的HTTP过滤器中的Router负责为HTTP代理实现高级路由功能 , 包括将DNS域映射到特定的虚拟主机、URL重定向、URL重写、重试、超时、基于权重或百分比进行流量分发、基于优先级的路由和基于哈希策略的路由等。Envoy通过配置的路由信息生成路由表 , 并由Router过滤器据此做出路由决策。
每个上游服务器可由一个称为端点的逻辑组件进行标识 , 它代表着此服务监听的套接字 , 因此也可直接作为上游服务的标识符使用。一个集群可以包含一到多个端点 , 而一个端点也可隶属于一到多个集群。
Envoy的集群管理器负责管理配置的所有上游集群 , 包括获知上游主机健康可用状态、负载状态、连接类型以及适用的上游通信协议 ( HTTP/1.1、HTTP/2 ) 等 , 并向前端的过滤器堆栈暴露API , 允许过滤器根据请求及处理结果按需建立与上游集群的L3/L4或L7的通信连接。一旦在配置中定义了集群 , 集群管理器需要知道如何解析集群中的成员 , 即端点配置信息。目前 , Envoy支持的端点配置支持包括静态配置、 严格DNS、逻辑DNS、原始目标和EDS几种。集群管理器可通过纯静态的配置信息加载并初始化集群 , 也可通过集群发现服务 ( Cluster Discovery Service , CDS ) API动态获取相关的配置 , 而后一种方式允许将配置信息存储在集中式的配置服务中 , 从而减少重启Envoy或重新分发配置的次数。
服务发现是面向服务架构的基石。为了更好地适配云原生环境 , Envoy内置了一个层次化的动态配置API用于集中式管理配置信息 , 该API可借助服务发现服务 ( Service Discovery Service , 即服务发现自身作为一种服务 ) 机制支持多种服务发现方式 , 从而为Envoy提供以下配置信息的动态发现和更新 , 如下图所示。

端点发现服务 (
Endpoint Discovery Service,EDS) : 用于为Envoy动态添加、更新和删除Service端点 , 这些端点通常是指上游集群后端提供特定服务的主机的套接字;EDS事实上是v1时代的SDS(Service Discovery Service) 的替代品 , 而且 ,SDS在v2版本中专用于指代Secret Discovery Service。集群发现服务 (
Cluster Discovery Service,CDS) : 用于动态添加、更新和删除Upstream集群 , 其中 , 每个集群本身都有自己的端点发现 (v1版本中的服务发现 ) 机制。路由发现服务 (
Route Discovery Service,RDS) : 用于动态添加或更新HTTP路由表。密钥发现服务 (
Secret Discovery Service,SDS) : 传递TLS密钥信息 , 从而LDS/CDS能将主要侦听器和集群配置通过密钥管理系统与密钥信息在传送时相分离。侦听器发现服务 (
Listener Discovery Service,LDS) : 用于动态添加、更新和删除整个侦听器 , 包括其完整的L4或L7过滤器堆栈。健康发现服务 (
Health Discovery Service,HDS) : 配置Envoy成为分布式健康状态检查网络的成员 , 负责将健康状态检查的相关信息报告给上级或集中式的健康状态检查服务。聚合发现服务 (
Aggregated Discovery Service,ADS) : 构建在单个Envoy API提供的数据平面服务之上的多个管理API( 例如Istio、Linkerd等 ) ;同时操作Envoy API时 , 使用单个管理服务器处理单个Envoy的所有更新操作有助于确保Envoy的数据一致性;此API允许所有其他API通过单个管理服务器的单个gRPC双向流进行编组 , 从而实现确定性排序。
另外还有ALS ( gRPC Access Log Service ) 、RTDS ( Runtime Discovery Service ) 、RLS ( Rate Limit Service ) 和CSDS ( Client Status Discovery Service ) 等 , 这些API统称为xDS , 相较于v1版本来说 , 它是一种新的REST-JSON API , 其中JSON/YAML格式是基于proto3规范的JSON映射机制派生而来的。另外 , xDS API通过gRPC流式传输进行更新 , 该方式有着较低资源需求 , 因而能降低更新延迟。
具体到应用层面 , Envoy既可以工作于微服务基础设施内部 , 作为面向服务架构内部的流量通信总线 ( 仅负责系统内部各服务间的通信 ) , 为服务管控Ingress或 ( 和 ) Engress流量 , 也能够作为整个微服务系统的边缘代理管控系统与外部交互的流量。因此 , 根据分发位置及执行的功能 , Envoy可以按如下图所示的方式分配到4种工作模式。

边缘代理 : 工作在整个系统架构的前端 , 接收来自外部客户端的请求 , 并将它们路由到相应的后端服务;除了必要的高级路由功能之外 , 处理
TLS/SSL连接也是其必要的功能之一。服务入站侦听器 : 负责接收并分发某特定服务的入站流量 , 可能会涉及应用程序的多个端口 , 并实现诸如重试、缓冲和熔断一类的功能。
服务出站侦听器 : 负责管控离开某特定服务的流量 , 协调服务间通信 , 实现诸如负载均衡和速率限制之类的功能;此监听器支持
HTTP/1.1或HTTP/2, 具体取决于应用程序的功能。外部服务出口侦听器 : 管理由系统内部服务到系统外部服务的流量。
作为Contour的数据平面 , 作用于Kubernetes集群级别的Ingress控制器时 , Envoy仅实现了上述4种部署类型中的第一种 , 即工作在Kubernetes系统的前端 , 主要用于管控南北向的流量。
而具体到配置层面 , Envoy在启动时需要经由bootstrap配置文件加载整个配置框架 , 它支持纯静态、纯动态或动静混合的配置方式。静态配置是指不支持在Envoy运行中变动的配置 , 而动态配置是经由xDS API向管理服务器注册、监视并动态加载的配置 , 配置信息的任何变动都能够由Envoy及时获取和装载。
资源的动态配置定义在dynamic_resources参数中 , 资源由相应的资源管理器借助xDS API进行服务发现时的响应结果生成 , 而静态方式通过static_resources配置参数在引导配置中定义。同时 , 动态及静态两种资源定义方式也能够基于特定的逻辑混合编排 , 例如在静态资源定义中仅使用EDS、仅使用EDS和 CDS , 或者使用EDS、CDS和RDS等。一般说来 , 管理接口 ( admin ) 和资源定义 ( static_resources或 dynamic_resources ) 是基本要件。
3.2. 部署Contour
Contour项目由Contour控制平面组件和Envoy数据平面组件组成 , 部署时需要由各自的控制器编排运行。Contour提供的示例部署清单将所有资源部署在名称空间projectcontour中 , 控制平面组件由Deployment控制器编排 , 出于高可用考虑 , 该组件通常运行两个副本 , 它们通过service/contour为 Envoy提供管理服务 , 而数据平面组件Envoy则由DaemonSet控制器编排运行在Kubernetes集群中的每个节点之上 , 它们通过service/envoy为客户端提供接入Ingress控制器的服务。具体的部署组件及工作逻辑如下图所示。

Contour提供的示例部署清单为https://projectcontour.io/quickstart/contour.yaml , 它默认为service/contour使用LoadBalancer类型 , 但我们的Kubernetes集群的部署环境为Baremetal , 因而需要修改其类型为NodePort , 但这里不建议使用ExternalIP , 因为该服务的外部流量策略为Local。另外 , 我们也可以通过修改daemonset/envoy中的Pod模板 , 以使得各Pod实例能够直接使用hostNetwork , 这些可用的解决方案都与Ingress Nginx相似。这里仍然选择使用前者 , 具体的变动如下面的Service资源清单所示。
下载资源清单文件
# wget https://projectcontour.io/quickstart/contour.yaml -O contour.yaml修改资源清单 , 这里修改
Service的类型。--- apiVersion: v1 kind: Service metadata: name: envoy namespace: projectcontour annotations: service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp spec: externalTrafficPolicy: Local # 仅将流量代理给本地的Envoy以减少跃点 , 提升性能 ports: - port: 80 name: http protocol: TCP targetPort: 8080 - port: 443 name: https protocol: TCP targetPort: 8443 selector: app: envoy type: NodePort # 若无外部LoadBalancer可用 , 这里修改为NodePort应用该资源清单 , 创建对应的资源
# kubectl apply -f contour.yaml查看对应的资源信息
# kubectl get pods -n projectcontour NAME READY STATUS RESTARTS AGE contour-855c85cd5d-4bjvt 1/1 Running 0 4m contour-855c85cd5d-hwmgk 1/1 Running 0 4m contour-certgen-v1.21.1-7rjbb 0/1 Completed 0 4m envoy-9j2jx 2/2 Running 0 4m envoy-v9b88 2/2 Running 0 4m
Ingress Contour示例 :
下面的示例中 , 定义了一个dev名称空间中的service/demoapp通过Contour发布到集群之外的Ingress资源 , 与前面使用Ingress Nginx控制器的Ingress资源的区别在于kubernetes.io/ingress.class注解信息的值或.spec.ingressClass字段的值。
创建测试用的
dev名称空间 , 以及后端应用和Service。# kubectl create namespace dev namespace/dev created # kubectl create deployment demoapp --image='vvoo/demoapp:v1.0' --replicas=3 -n dev deployment.apps/demoapp created # kubectl create service clusterip demoapp --tcp=80:80 -n dev service/demoapp created编写资源清单文件
contour-ingress-demo.yaml--- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: contour-ingress-demo namespace: dev annotations: kubernetes.io/ingress.class: "contour" # 指明负责解析该资源的控制器类型 spec: rules: - host: www.demoapp.io http: paths: - path: / pathType: Prefix backend: service: name: demoapp port: number: 80创建该
Ingress资源# kubectl apply -f contour-ingress-demo.yaml ingress.networking.k8s.io/contour-ingress-demo created # kubectl get ingress -n dev NAME CLASS HOSTS ADDRESS PORTS AGE contour-ingress-demo <none> www.demoapp.io 80 10s查看其详细信息 , 可以看到已成功关联到后端应用。
# kubectl describe ingress contour-ingress-demo -n dev Name: contour-ingress-demo Labels: <none> Namespace: dev Address: Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>) Rules: Host Path Backends ---- ---- -------- www.demoapp.io / demoapp:80 (10.244.1.16:80,10.244.2.19:80,10.244.2.20:80) Annotations: kubernetes.io/ingress.class: contour Events: <none>访问测试 , 通过
projectcontour名称空间中service/envoy上的80端口对应的NodePort来访问发布的demoapp应用。// 获取80端口映射的NodePort # kubectl get service/envoy -n projectcontour -o jsonpath="{.spec.ports[?(@.port==80)].nodePort}" 30104在外部主机上 , 将主机名
www.demoapp.io解析到kubernetes集群任意一个工作节点的IP地址进行请求测试。# curl http://www.demoapp.io:30104 vvoo demoapp v1.0 !! ClientIP: 10.244.1.13, ServerName: demoapp-744b464b8-lgqlg, ServerIP: 10.244.2.19!
由此或见 , 基于Kubernetes内置的Ingress发布服务时 , 底层使用的是Ingress Nginx还是Contour控制器并没有本质上的区别。但Contour提供的CRD资源类型HTTPProxy能够提供更为完整的路由功能集 , 大大丰富了Ingress的特性表现。
3.3. HTTPProxy基础
HTTPProxy资源几乎兼容Ingress资源的所有功能 , 只不过它使用独有的资源规范 , 具体的格式及简要说明如下所示。
apiVersion: projectcontour.io/v1 # API群组及版本;
kind: HTTPProxy # CRD资源的名称;
metadata:
name <string>
namespace <string> # 名称空间级别的资源
spec:
virtualhost <VirtualHost> # 定义FQDN格式的虚拟主机 , 类似于Ingress中host
fqdn <string> # 虚拟主机FQDN格式的名称
tls <TLS> # 启用HTTPS , 且默认以301将HTTP请求重定向至HTTPS
secretName <string> # 存储于证书和私钥信息的Secret资源名称
minimumProtocolVersion <string> # 支持的SSL/TLS协议的最低版本
passthrough <boolean> # 是否启用透传模式 , 启用时控制器不卸载HTTPS会话
clientValidation <DownstreamValidation> # 验证客户端证书 , 可选配置
caSecret <string> # 用于验证客户端证书的CA的证书
routes <[]Route> # 定义路由规则
conditions <[]Condition> # 流量匹配条件 , 支持PATH前缀和标头匹配两种检测机制
prefix <String> # PATH路径前缀匹配 , 类似于Ingress中的path字段
permitInsecure <Boolean> # 是否禁止默认的将HTTP重定向到HTTPS的功能
services <[]Service> # 后端服务 , 会对应转换为Envoy的Cluster定义
name <String> # 服务名称
port <Integer> # 服务端口
protocol <String> # 到达后端服务的协议 , 可用值为tls、h2或者h2c
validation <UpstreamValidation> # 是否校验服务端证书
caSecret <String>
subjectName <string> # 要求证书中使用的Subject值需要特别说明的是 , 在同一个conditions字段中使用多个prefix前缀时 , 前缀间将存在串联关系 , 例如对于第一个前缀/api和第二个前缀/docs来说 , 该条件实际匹配的是/api/docs路由前缀。但通常在一个条件中只应该使用单个prefix。
HTTPProxy基础示例 :
这里同样使用上面示例中创建的demoapp服务 , 只不过这里换乘HTTPProxy来实现 , 并添加tls证书信息。
创建证书信息
# openssl genrsa -out www.demoapp.io.key 2048 # openssl req -new -x509 -key www.demoapp.io.key -out www.demoapp.io.crt -subj /C=CN/ST=ShenZhen/L=ShenZhen/O=DevOps/CN=www.demoapp.io -days 3650 # kubectl create secret tls tls-ingress-demoapp-io --cert=www.demoapp.io.crt --key=www.demoapp.io.key -n dev编写资源清单
httpproxy-demo.yamlapiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: httpproxy-demo namespace: dev spec: virtualhost: fqdn: www.demoapp.io tls: secretName: tls-ingress-demoapp-io minimumProtocolVersion: "tlsv1.1" # 支持的TLS协议最小版本 routes: - conditions: - prefix: / # PATH路径前缀匹配 services: - name: demoapp port: 80 permitInsecure: true # 禁止将HTTP重定向到HTTPS创建该资源 , 并查看相关信息 , 该资源同样支持
get、describe等操作。# kubectl apply -f httpproxy-demo.yaml httpproxy.projectcontour.io/httpproxy-demo created # kubectl get httpproxy -n dev NAME FQDN TLS SECRET STATUS STATUS DESCRIPTION httpproxy-demo www.demoapp.io tls-ingress-demoapp-io valid Valid HTTPProxy # kubectl describe httpproxy httpproxy-demo -n dev ...... Spec: Routes: Conditions: Prefix: / Permit Insecure: true Services: Name: demoapp Port: 80 Virtualhost: Fqdn: www.demoapp.io Tls: Minimum Protocol Version: tlsv1.1 Secret Name: tls-ingress-demoapp-io Status: ...... Current Status: valid Description: Valid HTTPProxy Load Balancer: Events: <none>随后 , 同样通过访问
service/envoy的NodePort进行测试验证。分别对http和https协议发布的demoapp服务进行访问。# curl http://www.demoapp.io:30104 vvoo demoapp v1.0 !! ClientIP: 10.244.2.17, ServerName: demoapp-744b464b8-qb8jz, ServerIP: 10.244.2.20! # curl -k https://www.demoapp.io:32092 vvoo demoapp v1.0 !! ClientIP: 10.244.2.17, ServerName: demoapp-744b464b8-vsplx, ServerIP: 10.244.1.16!
3.4. HTTPProxy高级路由
除了能实现类似于Ingress资源的流量分发等基础功能 , HTTPProxy还封装了Envoy相当一部分高级路由功能的API , 例如基于标头的路由、流量镜像和流量分割等多种高级路由功能 , 能帮助用户实现诸如金丝雀部署、蓝绿部署和A/B测试等功能。相关的规范均定义在HTTPProxy的.spec.routes字段中 , 其简要格式及功能说明如下所示。
spec:
routes <[]Route> # 定义路由规则
conditions <[]Condition>
prefix <String>
header <HeaderCondition> # 请求报文标头匹配
name <String> # 标头名称
present <Boolean> # true表示存在该标头即满足条件 , 值false没有意义
contains <String> # 标头值必须包含的子串
notcontains <String> # 标头值不能包含的子串
exact <String> # 标头值精确的匹配
notexact <String> # 标头值精确反向匹配 , 即不能与指定的值相同
services <[]Service> # 后端服务 , 转换为Envoy的Cluster
name <String>
port <Integer>
protocol <String>
weight <Int64> # 服务权重 , 用于流量分割
mirror <Boolean> # 流量镜像
requestHeadersPolicy <HeadersPolicy> # 到上游服务器请求报文的标头策略
set <[]HeaderValue> # 添加标头或设置指定标头的值
name <String>
value <String>
remove <[]String> # 移除指定的标头
responseHeadersPolicy <HeadersPolicy> # 到下游客户端响应报文的标头策略
loadBalancerPolicy <LoadBalancerPolicy> # 指定要使用负载均衡策略
strategy <String> # 具体使用的策略 , 支持Random、RoundRobin、Cookie和WeightedLeastRequest , 默认为RoundRobin;
requestHeadersPolicy <HeadersPolicy> # 路由级别的请求报文标头策略
reHeadersPolicy <HeadersPolicy> # 路由级别的响应报文标头策略
pathRewritePolicy <PathRewritePolicy> # URL重写
replacePrefix <[]ReplacePrefix>
prefix <String> # PATH路由前缀
replacement <String> # 替换为的目标路径需要特别说明的是 , 在同一个conditions字段中以不同的列表项分别定义的多个头部条件彼此间存在" 逻辑与" 关系 , 这意味着请求报文需要同时满足头部条件的定义才能匹配到设置的规则。
高级路由功能的应用场景通常会依赖同一应用程序两个或以上数量的版本 , 为了避免其他依赖 , 这里测试新建一个dev名称空间并准备demoapp-v1.0和demoapp-v1.1两个版本的应用 , 它们都使用Deployment控制器编排 , 且分别有各自的Service对象。
# kubectl create ns dev
namespace/dev created
# kubectl create deployment demoappv10 --image='vvoo/demoapp:v1.0' -n dev
deployment.apps/demoappv10 created
# kubectl create deployment demoappv11 --image='vvoo/demoapp:v1.1' -n dev
deployment.apps/demoappv11 created
# kubectl create service clusterip demoappv10 --tcp=80 -n dev
service/demoappv10 created
# kubectl create service clusterip demoappv11 --tcp=80 -n dev
service/demoappv11 created3.4.1. 基于标头的路由示例
基于标头的流量匹配机制是指检测请求报文的特定头部是否存在 , 或者其值是否满足表述的条件 , 而后仅路由测试结果为True的请求报文 , 不能满足测试条件的报文将被忽略 , 它们可能会由后续的其他路由规则匹配后进行路由 , 或者由默认路由指定的后端予以服务。在conditions字段中的同一个列表项中同时指定的header和prefix之间是" 与" 关系 , 即报文必须同时满足两个条件 , 而不同列表项表达的筛选条件间为" 与" 关系 , 报文也需要同时满足其全部条件。
示例 :
下面的示例资源清单文件中在dev名称空间中定义了一个名为httpproxy-headers-routing的HTTPProxy资源 , 它在规则中定义了两个过滤条件 , 满足条件的请求被路由至demoappv10服务上 , 其他请求的路由目标是demoappv11服务。
编写资源清单文件
httpproxy-headers-routing.yamlapiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: httpproxy-headers-routing namespace: dev spec: virtualhost: fqdn: www.demoapp.io routes: - conditions: # 规则一 , 内部各条件间为与关系 , 且至多使用一个prefix - header: # 过滤条件一 , 未指定prefix则表示适用于所有PATH name: X-Canary # 指定要检测的头部名称 present: true # 报文头部存在即满足条件 , 无论其值是什么 - header: # 过滤条件二 , 与条件一位" 与" 关系 name: User-Agent # 指定要检测的头部名称 contains: curl # 指定的头部的值包含字符串" curl" 方满足条件 services: - name: demoappv10 port: 80 - services: # 规则二 , 虚拟主机未被前一路由规则匹配到的请求都将由该后端处理 - name: demoappv11 port: 80将
httpproxy/httpproxy-headers-routing资源应用到集群之上。# kubectl apply -f httpproxy-headers-routing.yaml httpproxy.projectcontour.io/httpproxy-headers-routing created # kubectl get httpproxy -n dev NAME FQDN TLS SECRET STATUS STATUS DESCRIPTION httpproxy-headers-routing www.demoapp.io valid Valid HTTPProxy进行访问测试 , 由
Envoy使用NodePort在集群外部向kubernetes集群任一节点发起测试请求 , 只有同时满足规则一中的两个条件的请求报文才会被路由到demoappv10后端。例如下面明确指定了自定义标头X-Canary的curl命令发起的测试请求及结果。# curl -H "X-Canary: true" http://www.demoapp.io:30104 vvoo demoapp v1.0 !! ClientIP: 10.244.2.17, ServerName: demoappv10-d5bc97985-7bqj4, ServerIP: 10.244.2.21!仅满足规则一种的一个条件或者不能满足规则一中的任何条件的报文请求 , 将被路由到
demoappv11后端 , 如下面的3个测试命令及结果。// 下面的命令仅满足规则一中的条件二 # curl http://www.demoapp.io:30104 vvoo demoapp v1.1 !! ClientIP: 10.244.2.17, ServerName: demoappv11-7cc7c9b9f9-482hv, ServerIP: 10.244.1.17! // 下面的curl命令模拟自身为chrome浏览器 , 仅满足规则一中的条件一 # curl -A "chrom" -H "X-Canary: On" http://www.demoapp.io:30104 vvoo demoapp v1.1 !! ClientIP: 10.244.2.17, ServerName: demoappv11-7cc7c9b9f9-482hv, ServerIP: 10.244.1.17! // 下面的命令无法满足规则一种的任意条件 # wget -q -O - http://www.demoapp.io:30104 vvoo demoapp v1.1 !! ClientIP: 10.244.1.13, ServerName: demoappv11-7cc7c9b9f9-482hv, ServerIP: 10.244.1.17!
基于标头路由有许多应用场景 , 例如用户分类路由或浏览器分类路由等 , 甚至是基于某些特定的标头模拟金丝雀发布等。
3.4.2. 流量切分示例
HTTPProxy支持在单个路由规则中同时指定多个后端服务 , 默认情况下 , 所有流量将以等量切分的方式平均分发到多个后端之上 , 每个后端内部再按照代理服务器配置的调度算法进行二级负载均衡。同时 , HTTPProxy也允许用户为每个后端服务使用weight字段指定一个特定流量百分比 , 从而将流量以指定的比例在不同的后端服务间进行分发 , 如下图所示。

基于流量分割的流量切分是完成灰度发布的常用手段。上线应用的新版本时 , 无论是出于产品稳定性还是用户接受程度等因素的考虑 , 直接以新代旧都充满风险。因此 , 灰度发布是应用程序在生产环境安全上线的一种重要手段 , 而对于Envoy来说 , 灰度发布仅是其流量治理功能的一种典型应用 , 结合分割策略便能实现常见的金丝雀部署、蓝绿部署和A/B测试等应用场景。
事实上 , 基于标头的流量分割算是" 基于请求内容" 灰度部署的一种实现 , 而流量分割则是" 基于流量比例" 进行灰度部署的方式。与Kubernetes的Deployment控制器的滚动更新机制相比 , HTTPProxy允许用户按需指定要切分的流量比例 , 而非按照Pod数量来固定分割的方式。
示例 :
下面的示例清单文件中定义的httpproxy元将发往虚拟主机www.demoap.io的以"/"为路径前缀的流量 , 按指定比例切分了demoappv10和demoappv11两个后端服务之上。
编写资源清单文件
httpproxy-traffic-splitting.yamlapiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: httpproxy-traffic-splitting namespace: dev spec: virtualhost: fqdn: www.demoapp.io routes: - conditions: - prefix: / services: - name: demoappv10 port: 80 weight: 90 # 该后端的流量比例 - name: demoappv11 port: 80 weight: 10 # 该后端的流量比例应用上面的资源清单 , 为了避免冲突 , 需将前面定义的
httpproxy资源卸载。# kubectl delete -f httpproxy-headers-routing.yaml httpproxy.projectcontour.io "httpproxy-headers-routing" deleted # kubectl apply -f httpproxy-traffic-splitting.yaml httpproxy.projectcontour.io/httpproxy-traffic-splitting created进行访问测试 , 这里向该虚拟主机发起多次请求测试 , 一般来说 , 样本空间越大 , 真实的流量切分比例越接近与规划。如下 , 发起
100次请求 , 并统计demoapp v1.0和demoapp v1.1响应的报文数比例。# v10=0; v11=0; for i in {1..100}; do if curl -s http://www.demoapp.io:30104/ |grep "v1.0" > /dev/null; then let v10++; else let v11++; fi; done; # echo "Version 1.0: $v10 Version 1.1: $v11" Version 1.0: 92 Version 1.1: 8
事实上 , 我们可以把这10%的流量比例当作" 金丝雀" , 测试足够长一段时间 , 确认无误后可通过逐步修改httpproxy-traffic-splitting资源中各后端服务上流量比例的方式完成流量迁移 , 从而实现灰度发布。以不存在" 中间状态" 的方式在两个后端服务间分割流量 , 要么完全在第一个服务上 , 要么一次性 迁往第二个服务的配置机制即为蓝绿发布。
1.4.3. 流量镜像示例
流量镜像用于百分百地在两个服务间复制流量。在支持蓝绿部署的场景中 , 流量镜像常用于将当前服务上的真实流量引入到未发布的新版本上进行测试。但流量镜像工作于" 只读" 模式 , 因为其响应报文会被全部丢弃。
示例 :
下面的示例清单文件中将demoappv10服务上的所有流量都镜像给了demoappv11一份完整的副本。
编写资源清单文件
httpproxy-traffic-mirror.yamlapiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: httpproxy-traffic-mirror namespace: dev spec: virtualhost: fqdn: www.demoapp.io routes: - conditions: - prefix: / services: - name: demoappv10 port: 80 - name: demoappv11 port: 80 mirror: true # 流量镜像 , 所有发往demoappv10的流量都镜像给当前服务一份应用上面的资源清单 , 为了避免冲突 , 需将前面定义的
httpproxy资源卸载。# kubectl delete -f httpproxy-traffic-splitting.yaml httpproxy.projectcontour.io "httpproxy-traffic-splitting" deleted # kubectl apply -f httpproxy-traffic-mirror.yaml httpproxy.projectcontour.io/httpproxy-traffic-splitting created进行访问测试 , 向虚拟主机发起请求 , 观察
demoappv11后端应用的日志。可以看到也有相关请求。# for i in {1..10}; do curl http://www.demoapp.io:30104/ ; done vvoo demoapp v1.0 !! ClientIP: 10.244.1.13, ServerName: demoappv10-d5bc97985-7bqj4, ServerIP: 10.244.2.21! vvoo demoapp v1.0 !! ClientIP: 10.244.2.17, ServerName: demoappv10-d5bc97985-7bqj4, ServerIP: 10.244.2.21! vvoo demoapp v1.0 !! ClientIP: 10.244.2.17, ServerName: demoappv10-d5bc97985-7bqj4, ServerIP: 10.244.2.21! vvoo demoapp v1.0 !! ClientIP: 10.244.1.13, ServerName: demoappv10-d5bc97985-7bqj4, ServerIP: 10.244.2.21! vvoo demoapp v1.0 !! ClientIP: 10.244.2.17, ServerName: demoappv10-d5bc97985-7bqj4, ServerIP: 10.244.2.21! vvoo demoapp v1.0 !! ClientIP: 10.244.1.13, ServerName: demoappv10-d5bc97985-7bqj4, ServerIP: 10.244.2.21! vvoo demoapp v1.0 !! ClientIP: 10.244.1.13, ServerName: demoappv10-d5bc97985-7bqj4, ServerIP: 10.244.2.21! vvoo demoapp v1.0 !! ClientIP: 10.244.2.17, ServerName: demoappv10-d5bc97985-7bqj4, ServerIP: 10.244.2.21! vvoo demoapp v1.0 !! ClientIP: 10.244.2.17, ServerName: demoappv10-d5bc97985-7bqj4, ServerIP: 10.244.2.21! vvoo demoapp v1.0 !! ClientIP: 10.244.1.13, ServerName: demoappv10-d5bc97985-7bqj4, ServerIP: 10.244.2.21! // 查看demoappv11后端应用的日志 # kubectl get pods -n dev -l app=demoappv11 NAME READY STATUS RESTARTS AGE demoappv11-7cc7c9b9f9-482hv 1/1 Running 0 48m # kubectl logs demoappv11-7cc7c9b9f9-482hv -n dev 10.244.1.13 - - [27/Jun/2022 08:12:17] "GET / HTTP/1.1" 200 - 10.244.2.17 - - [27/Jun/2022 08:12:17] "GET / HTTP/1.1" 200 - 10.244.2.17 - - [27/Jun/2022 08:12:17] "GET / HTTP/1.1" 200 - 10.244.1.13 - - [27/Jun/2022 08:12:17] "GET / HTTP/1.1" 200 - 10.244.2.17 - - [27/Jun/2022 08:12:17] "GET / HTTP/1.1" 200 - 10.244.1.13 - - [27/Jun/2022 08:12:17] "GET / HTTP/1.1" 200 - 10.244.1.13 - - [27/Jun/2022 08:12:17] "GET / HTTP/1.1" 200 - 10.244.2.17 - - [27/Jun/2022 08:12:17] "GET / HTTP/1.1" 200 - 10.244.2.17 - - [27/Jun/2022 08:12:17] "GET / HTTP/1.1" 200 - 10.244.1.13 - - [27/Jun/2022 08:12:17] "GET / HTTP/1.1" 200 -
1.4.4. 负载均衡策略示例
HTTPProxy中的负载均衡策略是路由规则中的定义 , 每个路由规则都可以为其后端调用的服务按需指定最为合用的负载均衡机制。目前 , HTTPProxy暴露了Envoy在集群上支持的部分调度算法 , 主要有如下几个。
RoundRobin: 按顺序轮询选择上游端点 , 此为默认策略。WeightedLeastRequest: 加权最少连接 , 但该算法仅随机选择两个健康的端点 , 并从中挑选出负载少的端点作为调度目标。Random: 从后端健康端点中随机挑选端点。Cookie: 粘性会话调度机制 , 把来自某客户端的所有请求始终调度给同一个后端端点。
示例 :
下面的示例清单文件中定义的路由规则会发往www.demoapp.io的所有流量均分给demoappv10和demoappv11这两个后端服务 , 各服务内部使用Random调度算法将流量分发至后端断点。
编写资源清单文件
httpproxy-lb-strategy.yamlapiVersion: projectcontour.io/v1 kind: HTTPProxy metadata: name: httpproxy-lb-strategy namespace: dev spec: virtualhost: fqdn: www.demoapp.io routes: - conditions: - prefix: / services: # 在两个服务间均分流量 - name: demoappv10 port: 80 - name: demoappv11 port: 80 loadBalancerPolicy: # 使用random调度算法 strategy: Random应用上面的资源清单 , 为了避免冲突 , 需将前面定义的
httpproxy资源卸载。# kubectl delete -f httpproxy-traffic-mirror.yaml httpproxy.projectcontour.io "httpproxy-traffic-splitting" deleted # kubectl apply -f httpproxy-lb-strategy.yaml httpproxy.projectcontour.io/httpproxy-lb-strategy created进行访问测试 , 这里向该虚拟主机发起多次请求测试 , 一般来说 , 样本空间越大 , 真实的流量切分比例越接近与规划。如下 , 发起
100次请求 , 并统计demoapp v1.0和demoapp v1.1响应的报文数比例。# v10=0; v11=0; for i in {1..100}; do if curl -s http://www.demoapp.io:30104/ |grep "v1.0" > /dev/null; then let v10++; else let v11++; fi; done; # echo "Version 1.0: $v10 Version 1.1: $v11" Version 1.0: 49 Version 1.1: 5
HTTPProxy路由规则中 , 每个后端服务都会对应地转换为Envoy上配置的一个集群 , 于是 , 指定的负载均衡策略也就成为该路由规则中各服务对应集群上共用的调度策略。
3.5. HTTPProxy服务韧性
Envoy提供了一系列的局部故障应对机制 , 包括超时、重试、主动健康状态检测、被动健康状态检测 ( 异常值探测 ) 和断路器等 , Contour的HTTPProxy API封装提供了前3种类型的故障应对机制 , 它们都定义在路由规则之中。可用的字段及简要的功能说明如下所示。
spec:
routes <[]Route>
timeoutPolicy <TimeoutPolicy> # 超时策略
response <String> # 等待服务器响应报文的超时时长
idle <String> # 超时后 , Envoy维持与客户端之间连接的空闲时长
retryPolicy <RetryPolicy> # 重试策略
count <Int64> # 重试的次数 , 默认为1
perTryTimeout <String> # 每次重试的超时时长
healthCheckPolicy <HTTPHealthCheckPolicy> # 主动健康状态检测
path <String> # 检测针对的路径 ( HTTP端点 )
host <String> # 检测时请求的虚拟主机
intervalSeconds <Int64> # 时间间隔 , 即检测频度 , 默认为5秒
timeoutSeconds <Int64> # 超时时长 , 默认为2秒
unhealthyThresholdCount <Int64> # 判定为非健康状态的阈值 , 即连续错误次数
healthyThresholdCount <Int64> # 判定为健康状态的阈值HTTPProxy上实施的这类局部故障应对机制均属于路由级别 , HTTPProxy将对该路由下的每个Service 无差别地同时生效。
3.5.1. 超时和重试
分布式环境中对远程资源和服务的调用可能会由于瞬态故障而失败 , 例如网络连接速度慢、超时、资源过量使用或暂时不可用等 , 这些故障多数情况下都能够在短时间内自行纠正 , 因而基于特定的策略重新发送请求 ( 重试机制 ) 可解决大部分的此类故障 , 其工作逻辑如下图所示。而且 , 通过透明地重试失败的操作 , 使应用程序在尝试连接到服务或网络资源时能够处理瞬态故障 , 可以显著提高应用程序的稳定性。
但是 , 也可能存在由于意外事件导致故障并且可能需要更长时间才能恢复的情形 , 而故障的严重性级别有所不同 , 从部分连接中断到服务完全不可用皆有可能。这种情况下 , 连续重试和长时间的等待都没有太大意义 , 因而应用程序应迅速 ( 等待一定的时间后自动超时 ) 接受该操作已经失败并相应地处理这种失败。
示例 :
下面的资源清单示例 ( httpproxy-retry-timeout.yaml ) 为部署的demoappv10服务定义了超时和重试策略 , 当上游服务器响应以5xx状态码时 , demoappv10将启动重试机制 , 最大尝试次数为3次 , 总体超时时长为2 秒。
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
name: httpproxy-retry-timeout
namespace: dev
spec:
virtualhost:
fqdn: www.demoapp.io
routes:
- timeoutPolicy: # 超时机制
responce: 2s
idle: 5s
retryPolicy: # 重试策略
count: 3 # 最大重试次数
perTryTimeout: 500ms # 单次尝试的超时时长
services:
- name: demoappv10
port: 80HTTPProxy仅支持当上游服务器响应以5xx状态码时重试 , 因此它所能够应对的错误场景有限。
3.5.2. 健康状态检测
HTTPProxy资源支持的健康状态检测功能 , 是由Envoy在集群级别对后端端点主动检测实现的 , Envoy通过发往每个上游端点的HTTP请求的响应状态码来判定端点健康与否。这种健康状态检测机制与Kubernetes上对容器实施的StartupProbe、LivenessProbe和ReadinessProbe机制有所不同 , 它并不经由Pod上定义的检测点 , 而是使用自定义的检测端点。我们知道 , Service会对其关联的后端Pod经由 Endpoint进行健康状态检测 , 而Contour的健康状态检测是在这各种检测之外 , 对Service关联的Pod对象又施加的一层自定义检测机制。
下面的资源清单 ( httpproxy-health-checks.yaml ) 示例在HTTPProxy上定义的健康状态检测将转换为Envoy的配置 , 并由Envoy实施检测。但相关的检测策略定义在路由规则上 , 而非服务级别 , 这意味着同一路由规则下的所有服务对应的Envoy集群将共享这种检测机制。
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
name: httpproxy-retry-timeout
namespace: dev
spec:
virtualhost:
fqdn: www.demoapp.io
routes:
- conditions:
- prefix: /
healthCheckPolicy:
path: /
intervalSeconds: 5
timeoutSeconds: 2
unhealthyThresholdCount: 3
healthyThresholdCount: 5
services:
- name: demoappv10
port: 80
- name: demoappv11
port: 80