部署在k8s集群中的应用如何暴露给外部的用户使用呢?可以通过NodePort和LoadBLancer类型的Service可以把应用暴露给外部用户使用,除此之外,k8s还提供了一个非常重要的资源对象可以用来暴露服务给外部用户,那就是Ingress。对于小规模的应用使用NodePort或许能够满足需求,但是当应用越来越多的时候,就会发现对于NodePort的管理就非常麻烦,这时候使用Ingress就非常方便,可以避免管理大量的端口。

Ingress其实就是从k8s集群外部访问集群的一个入口,将外部的请求转发到集群内不同的Service上,其实就相当于nginx、haproxy等负载均衡代理服务一样。

是否能够直接使用nginx呢?其实是不可以的,只使用nginx这种方式其实有很大缺陷,每次有新服务加入的时候怎么修改nginx配置?不可能手动或滚动更新前端的Nginx Pod,那在加上一个服务发现的工具如consul,貌似可以。但是Ingress实际上就是这么实现的。只是服务发现的功能自己实现了,不需要使用第三方的服务,然后加上一个域名规则定义,路由信息的刷新依靠Ingress Controller来提供。

Ingress Controller可以理解为一个监听器,通过不断的监听kube-apiserver,实时的感知后端Service、Pod的变化,当得到这些信息变化后,Ingress Controller再结合Ingress的配置,更新反向代理负载均衡器,达到服务发现的作用。其实这点和发现工具consul、soncul-template非常类似。
image.png现在可以使用的Ingress Controller有很多,比如traefik、nginx-controller、kubernetes Ingress for Kong、HAproxy Ingress controller,当然也可以自己实现一个Ingress Controller,现在普遍用的较多的是traefik和nginx-controller,traefik的性能较nginx-controller差,但是配置使用要简单许多。

安装

NGINX Ingress Controller是使用k8s ingress资源对象构建的,用ConfigMap来存储Nginx配置的一种Ingress Controller实现。

要使用Ingress对外暴露服务,就需要提前安装一个Ingress Controller,这里就先来安装 NGINX Ingress Controller,由于nginx-ingress所在的节点需要能够访问外网,这样域名可以解析到这些节点上直接使用,当然对于线上环境来说为了保证高可用,一般是需要运行多个nginx-ingress实例的,然后可以用一个nginx/haproxy作为入口,通过keepalived来访问边缘节点的VIP地址。

所谓边缘节点即集群内部用来向集群外暴露服务能力的节点,集群外部的服务通过该节点来调用集群内的服务,边缘节点是集群内外交流的一个Endpoint。

修改下资源清单文件:

  1. helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
  2. helm repo update
  3. helm fetch ingress-nginx/ingress-nginx
  4. tar -xvf ingress-nginx-3.15.2.tgz

测试环境只有master节点可以访问外网,在测试ingress-nginx固定到master节点,采用hostNetwork模式(生产环境可以使用LB+DaemonSet hostNetwork模式)。然后新建一个名为values-prod.yaml的values文件,用来覆盖ingress-nginx默认的value值,对应的数据如下:

# values-prod.yaml
controller:
  name: controller
  image:
    repository: cnych/ingress-nginx
    tag: "v0.41.2"
    digest: 

  dnsPolicy: ClusterFirstWithHostNet

  hostNetwork: true

  publishService:  # hostNetwork 模式下设置为false,通过节点IP地址上报ingress status数据
    enabled: false

  kind: DaemonSet

  tolerations:   # kubeadm 安装的集群默认情况下master是有污点,需要容忍这个污点才可以部署
  - key: "node-role.kubernetes.io/master"
    operator: "Equal"
    effect: "NoSchedule"

  nodeSelector:   # 固定到master1节点
    kubernetes.io/hostname: "master1"

  service:  # HostNetwork 模式不需要创建service
    enabled: false

defaultBackend:
  enabled: true
  name: defaultbackend
  image:
    repository: cnych/ingress-nginx-defaultbackend
    tag: "1.5"

然后使用helm安装ingress-nginx到指定的命名空间中:

➜ kubectl create ns ingress-nginx
➜ helm install --namespace ingress-nginx ingress-nginx ./ingress-nginx -f ./ingress-nginx/values-prod.yaml 
NAME: ingress-nginx
LAST DEPLOYED: Fri Dec 11 14:19:05 2020
NAMESPACE: ingress-nginx
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
The ingress-nginx controller has been installed.
Get the application URL by running these commands:
  export POD_NAME=$(kubectl --namespace ingress-nginx get pods -o jsonpath="{.items[0].metadata.name}" -l "app=ingress-nginx,component=controller,release=ingress-nginx")
  kubectl --namespace ingress-nginx port-forward $POD_NAME 8080:80
  echo "Visit http://127.0.0.1:8080 to access your application."

An example Ingress that makes use of the controller:

  apiVersion: networking.k8s.io/v1beta1
  kind: Ingress
  metadata:
    annotations:
      kubernetes.io/ingress.class: nginx
    name: example
    namespace: foo
  spec:
    rules:
      - host: www.example.com
        http:
          paths:
            - backend:
                serviceName: exampleService
                servicePort: 80
              path: /
    # This section is only required if TLS is to be enabled for the Ingress
    tls:
        - hosts:
            - www.example.com
          secretName: example-tls

If TLS is enabled for the Ingress, a Secret containing the certificate and key must also be provided:

  apiVersion: v1
  kind: Secret
  metadata:
    name: example-tls
    namespace: foo
  data:
    tls.crt: <base64 encoded cert>
    tls.key: <base64 encoded key>
  type: kubernetes.io/tls

部署完查看pod运行状态:

➜ kubectl get svc -n ingress-nginx
NAME                                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
ingress-nginx-controller-admission   ClusterIP   10.110.143.167   <none>        443/TCP   2m21s
ingress-nginx-defaultbackend         ClusterIP   10.104.156.141   <none>        80/TCP    2m21s
➜ kubectl get pods -n ingress-nginx
NAME                                            READY   STATUS    RESTARTS   AGE
ingress-nginx-controller-596955b554-vfmhq       1/1     Running   0          31s
ingress-nginx-defaultbackend-7bf9445d94-lkgw5   1/1     Running   0          3m52s
➜ POD_NAME=$(kubectl get pods -l app.kubernetes.io/name=ingress-nginx -n ingress-nginx -o jsonpath='{.items[0].metadata.name}')
➜ kubectl exec -it $POD_NAME -n ingress-nginx -- /nginx-ingress-controller --version
-------------------------------------------------------------------------------
NGINX Ingress controller
  Release:       v0.41.2
  Build:         d8a93551e6e5798fc4af3eb910cef62ecddc8938
  Repository:    https://github.com/kubernetes/ingress-nginx
  nginx version: nginx/1.19.4

-------------------------------------------------------------------------------

看到上面信息证明ingress-nginx部署成功了。

Ingress

创建一个ingress资源,如下:

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: extensions/v1beta1
kind: Ingress
metadata:
  name: my-nginx
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: ngdemo.qikqiak.com  # 将域名映射到 my-nginx 服务
    http:
      paths:
      - path: /
        backend:
          serviceName: my-nginx  # 将所有请求发送到 my-nginx 服务的 80 端口
          servicePort: 80     # 不过需要注意大部分Ingress controller都不是直接转发到Service
                            # 而是只是通过Service来获取后端的Endpoints列表,直接转发到Pod,这样可以减少网络跳转,提高性能

直接创建上面的资源对象:

➜ kubectl apply -f ngdemo.yaml
deployment.apps "my-nginx" created
service "my-nginx" created
ingress.extensions "my-nginx" created

在ingress资源对象中添加一个annotations: kubernetes.io/ingress.class: "nginx",这就是指定让这个Ingress通过nginx-ingress来处理。

上面的资源创建成功后,可以将域名ngdemo.qikqiak.com解析到ingress-nginx所在的边缘节点中的任意一个,当然也可以在本地/etc/hosts中添加对应的映射。

下图显示了客户端是如何通过Ingress Controller连接到其中一个Pod的流程,客户端首先对ngdemo.qikqiak.com执行DNS解析,得到Ingress Controller所在节点的IP,然后客户端像Ingress Controller发送HTTP请求,然后根据Ingress对象里面的描述匹配域名,找到对应的Service对象,并获取关联的Endpoints列表,将客户端请求转发给其中一个Pod。
image.png

URL Rewrite

Nginx Ingress Controller很多高级用法可以通过Ingress对象的annotation进行配置,比如常用的URL Rewrite功能,比如有个前端todo应用,代码地址:https://github.com/cnych/todo-app,直接部署这个应用进行测试:

➜ kubectl apply -f https://github.com/cnych/todo-app/raw/master/k8s/mongo.yaml
➜ kubectl apply -f https://github.com/cnych/todo-app/raw/master/k8s/web.yaml
➜ kubectl get pods
NAME                        READY   STATUS    RESTARTS   AGE
mongo-5c9fd978bb-txn9j      1/1     Running   0          149m
todo-566957d785-tdgs6       1/1     Running   0          3m31s
......
➜ kubectl get svc
NAME                       TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
kubernetes                 ClusterIP   10.96.0.1        <none>        443/TCP                      54d
mongo                      ClusterIP   10.96.95.11      <none>        27017/TCP                    150m
todo                       ClusterIP   10.111.105.47    <none>        3000/TCP                     145m
......

对应的Ingress资源对象如下:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: todo
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: todo.qikqiak.com
    http:
      paths:
      - path: /
        backend:
          serviceName: todo
          servicePort: 3000

这就是一个很常规的Ingress对象,部署该对象后,将域名解析后就可以正常访问到应用:
image.png
现在对访问的URL路径做一个Rewrite,比如在PATH中添加一个app的前缀,关于Rewrite的操作在Ingress-nginx官方文档中也给出对应的说明:
image.png
按照要求需要在path中匹配前缀app,然后通过rewrite-target制定目标,修改后的ingress对象如下:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: todo
  namespace: default
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  rules:
  - host: todo.qikqiak.com
    http:
      paths:
      - backend:
          serviceName: todo
          servicePort: 3000
        path: /app(/|$)(.*)

带上app的前缀去访问:
image.png
这时候页面可以正常访问,这是因为在path中通过正则表达式/app(/|$)(.*)将匹配的路径设置成rewrite-target的目标路径,所以访问todo.qikqiak/app的时候实际上相当于访问的就是后端服务的/路径,但是发现页面的样式丢失了:
image.png
这是因为静态资源路径是在/stylesheets路径下面,现在的 url rewrite过后,要正常访问也需要带上前缀才可以,这时候可以借助ingress-nginx中的configuration-snippet来对静态资源做一次跳转,如下:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: todo
  namespace: default
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/configuration-snippet: |
      rewrite ^/stylesheets/(.*)$ /app/stylesheets/$1 redirect;  # 添加 /app 前缀
      rewrite ^/images/(.*)$ /app/images/$1 redirect;  # 添加 /app 前缀
spec:
  rules:
  - host: todo.qikqiak.com
    http:
      paths:
      - backend:
          serviceName: todo
          servicePort: 3000
        path: /app(/|$)(.*)

更新ingress对象后,刷新页面显示正常:
image.png
要解决访问主域名出现404的问题,可以给应用设置一个app-root的注解,这样当访问主域名的时候会自动太哦转到指定的app-root目录下面,如下所示:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: todo
  namespace: default
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/app-root: /app/
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/configuration-snippet: |
      rewrite ^/stylesheets/(.*)$ /app/stylesheets/$1 redirect;  # 添加 /app 前缀
      rewrite ^/images/(.*)$ /app/images/$1 redirect;  # 添加 /app 前缀
spec:
  rules:
  - host: todo.qikqiak.com
    http:
      paths:
      - backend:
          serviceName: todo
          servicePort: 3000
        path: /app(/|$)(.*)

这时候更新应用后访问主域名http://todo.qikqiak.com就会自动跳转到[http://todo.qikqiak.com](http://todo.qikqiak.com)/app/路径下面,但是还有一个问题就是path路径其实也匹配了/app这样的路径,可能更加希望应用在最后添加/这样的slash,同样可以通过`configuration-snippet`配置来完成,如下:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: todo
  namespace: default
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/app-root: /app/
    nginx.ingress.kubernetes.io/rewrite-target: /$2
    nginx.ingress.kubernetes.io/configuration-snippet: |
      rewrite ^(/app)$ $1/ redirect;
      rewrite ^/stylesheets/(.*)$ /app/stylesheets/$1 redirect;
      rewrite ^/images/(.*)$ /app/images/$1 redirect;
spec:
  rules:
  - host: todo.qikqiak.com
    http:
      paths:
      - backend:
          serviceName: todo
          servicePort: 3000
        path: /app(/|$)(.*)

更新后就都会以/这样的slash结尾。

Basic Auth

还可以在Ingress Controller上面配置一些基本的Auth认证,比如Basic Auth,可以用htpasswd生成一个密码文件来验证身份。

➜ htpasswd -c auth foo
New password:
Re-type new password:
Adding password for user foo

然后根据上面的auth文件创建一个secret对象:

➜ kubectl create secret generic basic-auth --from-file=auth
secret/basic-auth created
➜ kubectl get secret basic-auth -o yaml
apiVersion: v1
data:
  auth: Zm9vOiRhcHIxJFNjcVhZcFN6JDc4Nm5ISFNaeDdwN2VscDM2WUo0YS8K
kind: Secret
metadata:
  creationTimestamp: "2019-12-08T06:40:39Z"
  name: basic-auth
  namespace: default
  resourceVersion: "9197951"
  selfLink: /api/v1/namespaces/default/secrets/basic-auth
  uid: 6b2aa299-b511-412e-85ea-d0e91e578af0
type: Opaque

然后创建一个具有Basic Auth的Ingress对象的my-nginx应用:

apiVersion: extensions/v1beta1
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:
  rules:
  - host: foo.bar.com
    http:
      paths:
      - path: /
        backend:
          serviceName: my-nginx
          servicePort: 80

创建资源对象,然后执行下面的命令:

➜ curl -v http://k8s.qikqiak.com -H 'Host: foo.bar.com'
* Rebuilt URL to: http://k8s.qikqiak.com/
*   Trying 123.59.188.12...
* TCP_NODELAY set
* Connected to k8s.qikqiak.com (123.59.188.12) port 80 (#0)
> GET / HTTP/1.1
> Host: foo.bar.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Server: openresty/1.15.8.2
< Date: Sun, 08 Dec 2019 06:44:35 GMT
< Content-Type: text/html
< Content-Length: 185
< 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>openresty/1.15.8.2</center>
</body>
</html>

出现401认证错误,然后带上配置的用户名和密码进行认证:

➜ curl -v http://k8s.qikqiak.com -H 'Host: foo.bar.com' -u 'foo:foo'
* Rebuilt URL to: http://k8s.qikqiak.com/
*   Trying 123.59.188.12...
* TCP_NODELAY set
* Connected to k8s.qikqiak.com (123.59.188.12) port 80 (#0)
* Server auth using Basic with user 'foo'
> GET / HTTP/1.1
> Host: foo.bar.com
> Authorization: Basic Zm9vOmZvbw==
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: openresty/1.15.8.2
< Date: Sun, 08 Dec 2019 06:46:27 GMT
< Content-Type: text/html
< Content-Length: 612
< Connection: keep-alive
< Vary: Accept-Encoding
< Last-Modified: Tue, 19 Nov 2019 12:50:08 GMT
< ETag: "5dd3e500-264"
< Accept-Ranges: bytes
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    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>

可以看到已经认证成功了。当然出来 Basic Auth 这一种简单的认证方式之外,NGINX Ingress Controller 还支持一些其他高级的认证,比如 OAUTH 认证之类的。

灰度发布

在日常工作中需要进行版本更新升级,所以经常会使用到滚动升级、蓝绿发布、灰度发布等不同的发布操作。而ingress-nginx支持通过Annotations配置来实现不同场景下的灰度发布和测试,可以满足金丝雀发布、蓝绿发布与A/B测试等业务场景。

ingress-nginx的Annotations支持以下4种Canary规则:

  • nginx.ingress.kubernetes.io/canary-by-header:基于Request Header的流量切分,适用于灰度发布以及A/B测试。当Request Header设置为Always时,请求将会被一直发送到Canary版本;当Request Header设置为never时,请求不会被发送到Canary入口;对于其他任何Header值,将忽略,并通过优先级将请求与其他金丝雀规则进行优先级的比较。
  • nginx.ingress.kubernetes.io/canary-by-header-value:要匹配的Request Header的值,用于通知Ingress将请求路由到Canary Ingress中指定的服务。当Request Heade设置为此值时,它将会被路由到Canary入口。该规则允许用户自定义Request Header的值,必须与上一个annotation(即canary-by-header)一直使用。
  • nginx.ingress.kubernetes.io/canary-weight:基于服务权重的流量切分,适用于蓝绿部署,权重范围0-100按百分比将请求路由到Canary Ingress中指定的服务。权重为0意味着该金丝雀规则不会向Canary入口的服务发送任何请求,权重为100意味着所有请求将被发送到Canary入口。
  • nginx.ingress.kubernetes.io/canary-ny-cookie:基于cookie的流量切分,适用于灰度发布与A/B测试。用于通知Ingress将请求路由到Canary Ingress中指定的服务的cookie。当cookie的值设置为always时,它将被路由到Canary入口;当cookie值设置为never时,请求不会被发送到Canary入口;对于其他任何值,将忽略cookie并将请求与其他金丝雀规则进行优先级比较。

需要注意的是金丝雀按优先级顺序进行排序:canary-by-header > canary-by-cookie > canary-weight

总的来说可以把以上的四个annotation规则划分为以下两类:

  • 基于权重的Canary规则

image.png

  • 基于用户请求的Canary规则

image.png

  1. 部署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
        image: cnych/echoserver
        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: extensions/v1beta1
kind: Ingress
metadata:
  name: production
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
  - host: echo.qikqiak.com
    http:
      paths:
      - backend:
          serviceName: production
          servicePort: 80

直接创建上面的几个资源对象:

➜ kubectl apply -f production.yaml
➜ kubectl apply -f production-ingress.yaml
➜ kubectl get pods -l app=production
NAME                         READY   STATUS    RESTARTS   AGE
production-856d5fb99-d6bds   1/1     Running   0          2m50s
➜ kubectl get ingress              
NAME         CLASS    HOSTS                ADDRESS        PORTS   AGE
production   <none>   echo.qikqiak.com     10.151.30.11   80      90s

应用部署成功后,将域名echo.qikqiak.com映射到master节点(ingress-nginx所在节点)的外网IP,然后即可正常访问应用:

➜ curl http://echo.qikqiak.com


Hostname: production-856d5fb99-d6bds

Pod Information:
    node name:  node1
    pod name:   production-856d5fb99-d6bds
    pod namespace:  default
    pod IP: 10.244.1.111

Server values:
    server_version=nginx: 1.13.3 - lua: 10008

Request Information:
    client_address=10.244.0.0
    method=GET
    real path=/
    query=
    request_version=1.1
    request_scheme=http
    request_uri=http://echo.qikqiak.com:8080/

Request Headers:
    accept=*/*
    host=echo.qikqiak.com
    user-agent=curl/7.64.1
    x-forwarded-for=171.223.99.184
    x-forwarded-host=echo.qikqiak.com
    x-forwarded-port=80
    x-forwarded-proto=http
    x-real-ip=171.223.99.184
    x-request-id=e680453640169a7ea21afba8eba9e116
    x-scheme=http

Request Body:
    -no body in request-
  1. 创建Canary版本

参考将上面production版本的文件,再创建一个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: cnych/echoserver
        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

接下来就可以通过配置Annotation规则进行流量切分。

  1. Annotation规则配置

    • 基于权重:基于权重的流量切分的典型应用场景就是蓝绿部署,可通过将权重设置为0或100来实现。例如,可将Green版本设置为主要部分,并将Blue版本的入口配置为Canary。最初,将权重设置为0,因此不会将流量代理到Blue版本,一旦新版本测试和验证都成功后,即可将Blue版本的权重设置为100,即所有流量从Green版本转向Blue。
      • 创建一个基于权重的Canary版本的应用路由Ingress对象 ```yaml

        canary-ingress.yaml

        apiVersion: extensions/v1beta1 kind: Ingress metadata: name: canary annotations: kubernetes.io/ingress.class: nginx nginx.ingress.kubernetes.io/canary: “true” # 要开启灰度发布机制,首先需要启用 Canary nginx.ingress.kubernetes.io/canary-weight: “30” # 分配30%流量到当前Canary版本 spec: rules:
    • host: echo.qikqiak.com http: paths:

      • backend: serviceName: canary servicePort: 80
        直接创建上面的资源对象即可:
        ```shell
        ➜ kubectl apply -f canary.yaml
        ➜ kubectl apply -f canary-ingress.yaml
        ➜ kubectl get pods
        NAME                         READY   STATUS    RESTARTS   AGE
        canary-66cb497b7f-48zx4      1/1     Running   0          7m48s
        production-856d5fb99-d6bds   1/1     Running   0          21m
        ......
        ➜ kubectl get svc
        NAME                       TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
        canary                     ClusterIP   10.106.91.106    <none>        80/TCP                       8m23s
        production                 ClusterIP   10.105.182.15    <none>        80/TCP                       22m
        ......
        ➜ kubectl get ingress
        NAME         CLASS    HOSTS                ADDRESS        PORTS   AGE
        canary       <none>   echo.qikqiak.com     10.151.30.11   80      108s
        production   <none>   echo.qikqiak.com     10.151.30.11   80      22m
        
        Canary 版本应用创建成功后,接下来我们在命令行终端中来不断访问这个应用,观察 Hostname 变化:
        ➜ for i in $(seq 1 10); do curl -s echo.qikqiak.com | grep "Hostname"; done
        Hostname: production-856d5fb99-d6bds
        Hostname: canary-66cb497b7f-48zx4
        Hostname: production-856d5fb99-d6bds
        Hostname: production-856d5fb99-d6bds
        Hostname: production-856d5fb99-d6bds
        Hostname: production-856d5fb99-d6bds
        Hostname: production-856d5fb99-d6bds
        Hostname: canary-66cb497b7f-48zx4
        Hostname: canary-66cb497b7f-48zx4
        Hostname: production-856d5fb99-d6bds
        
        由于给 Canary 版本应用分配了 30% 左右权重的流量,所以上面访问10次有3次访问到了 Canary 版本的应用,符合预期。
    • 基于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:
  kubernetes.io/ingress.class: nginx 
  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规则进行优先级的比较。

在请求的时候设置了canary:never,发现请求没有发送到Canary中去

➜ for i in $(seq 1 10); do curl -s -H "canary: never" echo.qikqiak.com | grep "Hostname"; done
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds

将请求的Header值为canary:other-value,所以ingress-nginx会通过优先级将请求与其他Canary规则进行优先级的比较,这里会进入canary-weight:30这个规则

➜ for i in $(seq 1 10); do curl -s -H "canary: other-value" echo.qikqiak.com | grep "Hostname"; done
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: canary-66cb497b7f-48zx4
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: production-856d5fb99-d6bds
Hostname: canary-66cb497b7f-48zx4
Hostname: production-856d5fb99-d6bds
Hostname: canary-66cb497b7f-48zx4

这时候在上一个版本的annotation的基础上添加一个nginx.ingress.kubernetes.io/canary-by-header-value: user-value这样的规则,就可以将请求路由到Canary Ingress中指定的服务

annotations:
  kubernetes.io/ingress.class: nginx 
  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" echo.qikqiak.com | grep "Hostname"; done
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4

基于cookie:与基于Request Header的的annotation用法规则类似。例如在A/B测试场景下,需要让地域为北京的用户访问Canary版本。那么当cookie的annotation设置为nginx.ingress.kubernetes.io/canary-by-cookie: "user_from_beijing",此时后台可对登录的用户请求进行检查,如果该用户访问源来自北京则设置cookie users_from_beijing的值为always,这样就可以确保北京的用户仅访问canary版本。
更新Canary版本的Ingress资源对象,基于cookie来进行流量切分:

annotations:
  kubernetes.io/ingress.class: nginx 
  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" echo.qikqiak.com | grep "Hostname"; done
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4
Hostname: canary-66cb497b7f-48zx4

可以看到应用都被路由到了 Canary 版本的应用中去了,如果将这个 Cookie 值设置为 never,则不会路由到 Canary 应用中。

HTTPS

如果需要用https来访问应用的话,就需要监听443端口,且需要证书,这里用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: extensions/v1beta1
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:
  rules:
  - host: foo.bar.com
    http:
      paths:
      - path: /
        backend:
          serviceName: my-nginx
          servicePort: 80
  tls:
  - hosts:
    - foo.bar.com
    secretName: foo-tls

除了自签名证书或者购买正规机构的 CA 证书之外,还可以通过 letsencrypt 来自动生成合法的证书。

Certmanager自动HTTPS

安装配置

cert-manager是一个云原生证书管理开源项目,用于在kubernetes集群中提供https证书并自动续费,支持Let's Encrypt/HashiCorp/Vault这些免费证书的签发。在kubernetes中,可通过kubernetes ingress和let’s encrypt实现外部服务的自动化https。
image.png
上面是官方给出的架构图,可以看到 cert-manager 在 Kubernetes 中定义了两个自定义类型资源:Issuer(ClusterIssuer)Certificate

  • Issure代表的是证书颁发者,可以定义各种提供者的证书颁发者,当前支持基于Let’s Encrypt/HashiCorp/Vault和CA的证书颁发者,还可以定义不同环境下的证书颁发者。
  • Certificate代表的是生成证书的请求,一般其中存在生成证书的元信息,如域名等等。

一旦在k8s中定义了上述两类资源,部署的cert-manager则会根据Issurecertificate生成tls证书,并将证书保存进k8s的Secret资源中,然后在Ingress资源中就可以引用到这些生成的Secret资源作为tls证书使用,对于已经生成的证书,还会定期检查证书的有效期,如即将超过有效期,还会自动续期。

要在 Kubernetes 集群上安装 cert-manager 也非常简单,官方提供了一个单一的资源清单文件,包含了所有的资源对象,所以直接安装即可:

# Kubernetes 1.16+
➜ kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.1.0/cert-manager.yaml
# Kubernetes <1.16
➜ kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.1.0/cert-manager-legacy.yaml

上面的命令会创建一个名为cert-manager的命名空间,安装大量的CRD以及AdmissionWebhook对象,可通过如下命令查看是否安装成功:

➜ kubectl get pods -n cert-manager
NAME                                      READY   STATUS    RESTARTS   AGE
cert-manager-5597cff495-q6rzh             1/1     Running   0          5m31s
cert-manager-cainjector-bd5f9c764-5sc7d   1/1     Running   0          5m31s
cert-manager-webhook-5f57f59fbc-mvcq4     1/1     Running   0          5m30s

正常情况下可以看到 cert-manager、cert-manager-cainjector 以及 cert-manager-webhook 这几个 Pod 处于 Running 状态。我们可以通过下面的测试来验证下是否可以签发基本的证书类型,创建一个 Issuer 资源对象来测试 webhook 工作是否正常(在开始签发证书之前,必须在群集中至少配置一个 Issuer 或 ClusterIssuer 资源):

➜ cat <<EOF > test-selfsigned.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: cert-manager-test
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: test-selfsigned
  namespace: cert-manager-test
spec:
  selfSigned: {}  # 配置自签名的证书机构类型
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: selfsigned-cert
  namespace: cert-manager-test
spec:
  dnsNames:
  - example.com
  secretName: selfsigned-cert-tls
  issuerRef:
    name: test-selfsigned
EOF

这里我们创建了一个名为 cert-manager-test 的命名空间,创建了一个自签名的 Issuer 证书颁发机构,然后使用这个 Issuer 来创建一个证书请求的 Certificate 对象,直接创建上面的资源清单即可:

➜ kubectl apply -f test-selfsigned.yaml
namespace/cert-manager-test created
issuer.cert-manager.io/test-selfsigned created
certificate.cert-manager.io/selfsigned-cert created

创建完成后可以检查新创建的证书状态,在cert-manager处理证书请求之前,可能需要稍等几秒:

➜ kubectl describe certificate -n cert-manager-test
Name:         selfsigned-cert
Namespace:    cert-manager-test
......
Spec:
  Dns Names:
    example.com
  Issuer Ref:
    Name:       test-selfsigned
  Secret Name:  selfsigned-cert-tls
Status:
  Conditions:
    Last Transition Time:  2020-12-12T03:29:07Z
    Message:               Certificate is up to date and has not expired
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Not After:               2021-03-12T03:29:06Z
  Not Before:              2020-12-12T03:29:06Z
  Renewal Time:            2021-02-10T03:29:06Z
  Revision:                1
Events:
  Type    Reason     Age   From          Message
  ----    ------     ----  ----          -------
  Normal  Issuing    6s    cert-manager  Issuing certificate as Secret does not exist
  Normal  Generated  6s    cert-manager  Stored new private key in temporary Secret resource "selfsigned-cert-sppz7"
  Normal  Requested  6s    cert-manager  Created new CertificateRequest resource "selfsigned-cert-z4nvl"
  Normal  Issuing    5s    cert-manager  The certificate has been successfully issued


从上面的 Events 事件中我们可以证书已经成功签发了,生成的证书存放在一个名为 selfsigned-cert-tls 的 Secret 对象下面:

➜ kubectl get secret -n cert-manager-test                            
NAME                  TYPE                                  DATA   AGE
default-token-t928x   kubernetes.io/service-account-token   3      64s
selfsigned-cert-tls   kubernetes.io/tls                     3      63s
➜ kubectl get secret -n cert-manager-test selfsigned-cert-tls -o yaml
apiVersion: v1
data:
  ca.crt: ......
  tls.crt: ......
  tls.key: ......
kind: Secret
......
  name: selfsigned-cert-tls
  namespace: cert-manager-test
  resourceVersion: "13461084"
  selfLink: /api/v1/namespaces/cert-manager-test/secrets/selfsigned-cert-tls
  uid: 42e456dc-6d34-4269-b207-f1f3bd50db8b
type: kubernetes.io/tls

到这里证明cert-manager已经安装成功了。需要注意的是cert-manager的功能非常强大,不只是可以支持 ACME 类型的证书签发,还支持其他众多的类型,比如 SelfSigned(自签名)、CA、Vault、Venafi、External、ACME,只是我们一般主要是使用 ACME 来帮我们生成自动化的证书。
下面就来使用cert-manager结合ingress-nginx为Kubernetes应用自动签发 Let’s Encrypt 类型的 HTTPS 证书。

自动化HTTPS

Let’s Encrypt使用ACME协议来校验域名是否真的属于你,校验成功后就可以自动颁发免费证书,证书有效期只有90天,在到期前需要再校验一次来实现续期,而cert-manager是可以自动续期的,所以事实上并不用担心证书过期问题。目前主要有HTTP和DNS两种校验方式。

HTTP-01校验

HTTP-01的校验是通过给你的域名指向的HTTP服务增加一个临时location,在校验的时候Let’s Encrypt会发送http请求到http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>,其中YOUR_DOMAIN就是被校验的域名,TOKEN是cert-manager生成的一个路径,它通过修改Ingress规则来增加这个临时校验路径并指向提供TOEKN的服务。Let’s Encrypt会对比TOKEN是否符合预期,校验成功后就会颁发证书了,不过这种不支持泛域名证书。