云原生系列Kubernetes篇 使用 Ingress 做 HTTP 负载均衡

本文来自正在规划的Go语言&云原生自我提升系列,欢迎关注后续文章。

一个应用很重要的部分是网络流量的来来回回。服务发现一章中提到,Kubernetes拥有一些能力可让服务暴露到集群之外。对于很多用户的简单用例,这种能力足够用了。

但服务对象在OSI模型的第4层操作。也就是说它只转发TCP和UDP连接,不会深入到连接内部。因此,在集群上托管多个应用使用多个不同的对外暴露的服务。在服务为NodePort类型时,需要让客户端连接到各个服务的具体端口。而在服务类型为LoadBalancer时,要为每个服务分配云资源(通常昂贵或稀少)。但对HTTP(7层)服务,则有更好的方案。

在非Kubernetes场景下解决这类问题,人们常常会想到“虚拟主机”。通过这种机制可以在单IP上托管多个HTTP站点。通常会使用到负载均衡或反射代理在HTTP (80) 和 HTTPS (443)端口上接收进入的网络连接。然后程序会解析HTTP连接,根据请求的Host头和URL路径,将HTTP调用代理至其它程序。这样,负载均衡或反向代理将流量转发去解码或将进入的连接转发到正确upstream服务端。

Kubernetes将其HTTP负载均衡系统称为Ingress。Ingress是Kubernetes中刚刚讨论到的“虚拟主机”的原生实现方式。这种方式更复杂的方面是用户需要管理负载均衡配置文件。对于动态环境,随机虚拟主机的扩充,就会变得很复杂。Kubernetes Ingress系统对其简化的方式有:(a)标准化配置,(b) 将其迁移至Kubernetes对象中,(c) 将多个Ingress对象合并至单个负载均衡配置中。

典型的软件实现如图8-1所示。Ingress控制器是一个由两部分组成的软件系统。第一部分是Ingress代理,使用LoadBalancer类型服务对集群外暴露。这一代码将请求发送给上游服务端。另一个组件是Ingress协调器(reconciler)或operator。Ingress operator负责读取和监控Kubernetes API中的Ingress对象,以及重新配置Ingress代理按Ingress资源所指定的方式路由流量。有多种不同的Ingress实现。其中一些这两个组件位于单个容器中,另一些不同的组件单独部署在Kubernetes集群中。图8-1为Ingress控制器的一个示例。

图8-1 典型软件的Ingress控制器配置

图8-1 典型软件的Ingress控制器配置

Ingress规范与Ingress控制器

虽然概念上不复杂,但在实现层面Ingress与Kubernetes中的其它常规资源对象均不同。尤其是它拆分为了普通资源规范和控制器实现。Kubernetes中没有内置“标准”的Ingress控制器,因而用户需要从多种可选实现中选择一种。

用户可以像其它对象一样创建和修改Ingress对象。但默认没有运行的代码在实际作用于这些对象。由用户(或他们使用的发行版)安装、管理外部控制器。这样控制器才是可插拔的。

Ingress最终这样实现是有一些原因的。首先,没存在可以到处使用的某一个HTTP负载均衡。除了大量的软件负载均衡(开源与自主研发的),还有一些由云厂商提供的负载均衡服务(如AWS的ELB),以及硬件负载均衡。另一个原因是Ingress对象在Kubernetes中添加其它通用扩展能力(参见扩展Kubernetes一章)之前就已经存在了。随着Ingress的发展,很可能会演进到使用这些机制。

安装Contour

存在很多种Ingress控制器,比如这里我们使用的Ingress控制器为Contour。它是一个用于配置开源负载均衡Envoy(CNCF项目)的控制器。Envoy用于动态通过API进行配置。Contour Ingress控制器将Ingress对象翻译成Envoy可以理解的内容。

注:Contour项目由Heptio与真实客户共同创建,已在生产中进行使用,但它现在是一个独立开源项目。

可通过一行命令安装Contour:

1
$ kubectl apply -f https://projectcontour.io/quickstart/contour.yaml

注意这条命令要求执行的用户具有cluster-admin权限。

对于大部分配置都可以使用这条命令。它会创建一个名为projectcontour的命名空间。在命名空间内创建一个部署(两个副本)以及一个对外的LoadBalancer服务。此外它会通过服务配置正确的权限并为一些在Ingress展望一节中讨论的扩展能力安装CustomResourceDefinition(参见扩展Kubernetes一章)。

因其是全局安装,需要保证在所安装的集群上具备管理员权限。安装完成后,可通过如下命令获取Contour的外部地址:

1
2
3
$  kubectl get -n projectcontour service envoy -o wide
NAME CLUSTER-IP EXTERNAL-IP PORT(S) ...
contour 10.106.53.14 a477...amazonaws.com 80:30274/TCP ...

查看EXTERNAL-IP一列。它可能是IP地址(GCP和Azure)或主机名(AWS)。其它云或环境可能会不同。如果你的Kubernetes集群不支持LoadBalancer类型服务,则需要在安装Contour时修改YAML为type: NodePort,将流量通过你的配置所接受的机制路由到集群上的机器。

如果使用的是minikube,在EXTERNAL-IP一列可能不会显示内容。要解决这一问题,需要单独打开一个终端窗口,运行minikube tunnel。这会配置网络路由,为每个type: LoadBalancer的服务分配一个独立 IP。

配置DNS

要让Ingress很好地运行,需要为负载均衡配置指向外部地址的DNS。可以将多个主机名映射到单个外部端点,Ingress控制器会将进入的请求根据主机名转发到相应在的上游服务。

本章中我们假设你有一个域名example.com,需要配置两条DNS:alpaca.example.combandicoot.example.com。如果对外负载均衡有IP地址,应创建A记录。如果所持有的是主机名,应配置CNAME记录。

ExternalDNS项目是一个用于代你管理DNS记录的集群插件。ExternalDNS监控Kubernetes集群并将Kubernetes服务资源的IP地址同步给外部DNS服务商。ExternalDNS支持很多种DNS服务商,包含传统的域名提供商和公有云服务商。

配置本地host文件

如果没有域名或者是使用的本地解决方案,如minikube,可以通过编辑 /etc/hosts文件添加IP地址来完成本地配置。这需要具备主机的admin/root权限。不同平台文件位置也不同,使用修改生效可能也需要做额外操作。例如,Windows上通常位于C:\Windows\System32\drivers\etc\hosts,而在最近版本的macOS上,需要运行sudo killall -HUP mDNSResponder来使修改生效。

编辑该文件添加如下内容:

1
<ip-address> alpaca.example.com bandicoot.example.com

有关<ip-address>,请使用Contour的对外IP地址。如果只有主机名(如AWS),可通过执行host -t a <address> 来获取IP地址(未来可能会发生改变)。

在完成测试后别忘了取消这些修改。

使用Ingress

现在我们已经配置好了Ingress控制器,我们就开始使用它吧。首先,通过如下命令创建一些上游(有时也称为后端)服务以供使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ kubectl create deployment be-default \
--image=gcr.io/kuar-demo/kuard-amd64:blue \
--replicas=3 \
--port=8080
$ kubectl expose deployment be-default
$ kubectl create deployment alpaca \
--image=gcr.io/kuar-demo/kuard-amd64:green \
--replicas=3 \
--port=8080
$ kubectl expose deployment alpaca
$ kubectl create deployment bandicoot \
--image=gcr.io/kuar-demo/kuard-amd64:purple \
--replicas=3 \
--port=8080
$ kubectl expose deployment bandicoot
$ kubectl get services -o wide

NAME CLUSTER-IP ... PORT(S) ... SELECTOR
alpaca 10.115.245.13 ... 8080/TCP ... run=alpaca
bandicoot 10.115.242.3 ... 8080/TCP ... run=bandicoot
be-default 10.115.246.6 ... 8080/TCP ... run=be-default
kubernetes 10.115.240.1 ... 443/TCP ... <none>

最简单的用法

使用Ingress最简单的用法是将其获得的所有内容盲传给上游服务。kubectl中对Ingress的操作命令支持有限,所以我们使用YAML文件(见例8-1)。

例8-1 simple-ingress.yaml

1
2
3
4
5
6
7
8
9
10
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: simple-ingress
spec:
defaultBackend:
service:
name: alpaca
port:
number: 8080

通过kubectl apply创建这个Ingress:

1
2
$ kubectl apply -f simple-ingress.yaml
ingress.extensions/simple-ingress created

可通过kubectl getkubectl describe验证配置是否正确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
simple-ingress * 80 13m

$ kubectl describe ingress simple-ingress
Name: simple-ingress
Namespace: default
Address:
Default backend: alpaca:8080
(172.17.0.6:8080,172.17.0.7:8080,172.17.0.8:8080)
Rules:
Host Path Backends
---- ---- --------
* * alpaca:8080 (172.17.0.6:8080,172.17.0.7:8080,172.17.0.8:8080)
Annotations:
...

Events: <none>

这样配置后任何发往Ingress控制器的HTTP请求都会被转发给alpaca服务。现在可以通过服务的任何一个IP/CNAME访问kuardalpaca实例,在本例中即为alpaca.example.combandicoot.example.com。现在type: LoadBalancer服务还没体现出多少价值。下一节中会探索更复杂的配置。

使用主机名

将流量根据请求的属性进行转发时就开始变得有趣了。最常见的例子是让Ingress系统查看HTTP主机头(在原URL中设置为DNS域名),将流量根据头进行转发。我们再添加一个Ingress对象用于将指向alpaca.example.com的流量转发给alpaca服务(见例8-2)。

例8-2 host-ingress.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: host-ingress
spec:
defaultBackend:
service:
name: be-default
port:
number: 8080
rules:
- host: alpaca.example.com
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: alpaca
port:
number: 8080

通过kubectl apply创建这一Ingress:

1
2
$ kubectl apply -f host-ingress.yaml
ingress.extensions/host-ingress created

通过如下命令验证配置是否正确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
host-ingress alpaca.example.com 80 54s
simple-ingress * 80 13m

$ kubectl describe ingress host-ingress
Name: host-ingress
Namespace: default
Address:
Default backend: be-default:8080 (<none>)
Rules:
Host Path Backends
---- ---- --------
alpaca.example.com
/ alpaca:8080 (<none>)
Annotations:
...

Events: <none>

有一些会产生困扰的事。首先,涉及到了default-http-backend。这是一种部分Ingress控制器在没有其它方式时用于处理请求的惯例。这些控制器将请求发送给kube-system命名空间中名为default-http-backend的服务。这一惯例见于kubectl的客户端。其次是alpaca后端服务没有列出端点。这个kubectl的bug 在Kubernetes v1.14中得以修复。

不管怎么说,现在可以通过http://alpaca.example.com 来访问alpaca服务了。如果通过其它访问访问服务端点,会获取到默认服务。

使用路径

另一个有趣的场景是流量转发不仅依赖于主机名,也可取决于HTTP请求的路径。我们可以在paths中指定路径来轻松实现(见例8-3)。本例中,我们将所有来自http://bandicoot.example.com 的流量转发给bandicoot服务,但将http://bandicoot.example.com/a 的服务发送给alpaca服务。这一场景可用于在同一域名的不路径上托管多个服务。

例8-3 path-ingress.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-ingress
spec:
rules:
- host: bandicoot.example.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: bandicoot
port:
number: 8080
- pathType: Prefix
path: "/a/"
backend:
service:
name: alpaca
port:
number: 8080

在同一主机有多个路径位于Ingress系统中时,匹配最长前缀。因此,本例中,以/a/开头的流量会被转发给alpaca服务,而其它的所有流量(以/开头)都会转发到bandicoot服务。

在请求被代理到上游服务时,路径保持不变。也就是bandicoot.example.com/a/请求显示所配置请求主机名和路径的上游服务。上游服务在接收该子路径的流量时要处于就绪状态。kuard有一些特别的测试代码,它使用预定义的子路径(/a//b//c/)和根路径(/)一起提供响应。

清理

执行如下命令完成清理:

1
2
3
$ kubectl delete ingress host-ingress path-ingress simple-ingress
$ kubectl delete service alpaca bandicoot be-default
$ kubectl delete deployment alpaca bandicoot be-default

高级Ingress课题和技巧

Ingress支持一些高级特性。对这些特性的支持根据Ingress控制器的实现不同而不同,并且不同控制器可能对同一特性的实现存在些许差别。

很多扩展我都通过对Ingress对象添加注解进行暴露。注意这些注解很难验证、容易出错。大部分注解应用于整个Ingress对象,因此可能比想象的更通用。要为这些注解添加作用域,可以将单个Ingress对象拆分为多个Ingress对象。Ingress控制器会进行读取及合并。

运行多个Ingress控制器

有很多种Ingress控制器实现,你可能会想对单个集群运行多个Ingress控制器。处理这一状况,有IngressClass资源可让Ingress资源请求具体的实现。在创建Ingress资源时,使用spec.ingressClassName字段指定特定的Ingress资源。

注:Kubernetes 1.18版本之前,还不存在IngressClassName字段,使用的是kubernetes.io/ingress.class注解。虽然很多控制器仍支持它,推荐不要再使用这一注解,因未来很有可能会被控制器所弃用。

如不存在spec.ingressClassName注解,会使用默认Ingress控制器。通过对相应的IngressClass资源添加ingressclass.kubernetes.io/is-default-class注解来进行指定。

多个Ingress对象

如果指定了多个Ingress对象,Ingress控制器应全数读取并将它们合并为一个连续的配置。但如果指定的是重复或冲突的配置,结果就不一定了。不同Ingress控制器处理方式可能不同。即使是单个实现也可能因不易察觉的因素而出现不同。

Ingress和命名空间

Ingress与命名空间的配合有些方式不那么明显。首先,由于大量的安全问题,Ingress对象仅能指向同一命名空间的一个上游服务。也就是说不能使用Ingress对象指向另一个命名空间的子路径服务。

但不同命名空间中的多个Ingress对象可指向同一主机上的子路径。这些Ingress对象会进行合并,组成Ingress控制器的最终配置。

跨命名空间的操作意味着跨集群全局协调Ingress是有必要的。如果不小心,一个命名空间中的Ingress可能会导致另一个命名空间出现问题(或不确定行为)。

通过Ingress控制器不限制允许哪些命名空间指定哪些主机名或路径。高阶用户可以尝试使用自定义的准许控制器强制实现这种策略。在Ingress展望一节中也会讲到处理这问题的一些Ingress演进。

路径重写

有些Ingress控制器实现可支持路径重写。可使用它来修改所代理的HTTP请求路径。通常用Ingress对象上的注解来指定,应用于所有该对象所指定的请求。例如,如果使用NGINX Ingress控制器,可以指定一个nginx.ingress​.kuber⁠netes.io/rewrite-target: /注解。这有时可让上游服务操作子路径,即使原本它不具备这种功能。

很多实现不仅是支持路径重写,还实现了用正则表达式指定路径。例如NGINX控制器允许使用正则表达式捕获路径的一部分,然后在重写时使用这部分捕获的内容。其实现方式(以使用哪种正则表达式模式)则在各实现中并不相同。

但路径重写并不是银弹,有时可能会产生bug。很多web应用都假定可使用绝对路径在内部进行链接。在这种情况下,托管在/subpath的应用可能会在请求/时出现。而接下来可能会将用户发往/app-path。这时就会存在它是该应用的内部链接(这里本应是/subpath/app-path)还是来自另一个应用。出于这一原因,对复杂应用但凡能避免请不要使用子路径。

TLS 服务

在为网站提供服务时,使用TLS和HTTPS来提升服务越来越有必要了。Ingress(及大部分Ingress控制器)提供了这一支持。

首先,用户需要指定TLS的证书文件和密钥(参见例8-4)。也可通过kubectl create secret tls <secret-name> --cert <certificate-pem-file> --key <private-key-pem-file>来创建一个密钥。

例8-4 tls-secret.yaml

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Secret
metadata:
creationTimestamp: null
name: tls-secret-name
type: kubernetes.io/tls
data:
tls.crt: <base64 encoded certificate>
tls.key: <base64 encoded private key>

上传好证书后,可以在Ingress对象中引用它。这会指定一系统证书以及证书所作用的主机名(参见例8-5)。同样如若多个Ingress对象为同一主机名指定证书,结果是不确定的。

例8-5 tls-ingress.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-ingress
spec:
tls:
- hosts:
- alpaca.example.com
secretName: tls-secret-name
rules:
- host: alpaca.example.com
http:
paths:
- backend:
serviceName: alpaca
servicePort: 8080

上传、管理TLS密钥并不简单。此外,证书有时会产生大量的费用。要解决这一问题,有一个非营利证书机构Let’s Encrypt可通过API进行操作。因其是API操作,可配置Kubernetes集群来自动获取并安装TLS证书。配置有些麻烦,但一旦生效,使用会很简单。剩下的部分有一家英国创业公司Jetstack创建了开源项目cert-manager,已入驻CNCF。官网cert-manager.ioGitHub仓库上详细讲解了如何安装和使用cert-manager。

其它Ingress实现

有很多种Ingress控制器实现,都是基于Ingress基础对象添加了各种功能。这是一个非常活跃的生态。

首先,各云厂商有Ingress实现为其云暴露特定的7层负载均衡。这些控制器不是配置在Pod中运行的软件负载均衡,而是获取Ingress对象,使用它们通过API来配置云端负载均衡。这减少了集群的负载以及实施者的管理负担,但通常是收费的。

最常用的Ingress控制器可能是开源的NGINX Ingress控制器。也有基于收费的NGINX Plus的商业控制器。开源控制器基本上是读取Ingress对象,将它们合并到一个NGINX配置文件。然后发信号让NGINX进行使用新配置重启。开源的NGINX控制器有大量的特性和通过注解暴露的选项。

EmissaryGloo是另外两个基于Envoy的Ingress控制器,主要聚焦于API网关领域。

Traefik是一个用Go实现的反向代理,也可用作Ingress控制器。它具有很多特性以及对开发者友好的控制面板。

这只是冰山一角。Ingress生态系统非常活跃,有很多基于Ingress对象提供各种功能的新项目和商业方案。

Ingress展望

我们已经学习到,Ingress对象对配置L7负载均衡提供了非常有用的抽象,但它并没有扩展用户需要的所有功能,各种控制器实现进行的补充。Ingress对很多这种特性定义不足。实现的方式八仙过海,这让在实现之间配置的可迁移性变差。

另一个问题是Ingress的配置很容易出错。多对象合并为不同实现采取不同方式处理冲突打开了大门。此外,跨命名空间的合并也打破了命名空间隔离的想法。

Ingress创建的时候还没出现服务网格(以Istio和Linkerd为代表)。Ingress和服务网格的交集仍在定义中。Service mesh在服务网格一章中会进行详细讲解。

Kubernetes未来的HTTP负载均衡应该是网关API,正由Kubernetes致力于网络的特别兴趣小组(SIG)进行开发。Gateway API项目意在开发一个在Kubernetes中路由的更现代的API。虽然更关注HTTP负载均衡,Gateway也包含了控制4层(TCP)负载均衡的资源。Gateway API的开发还远未成熟,因些强烈推荐大家继续使用Kubernetes中现有的Ingress和Service资源。Gateway API现在的进展请见官网

小结

Ingress是Kubernetes的一套特有系统。它只是一种模式,必须要安装这种模式的控制器实现并单独管理。但它是以实用且成本高效地向用户暴露服务的关键系统。Kubernetes还在不断成熟,Ingress也会不断发展。