一、ingress
在Kubernetes集群中,Ingress作为集群内服务对外暴露的访问接入点,几乎承载着集群内服务访问的所有流量。Ingress是Kubernetes中的一个资源对象,用来管理集群外部访问集群内部服务的方式。可以通过Ingress资源来配置不同的转发规则,从而实现根据不同的规则设置访问集群内不同的Service所对应的后端Pod。
下面是 Ingress 的一个简单示例,可将所有流量都发送到同一 Service:
ingress 资源
一个最小的 Ingress 资源示例:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx-example
rules:
- http:
paths:
- path: /testpath
pathType: Prefix
backend:
service:
name: test
port:
number: 80
上面这个 Ingress 资源的定义,一个路径为 /testpath
的路由,所有 /testpath/**
的请求,会被 Ingress 转发至名为 test 的服务的 80 端口的 /
路径下。
此外 Ingress 经常使用注解 annotations
来配置一些选项,当然这具体取决于 Ingress 控制器的实现方式,不同的 Ingress 控制器支持不同的注解。
不同的集群版本要使用不同的 apiVersion
。这里使用的 apiVersion 是 networking.k8s.io/v1
。networking.k8s.io
API组在Kubernetes 1.14版本中首次引入,当时使用的是extensions/v1beta1
版本。在随后的版本中,Kubernetes社区决定将Ingress资源从extensions
API组迁移到networking.k8s.io
API组,并于Kubernetes 1.19版本中正式引入networking.k8s.io/v1
版本的Ingress资源。
可以使用 kubectl explain ingress.spec
命令来了解 Ingress 资源清单的描述:
% kubectl explain ingress.spec
KIND: Ingress
VERSION: networking.k8s.io/v1
RESOURCE: spec <Object>
DESCRIPTION:
Spec is the desired state of the Ingress. More info:
https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
IngressSpec describes the Ingress the user wishes to exist.
FIELDS:
defaultBackend <Object>
DefaultBackend is the backend that should handle requests that don't match
any rule. If Rules are not specified, DefaultBackend must be specified. If
DefaultBackend is not set, the handling of requests that do not match any
of the rules will be up to the Ingress controller.
ingressClassName <string>
IngressClassName is the name of the IngressClass cluster resource. The
associated IngressClass defines which controller will implement the
resource. This replaces the deprecated `kubernetes.io/ingress.class`
annotation. For backwards compatibility, when that annotation is set, it
must be given precedence over this field. The controller may emit a warning
if the field and annotation have different values. Implementations of this
API should ignore Ingresses without a class specified. An IngressClass
resource may be marked as default, which can be used to set a default value
for this field. For more information, refer to the IngressClass
documentation.
rules <[]Object>
A list of host rules used to configure the Ingress. If unspecified, or no
rule matches, all traffic is sent to the default backend.
tls <[]Object>
TLS configuration. Currently the Ingress only supports a single TLS port,
443. If multiple members of this list specify different hosts, they will be
multiplexed on the same port according to the hostname specified through
the SNI TLS extension, if the ingress controller fulfilling the ingress
supports SNI.
从上面描述可以看出 Ingress 资源对象中有几个重要的属性:defaultBackend
、ingressClassName
、rules
、tls
。
rules
其中核心部分是 rules
属性的配置,每个路由规则都在下面进行配置:
host
:可选字段,上面我们没有指定 host 属性,所以该规则适用于通过指定 IP 地址的所有入站 HTTP 通信,如果提供了 host 域名,则rules
则会匹配该域名的相关请求,此外host
主机名可以是精确匹配(例如foo.bar.com
)或者使用通配符来匹配(例如*.foo.com
)。http.paths
:定义访问的路径列表,比如上面定义的/testpath
,每个路径都有一个由backend.service.name
和backend.service.port.number
定义关联的 Service 后端,在控制器将流量路由到引用的服务之前,host
和path
都必须匹配传入的请求才行。backend
:该字段其实就是用来定义后端的 Service 服务的,与路由规则中host
和path
匹配的流量会将发送到对应的 backend 后端去。
此外一般情况下在 Ingress 控制器中会配置一个 defaultBackend
默认后端,当请求不匹配任何 Ingress 中的路由规则的时候会使用该后端。defaultBackend
通常是 Ingress 控制器的配置选项,而非在 Ingress 资源中指定。
resource
backend
后端除了可以引用一个 Service 服务之外,还可以通过一个 resource
资源进行关联,Resource
是当前 Ingress 对象命名空间下引用的另外一个 Kubernetes 资源对象,但是需要注意的是 Resource
与 Service
配置是互斥的,只能配置一个,Resource
后端的一种常见用法是将所有入站数据导向带有静态资产的对象存储后端,如下所示:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-resource-backend
spec:
rules:
- http:
paths:
- path: /icons
pathType: ImplementationSpecific
backend:
resource:
apiGroup: k8s.example.com
kind: StorageBucket #这个类似于对象存储的后端之类的。一般我们会把静态资源放到对象存储上面,然后前面加上cdn之类的。
name: icon-assets
该 Ingress 资源对象描述了所有的 /icons
请求会被路由到同命名空间下的名为 icon-assets
的 StorageBucket
资源中去进行处理。
pathType
上面的示例中在定义路径规则的时候都指定了一个 pathType
的字段,事实上每个路径都需要有对应的路径类型,当前支持的路径类型有三种:
ImplementationSpecific
:该路径类型的匹配方法取决于IngressClass
,具体实现可以将其作为单独的 pathType 处理或者与Prefix
或Exact
类型作相同处理。Exact
:精确匹配 URL 路径,且区分大小写。Prefix
:基于以/
分隔的 URL 路径前缀匹配,匹配区分大小写,并且对路径中的元素逐个完成,路径元素指的是由/
分隔符分隔的路径中的标签列表。
Exact
比较简单,就是需要精确匹配 URL 路径,对于 Prefix
前缀匹配,需要注意如果路径的最后一个元素是请求路径中最后一个元素的子字符串,则不会匹配,例如 /foo/bar
可以匹配 /foo/bar/baz
, 但不匹配 /foo/barbaz
,可以查看下表了解更多的匹配场景(来自官网):
在某些情况下,Ingress 中的多条路径会匹配同一个请求,这种情况下最长的匹配路径优先,如果仍然有两条同等的匹配路径,则精确路径类型优先于前缀路径类型。
ingressClass
Kubernetes 1.18 起,正式提供了一个 IngressClass
资源,作用与 kubernetes.io/ingress.class
注解类似,因为可能在集群中有多个 Ingress 控制器,可以通过该对象来定义我们的控制器,例如:
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: external-lb # ingressClass 的名字,需要设置在ingress中
spec:
controller: nginx-ingress-internal-controller # ingress 控制器的名字
parameters:
apiGroup: k8s.example.com
kind: IngressParameters
name: external-lb
其中重要的属性是 metadata.name
和 spec.controller
,前者是这个 IngressClass
的名称,需要设置在 Ingress 中,后者是 Ingress 控制器的名称。
Ingress 中的 spec.ingressClassName
属性就可以用来指定对应的 IngressClass,并进而由 IngressClass 关联到对应的 Ingress 控制器,如:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
spec:
ingressClassName: external-lb # 上面定义的 IngressClass 对象名称
defaultBackend:
service:
name: myapp
port:
number: 80
不过需要注意的是 spec.ingressClassName
与老版本的 kubernetes.io/ingress.class
注解的作用并不完全相同,因为 ingressClassName
字段引用的是 IngressClass
资源的名称,IngressClass
资源中除了指定了 Ingress 控制器的名称之外,还可能会通过 spec.parameters
属性定义一些额外的配置。
比如 parameters 字段有一个 scope 和 namespace 字段,可用来引用特定于命名空间的资源,对 Ingress 类进行配置。 scope 字段默认为 Cluster,表示默认是集群作用域的资源。将 scope 设置为 Namespace 并设置 namespace 字段就可以引用某特定命名空间中的参数资源,比如:
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: external-lb
spec:
controller: nginx-ingress-internal-controller
parameters:
apiGroup: k8s.example.com
kind: IngressParameters
name: external-lb
namespace: external-configuration
scope: Namespace
由于一个集群中可能有多个 Ingress 控制器,所以我们还可以将一个特定的 IngressClass
对象标记为集群默认是 Ingress 类。只需要将一个 IngressClass 资源的 ingressclass.kubernetes.io/is-default-class
注解设置为 true 即可,这样未指定 ingressClassName
字段的 Ingress 就会使用这个默认的 IngressClass。
如果集群中有多个 IngressClass
被标记为默认,准入控制器将阻止创建新的未指定 ingressClassName
的 Ingress 对象。最好的方式还是确保集群中最多只能有一个 IngressClass
被标记为默认。
TLS
Ingress 资源对象还可以用来配置 Https 的服务,可以通过设定包含 TLS 私钥和证书的 Secret 来保护 Ingress。 Ingress 只支持单个 TLS 端口 443,如果 Ingress 中的 TLS 配置部分指定了不同的主机,那么它们将根据通过 SNI TLS 扩展指定的主机名 (如果 Ingress 控制器支持 SNI)在同一端口上进行复用。需要注意 TLS Secret 必须包含名为 tls.crt
和 tls.key
的键名,例如:
apiVersion: v1
kind: Secret
metadata:
name: testsecret-tls
namespace: default
data:
tls.crt: base64 编码的 cert
tls.key: base64 编码的 key
type: kubernetes.io/tls
在 Ingress 中引用此 Secret 将会告诉 Ingress 控制器使用 TLS 加密从客户端到负载均衡器的通道,我们需要确保创建的 TLS Secret 创建自包含 https-example.foo.com
的公用名称的证书,如下所示:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tls-example-ingress
spec:
tls:
- hosts:
- https-example.foo.com
secretName: testsecret-tls
rules:
- host: https-example.foo.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service1
port:
number: 80
现在了解了如何定义 Ingress 资源对象了,但是仅创建 Ingress 资源本身没有任何效果。还需要部署 Ingress 控制器,例如 ingress-nginx
,现在可以供大家使用的 Ingress 控制器有很多,比如 traefik、nginx-controller、Kubernetes Ingress Controller for Kong、HAProxy Ingress controller,当然你也可以自己实现一个 Ingress Controller,现在普遍用得较多的是 ingress-nginx、apisix 以及 traefik,traefik 的性能比 ingress-nginx 差,但是配置使用要简单许多。
实际上社区目前还在开发一组高配置能力的 API,被称为 Gateway API,新 API 会提供一种 Ingress 的替代方案,它的存在目的不是替代 Ingress,而是提供一种更具配置能力的新方案。
ingress-controller
Ingress:Ingress公开了从集群外部到集群内服务的HTTP和HTTPS路由的规则集合,而具体实现流量路由则是由Ingress Controller负责。
- Ingress:K8s中的一个抽象资源,给管理员提供一个暴露应用的入口定义方法
- Ingress Controller:根据Ingress生成具体的路由规则,并对Pod负载均衡器
Ingress Controller 可以理解为一个监听器,通过不断地监听 kube-apiserver,实时的感知后端 Service、Pod的变化,当得到这些信息变化后,Ingress Controller 再结合 Ingress 的配置,更新反向代理负载均衡器,达到服务发现的作用。其实这点和服务发现工具 consul、 consul-template
非常类似。
Ingress管理的负载均衡器,为集群提供全局的负载均衡能力。
使用流程:
- 部署Ingress Controller
- 创建Ingress规则
ingress Controller 怎么工作的?
Ingress Controller通过与 Kubernetes API 交互,动态的去感知集群中 Ingress 规则变化,然后读取它,按照自定义的规则,规则就是写明了哪个域名对应哪个service,生成一段 Nginx 配置,应用到管理的Nginx服务,然后热加载生效。以此来达到Nginx负载均衡器配置及动态更新的问题。
流程包流程:客户端 ->Ingress Controller(nginx) -> 分布在各节点Pod
二、ingress-nginx
Kubernetes Ingress 是一种 API 对象,它提供路由规则来管理外部用户对 Kubernetes 集群内的 Service 的访问。而 Ingress Controller 是 Ingress API 的真正实现。Ingress Controller 通常是一个 LoadBalancer,用于将外部流量路由到 Kubernetes 集群,负责 L4 - L7 网络服务。
Ingress 资源对象只是一个路由请求描述配置文件,要让其真正生效还需要对应的 Ingress 控制器才行,Ingress 控制器有很多,使用最多的是 ingress-nginx,它是基于 Nginx 的 Ingress 控制器。
运行原理
ingress-nginx
控制器主要是用来组装一个 nginx.conf
的配置文件,当配置文件发生任何变动的时候就需要重新加载 Nginx 来生效,但是并不会只在影响 upstream
配置的变更后就重新加载 Nginx,控制器内部会使用一个 lua-nginx-module
来实现该功能。
Kubernetes 控制器使用控制循环模式来检查控制器中所需的状态是否已更新或是否需要变更,所以 ingress-nginx
需要使用集群中的不同对象来构建模型,比如 Ingress、Service、Endpoints、Secret、ConfigMap 等可以生成反映集群状态的配置文件的对象,控制器需要一直 Watch 这些资源对象的变化,但是并没有办法知道特定的更改是否会影响到最终生成的 nginx.conf
配置文件,所以一旦 Watch 到了任何变化控制器都必须根据集群的状态重建一个新的模型,并将其与当前的模型进行比较,如果模型相同则就可以避免生成新的 Nginx 配置并触发重新加载,否则还需要检查模型的差异是否只和端点有关,如果是这样,则然后需要使用 HTTP POST 请求将新的端点列表发送到在 Nginx 内运行的 Lua 处理程序,并再次避免生成新的 Nginx 配置并触发重新加载,如果运行和新模型之间的差异不仅仅是端点,那么就会基于新模型创建一个新的 Nginx 配置了,这样构建模型最大的一个好处就是在状态没有变化时避免不必要的重新加载,可以节省大量 Nginx 重新加载。
下面简单描述了需要重新加载的一些场景:
- 创建了新的 Ingress 资源
- TLS 添加到现有 Ingress
- 从 Ingress 中添加或删除 path 路径
- Ingress、Service、Secret 被删除了
- Ingress 的一些缺失引用对象变可用了,例如 Service 或 Secret
- 更新了一个 Secret
对于集群规模较大的场景下频繁的对 Nginx 进行重新加载显然会造成大量的性能消耗,所以要尽可能减少出现重新加载的场景。
处理流程
下图是 Nginx Ingress Controller(简称 IC)处理新 Ingress 资源的流程:
- 用户创建新 Ingress 资源
- 在集群内部,IC 进程拥有资源的缓存。该缓存仅包含 IC 感兴趣的资源,比如 Ingress。缓存通过监听资源变更的方式,保持与 Kubernetes API 同步
- 一旦缓存有新 Ingress 资源,它通知控制循环关于被变更的资源
- 控制循环从缓存获取 Ingress 资源的最新版本。因为 Ingress 资源引用其它资源,比如 TLS Secret(Secret 也是 Kubernetes 的一种 API 对象),所以控制循环也获取所有被引用资源的最新版本
- 控制循环根据 TLS Secret 生成 TLS 证书和密钥,并且将它们写到文件系统
- 控制循环生成与 Ingress 资源相对应的 Nginx 配置文件,并且将它们写入文件系统
- 控制循环重载(reload)Nginx,等待 Nginx 重载成功。重载的部分包括:
- Nginx 读取 TLS 证书和密钥
- Nginx 读取配置文件
- 控制循环为 Ingress 资源发出事件,更新它的状态。如果重载失败,那么事件包含错误信息
更详细介绍可以参考官网:https://docs.nginx.com/nginx-ingress-controller/overview/design/
安装
安装 ingress-nginx
有多种方式,我们这里直接使用下面的命令进行一键安装:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.5.1/deploy/static/provider/cloud/deploy.yaml
会自动创建一个名为 ingress-nginx
的命名空间,会生成如下几个 Pod:
% kubectl get pods -n ingress-nginx
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-6bd9dddc78-8rl6q 1/1 Running 0 57d
ingress-nginx-controller-6bd9dddc78-ftjjs 1/1 Running 0 57d
ingress-nginx-controller-6bd9dddc78-pzj9v 1/1 Running 0 57d
还会创建两个 svc 对象
% kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller LoadBalancer 172.17.204.150 1.2.3.4 80:35096/TCP,443:31407/TCP 57d
ingress-nginx-controller-admission ClusterIP 172.17.13.12 <none> 443/TCP 57d
其中 ingress-nginx-controller-admission
是为准入控制器提供服务的,我们也是强烈推荐开启该准入控制器,这样当我们创建不合要求的 Ingress 对象后就会直接被拒绝了,另外一个 ingress-nginx-controller
就是 ingress 控制器对外暴露的服务,默认是一个 LoadBalancer 类型的 Service。
到这里 ingress-nginx
就部署成功了,安装完成后还会创建一个名为 nginx
的 IngressClass
对象:
% kubectl get ingressclass
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 57d
查看 ingressclass 的 yaml 文件
% kubectl get ingressclass nginx -o yaml
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
annotations:
ingressclass.kubernetes.io/is-default-class: "true"
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.3.1
name: nginx
resourceVersion: "872675654"
uid: 1344e2a9-0533-4098-bf50-2503a0ae94bb
spec:
controller: k8s.io/ingress-nginx
这里我们只提供了一个 controller
属性,对应的值和 ingress-nginx 的启动参数中的 controller-class
一致的。
- args:
- /nginx-ingress-controller
- --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller
- --election-id=ingress-nginx-leader
- --controller-class=k8s.io/ingress-nginx
- --ingress-class=nginx
- --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
- --validating-webhook=:8443
- --validating-webhook-certificate=/usr/local/certificates/cert
- --validating-webhook-key=/usr/local/certificates/key
第一个示例
# my-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
spec:
selector:
matchLabels:
app: my-nginx
template:
metadata:
labels:
app: my-nginx
spec:
containers:
- name: my-nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: my-nginx
labels:
app: my-nginx
spec:
ports:
- port: 80
protocol: TCP
name: http
selector:
app: my-nginx
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-nginx
spec:
ingressClassName: nginx # 使用 nginx 的 IngressClass(关联的 ingress-nginx 控制器)
rules:
- host: abc.abc.com # 将域名映射到 my-nginx 服务
http:
paths:
- path: /
pathType: Prefix
backend:
service: # 将所有请求发送到 my-nginx 服务的 80 端口
name: my-nginx
port:
number: 80
# 不过需要注意大部分Ingress控制器都不是直接转发到Service
# 而是只是通过Service来获取后端的Endpoints列表,直接转发到Pod,这样可以减少网络跳转,提高性能
直接创建上面的资源对象:
kubectl apply -f my-nginx.yaml -n wsj
deployment.apps/my-nginx created
service/my-nginx created
ingress.networking.k8s.io/my-nginx created
% kubectl get ingress -n wsj
NAME CLASS HOSTS ADDRESS PORTS AGE
my-nginx nginx abc.uisee.com 1.2.3.4 80 80s
如果不配置域名解析,可以需要绑定hosts,访问
% curl abc.abc.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
ingress-nginx
控制器的核心原理就是将 Ingress
这些资源对象映射翻译成 Nginx 配置文件 nginx.conf
,通过查看控制器中的配置文件来验证这点:
$ kubectl exec -it ingress-nginx-controller-68b46f9864-zs8k9 -n ingress-nginx -- cat /etc/nginx/nginx.conf
......
upstream upstream_balancer {
### Attention!!!
#
# We no longer create "upstream" section for every backend.
# Backends are handled dynamically using Lua. If you would like to debug
# and see what backends ingress-nginx has in its memory you can
# install our kubectl plugin https://kubernetes.github.io/ingress-nginx/kubectl-plugin.
# Once you have the plugin you can use "kubectl ingress-nginx backends" command to
# inspect current backends.
#
###
server 0.0.0.1; # placeholder
balancer_by_lua_block {
balancer.balance()
}
keepalive 320;
keepalive_time 1h;
keepalive_timeout 60s;
keepalive_requests 10000;
}
......
## start server abc.abc.com
server {
server_name abc.abc.com ;
listen 80 ;
listen [::]:80 ;
listen 443 ssl http2 ;
listen [::]:443 ssl http2 ;
set $proxy_upstream_name "-";
ssl_certificate_by_lua_block {
certificate.call()
}
location / {
set $namespace "default";
set $ingress_name "my-nginx";
set $service_name "my-nginx";
set $service_port "80";
set $location_path "/";
set $global_rate_limit_exceeding n;
rewrite_by_lua_block {
lua_ingress.rewrite({
force_ssl_redirect = false,
ssl_redirect = true,
force_no_ssl_redirect = false,
preserve_trailing_slash = false,
use_port_in_redirects = false,
global_throttle = { namespace = "", limit = 0, window_size = 0, key = { }, ignored_cidrs = { } },
})
balancer.rewrite()
plugins.run()
}
# be careful with `access_by_lua_block` and `satisfy any` directives as satisfy any
# will always succeed when there's `access_by_lua_block` that does not have any lua code doing `ngx.exit(ngx.DECLINED)`
# other authentication method such as basic auth or external auth useless - all requests will be allowed.
#access_by_lua_block {
#}
header_filter_by_lua_block {
lua_ingress.header()
plugins.run()
}
body_filter_by_lua_block {
plugins.run()
}
log_by_lua_block {
balancer.log()
monitor.call()
plugins.run()
}
port_in_redirect off;
set $balancer_ewma_score -1;
set $proxy_upstream_name "default-my-nginx-80";
set $proxy_host $proxy_upstream_name;
set $pass_access_scheme $scheme;
set $pass_server_port $server_port;
set $best_http_host $http_host;
set $pass_port $pass_server_port;
set $proxy_alternative_upstream_name "";
client_max_body_size 1m;
proxy_set_header Host $best_http_host;
# Pass the extracted client certificate to the backend
# Allow websocket connections
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Request-ID $req_id;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Host $best_http_host;
proxy_set_header X-Forwarded-Port $pass_port;
proxy_set_header X-Forwarded-Proto $pass_access_scheme;
proxy_set_header X-Forwarded-Scheme $pass_access_scheme;
proxy_set_header X-Scheme $pass_access_scheme;
# Pass the original X-Forwarded-For
proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;
# mitigate HTTPoxy Vulnerability
# https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
proxy_set_header Proxy "";
# Custom headers to proxied server
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
proxy_buffer_size 4k;
proxy_buffers 4 4k;
proxy_max_temp_file_size 1024m;
proxy_request_buffering on;
proxy_http_version 1.1;
proxy_cookie_domain off;
proxy_cookie_path off;
# In case of errors try the next upstream server before returning an error
proxy_next_upstream error timeout;
proxy_next_upstream_timeout 0;
proxy_next_upstream_tries 3;
proxy_pass http://upstream_balancer;
proxy_redirect off;
}
}
......
在 nginx.conf
配置文件中看到上面新增的 Ingress 资源对象的相关配置信息,不过需要注意的是现在并不会为每个 backend 后端都创建一个 upstream
配置块,现在是使用 Lua 程序进行动态处理的,所以没有直接看到后端的 Endpoints 相关配置数据。
kubectl 安装插件 ingress-nginx
默认 kubectl 不支持控制 ingress-nginx ,想要使用 kubectl 控制 ingress-nginx 需要安装插件。
% kubectl ingress-nginx --help
error: unknown command "ingress-nginx" for "kubectl"
第一步:先安装 krew
确保已安装 git,macOS/Linux 然后运行下面的
(
set -x; cd "$(mktemp -d)" &&
OS="$(uname | tr '[:upper:]' '[:lower:]')" &&
ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" &&
KREW="krew-${OS}_${ARCH}" &&
curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/${KREW}.tar.gz" &&
tar zxvf "${KREW}.tar.gz" &&
./"${KREW}" install krew
)
更新shell的profile文件
$ vim .zprofile
# 新增
export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"
$ source .zprofile
运行kubectl krew
以检查安装
% kubectl krew
krew is the kubectl plugin manager.
You can invoke krew through kubectl: "kubectl krew [command]..."
Usage:
kubectl krew [command]
Available Commands:
help Help about any command
index Manage custom plugin indexes
info Show information about an available plugin
install Install kubectl plugins
list List installed kubectl plugins
search Discover kubectl plugins
uninstall Uninstall plugins
update Update the local copy of the plugin index
upgrade Upgrade installed plugins to newer versions
version Show krew version and diagnostics
Flags:
-h, --help help for krew
-v, --v Level number for the log level verbosity
Use "kubectl krew [command] --help" for more information about a command.
第二步:安装 ingress-nginx 插件
kubectl krew install ingress-nginx
(MacOS 平台暂不支持)
% kubectl krew install ingress-nginx
Updated the local copy of plugin index.
Installing plugin: ingress-nginx
W1227 17:18:07.974339 42213 install.go:164] failed to install plugin "ingress-nginx": plugin "ingress-nginx" does not offer installation for this platform
failed to install some plugins: [ingress-nginx]: plugin "ingress-nginx" does not offer installation for this platform
nginx 配置示例
用于测试的应用都使用下面的 nginx 应用:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
template: # pod 模板
metadata:
labels:
app: nginx
spec:
containers:
- name: app
image: nginx # 该应用进程暴露的是80端口
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
selector:
app: nginx
ports:
- name: http
port: 80 # 这个是Service的端口
Basic Auth
在 Ingress 对象上配置一些基本的 Auth 认证,比如 Basic Auth,可以用 htpasswd
生成一个密码文件来验证身份验证。
- 先创建一个密码文件
% htpasswd -c auth foo
New password:
Re-type new password:
Adding password for user foo
- 利用上面生成的文件,创建一个secret对象
% kubectl create secret generic basic-auth --from-file=auth -n wsj
secret/basic-auth created
查看secret yaml
% kubectl get secret basic-auth -n wsj -o yaml
apiVersion: v1
data:
auth: Zm9vOiRhcHIxJExoLlJWYW1sJDE1VDlxTTZGcmJFeE9ubVM5QS5Qai8K
kind: Secret
metadata:
name: basic-auth
namespace: wsj
type: Opaque
- 对上面的 my-nginx deployment 创建一个具有 Basic-auth 的 ingress 对象
% kubectl apply -f my-nginx.yaml -n wsj
deployment.apps/nginx created
service/nginx created
# ingress-basic-auth.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-auth
annotations:
nginx.ingress.kubernetes.io/auth-type: basic # 认证类型
nginx.ingress.kubernetes.io/auth-secret: basic-auth # 包含 user/password 定义的 secret 对象名
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required - foo" # 要显示的带有适当上下文的消息,说明需要身份验证的原因
spec:
ingressClassName: nginx # 使用 nginx 的 IngressClass(关联的 ingress-nginx 控制器)
rules:
- host: abc.abc.com # 将域名映射到 nginx 服务
http:
paths:
- path: /
pathType: Prefix
backend:
service: # 将所有请求发送到 nginx 服务的 80 端口
name: nginx
port:
number: 80
% kubectl get ingress -n wsj
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress-with-auth nginx abc.abc.com 1.2.3.4 80 4m
- 通过curl命令,访问服务
% curl -v abc.abc.com
* Trying 113.31.105.66:80...
* Connected to abc.abc.com (113.31.105.66) port 80 (#0)
> GET / HTTP/1.1
> Host: abc.abc.com
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Date: Wed, 27 Dec 2023 10:12:51 GMT
< Content-Type: text/html
< Content-Length: 172
< Connection: keep-alive
< WWW-Authenticate: Basic realm="Authentication Required - foo"
<
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Connection #0 to host abc.abc.com left intact
访问出现 401 认证失败错误,然后带上配置的用户名和密码进行认证:
% curl -v abc.abc.com -u 'foo:abc123'
* Trying 113.31.105.66:80...
* Connected to abc.abc.com (113.31.105.66) port 80 (#0)
* Server auth using Basic with user 'foo'
> GET / HTTP/1.1
> Host: abc.abc.com
> Authorization: Basic Zm9vOmFiYzEyMw==
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Wed, 27 Dec 2023 10:16:13 GMT
< Content-Type: text/html
< Content-Length: 615
< Connection: keep-alive
< Last-Modified: Tue, 24 Oct 2023 13:46:47 GMT
< ETag: "6537cac7-267"
< Accept-Ranges: bytes
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
* Connection #0 to host abc.abc.com left intact
已经认证成功了。
- 配置外部 Basic Auth 认证
除了可以使用自己在本地集群创建的 Auth 信息之外,还可以使用外部的 Basic Auth 认证信息,比如使用 https://httpbin.org 的外部 Basic Auth 认证,创建如下所示的 Ingress 资源对象:
# ingress-basic-auth-external.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
# 配置外部认证服务地址
nginx.ingress.kubernetes.io/auth-url: https://httpbin.org/basic-auth/user/passwd
name: external-auth
namespace: default
spec:
ingressClassName: nginx
rules:
- host: abc.abc.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
URL Rewrite
ingress-nginx
很多高级的用法可以通过 Ingress 对象的 annotation
进行配置,比如常用的 URL Rewrite 功能。很多时候我们会将 ingress-nginx
当成网关使用,比如对访问的服务加上 /app
这样的前缀,在 nginx
的配置里面我们知道有一个 proxy_pass
指令可以实现:
location /app/ {
proxy_pass http://127.0.0.1/remote/;
}
proxy_pass
后面加了 /remote
这个路径,此时会将匹配到该规则路径中的 /app
用 /remote
替换掉,相当于截掉路径中的 /app
。同样的在 Kubernetes 中使用 ingress-nginx
又该如何来实现呢?可以使用 rewrite-target
的注解来实现这个需求,比如现在想要通过 abc.abc.com/gateway/
来访问到 Nginx 服务,则需要对访问的 URL 路径做一个 Rewrite,在 PATH 中添加一个 gateway 的前缀,关于 Rewrite 的操作在 ingress-nginx 官方文档中也给出对应的说明:
- 先创建一个ingress
按照要求需要在 path
中匹配前缀 gateway
,然后通过 rewrite-target
指定目标,Ingress 对象如下所示:
# ingress-rewrite.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rewrite
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: abc.abc.com
http:
paths:
- path: /gateway(/|$)(.*)
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
- 访问域名 /
更新后,可以预见到直接访问域名肯定是不行了,因为没有匹配 /
的 path 路径:
% curl abc.abc.com
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
- 带上 path 访问
带上 gateway
的前缀再去访问就正常了:
% curl abc.abc.com/gateway
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
可以看到已经可以访问到了,这是因为在 path
中通过正则表达式 /gateway(/|$)(.*)
将匹配的路径设置成了 rewrite-target
的目标路径了,所以访问 abc.abc.com/gateway
的时候实际上相当于访问的就是后端服务的 /
路径。
- 主域名跳转
要解决访问主域名出现 404 的问题,可以给应用设置一个 app-root
的注解,这样当访问主域名的时候会自动跳转到指定的 app-root
目录下面,如下所示:
# ingress-rewrite2.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rewrite
annotations:
nginx.ingress.kubernetes.io/app-root: /gateway/
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: abc.abc.com
http:
paths:
- path: /gateway(/|$)(.*)
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
- 访问主域名
这个时候我们更新应用后访问主域名 abc.abc.com
就会自动跳转到 abc.abc.com/gateway/
路径下面去了。
% curl abc.abc.com
<html>
<head><title>302 Found</title></head>
<body>
<center><h1>302 Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
- path后增加/
但是还有一个问题是的 path 路径其实也匹配了 /gateway
这样的路径,可能更加希望应用在最后添加一个 /
这样的 slash,同样可以通过 configuration-snippet
配置来完成,如下 Ingress 对象:
# ingress-rewrite3.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: rewrite
annotations:
nginx.ingress.kubernetes.io/app-root: /gateway/
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/configuration-snippet: |
rewrite ^(/gateway)$ $1/ redirect;
spec:
ingressClassName: nginx
rules:
- host: abc.abc.com
http:
paths:
- path: /gateway(/|$)(.*)
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
更新后应用就都会以 /
这样的 slash 结尾了。
灰度发布
在日常工作中经常需要对服务进行版本更新升级,所以经常会使用到滚动升级、蓝绿发布、灰度发布等不同的发布操作。而 ingress-nginx
支持通过 Annotations 配置来实现不同场景下的灰度发布和测试,可以满足金丝雀发布、蓝绿部署与 A/B 测试等业务场景。首先需要添加 nginx.ingress.kubernetes.io/canary:true
注解来启用 canary 功能,然后可以启用以下配置金丝雀的注解:
nginx.ingress.kubernetes.io/canary-by-header
:基于 Request Header 的流量切分,适用于灰度发布以及 A/B 测试。当 Request Header 设置为 always 时,请求将会被一直发送到 Canary 版本;当 Request Header 设置为 never 时,请求不会被发送到 Canary 入口;对于任何其他 Header 值,将忽略 Header,并通过优先级将请求与其他金丝雀规则进行优先级的比较。nginx.ingress.kubernetes.io/canary-by-header-value
:要匹配的 Request Header 的值,用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务。当 Request Header 设置为此值时,它将被路由到 Canary 入口。该规则允许用户自定义 Request Header 的值,必须与上一个 annotation (canary-by-header
) 一起使用。nginx.ingress.kubernetes.io/canary-by-header-pattern
:这与canary-by-header-value
的工作方式相同,只是它进行 PCRE 正则匹配。请注意,当设置canary-by-header-value
时,此注解将被忽略,当给定的 Regex 在请求处理过程中导致错误时,该请求将被视为不匹配。nginx.ingress.kubernetes.io/canary-weight
:基于服务权重的流量切分,适用于蓝绿部署,权重范围 0 - 100 按百分比将请求路由到 Canary Ingress 中指定的服务。权重为 0 意味着该金丝雀规则不会向 Canary 入口的服务发送任何请求,权重为 100 意味着所有请求都将被发送到 Canary 入口。nginx.ingress.kubernetes.io/canary-by-cookie
:基于 cookie 的流量切分,适用于灰度发布与 A/B 测试。用于通知 Ingress 将请求路由到 Canary Ingress 中指定的服务的 cookie。当 cookie 值设置为 always 时,它将被路由到 Canary 入口;当 cookie 值设置为 never 时,请求不会被发送到 Canary 入口;对于任何其他值,将忽略 cookie 并将请求与其他金丝雀规则进行优先级的比较。nginx.ingress.kubernetes.io/canary-weight-total
:流量总权重,如果未指定,则默认为 100。
需要注意的是金丝雀规则按优先顺序进行排序:
canary-by-header - > canary-by-cookie - > canary-weight
总的来说可以把以上的几个 annotation 规则划分为以下两类:
- 基于权重的 Canary 规则
- 基于用户请求的 Canary 规则
下面通过一个示例应用来对灰度发布功能进行说明。
第一步. 部署 Production 应用
首先创建一个 production 环境的应用资源清单:
# production.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: production
labels:
app: production
spec:
selector:
matchLabels:
app: production
template:
metadata:
labels:
app: production
spec:
containers:
- name: production
# arm架构使用该镜像:mirrorgooglecontainers/echoserver-arm:1.8
image: mirrorgooglecontainers/echoserver:1.10
ports:
- containerPort: 8080
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
name: production
labels:
app: production
spec:
ports:
- port: 80
targetPort: 8080
name: http
selector:
app: production
创建一个用于 production 访问的 ingress 资源对象:
# production-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: production
spec:
ingressClassName: nginx
rules:
- host: abc.abc.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: production
port:
number: 80
部署上面两个yaml文件
% kubectl apply -f production.yaml -n wsj
% kubectl apply -f production-ingress.yaml -n wsj
访问服务
% curl abc.abc.com
Hostname: production-5d84fbdb58-69lgg
Pod Information:
node name: 10.23.90.170
pod name: production-5d84fbdb58-69lgg
pod namespace: wsj
pod IP: 10.23.39.50
Server values:
server_version=nginx: 1.13.3 - lua: 10008
Request Information:
client_address=10.23.191.97
method=GET
real path=/
query=
request_version=1.1
request_scheme=http
request_uri=http://abc.abc.com:8080/
Request Headers:
accept=*/*
host=abc.abc.com
user-agent=curl/8.1.2
x-forwarded-for=1.2.3.4
x-forwarded-host=abc.abc.com
x-forwarded-port=80
x-forwarded-proto=http
x-forwarded-scheme=http
x-real-ip=1.2.3.4
x-request-id=b855c4bexxxxxxxxf8e16724c3c9d091
x-scheme=http
Request Body:
-no body in request-
第二步. 创建 Canary 版本
参考将上述 Production 版本的 production.yaml
文件,再创建一个 Canary 版本的应用。
# canary.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: canary
labels:
app: canary
spec:
selector:
matchLabels:
app: canary
template:
metadata:
labels:
app: canary
spec:
containers:
- name: canary
image: mirrorgooglecontainers/echoserver:1.10
ports:
- containerPort: 8080
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
---
apiVersion: v1
kind: Service
metadata:
name: canary
labels:
app: canary
spec:
ports:
- port: 80
targetPort: 8080
name: http
selector:
app: canary
部署
% kubectl apply -f canary.yaml -n wsj
deployment.apps/canary created
service/canary created
接下来就可以通过配置 Annotation 规则进行流量切分了。
第三步. Annotation 规则配置
1. 基于权重:基于权重的流量切分的典型应用场景就是蓝绿部署,可通过将权重设置为 0 或 100 来实现。例如,可将 Green 版本设置为主要部分,并将 Blue 版本的入口配置为 Canary。最初,将权重设置为 0,因此不会将流量代理到 Blue 版本。一旦新版本测试和验证都成功后,即可将 Blue 版本的权重设置为 100,即所有流量从 Green 版本转向 Blue。
创建一个基于权重的 Canary 版本的应用路由 Ingress 对象。
# canary-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: canary
annotations:
nginx.ingress.kubernetes.io/canary: "true" # 要开启灰度发布机制,首先需要启用 Canary
nginx.ingress.kubernetes.io/canary-weight: "30" # 分配30%流量到当前Canary版本
spec:
ingressClassName: nginx
rules:
- host: abc.abc.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: canary
port:
number: 80
部署
% kubectl apply -f canary-ingress.yaml -n wsj
ingress.networking.k8s.io/canary created
查看
% kubectl get ingress -n wsj
NAME CLASS HOSTS ADDRESS PORTS AGE
canary nginx abc.abc.com 113.31.105.66 80 72s
production nginx abc.abc.com 113.31.105.66 80 10m
在命令行终端中来不断访问这个应用,观察 Hostname 变化:
% for i in $(seq 1 10); do curl -s abc.abc.com | grep "Hostname"; done
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: canary-6cc5497cfd-wfbxn
Hostname: production-5d84fbdb58-69lgg
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: production-5d84fbdb58-69lgg
Hostname: canary-6cc5497cfd-wfbxn
Hostname: production-5d84fbdb58-69lgg
由于给 Canary 版本应用分配了 30% 左右权重的流量,所以上面访问 10 次有 4 次(不是一定)访问到了 Canary 版本的应用,符合预期。
2. 基于 Request Header: 基于 Request Header 进行流量切分的典型应用场景即灰度发布或 A/B 测试场景。
在上面的 Canary 版本的 Ingress 对象中新增一条 annotation 配置 nginx.ingress.kubernetes.io/canary-by-header: canary
(这里的 value 可以是任意值),使当前的 Ingress 实现基于 Request Header 进行流量切分,由于 canary-by-header
的优先级大于 canary-weight
,所以会忽略原有的 canary-weight
的规则。
annotations:
nginx.ingress.kubernetes.io/canary: "true" # 要开启灰度发布机制,首先需要启用 Canary
nginx.ingress.kubernetes.io/canary-by-header: canary # 基于header的流量切分
nginx.ingress.kubernetes.io/canary-weight: "30" # 会被忽略,因为配置了 canary-by-headerCanary版本
更新上面的 Ingress 资源对象后,在请求中加入不同的 Header 值,再次访问应用的域名。
注意:当 Request Header 设置为 never 或 always 时,请求将不会或一直被发送到 Canary 版本,对于任何其他 Header 值,将忽略 Header,并通过优先级将请求与其他 Canary 规则进行优先级的比较。
% for i in $(seq 1 10); do curl -s -H "canary: never" abc.abc.com | grep "Hostname"; done
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
在请求的时候设置了 canary: never
这个 Header 值,所以请求没有发送到 Canary 应用中去。如果设置为其他值呢:
% for i in $(seq 1 10); do curl -s -H "canary: other-value" abc.abc.com | grep "Hostname"; done
Hostname: canary-6cc5497cfd-wfbxn
Hostname: production-5d84fbdb58-69lgg
Hostname: canary-6cc5497cfd-wfbxn
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: canary-6cc5497cfd-wfbxn
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
由于请求设置的 Header 值为 canary: other-value
,所以 ingress-nginx 会通过优先级将请求与其他 Canary 规则进行优先级的比较,这里也就会进入 canary-weight: "30"
这个规则去。
这个时候可以在上一个 annotation (即 canary-by-header
)的基础上添加一条 nginx.ingress.kubernetes.io/canary-by-header-value: user-value
这样的规则,就可以将请求路由到 Canary Ingress 中指定的服务了。
annotations:
nginx.ingress.kubernetes.io/canary: "true" # 要开启灰度发布机制,首先需要启用 Canary
nginx.ingress.kubernetes.io/canary-by-header-value: user-value
nginx.ingress.kubernetes.io/canary-by-header: canary # 基于header的流量切分
nginx.ingress.kubernetes.io/canary-weight: "30" # 分配30%流量到当前Canary版本
同样更新 Ingress 对象后,重新访问应用,当 Request Header 满足 canary: user-value
时,所有请求就会被路由到 Canary 版本:
% for i in $(seq 1 10); do curl -s -H "canary: user-value" abc.abc.com | grep "Hostname"; done
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
3. 基于 Cookie:与基于 Request Header 的 annotation 用法规则类似。例如在 A/B 测试场景下,需要让地域为北京的用户访问 Canary 版本。那么当 cookie 的 annotation 设置为 nginx.ingress.kubernetes.io/canary-by-cookie: "users_from_Beijing"
,此时后台可对登录的用户请求进行检查,如果该用户访问源来自北京则设置 cookie users_from_Beijing
的值为 always
,这样就可以确保北京的用户仅访问 Canary 版本。
同样更新 Canary 版本的 Ingress 资源对象,采用基于 Cookie 来进行流量切分,
annotations:
nginx.ingress.kubernetes.io/canary: "true" # 要开启灰度发布机制,首先需要启用 Canary
nginx.ingress.kubernetes.io/canary-by-cookie: "users_from_Beijing" # 基于 cookie
nginx.ingress.kubernetes.io/canary-weight: "30" # 会被忽略,因为配置了 canary-by-cookie
更新上面的 Ingress 资源对象后,在请求中设置一个 users_from_Beijing=always
的 Cookie 值,再次访问应用的域名。
% for i in $(seq 1 10); do curl -s -b "users_from_Beijing=always" abc.abc.com | grep "Hostname"; done
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
Hostname: canary-6cc5497cfd-wfbxn
可以看到应用都被路由到了 Canary 版本的应用中去了,如果将这个 Cookie 值设置为 never,则不会路由到 Canary 应用中。
% for i in $(seq 1 10); do curl -s -b "users_from_Beijing=never" abc.abc.com | grep "Hostname"; done
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
Hostname: production-5d84fbdb58-69lgg
HTTPS
如果需要用 HTTPS 来访问这个应用的话,就需要监听 443 端口了,同样用 HTTPS 访问应用必然就需要证书,这里用 openssl
来创建一个自签名的证书:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=foo.bar.com"
然后通过 Secret 对象来引用证书文件:
# 要注意证书文件名称必须是 tls.crt 和 tls.key
$ kubectl create secret tls foo-tls --cert=tls.crt --key=tls.key
secret/who-tls created
这个时候就可以创建一个 HTTPS 访问应用的:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-auth
annotations:
# 认证类型
nginx.ingress.kubernetes.io/auth-type: basic
# 包含 user/password 定义的 secret 对象名
nginx.ingress.kubernetes.io/auth-secret: basic-auth
# 要显示的带有适当上下文的消息,说明需要身份验证的原因
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required - foo"
spec:
ingressClassName: nginx
tls: # 配置 tls 证书
- hosts:
- foo.bar.com
secretName: foo-tls
rules:
- host: foo.bar.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
除了自签名证书或者购买正规机构的 CA 证书之外,还可以通过一些工具来自动生成合法的证书,cert-manager 是一个云原生证书管理开源项目,可以用于在 Kubernetes 集群中提供 HTTPS 证书并自动续期,支持 Let's Encrypt/HashiCorp/Vault
这些免费证书的签发。在 Kubernetes 中,可以通过 Kubernetes Ingress 和 Let's Encrypt 实现外部服务的自动化 HTTPS。
TCP 与 UDP
由于在 Ingress 资源对象中没有直接对 TCP 或 UDP 服务的支持,要在 ingress-nginx
中提供支持,需要在控制器启动参数中添加 --tcp-services-configmap
和 --udp-services-configmap
标志指向一个 ConfigMap,其中的 key 是要使用的外部端口,value 值是使用格式 <namespace/service name>:<service port>:[PROXY]:[PROXY]
暴露的服务,端口可以使用端口号或者端口名称,最后两个字段是可选的,用于配置 PROXY 代理。
比如现在要通过 ingress-nginx
来暴露一个 MongoDB 服务,首先创建如下的应用:
# mongo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongo
labels:
app: mongo
spec:
selector:
matchLabels:
app: mongo
template:
metadata:
labels:
app: mongo
spec:
volumes:
- name: data
emptyDir: {}
containers:
- name: mongo
image: mongo:4.0
ports:
- containerPort: 27017
volumeMounts:
- name: data
mountPath: /data/db
---
apiVersion: v1
kind: Service
metadata:
name: mongo
spec:
selector:
app: mongo
ports:
- port: 27017
部署
% kubectl apply -f mongo.yaml -n wsj
deployment.apps/mongo created
service/mongo created
查看
% kubectl get svc mongo -n wsj
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
mongo ClusterIP 172.17.115.130 <none> 27017/TCP 53s
% kubectl get deploy mongo -n wsj
NAME READY UP-TO-DATE AVAILABLE AGE
mongo 1/1 1 1 90s
要通过 ingress-nginx
来暴露上面的 MongoDB 服务,需要创建一个如下所示的 ConfigMap:(因为涉及修改公共服务,仅做记录)
# tcp-ingress.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-tcp
namespace: ingress-nginx
data:
"27017": wsj/mongo:27017
然后在 ingress-nginx
的启动参数中添加 --tcp-services-configmap=$(POD_NAMESPACE)/ingress-nginx-tcp
这样的配置:
containers:
- args:
- /nginx-ingress-controller
- --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller
- --election-id=ingress-nginx-leader
- --controller-class=k8s.io/ingress-nginx
- --ingress-class=nginx
- --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
- --tcp-services-configmap=$(POD_NAMESPACE)/ingress-nginx-tcp
- --validating-webhook=:8443
- --validating-webhook-certificate=/usr/local/certificates/cert
- --validating-webhook-key=/usr/local/certificates/key
env:
重新部署即可。由于我们这里安装的 ingress-nginx
是通过 LoadBalancer 的 Service 暴露出去的,那么自然我们也需要通过 Service 去暴露我们这里的 tcp 端口,所以我们还需要更新 ingress-nginx 的 Service 对象,如下所示:
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.5.1
annotations:
lb.kubesphere.io/v1alpha1: openelb
protocol.openelb.kubesphere.io/v1alpha1: layer2
eip.openelb.kubesphere.io/v1alpha2: eip-pool
name: ingress-nginx-controller
namespace: ingress-nginx
spec:
externalTrafficPolicy: Local
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- appProtocol: http
name: http
port: 80
protocol: TCP
targetPort: http
- appProtocol: https
name: https
port: 443
protocol: TCP
targetPort: https
- name: mongo # 暴露 27017 端口
port: 27017
protocol: TCP
targetPort: 27017
selector:
app.kubernetes.io/component: controller
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/name: ingress-nginx
type: LoadBalancer
更新该 Service 对象即可:
☸ ➜ kubectl get svc ingress-nginx-controller -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx-controller LoadBalancer 10.96.127.133 172.18.0.10 80:30877/TCP,443:30615/TCP,27017:30696/TCP 3d18h
现在我们就可以通过 LB 地址 172.18.0.10
加上暴露的 27017 端口去访问 Mongo 服务了,比如我们这里在节点上安装了 MongoDB 客户端 mongosh
,使用命令 mongosh "mongodb://172.18.0.10:27017"
就可以访问到我们的 Mongo 服务了:
☸ ➜ mongosh "mongodb://172.18.0.10:27017"
Current Mongosh Log ID: 63f0814f292e3b15b3db5e2d
Connecting to: mongodb://172.18.0.10:27017/?directConnection=true&appName=mongosh+1.7.1
Using MongoDB: 4.0.28
Using Mongosh: 1.7.1
For mongosh info see: https://docs.mongodb.com/mongodb-shell/
# ......
test> show dbs
admin 32.00 KiB
config 12.00 KiB
local 32.00 KiB
test>
同样的我们也可以去查看最终生成的 nginx.conf
配置文件:
☸ ➜ kubectl exec -it ingress-nginx-controller-7dfbf8d769-jfv27 -n ingress-nginx -- cat /etc/nginx/nginx.conf
......
# TCP services
server {
preread_by_lua_block {
ngx.var.proxy_upstream_name="tcp-default-mongo-27017";
}
listen 27017;
listen [::]:27017;
proxy_timeout 600s;
proxy_next_upstream on;
proxy_next_upstream_timeout 600s;
proxy_next_upstream_tries 3;
proxy_pass upstream_balancer;
}
# UDP services
# Stream Snippets
TCP 相关的配置位于 stream
配置块下面。从 Nginx 1.9.13 版本开始提供 UDP 负载均衡,同样我们也可以在 ingress-nginx
中来代理 UDP 服务,比如我们可以去暴露 kube-dns
的服务,同样需要创建一个如下所示的 ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-udp
namespace: ingress-nginx
data:
53: "kube-system/kube-dns:53"
然后需要在 ingress-nginx
参数中添加一个 - --udp-services-configmap=$(POD_NAMESPACE)ingress-nginx-udp
这样的配置,Service 中也要加上暴露的 53 端口,然后重新更新即可。
全局配置
除了可以通过 annotations
对指定的 Ingress 进行定制之外,还可以配置 ingress-nginx
的全局配置,在控制器启动参数中通过标志 --configmap
指定了一个全局的 ConfigMap 对象,可以将全局的一些配置直接定义在该对象中即可:
containers:
- args:
- /nginx-ingress-controller
- --configmap=$(POD_NAMESPACE)/ingress-nginx-controller
......
这里用于全局配置的 ConfigMap 名就为 ingress-nginx-controller
:
☸ ➜ kubectl get configmap ingress-nginx-controller -n ingress-nginx
NAME DATA AGE
ingress-nginx-controller 1 7d
比如可以添加如下所示的一些常用配置:
➜ kubectl edit configmap ingress-nginx-controller -n ingress-nginx
apiVersion: v1
data:
allow-snippet-annotations: "true"
client-header-buffer-size: 32k
client-max-body-size: 5m
use-gzip: "true"
gzip-level: "7"
large-client-header-buffers: 4 32k
proxy-connect-timeout: 11s
proxy-read-timeout: 12s
keep-alive: "75" # 启用keep-alive,连接复用,提高QPS
keep-alive-requests: "100"
upstream-keepalive-connections: "10000"
upstream-keepalive-requests: "100"
upstream-keepalive-timeout: "60"
disable-ipv6: "true"
disable-ipv6-dns: "true"
max-worker-connections: "65535"
max-worker-open-files: "10240"
kind: ConfigMap
......
修改完成后 Nginx 配置会自动重载生效,可以查看 nginx.conf
配置文件进行验证:
☸ ➜ kubectl exec -it ingress-nginx-controller-gc582 -n ingress-nginx -- cat /etc/nginx/nginx.conf |grep large_client_header_buffers
large_client_header_buffers 4 32k;
此外往往还需要对 ingress-nginx
部署的节点进行性能优化,修改一些内核参数,使得适配 Nginx 的使用场景,一般是直接去修改节点上的内核参数(可以参考官方博客 https://www.nginx.com/blog/tuning-nginx/ 进行调整),为了能够统一管理,可以使用 initContainers
来进行配置:
initContainers:
- command:
- /bin/sh
- -c
- |
mount -o remount rw /proc/sys
sysctl -w net.core.somaxconn=655350 # 积压队列设置,具体的配置视具体情况而定
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -w fs.file-max=1048576
sysctl -w fs.inotify.max_user_instances=16384
sysctl -w fs.inotify.max_user_watches=524288
sysctl -w fs.inotify.max_queued_events=16384
image: busybox
imagePullPolicy: IfNotPresent
name: init-sysctl
securityContext:
capabilities:
add:
- SYS_ADMIN
drop:
- ALL
......
部署完成后通过 initContainers
就可以修改节点内核参数了,生产环境建议对节点内核参数进行相应的优化。性能优化需要有丰富的经验,关于 nginx 的性能优化可以参考文章 https://cloud.tencent.com/developer/article/1026833。
gRPC
ingress-nginx 控制器同样也支持 gRPC 服务的。gRPC 是 Google 开源的一个高性能 RPC 通信框架,通过 Protocol Buffers 作为其 IDL,在不同语言开发的平台上使用,同时 gRPC 基于 HTTP/2 协议实现,提供了多路复用、头部压缩、流控等特性,极大地提高了客户端与服务端的通信效率。
在 gRPC 服务中,客户端应用可以同本地方法一样调用到位于不同服务器上的服务端应用方法,可以很方便地创建分布式应用和服务。同其他 RPC 框架一样,gRPC 也需要定义一个服务接口,同时指定被远程调用的方法和返回类型。服务端需要实现被定义的接口,同时运行一个 gRPC 服务器来处理客户端请求。
这里使用 gRPC 示例应用 https://github.com/grpc/grpc-go/blob/master/examples/features/reflection/server/main.go 来进行说明。首先需要将该应用构建成一个容器镜像,可以用如下的 Dockerfile 进行构建:
FROM golang:buster as build
WORKDIR /go/src/greeter-server
RUN curl -o main.go https://raw.githubusercontent.com/grpc/grpc-go/master/examples/features/reflection/server/main.go && \
go mod init greeter-server && \
go mod tidy && \
go build -o /greeter-server main.go
FROM gcr.io/distroless/base-debian10
COPY --from=build /greeter-server /
EXPOSE 50051
CMD ["/greeter-server"]
然后就可以使用该镜像来部署应用了,对应的资源清单文件如下所示:
# grpc-ingress-app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: go-grpc-greeter-server
name: go-grpc-greeter-server
spec:
selector:
matchLabels:
app: go-grpc-greeter-server
template:
metadata:
labels:
app: go-grpc-greeter-server
spec:
containers:
- image: xxxx/go-grpc-greeter-server:v0.1 # 换成你自己的镜像
resources:
limits:
cpu: 100m
memory: 100Mi
requests:
cpu: 50m
memory: 50Mi
name: go-grpc-greeter-server
ports:
- containerPort: 50051
---
apiVersion: v1
kind: Service
metadata:
labels:
app: go-grpc-greeter-server
name: go-grpc-greeter-server
spec:
ports:
- port: 80
protocol: TCP
targetPort: 50051
selector:
app: go-grpc-greeter-server
type: ClusterIP
直接应用上面的资源清单即可:
☸ ➜ kubectl apply -f grpc-ingress.yaml
☸ ➜ kubectl get pods -l app=go-grpc-greeter-server
NAME READY STATUS RESTARTS AGE
go-grpc-greeter-server-bb776ccb4-wqkx6 1/1 Running 0 98s
☸ ➜ kubectl get svc -l app=go-grpc-greeter-server
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
go-grpc-greeter-server ClusterIP 10.96.83.53 <none> 80/TCP 104s
接下来就需要创建 Ingress 对象来暴露上面的 gRPC 服务了,由于 gRPC 服务只运行在 HTTPS 端口(默认 443)上,因此需要域名和对应的 SSL 证书,这里我们使用域名 abc.abc.com
和自签的 SSL 证书。
申请 SSL 证书
使用 Ingress 转发 gRPC 服务需要对应域名拥有 SSL 证书,使用 TLS 协议进行通信,这里使用 OpenSSL 来生成自签证书。
复制以下内容并保存至 openssl.cnf
文件中。
[ req ]
#default_bits = 2048
#default_md = sha256
#default_keyfile = privkey.pem
distinguished_name = req_distinguished_name
attributes = req_attributes
req_extensions = v3_req
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_min = 2
countryName_max = 2
stateOrProvinceName = State or Province Name (full name)
localityName = Locality Name (eg, city)
0.organizationName = Organization Name (eg, company)
organizationalUnitName = Organizational Unit Name (eg, section)
commonName = Common Name (eg, fully qualified host name)
commonName_max = 64
emailAddress = Email Address
emailAddress_max = 64
[ req_attributes ]
challengePassword = A challenge password
challengePassword_min = 4
challengePassword_max = 20
[v3_req]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = abc.abc.com
然后执行以下命令签署证书请求:
$ openssl req -new -nodes -keyout grpc.key -out grpc.csr -config openssl.cnf -subj "/C=CN/ST=Beijing/L=Beijing/O=abcabc/OU=TrainService/CN=abc.abc.com"
然后执行以下命令签署证书:
$ openssl x509 -req -days 3650 -in grpc.csr -signkey grpc.key -out grpc.crt -extensions v3_req -extfile openssl.cnf
命令执行成功后,可得到证书 grpc.crt
与私钥文件 grpc.key
,然后执行以下命令将名称为 grpc-secret 的 TLS Secret 添加到集群中。
$ kubectl create secret tls grpc-secret --key grpc.key --cert grpc.crt
创建 Ingress 资源
然后创建如下所示的 Ingress 对象来暴露 gRPC 服务:
# grpc-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# 必须要配置以指明后端服务为gRPC服务
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
name: grpc-ingress
namespace: default
spec:
ingressClassName: nginx
rules:
- host: abc.abc.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: go-grpc-greeter-server
port:
number: 80
tls:
- secretName: grpc-secret
hosts:
- abc.abc.com
注意在该对象中添加了一个注解 nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
,表示后端服务为 gRPC 服务,所以必须要加上,同样直接创建该对象即可。
☸ ➜ kubectl get ingress grpc-ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
grpc-ingress nginx abc.abc.com 172.18.0.10 80, 443 2m13s
测试
需要安装一个 gRPCurl 工具,该工具类似于 cURL,但用于与 gRPC 服务器交互的命令行工具。
本地安装 gRPCurl
工具后,可以输入 grpcurl <域名>:443 list
命令验证请求是否成功转发到后端服务。我们这里使用域名 abc.abc.com
以及自签证书,所以执行该命令的时候需要添加一个 -insecure
参数:
☸ ➜ grpcurl -insecure abc.abc.com:443 list
grpc.examples.echo.Echo
grpc.reflection.v1alpha.ServerReflection
helloworld.Greeter
☸ ➜ grpcurl -insecure abc.abc.com:443 helloworld.Greeter/SayHello
{
"message": "Hello "
}
正常会输出如上所示的结果,表明流量被 Ingress 成功转发到了后端 gRPC 服务。
参考
tke 上 nginx-ingress 实现 grpc 转发