在之前的文章中,我们已经多次使用到了 Service
这个 Kubernetes 里重要的服务对象。而 Kubernetes 之所以需要 Service
,一方面是因为 Pod 的 IP 不是固定的,另一方面则是因为一组 Pod 实例之间总会有负载均衡的需求。
一个最典型的 Service 定义,如下所示:
apiVersion: v1
kind: Service
metadata:
name: hostnames
spec:
selector:
app: hostnames
ports:
- name: default
protocol: TCP
port: 80
targetPort: 9376
这个 Service 的例子,相信你不会陌生。其中,我使用了 selector
字段来声明这个 Service 只代理携带了 app=hostnames
标签的 Pod。并且,这个 Service 的 80 端口,代理的是 Pod 的 9376 端口。
然后,我们的应用的 Deployment,如下所示:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hostnames
spec:
selector:
matchLabels:
app: hostnames
replicas: 3
template:
metadata:
labels:
app: hostnames
spec:
containers:
- name: hostnames
image: k8s.gcr.io/serve_hostname
ports:
- containerPort: 9376
protocol: TCP
这个应用的作用,就是每次访问 9376 端口时,返回它自己的 hostname。
而被 selector 选中的 Pod,就称为 Service 的 Endpoints
,你可以使用 kubectl get ep
命令看到它们,如下所示:
$ kubectl get endpoints hostnames
NAME ENDPOINTS
hostnames 10.244.0.5:9376,10.244.0.6:9376,10.244.0.7:9376
需要注意的是,只有处于 Running 状态,且 readinessProbe
检查通过的 Pod,才会出现在 Service 的 Endpoints
列表里。并且,当某一个 Pod 出现问题时,Kubernetes 会自动把它从 Service 里摘除掉。
而此时,通过该 Service 的 VIP 地址 10.0.1.175,你就可以访问到它所代理的 Pod 了:
$ kubectl get svc hostnames
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
hostnames ClusterIP 10.0.1.175 80/TCP 5s
$ curl 10.0.1.175:80
hostnames-0uton
$ curl 10.0.1.175:80
hostnames-yp2kp
$ curl 10.0.1.175:80
hostnames-bvc05
这个 VIP
地址是 Kubernetes 自动为 Service 分配的。而像上面这样,通过三次连续不断地访问 Service 的 VIP 地址和代理端口 80,它就为我们依次返回了三个 Pod 的 hostname
。这也正印证了 Service 提供的是 Round Robin
方式的负载均衡。对于这种方式,我们称为:ClusterIP 模式的 Service
。
实际上,Service 是由 kube-proxy
组件,加上 iptables
来共同实现的。
举个例子,对于我们前面创建的名叫 hostnames
的 Service 来说,一旦它被提交给 Kubernetes
,那么 kube-proxy
就可以通过 Service 的 Informer
感知到这样一个 Service 对象的添加。而作为对这个事件的响应,它就会在宿主机上创建这样一条 iptables
规则(你可以通过 iptables-save
看到它),如下所示:
-A KUBE-SERVICES -d 10.0.1.175/32 -p tcp -m comment --comment "default/hostnames: cluster IP" -m tcp --dport 80 -j KUBE-SVC-NWV5X2332I4OT4T3
可以看到,这条 iptables
规则的含义是:凡是目的地址是 10.0.1.175
、目的端口是 80 的 IP 包,都应该跳转到另外一条名叫 KUBE-SVC-NWV5X2332I4OT4T3
的 iptables
链进行处理。
而我们前面已经看到,10.0.1.175
正是这个 Service 的 VIP。所以这一条规则,就为这个 Service
设置了一个固定的入口地址。并且,由于 10.0.1.175
只是一条 iptables
规则上的配置,并没有真正的网络设备,所以你 ping
这个地址,是不会有任何响应的。
那么,我们即将跳转到的 KUBE-SVC-NWV5X2332I4OT4T3
规则,又有什么作用呢?实际上,它是一组规则的集合,如下所示:
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-WNBA2IHDGP2BOBGZ
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-X3P2623AGDH6CDF3
-A KUBE-SVC-NWV5X2332I4OT4T3 -m comment --comment "default/hostnames:" -j KUBE-SEP-57KPRZ3JQVENLNBR
可以看到,这一组规则,实际上是一组随机模式(–mode random
)的 iptables
链。
而随机转发的目的地,分别是 KUBE-SEP-WNBA2IHDGP2BOBGZ
、KUBE-SEP-X3P2623AGDH6CDF3
和 KUBE-SEP-57KPRZ3JQVENLNBR
。
而这三条链指向的最终目的地,其实就是这个 Service
代理的三个 Pod。所以这一组规则,就是 Service 实现负载均衡的位置。
需要注意的是,
iptables
规则的匹配是从上到下逐条进行的,所以为了保证上述三条规则每条被选中的概率都相同,我们应该将它们的probability
字段的值分别设置为1/3
(0.333…)、1/2
和1
。
这么设置的原理很简单:第一条规则被选中的概率就是 1/3
;而如果第一条规则没有被选中,那么这时候就只剩下两条规则了,所以第二条规则的 probability
就必须设置为 1/2
;类似地,最后一条就必须设置为 1
。
通过查看上述三条链的明细,我们就很容易理解 Service 进行转发的具体原理了,如下所示:
-A KUBE-SEP-57KPRZ3JQVENLNBR -s 10.244.3.6/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-57KPRZ3JQVENLNBR -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.3.6:9376
-A KUBE-SEP-WNBA2IHDGP2BOBGZ -s 10.244.1.7/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-WNBA2IHDGP2BOBGZ -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.1.7:9376
-A KUBE-SEP-X3P2623AGDH6CDF3 -s 10.244.2.3/32 -m comment --comment "default/hostnames:" -j MARK --set-xmark 0x00004000/0x00004000
-A KUBE-SEP-X3P2623AGDH6CDF3 -p tcp -m comment --comment "default/hostnames:" -m tcp -j DNAT --to-destination 10.244.2.3:9376
可以看到,这三条链,其实是三条 DNAT
规则。但在 DNAT
规则之前,iptables
对流入的 IP 包还设置了一个“标志”(–set-xmark)
。这个“标志”的作用,我会在以后的文章再来说明。
而 DNAT
规则的作用,就是在 PREROUTING
检查点之前,也就是在路由之前,将流入 IP 包的目的地址和端口,改成–to-destination
所指定的新的目的地址和端口。可以看到,这个目的地址和端口,正是被代理 Pod 的 IP 地址和端口
。
这样,访问 Service VIP 的 IP 包经过上述 iptables
处理之后,就已经变成了访问具体某一个后端 Pod 的 IP 包了。不难理解,这些 Endpoints
对应的 iptables
规则,正是 kube-proxy
通过监听 Pod
的变化事件,在宿主机上生成并维护的。
以上,就是
Service
最基本的工作原理。
此外,你可能已经听说过,Kubernetes 的 kube-proxy
还支持一种叫作 IPVS
的模式。
其实,通过上面的讲解,可以看到,kube-proxy
通过 iptables
处理 Service
的过程,其实需要在宿主机上设置相当多的 iptables
规则。而且,kube-proxy
还需要在控制循环里不断地刷新这些规则来确保它们始终是正确的。
不难想到,当你的宿主机上有大量 Pod
的时候,成百上千条 iptables
规则不断地被刷新,会大量占用该宿主机的 CPU 资源,甚至会让宿主机“卡”在这个过程中。
而
IPVS
模式的 Service,就是解决这个问题的一个行之有效的方法。
IPVS
模式的工作原理,其实跟 iptables
模式类似。当我们创建了前面的 Service 之后,kube-proxy
首先会在宿主机上创建一个虚拟网卡
(叫作:kube-ipvs0),并为它分配 Service VIP
作为IP
地址,如下所示:
# ip addr
...
73:kube-ipvs0:,NOARP> mtu 1500 qdisc noop state DOWN qlen 1000
link/ether 1a:ce:f5:5f:c1:4d brd ff:ff:ff:ff:ff:ff
inet 10.0.1.175/32 scope global kube-ipvs0
valid_lft forever preferred_lft forever
而接下来,kube-proxy
就会通过 Linux 的 IPVS 模块,为这个 IP 地址设置三个 IPVS
虚拟主机,并设置这三个虚拟主机之间使用轮询模式 (rr)
来作为负载均衡策略。我们可以通过 ipvsadm
查看到这个设置,如下所示:
# ipvsadm -ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.102.128.4:80 rr
-> 10.244.3.6:9376 Masq 1 0 0
-> 10.244.1.7:9376 Masq 1 0 0
-> 10.244.2.3:9376 Masq 1 0 0
可以看到,这三个 IPVS
虚拟主机的 IP 地址和端口,对应的正是三个被代理的 Pod
。这时候,任何发往 10.102.128.4:80
的请求,就都会被 IPVS
模块转发到某一个后端 Pod 上了。
而相比于 iptables
,IPVS
在内核中的实现其实也是基于 Netfilter
的 NAT
模式,所以在转发这一层上,理论上 IPVS 并没有显著的性能提升。
但是,IPVS 并不需要在宿主机上为每个 Pod 设置 iptables
规则,而是把对这些“规则”
的处理放到了内核态
,从而极大地降低了维护这些规则的代价。这也正印证了我在前面提到过的,“将重要操作放入内核态”
是提高性能的重要手段。
不过需要注意的是,
IPVS
模块只负责上述的负载均衡和代理功能。而一个完整的 Service 流程正常工作所需要的包过滤
、SNAT
等操作,还是要靠iptables
来实现。只不过,这些辅助性的iptables
规则数量有限,也不会随着Pod
数量的增加而增加。
所以,在大规模集群里,建议你为 kube-proxy
设置–proxy-mode=ipvs
来开启这个功能。
在 Kubernetes 中,Service
和 Pod
都会被分配对应的 DNS A
记录(从域名解析 IP 的记录)。
对于 ClusterIP
模式的 Service
来说(比如我们上面的例子),它的 A 记录的格式是:..svc.cluster.local
。当你访问这条 A 记录的时候,它解析到的就是该 Service 的 VIP 地址。
而对于指定了 clusterIP=None
的 Headless Service
来说,它的 A 记录的格式也是:..svc.cluster.local
。但是,当你访问这条 A 记录的时候,它返回的是所有被代理的 Pod 的 IP 地址的集合。当然,如果你的客户端没办法解析这个集合的话,它可能会只会拿到第一个 Pod 的 IP 地址。
此外,对于 ClusterIP
模式的 Service
来说,它代理的 Pod
被自动分配的 A 记录的格式是:..pod.cluster.local
。这条记录指向 Pod 的 IP 地址。
而对 Headless Service
来说,它代理的 Pod 被自动分配的 A 记录的格式是:...svc.cluster.local
。这条记录也指向 Pod 的 IP 地址。
但如果你为 Pod
指定了 Headless Service
,并且 Pod 本身声明了 hostname
和 subdomain
字段,那么这时候 Pod 的 A 记录就会变成:...svc.cluster.local
,比如:
apiVersion: v1
kind: Service
metadata:
name: default-subdomain
spec:
selector:
name: busybox
clusterIP: None
ports:
- name: foo
port: 1234
targetPort: 1234
---
apiVersion: v1
kind: Pod
metadata:
name: busybox1
labels:
name: busybox
spec:
hostname: busybox-1
subdomain: default-subdomain
containers:
- image: busybox
command:
- sleep
- "3600"
name: busybox
在上面这个 Service
和 Pod
被创建之后,你就可以通过 busybox-1.default-subdomain.default.svc.cluster.local
解析到这个 Pod 的 IP 地址了。
需要注意的是,在 Kubernetes 里,
/etc/hosts
文件是单独挂载的,这也是为什么 kubelet 能够对hostname
进行修改并且 Pod 重建后依然有效的原因。这跟 Docker 的 Init 层是一个原理。
介绍了 Service 机制的工作原理。我们能够明白这样一个事实:Service
的访问信息在 Kubernetes
集群之外,其实是无效的。
这其实也容易理解:所谓 Service
的访问入口,其实就是每台宿主机上由 kube-proxy
生成的 iptables
规则,以及 kube-dns
生成的 DNS
记录。而一旦离开了这个集群,这些信息对用户来说,也就自然没有作用了。
所以,在使用 Kubernetes 的 Service
时,一个必须解决的问题就是:如何从外部(Kubernetes 集群之外),访问到 Kubernetes 里创建的 Service
?
这里最常用的一种方式就是:
NodePort
。
我来为你举个例子:
apiVersion: v1
kind: Service
metadata:
name: my-nginx
labels:
run: my-nginx
spec:
type: NodePort
ports:
- nodePort: 8080
targetPort: 80
protocol: TCP
name: http
- nodePort: 443
protocol: TCP
name: https
selector:
run: my-nginx
在这个 Service
的定义里,我们声明它的类型是,type=NodePort
。然后,我在 ports 字段里声明了 Service 的 8080
端口代理 Pod 的 80
端口,Service 的 443
端口代理 Pod 的 443 端口
。
当然,如果你不显式地声明 nodePort
字段,Kubernetes 就会为你分配随机的可用端口来设置代理。这个端口的范围默认是 30000-32767
,你可以通过 kube-apiserver
的–service-node-port-range
参数来修改它。
那么这时候,要访问这个 Service,你只需要访问:
<任何一台宿主机的IP地址>:8080
就可以访问到某一个被代理的 Pod 的 80 端口了。
而在理解 Service 的工作原理之后,NodePort
模式也就非常容易理解了。显然,kube-proxy
要做的,就是在每台宿主机上生成这样一条 iptables 规则:
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/my-nginx: nodePort" -m tcp --dport 8080 -j KUBE-SVC-67RL4FN6JRUPOJYM
而我在上一篇文章中已经讲到,KUBE-SVC-67RL4FN6JRUPOJYM
其实就是一组随机模式的 iptables
规则。所以接下来的流程,就跟 ClusterIP
模式完全一样了。需要注意的是,在 NodePort
方式下,Kubernetes 会在 IP 包离开宿主机发往目的 Pod 时,对这个 IP 包做一次 SNAT
操作,如下所示:
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE
可以看到,这条规则设置在 POSTROUTING
检查点,也就是说,它给即将离开这台主机的 IP 包,进行了一次 SNAT
操作,将这个 IP 包的源地址替换成了这台宿主机上的 CNI
网桥地址,或者宿主机本身的 IP 地址(如果 CNI 网桥不存在的话)。
当然,这个
SNAT
操作只需要对Service
转发出来的IP
包进行(否则普通的 IP 包就被影响了)。而iptables
做这个判断的依据,就是查看该 IP 包是否有一个“0x4000”的“
标志”。你应该还记得,这个标志正是在 IP 包被执行DNAT
操作之前被打上去的。
为什么一定要对流出的包做
SNAT
操作呢?
这里的原理其实很简单,如下所示:
client
\ ^
\ \
v \
node 1 <--- node 2
| ^ SNAT
| | --->
v |
endpoint
当一个外部的 client
通过 node 2
的地址访问一个 Service 的时候,node 2
上的负载均衡规则,就可能把这个 IP 包转发给一个在 node 1
上的 Pod
。这里没有任何问题。
而当 node 1
上的这个 Pod 处理完请求之后,它就会按照这个 IP 包的源地址发出回复。
可是,如果没有做 SNAT
操作的话,这时候,被转发来的 IP 包的源地址就是 client 的 IP
地址。所以此时,Pod
就会直接将回复发给client
。对于 client
来说,它的请求明明发给了 node 2
,收到的回复却来自 node 1
,这个 client 很可能会报错。
所以,在上图中,当 IP
包离开 node 2
之后,它的源 IP
地址就会被 SNAT
改成 node 2
的 CNI 网桥地址
或者 node 2 自己的地址
。这样,Pod 在处理完成之后就会先回复给 node 2
(而不是 client),然后再由 node 2
发送给 client
。
当然,这也就意味着这个 Pod 只知道该 IP 包来自于 node 2
,而不是外部的 client
。对于 Pod 需要明确知道所有请求来源的场景来说,这是不可以的。
所以这时候,你就可以将 Service 的 spec.externalTrafficPolicy
字段设置为 local
,这就保证了所有 Pod 通过 Service 收到请求之后,一定可以看到真正的、外部 client
的源地址。
从外部访问 Service 的第二种方式,适用于公有云上的 Kubernetes 服务。这时候,你可以指定一个 LoadBalancer
类型的 Service,如下所示:
---
kind: Service
apiVersion: v1
metadata:
name: example-service
spec:
ports:
- port: 8765
targetPort: 9376
selector:
app: example
type: LoadBalancer
在公有云
提供的 Kubernetes 服务里,都使用了一个叫作 CloudProvider
的转接层,来跟公有云本身的 API 进行对接。所以,在上述 LoadBalancer
类型的 Service 被提交后,Kubernetes 就会调用 CloudProvider 在公有云上为你创建一个负载均衡服务
,并且把被代理的 Pod 的 IP
地址配置给负载均衡服务做后端。
而第三种方式,是 Kubernetes 在 1.7 之后支持的一个新特性,叫作 ExternalName
。举个例子:
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
type: ExternalName
externalName: my.database.example.com
在上述 Service 的 YAML 文件中,我指定了一个 externalName=my.database.example.com
的字段。而且你应该会注意到,这个 YAML 文件里不需要指定 selector
。
这时候,当你通过 Service 的 DNS
名字访问它的时候,比如访问:my-service.default.svc.cluster.local
。那么,Kubernetes 为你返回的就是my.database.example.com。所以说,ExternalName 类型的 Service,其实是在 kube-dns
里为你添加了一条 CNAME 记录
。这时,访问 my-service.default.svc.cluster.local
就和访问 my.database.example.com
这个域名是一个效果了。
此外,Kubernetes 的 Service 还允许你为 Service 分配公有 IP 地址
,比如下面这个例子:
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
externalIPs:
- 80.11.12.10
在上述 Service 中,我为它指定的 externalIPs=80.11.12.10
,那么此时,你就可以通过访问 80.11.12.10:80
访问到被代理的 Pod 了。不过,在这里 Kubernetes 要求 externalIPs
必须是至少能够路由到一个 Kubernetes 的节点
。
实际上,在理解了
Kubernetes Service
机制的工作原理之后,很多与 Service 相关的问题,其实都可以通过分析 Service 在宿主机上对应的iptables 规则
(或者 IPVS 配置)得到解决。
详细讲解了从外部访问 Service 的三种方式(NodePort
、LoadBalancer
和 External Name
)和具体的工作原理
通过上述讲解不难看出,所谓 Service
,其实就是 Kubernetes
为 Pod
分配的、固定的、基于 iptables
(或者 IPVS)的访问入口。而这些访问入口代理的 Pod
信息,则来自于 Etcd
,由 kube-proxy
通过控制循环来维护。
并且,你可以看到,Kubernetes 里面的 Service 和 DNS 机制,也都不具备强多租户能力。
上面我们详细讲解了将 Service 暴露给外界的三种方法。其中有一个叫作 LoadBalancer
类型的 Service,它会为你在 Cloud Provider
(比如:Google Cloud 或者 OpenStack)里创建一个与该 Service 对应的负载均衡服务。
但是,你也应该能感受到,由于每个 Service
都要有一个负载均衡服务,所以这个做法实际上既浪费成本又高。作为用户,我其实更希望看到 Kubernetes 为我内置一个全局的负载均衡器
。然后,通过我访问的 URL,把请求转发给不同的后端 Service
。
这种全局的、为了代理不同后端 Service 而设置的负载均衡服务,就是 Kubernetes 里的Ingress
服务。
所以,
Ingress
的功能其实很容易理解:所谓Ingress
,就是 Service 的“Service”。
举个例子,假如我现在有这样一个站点:https://cafe.example.com
。其中,https://cafe.example.com/coffee
,对应的是“咖啡点餐系统”。而,https://cafe.example.com/tea
,对应的则是“茶水点餐系统”。这两个系统,分别由名叫 coffee
和 tea
这样两个 Deployment
来提供服务。
现在,我们可以使用 Kubernetes 的
Ingress
来创建一个统一的负载均衡器,从而实现当用户访问不同的域名时,能够访问到不同的Deployment
上述功能,在 Kubernetes 里就需要通过 Ingress
对象来描述,如下所示:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: cafe-ingress
spec:
tls:
- hosts:
- cafe.example.com
secretName: cafe-secret
rules:
- host: cafe.example.com
http:
paths:
- path: /tea
backend:
serviceName: tea-svc
servicePort: 80
- path: /coffee
backend:
serviceName: coffee-svc
servicePort: 80
在上面这个名叫 cafe-ingress.yaml 文件中,最值得我们关注的,是 rules
字段。在 Kubernetes 里,这个字段叫作:IngressRule
。
IngressRule
的 Key,就叫做:host
。它必须是一个标准的域名格式(Fully Qualified Domain Name)的字符串,而不能是 IP 地址。
而 host 字段定义的值,就是这个 Ingress
的入口。这也就意味着,当用户访问 cafe.example.com
的时候,实际上访问到的是这个 Ingress
对象。这样,Kubernetes 就能使用 IngressRule
来对你的请求进行下一步转发。
而接下来 IngressRule
规则的定义,则依赖于 path
字段。你可以简单地理解为,这里的每一个 path
都对应一个后端 Service
。所以在我们的例子里,我定义了两个 path
,它们分别对应 coffee
和 tea
这两个 Deployment 的 Service(即:coffee-svc
和 tea-svc
)。
通过上面的讲解,不难看到,所谓 Ingress 对象,其实就是 Kubernetes 项目对
“反向代理”
的一种抽象。
这就是为什么在每条 IngressRule
里,需要有一个 host
字段来作为这条 IngressRule
的入口,然后还需要有一系列 path
字段来声明具体的转发策略。这其实跟 Nginx
、HAproxy
等项目的配置文件的写法是一致的。
而有了 Ingress
这样一个统一的抽象,Kubernetes 的用户就无需关心 Ingress
的具体细节了。在实际的使用中,你只需要从社区里选择一个具体的 Ingress Controller
,把它部署在 Kubernetes 集群里即可。
然后,这个 Ingress Controller
会根据你定义的 Ingress
对象,提供对应的代理能力。目前,业界常用的各种反向代理项目,比如 Nginx、HAProxy、Envoy、Traefik
等,都已经为 Kubernetes 专门维护了对应的 Ingress Controller
。
接下来,我就以最常用的
Nginx Ingress Controller
为例,在我们前面用kubeadm
部署的Bare-metal
环境中,实践一下Ingress
机制的使用过程。
部署 Nginx Ingress Controller
的方法非常简单,如下所示:
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml
其中,在mandatory.yaml
这个文件里,正是 Nginx 官方为你维护的 Ingress Controller
的定义。我们来看一下它的内容:
kind: ConfigMap
apiVersion: v1
metadata:
name: nginx-configuration
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-ingress-controller
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
template:
metadata:
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
annotations:
...
spec:
serviceAccountName: nginx-ingress-serviceaccount
containers:
- name: nginx-ingress-controller
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.20.0
args:
- /nginx-ingress-controller
- --configmap=$(POD_NAMESPACE)/nginx-configuration
- --publish-service=$(POD_NAMESPACE)/ingress-nginx
- --annotations-prefix=nginx.ingress.kubernetes.io
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
# www-data -> 33
runAsUser: 33
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
- name: http
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
可以看到,在上述 YAML 文件中,我们定义了一个使用 nginx-ingress-controller
镜像的 Pod。需要注意的是,这个 Pod 的启动命令需要使用该 Pod 所在的 Namespace
作为参数。而这个信息,当然是通过 Downward API
拿到的,即:Pod 的 env 字段里的定义(env.valueFrom.fieldRef.fieldPath
)。
而这个 Pod 本身,就是一个监听 Ingress 对象以及它所代理的后端 Service 变化的控制器。
当一个新的 Ingress
对象由用户创建后,nginx-ingress-controller
就会根据 Ingress
对象里定义的内容,生成一份对应的 Nginx 配置文件(/etc/nginx/nginx.conf
),并使用这个配置文件启动一个 Nginx
服务。
而一旦
Ingress
对象被更新,nginx-ingress-controller
就会更新这个配置文件。需要注意的是,如果这里只是被代理的 Service 对象被更新,nginx-ingress-controller
所管理的Nginx
服务是不需要重新加载(reload
)的。这当然是因为nginx-ingress-controller
通过Nginx Lua
方案实现了Nginx Upstream
的动态配置。
此外,nginx-ingress-controller
还允许你通过 Kubernetes 的 ConfigMap
对象来对上述 Nginx 配置文件进行定制。这个 ConfigMap
的名字,需要以参数的方式传递给 nginx-ingress-controller
。而你在这个 ConfigMap 里添加的字段,将会被合并到最后生成的 Nginx 配置文件当中。
可以看到,一个
Nginx Ingress Controller
为你提供的服务,其实是一个可以根据Ingress
对象和被代理后端 Service 的变化,来自动进行更新的Nginx 负载均衡器
。
当然,为了让用户能够用到这个 Nginx,我们就需要创建一个 Service
来把 Nginx Ingress Controller
管理的 Nginx
服务暴露出去,如下所示:
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/provider/baremetal/service-nodeport.yaml
由于我们使用的是 Bare-metal 环境,所以 service-nodeport.yaml
文件里的内容,就是一个 NodePort
类型的 Service
,如下所示:
apiVersion: v1
kind: Service
metadata:
name: ingress-nginx
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
type: NodePort
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
- name: https
port: 443
targetPort: 443
protocol: TCP
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
可以看到,这个 Service 的唯一工作,就是将所有携带 ingress-nginx
标签的 Pod 的 80 和 433 端口暴露出去。
而如果你是公有云上的环境,你需要创建的就是 LoadBalancer 类型的 Service 了。
上述操作完成后,你一定要记录下这个 Service 的访问入口,即:宿主机的地址和 NodePort
的端口,如下所示:
$ kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx NodePort 10.105.72.96 80:30044/TCP,443:31453/TCP 3h
为了后面方便使用,我会把上述访问入口设置为环境变量:
$ IC_IP=10.168.0.2 # 任意一台宿主机的地址
$ IC_HTTPS_PORT=31453 # NodePort端口
在 Ingress Controller
和它所需要的 Service
部署完成后,我们就可以使用它了。
我们现在可以创建一开始定义的 Ingress
对象了,如下所示:
$ kubectl create -f cafe-ingress.yaml
这时候,我们就可以查看一下这个 Ingress 对象的信息,如下所示:
$ kubectl get ingress
NAME HOSTS ADDRESS PORTS AGE
cafe-ingress cafe.example.com 80, 443 2h
$ kubectl describe ingress cafe-ingress
Name: cafe-ingress
Namespace: default
Address:
Default backend: default-http-backend:80 ()
TLS:
cafe-secret terminates cafe.example.com
Rules:
Host Path Backends
---- ---- --------
cafe.example.com
/tea tea-svc:80 ()
/coffee coffee-svc:80 ()
Annotations:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal CREATE 4m nginx-ingress-controller Ingress default/cafe-ingress
可以看到,这个 Ingress
对象最核心的部分,正是 Rules
字段。其中,我们定义的 Host 是cafe.example.com
,它有两条转发规则(Path),分别转发给 tea-svc
和 coffee-svc
。
接下来,我们就可以通过访问这个 Ingress
的地址和端口,访问到我们前面部署的应用了,比如,当我们访问https://cafe.example.com:443/coffee
时,应该是 coffee
这个 Deployment 负责响应我的请求。我们可以来尝试一下:
$ curl --resolve cafe.example.com:$IC_HTTPS_PORT:$IC_IP https://cafe.example.com:$IC_HTTPS_PORT/coffee --insecureServer address: 10.244.1.56:80
Server name: coffee-7dbb5795f6-vglbv
Date: 03/Nov/2018:03:55:32 +0000
URI: /coffee
Request ID: e487e672673195c573147134167cf898
而当我访问https://cafe.example.com:433/tea
的时候,则应该是 tea 这个 Deployment 负责响应我的请求(Server name: tea-7d57856c44-lwbnp),如下所示:
$ curl --resolve cafe.example.com:$IC_HTTPS_PORT:$IC_IP https://cafe.example.com:$IC_HTTPS_PORT/tea --insecure
Server address: 10.244.1.58:80
Server name: tea-7d57856c44-lwbnp
Date: 03/Nov/2018:03:55:52 +0000
URI: /tea
Request ID: 32191f7ea07cb6bb44a1f43b8299415c
可以看到,Nginx Ingress Controller
为我们创建的 Nginx 负载均衡器
,已经成功地将请求转发给了对应的后端 Service。
还有一个场景就是,当请求没有匹配到任何一条 IngressRule
,那么会发生什么呢?
首先,既然 Nginx Ingress Controller
是用 Nginx 实现的,那么它当然会为你返回一个 Nginx
的 404 页面。
不过,Ingress Controller
也允许你通过 Pod 启动命令里的–default-backend-service
参数,设置一条默认规则,比如:–default-backend-service=nginx-default-backend
。这样,任何匹配失败的请求,就都会被转发到这个名叫 nginx-default-backend
的 Service。所以,你就可以通过部署一个专门的 Pod
,来为用户返回自定义的 404 页面
了。