从前面的学习,我们可以了解到Kubernetes暴露服务的方式目前常见的只有三种:LoadBlancer Service、NodePort Service、Ingress;而我们需要将集群内服务提供外界访问就会面临以下几个问题:
Pod 漂移问题
Kubernetes 具有强大的副本控制能力,能保证在任意副本(Pod)挂掉时自动启动一个新的,还可以进行动态扩容等。那么自然随着 Pod 的销毁和创建,Pod IP 也会动态变化;那么通过Pod IP去访问某个服务的话是不现实的,而且如果是多个pod实例,怎么做到负载均衡呢?这里借助于 Kubernetes 的 Service 机制,Service 可以以标签的形式选定一组带有指定标签的 Pod,并监控和自动负载他们的 Pod IP,那么我们向外暴露只暴露 Service IP 就行了;这就是 NodePort 模式:即在每个节点上开起一个端口,然后转发到内部 Pod IP 上。
端口管理问题
如果采用 NodePort 方式暴露服务需要的面临问题是,服务一旦多起来,NodePort 在每个节点上开启的端口会及其庞大,而且很容易造成端口冲突,效率低,难以维护。这时,我们可以在前端部署一个Nginx Pod直接对内进行转发。因为Pod与Pod之间是可以互相通信的,而Pod是可以共享宿主机的网络名称空间的,也就是说当在共享网络名称空间时,Pod上所监听的就是Node上的端口。那么这又该如何实现呢?简单的实现就是使用 DaemonSet 在每个 Node 上监听 80,然后写好规则,因为 Nginx 外面绑定了宿主机 80 端口(就像 NodePort),本身又在集群内,那么向后直接转发到相应 Service IP 就行了。
域名分配及动态更新问题
从上面的方法,采用 Nginx Pod 似乎已经解决了问题,但是其实这里面有一个很大缺陷:当每次有新服务加入又该如何修改 Nginx 配置呢??我们知道使用Nginx可以通过虚拟主机域名进行区分不同的服务,而每个服务通过upstream进行定义不同的负载均衡池,再加上location进行负载均衡的反向代理,在日常使用中只需要修改nginx.conf即可实现,那在K8S中又该如何实现这些配置呢???
假设后端的服务初始服务只有api,后面增加了web和docs服务,那么又该如何将这2个服务加入到Nginx Pod进行调度呢?总不能每次手动去修改nginx的相关配置然后重启nginx的pod吧!!此时 Ingress 出现了,Ingress 包含两大组件:Ingress Controller 和 Ingress。
Ingress是kubernetes中的一种资源对象,作用是定义请求如何转发到service的规则,简单的理解就是你原来需要改 Nginx 配置,然后配置各种域名对应哪个 Service,现在把这个动作抽象出来,变成一个 Ingress 对象,你可以用 yaml 创建,每次不要去改 Nginx 了,直接改 yaml 然后创建/更新就行了;那么问题来了:”Nginx 该怎么处理?”
而Ingress Controller 这东西就是解决 “Nginx 的处理方式” 的;Ingress Controoler 通过与 Kubernetes API 交互,动态的去感知集群中 Ingress 规则变化,然后对它进行解析,按照它自己模板生成一段 Nginx 配置,再写到 Nginx Pod 里,最后 reload 一下,工作流程如下图:
Ingress Controller的实现方式常见的有两种,nginx和traefik,工作架构如下,借用traefik官方的图:
通过设置可以将api.domain.com进来的流量路由到集群里api的pod上,你可以将domain.com/web流量路由到web的一组pod上(注意:ingress nginx是怎么识别那些Pod是那个应该加入那个upstream呢?这里就用到了service使用service的labels将一组服务关联起来,虽然创建了service但是ingress controller在解析service的是时候实际是解析到后面的Pod上。)
看起来按照nginx来理解转发是client——>nginx——>svc——>pod;
实际上转发是client—–>nginx——>pod,是直接负载到svc后面的Pod上面的
选择适合自己k8s集群版本的nginx-ingress,具体可以参考https://github.com/kubernetes/ingress-nginx/tree/controller-v1.8.1
,大致如下:
Ingress-NGINX version | k8s supported version | Alpine Version | Nginx Version | Helm Chart Version |
---|---|---|---|---|
v1.8.1 | 1.27,1.26, 1.25, 1.24 | 3.18.2 | 1.21.6 | 4.7.* |
v1.8.0 | 1.27,1.26, 1.25, 1.24 | 3.18.0 | 1.21.6 | 4.7.* |
v1.7.1 | 1.27,1.26, 1.25, 1.24 | 3.17.2 | 1.21.6 | 4.6.* |
v1.7.0 | 1.26, 1.25, 1.24 | 3.17.2 | 1.21.6 | 4.6.* |
v1.6.4 | 1.26, 1.25, 1.24, 1.23 | 3.17.0 | 1.21.6 | 4.5.* |
v1.5.1 | 1.25, 1.24, 1.23 | 3.16.2 | 1.21.6 | 4.4.* |
v1.4.0 | 1.25, 1.24, 1.23,1.22 | 3.16.2 | 1.19.10+ | 4.3.0 |
v1.3.1 | 1.24, 1.23, 1.22, 1.21, 1.20 | 3.16.2 | 1.19.10+ | 4.2.5 |
v1.3.0 | 1.24, 1.23, 1.22, 1.21, 1.20 | 3.16.0 | 1.19.10+ | 4.2.3 |
v1.2.1 | 1.23, 1.22, 1.21, 1.20, 1.19 | 3.14.6 | 1.19.10+ | 4.1.4 |
v1.1.3 | 1.23, 1.22, 1.21, 1.20, 1.19 | 3.14.4 | 1.19.10+ | 4.0.19 |
v1.1.2 | 1.23, 1.22, 1.21, 1.20, 1.19 | 3.14.2 | 1.19.9+ | 4.0.18 |
v1.1.1 | 1.23, 1.22, 1.21, 1.20, 1.19 | 3.14.2 | 1.19.9+ | 4.0.17 |
v1.1.0 | 1.22, 1.21, 1.20, 1.19 | 3.14.2 | 1.19.9+ | 4.0.13 |
v1.0.5 | 1.22, 1.21, 1.20, 1.19 | 3.14.2 | 1.19.9+ | 4.0.9 |
v1.0.4 | 1.22, 1.21, 1.20, 1.19 | 3.14.2 | 1.19.9+ | 4.0.6 |
v1.0.3 | 1.22, 1.21, 1.20, 1.19 | 3.14.2 | 1.19.9+ | 4.0.5 |
v1.0.2 | 1.22, 1.21, 1.20, 1.19 | 3.14.2 | 1.19.9+ | 4.0.3 |
v1.0.1 | 1.22, 1.21, 1.20, 1.19 | 3.14.2 | 1.19.9+ | 4.0.2 |
v1.0.0 | 1.22, 1.21, 1.20, 1.19 | 3.13.5 | 1.20.1 | 4.0.1 |
由于我的kubernetes版本比较老为1.19.16,按表看应该是nginx版本大于1.19.10的都可以
部署Ingress的步骤:(注意先后顺序,如果先执行了mandatory.yaml文件在执行service-nodeport.yaml文件使用kubectl logs -f 看ingress-controller的pod的日志会有很多报错信息)
1)下载Ingress-controller相关的yaml文件,并给Ingress-controller创建独立的名称空间命名为ingress-nginx;
2)按需修改下载的yaml文件
3)创建Ingress-controller的service,以实现接入集群外部流量;
4)部署Ingress-controller;
5)部署后端的服务,如nginx,并通过service进行暴露;
6)部署Ingress,编写规则,使Ingress-controller和后端服务的Pod组进行关联。
1)下载yaml文件
#部署nginx ingress服务的yaml文件
[root@k8s-m1 nginx-ingress]# wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.24.0/deploy/mandatory.yaml
#用于暴露服务供外部用户访问
[root@k8s-m1 nginx-ingress]# wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.24.0/deploy/provider/baremetal/service-nodeport.yaml
2)修改yaml文件
入口nginx-ingress的访问方式有两种
#添加了nodeport的端口,主要为了固定端口
[root@k8s-m1 nginx-ingress]# cat service-nodeport.yaml
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
nodePort: 30080
- name: https
port: 443
targetPort: 443
protocol: TCP
nodePort: 30443
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
3)部署Ingress Nginx
[root@k8s-m1 nginx-ingress]# kubectl apply -f service-nodeport.yaml -f mandatory.yaml
clusterrolebinding.rbac.authorization.k8s.io/nginx-ingress-clusterrole-nisa-binding created
deployment.apps/nginx-ingress-controller created
#检查部署情况,pod是否正常,日志是否有报错等
[root@k8s-m1 nginx-ingress]# kubectl get po -n ingress-nginx
NAME READY STATUS RESTARTS AGE
nginx-ingress-controller-7c9bd444c-bhsvn 1/1 Running 0 113s
[root@k8s-m1 nginx-ingress]# kubectl logs -n ingress-nginx nginx-ingress-controller-7c9bd444c-bhsvn
4)创建后端服务用于测试
[root@k8s-m1 nginx-ingress]# cat nginx-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
labels:
app: nginx
spec:
selector:
matchLabels:
tier: frontend
replicas: 2
template:
metadata:
labels:
tier: frontend
spec:
containers:
- name: nginx-gateway
image: nginx
ports:
- containerPort: 80
[root@k8s-m1 nginx-ingress]# cat nginx-svc.yml
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
ports:
- port: 80
selector:
tier: frontend
[root@k8s-m1 nginx-ingress]# kubectl apply -f nginx-deployment.yml -f nginx-svc.yml
5)创建ingress
ingress资源定义格式查看
[root@k8s-m1~]# kubectl explain ingress
编写ingress的配置清单:
[root@k8s-m1 nginx-ingress]# cat my-nginx-ingress.yml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress-fronted
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- host: nginx.margu.com
http:
paths:
- path: #配置访问路径,如果通过url进行转发,需要修改;空默认为访问的路径为"/"
backend: #配置后端服务
serviceName: nginx-service
servicePort: 80 #注意端口要与之前svc里面的一致
进入ingress nginx的pod可以看到nginx的配置里面已经有根据ingress规则生成的相关配置信息,如下:
[root@k8s-m1 nginx-ingress]# kubectl exec -it -n ingress-nginx nginx-ingress-controller-7c9bd444c-bhsvn -- /bin/bash
www-data@nginx-ingress-controller-7c9bd444c-bhsvn:/etc/nginx$ cat nginx.conf|more
## start server nginx.margu.com
server {
server_name nginx.margu.com ;
listen 80;
set $proxy_upstream_name "-";
location / {
set $namespace "default";
set $ingress_name "ingress-fronted";
set $service_name "nginx-service";
set $service_port "80";
set $location_path "/";
rewrite_by_lua_block {
balancer.rewrite()
}
header_filter_by_lua_block {
}
body_filter_by_lua_block {
}
log_by_lua_block {
balancer.log()
monitor.call()
}
找一台非集群的机器,向hosts文件内添加域名nginx.margu.com设置到集群的任意一个node节点ip上(因为我们使用的是nodeport的方式暴露的ingress nginx,所以每个节点都会有端口暴露出来),打开浏览器访问nginx.margu.com即可发现集群内的nginx已经暴露在集群外。如下:
配置域名解析(Node节点IP),如我的环境以下任意一条域名解析都是可以的
192.168.2.140 nginx.margu.com
192.168.2.141 nginx.margu.com
192.168.2.142 nginx.margu.com
6)多个Ingress controllers:
如果环境中运行多个Ingress controllers,则需要kubernetes.io/ingress.class: "nginx"指定将ingress对象加入到那个Ingress
controllers中
metadata:
name: foo
annotations:
kubernetes.io/ingress.class: "pre"
将以pre控制器作为目标,使它忽略其他种类控制器。
metadata:
name: foo
annotations:
kubernetes.io/ingress.class: "pro"
将以pro控制器作为目标,使它忽略其他种类控制器。
Rewrite(重写):
必须条件:当集群中有多个Ingress controller时需要通过指定Ingress.class注释来确保您的Ingress只针对一个Ingress controller生效,并且的集群中必须运行着这个Ingress controller。ingress可以使用以下标签来控制重写,详情可参考官网https://kubernetes.github.io/ingress-nginx/examples/rewrite/
名称 | 描述 | 值 |
---|---|---|
nginx.ingress.kubernetes.io/rewrite-target | Target URI where the traffic must be redirected | string |
nginx.ingress.kubernetes.io/ssl-redirect | Indicates if the location section is only accessible via SSL (defaults to True when Ingress contains a Certificate) | bool |
nginx.ingress.kubernetes.io/force-ssl-redirect | Forces the redirection to HTTPS even if the Ingress is not TLS Enabled | bool |
nginx.ingress.kubernetes.io/app-root | Defines the Application Root that the Controller must redirect if it’s in / context | string |
nginx.ingress.kubernetes.io/use-regex | Indicates if the paths defined on an Ingress use regular expressions | bool |
7)配置无host字段ingress
[root@k8s-m1 nginx-ingress]# cat no-domain-ingress.yml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: no-domain-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/server-alias: "nginx.margu.com"
nginx.ingress.kubernetes.io/use-regex: "true" #支持nginx的rui的正则匹配
spec:
rules:
- http:
paths:
- path: /(.*)
backend:
serviceName: nginx-service
servicePort: 80
[root@k8s-m1 nginx-ingress]#
在配置中我们没有host字段,这将在ingress-nginx配置中会以如下形式展示,此种方式我们可以用IP或域名的方式访问,因为我们还配置了nginx.ingress.kubernetes.io/server-alias: “nginx.margu.com”,所以当我们用域名访问时用nginx.margu.com访问。
但是需要注意的是,在使用无域名的Ingress转发规则时,将默认使用HTTPS安全协议进行转发。如需使用非安全的HTTP,还需要额外调整Ingress Controller的相关配置,通常在一个安全的网络环境下使用。
## start server _
server {
server_name _ nginx.margu.com;
listen 80 default_server reuseport backlog=4096;
set $proxy_upstream_name "-";
listen 443 default_server reuseport backlog=4096 ssl http2;
# PEM sha: e177c1bdc45a48a1cd43a53bd559f8254c442ce7
ssl_certificate /etc/ingress-controller/ssl/default-fake-certificate.pem;
ssl_certificate_key /etc/ingress-controller/ssl/default-fake-certificate.pem;
location ~* "^/(.*)" {
set $namespace "default";
set $ingress_name "no-domain-ingress";
set $service_name "nginx-service";
set $service_port "80";
set $location_path "/(.*)";
为了安全起见,我们一般都是要求客户端通过https进行访问,下面我们进行测试。注意:我们需要标准证书格式,不能将key+server+ca放在一个pem文件中。
1)准备证书
[root@k8s-m1 nginx-ingress]# openssl genrsa -out tls.key 2048
Generating RSA private key, 2048 bit long modulus
.......+++
.......................................................................+++
e is 65537 (0x10001)
[root@k8s-m1 nginx-ingress]# openssl req -new -x509 -key tls.key -out tls.crt -subj /C=CN/ST=Beijing/L=Beijing/O=DevOps/CN=nginx.margu.com
2)生成secret
[root@k8s-m1 nginx-ingress]# kubectl create secret tls nginx-ingress-secret --cert=tls.crt --key=tls.key
secret/nginx-ingress-secret created
[root@k8s-m1 nginx-ingress]# kubectl get secret
NAME TYPE DATA AGE
nginx-ingress-secret kubernetes.io/tls 2 8s
[root@k8s-m1 nginx-ingress]# kubectl describe secret nginx-ingress-secret
Name: nginx-ingress-secret
Namespace: default
Labels: <none>
Annotations: <none>
Type: kubernetes.io/tls
Data
====
tls.crt: 1289 bytes
tls.key: 1679 bytes
3)ingress规则定义并部署
[root@k8s-m1 nginx-ingress]# cat my-nginx-tls-ingress.yml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress-fronted
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
tls:
- hosts:
- nginx.margu.com
secretName: nginx-ingress-secret
rules:
- host: nginx.margu.com
http:
paths:
- path: #配置访问路径,如果通过url进行转发,需要修改;空默认为访问的路径为"/"
backend: #配置后端服务
serviceName: nginx-service
servicePort: 80
[root@k8s-m1 nginx-ingress]# kubectl apply -f my-nginx-tls-ingress.yml
Warning: extensions/v1beta1 Ingress is deprecated in v1.14+, unavailable in v1.22+; use networking.k8s.io/v1 Ingress
ingress.extensions/ingress-fronted-tls created
[root@k8s-m1 nginx-ingress]# kubectl get ingress
Warning: extensions/v1beta1 Ingress is deprecated in v1.14+, unavailable in v1.22+; use networking.k8s.io/v1 Ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress-fronted <none> * 80 24m
ingress-fronted-tls <none> nginx.margu.com 80, 443 16s
4)访问测试:https://nginx.margu.com:30443
5)不想添加端口访问,改使用正常的80/443端口进行访问
#先删除开始nginx-ingress的nodeport暴露方式
[root@k8s-m1 nginx-ingress]# kubectl delete -f nginx-svc.yml
#修改yaml,在template下面spec处添加hostNetwork和hostPID,如下
[root@k8s-m1 nginx-ingress]# vim mandatory.yaml
spec:
hostNetwork: true
hostPID: true
serviceAccountName: nginx-ingress-serviceaccount
containers:
- name: nginx-ingress-controller
image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.23.0
......
[root@k8s-m1 nginx-ingress]# kubectl apply -f mandatory.yaml
[root@k8s-m1 nginx-ingress]# kubectl get po -n ingress-nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-ingress-controller-554586bf4d-74pp2 1/1 Running 0 30s 192.168.2.142 k8s-m3 <none> <none>
I
测试:此种方法只会在nginx-ingress 的pod所在节点上暴露端口80和443,所以在本地hosts中添加域名解析时需要注意IP地址。如上,需要解析到192.168.2.142这个IP地址。实际使用中,我们可以通过打标签的方式在多个节点部署nginx-ingess的示例,然后通过前端负载均衡器进行负载。
hosts:192.168.2.142 nginx.margu.com
在实际使用中,为保证服务的高可用和负载均衡,我们往往需要部署多个nginx-ingress的pod,并使用haproxy+keepalived或者某些平台自带的负载均衡器进行负载。上面的例子里入口ingress-nginx使用的是nodePort的方式,Nodeport端口不是常用的web端口(但是可以修改Nodeport的范围改成web端口),如果当流量进来负载到某个node上的时候因为Ingress Controller的pod不在这个node上,会走这个node的kube-proxy转发到Ingress Controller的pod上,多转发了一次。(例如:我们Ingress Controller的pod在192.168.2.142上,但是域名解析的是192.168.2.140上,这个时候192.168.2.140这个node上的kube-proxy会把请求转发到192.168.2.142上)。故建议使用daemonset+nodeSelector的方式来ingress controller的Pod负载(每个Node节点一个ingress controller的Pod),并在前端添加使用一个负载均衡器。
不创建nginx svc,效率最高(不使用nodeport的方式进行暴露)。如果我们使用Nodeport的方式,流量是NodeIP->svc->ingress-controller(pod)这样的话会多走一层svc层,不管svc层是使用iptables还是lvs都会降低效率。如果使用hostNetwork的方式就是直接走Node节点的主机网络,唯一要注意的是hostNetwork下pod会继承宿主机的网络协议,也就是使用了主机的dns,会导致svc的请求直接走宿主机的上到公网的dns服务器而非集群里的dns server,需要设置pod的dnsPolicy: ClusterFirstWithHostNet即可解决
Ingress Controller部署方式:
使用 daemonSet + nodeSeletor 方式部署
1)创建一个名为ingress-nginx的service
首先我们要创建一个ingress-nginx的svc不然ingress nginx的log里会一直刷找不到ingress-nginx的svc不处理的话会狂刷log导致机器load过高,所以先创建一个同名的svc即可解决,例如创建一个不带选择器clusterip为None的。
[root@k8s-m1 nginx-ingress]# cat ingress-nginx-no-clusterip-service.yaml
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: ClusterIP
clusterIP: "None"
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
- name: https
port: 443
targetPort: 443
protocol: TCP
[root@k8s-m1 nginx-ingress]# kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx ClusterIP None <none> 80/TCP,443/TCP 20s
2)修改ingress-controller mandatory.yaml文件
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nginx-ingress-controller
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/part-of: ingress-nginx
spec:
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:
prometheus.io/port: "10254"
prometheus.io/scrape: "true"
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
serviceAccountName: nginx-ingress-serviceaccount
·······
[root@k8s-m1 nginx-ingress]# kubectl apply -f mandatory.yaml
[root@k8s-m1 nginx-ingress]# kubectl get pod -n ingress-nginx -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-ingress-controller-92rws 1/1 Running 0 4m35s 192.168.2.141 k8s-m2 <none> <none>
nginx-ingress-controller-ffbww 1/1 Running 0 4m35s 192.168.2.142 k8s-m3 <none> <none>
nginx-ingress-controller-qtnbg 1/1 Running 0 4m35s 192.168.2.140 k8s-m1 <none> <none>
说明:修改部署方式为DaemonSet以及删除replicas副本集参数,新增 hostNetwork: true 和 dnsPolicy: ClusterFirstWithHostNet参数。我们可以看到Pod的IP就直接是Node节点的IP。
3)使用负载均衡器
使用平台自带的负载均衡器或者自己搭建一个haproxy+keepalived(后面分享)
最后,不同版本之间略有不同,请根据大致思路选择适合自己的版本。
更多关于kubernetes的知识分享,请前往博客主页。编写过程中,难免出现差错,敬请指出