本文来自正在规划的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控制器配置
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.com
和bandicoot.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 get
和kubectl 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访问kuard
的alpaca
实例,在本例中即为alpaca.example.com
或bandicoot.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控制器
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.kubernetes.io/rewrite-target: /
注解。这有时可让上游服务操作子路径,即使原本它不具备这种功能。
很多实现不仅是支持路径重写,还实现了用正则表达式指定路径。例如NGINX控制器允许使用正则表达式捕获路径的一部分,然后在重写时使用这部分捕获的内容。其实现方式(以使用哪种正则表达式模式)则在各实现中并不相同。
/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.io或GitHub仓库上详细讲解了如何安装和使用cert-manager。
其它Ingress实现
有很多种Ingress控制器实现,都是基于Ingress基础对象添加了各种功能。这是一个非常活跃的生态。
首先,各云厂商有Ingress实现为其云暴露特定的7层负载均衡。这些控制器不是配置在Pod中运行的软件负载均衡,而是获取Ingress对象,使用它们通过API来配置云端负载均衡。这减少了集群的负载以及实施者的管理负担,但通常是收费的。
最常用的Ingress控制器可能是开源的NGINX Ingress控制器。也有基于收费的NGINX Plus的商业控制器。开源控制器基本上是读取Ingress对象,将它们合并到一个NGINX配置文件。然后发信号让NGINX进行使用新配置重启。开源的NGINX控制器有大量的特性和通过注解暴露的选项。
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也会不断发展。