Istio 从 v1alpha3 开始,用 Ingress Gateway 组件替代了符合 Kubernetes 规范的 Ingress Controller,因此对入站流量具有了更大的控制能力,但是用法也有了较大不同。
安装:在使用 Helm 进行 Istio 部署的时候,需要使用下面的设置来启用 Ingress Gateway:
gateways: enabled: true istio-ingressgateway: enabled: true
基本概念
VirtualService
是流量控制的核心组件,起着承上(Gateway
)启下(DestinationRule
)的作用。这三种对象在本文中涉及的主要职责:
-
Gateway
对象:选择 Gateway Ingress Controller。
设置端点开放方式:域名、端口、入站协议等。
-
DestinationRule
:定义负载均衡行为。
定义服务版本子集。
-
VirtualService
:根据 Gateway 选择服务版本。
根据 VirtualService 对象文档中的说明,VirtaulService
对象中的 gateways
字段的类型为string[]
,包括一或多个字符串值。如果省略该字段内容,会隐式的赋值为 ["mesh"]
,mesh
网关指代所有的网格内通信。如果要对外提供服务,就需要定义 Gateway
对象,并在 gateways
字段中进行赋值。
注意,一旦在
gateways
中填写了非缺省的对象名称,要继续对内部通信进行流量控制,必须显式的将内置的mesh
对象名称也加入到列表之中。
准备工作
环境准备
安装和部署 Kubernetes 以及 Istio,并启用 Ingress Gateway 支持。
将备用的两个域名(假设为
flask.example2.com
以及flask.example1.com
)指向istio-ingressgateway
服务的外部 IP。为测试域名生成 HTTPS 证书。
域名准备
工作负载
首先编写一个基于 Flask 的 Python 脚本,并打入镜像,脚本功能很简单,显示环境变量中的 “VERSION”。
接下来编写 Dockerfile,安装 Python3、Flask,并进行构建和推送。
编写 Kubernetes 工作负载文件,常见的 Deployment + Service 模式。
在 Kubernetes 上运行工作负载。
以上涉及代码可以在页面尾部获取。
目标规则定义
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: flask spec: host: flask trafficPolicy: loadBalancer: simple: RANDOM subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2
这里根据工作负载中的标签,将 flask
服务拆分为 v1
和 v2
两个版本。
在外网开放服务
这里我们设计,将所有外网请求经过 flask.example1.com
路由到 v2
版本上。
首先要编写一个 Gateway
定义:
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: flask-gateway spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - flask.example1.com - flask.example2.com
这里定义了一个 Gateway
,其中要求 80 端口响应两个域名的请求。接下来定义一个 VirtualService
:
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: flask spec: hosts: - "flask.example1.com" gateways: - flask-gateway http: - route: - destination: host: flask subset: v2
然后使用 curl 进行访问:curl http://flask.example1.com
会发现返回了字符串 “v2”,证明我们的定义生效了,该域名指向了 v2
服务。
再用 curl 访问第二个域名 curl http://flask.example2.com
,会看到返回了 404,这是因为我们的 Gateway
虽然接收了请求,但是并没有服务路由完成这一请求。那么可以再定义一个 VirtualService
,让 flask.example2.com
域名指向 v1
:
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: flask1 spec: hosts: - "flask.example2.com" gateways: - flask-gateway http: - route: - destination: host: flask subset: v1
再次使用 curl http://flask.example2.com
,访问,会看到返回结果变成了 v1
,证明我们的服务定义生效了。
将一个域名服务升级为 tls
随意修改一个需求,要求开放 flask.example2.com
为 http,而 flask.example1.com
升级为 https,既然涉及 https,就要用到证书,官方文档(中文版哦)陈述如下:
具体说来就是使用 kubectl 命令在命名空间 istio-system 中创建一个 secret 对象,命名为 istio-ingressgateway-certs。Istio 网关会自动载入这个 secret。
这里的 secret 必须 在 istio-system 命名空间中,并且命名为 istio-ingressgateway-certs,否则就不会被正确载入,也就无法 Istio gateway 中使用了。
接着是使用命令为 flask.example1.com
的证书创建 Secret
:
$ kubectl create -n istio-system secret tls \ istio-ingressgateway-certs \ --key key.pem --cert cert.pem
最后修改我们的 Gateway
定义:
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: flask-gateway spec: selector: istio: ingressgateway servers: - port: number: 80 name: http protocol: HTTP hosts: - flask.example2.com - port: number: 443 name: https-rocks protocol: HTTPS tls: mode: SIMPLE serverCertificate: /etc/istio/ingressgateway-certs/tls.crt privateKey: /etc/istio/ingressgateway-certs/tls.key hosts: - flask.example1.com
然后再次使用 curl 分别访问 https://flask.example1.com/version
以及 http://flask.example2.com/version
,会发现返回了预期结果,而一旦错用 http
和 https
,就会出现异常,这是因为我们的 Gateway
限制了端口。
这里如此操作的原因可以在他的部署中找到:
$ kubectl get deployment istio-ingressgateway \ -o yaml -n istio-system
返回的 YAML 中会有这么一段
... - mountPath: /etc/istio/ingressgateway-ca-certs name: ingressgateway-ca-certs readOnly: true ... volumes: - name: ingressgateway-certs secret: defaultMode: 420 optional: true secretName: istio-ingressgateway-certs ...
上方代码可以看到,Ingress Gateway 中用可选方式加载了一个名称为 istio-ingressgateway-certs
的 Secret
,并 Mount 到了 /etc/istio/ingressgateway-ca-certs
目录之中,这就是上文中要求我们固定 Secret 名称和命名空间的原因。众所周知,tls
类型的 Secret
加载后会成为 tls.key
和 tls.crt
两个文件,因此在我们的 Gateway
定义中就使用了 /etc/istio/ingressgateway-certs/tls.crt
这样的文件名。
所有域名都升级为 tls
根据上一节的描写,不难发现按照官方文档,一个 Gateway
是无法处理两个域名的 https 的:
tls secret 只能包含一个证书对。
泛域名证书可以完成这一任务,但因为 Envoy 的限制,这里无法同时使用两个泛域名。
讨论到这里就很明显了,关键在于如何加载多个证书对,可以修改前面所说的加载指令为加载多个 Secret
,或者干脆换成 Configmap
,当然这样会引起服务中断,Configmap
用于存放证书也略显粗糙——好在我们还可以换用 Generic
类型的证书,这里我们可以删除原有 Secret 重新创建 :
kubectl delete secret istio-ingressgateway-certs \ -n istio-system kubectl create secret generic \ istio-ingressgateway-certs \ -n istio-system \ --from-file=rocks-crt.pem \ --from-file=rocks-key.pem \ --from-file=xyz-crt.pem \ --from-file=xyz-key.pem
接下来改造我们的 Gateway
定义:
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: flask-gateway spec: selector: istio: ingressgateway servers: - port: number: 443 name: https-rocks protocol: HTTPS tls: mode: SIMPLE serverCertificate: /etc/istio/ingressgateway-certs/rocks-crt.pem privateKey: /etc/istio/ingressgateway-certs/rocks-key.pem hosts: - "flask.example1.com" - port: number: 443 name: https-xyz protocol: HTTPS tls: mode: SIMPLE serverCertificate: /etc/istio/ingressgateway-certs/xyz-crt.pem privateKey: /etc/istio/ingressgateway-certs/xyz-key.pem hosts: - "flask.example2.com"
这样一来,我们开放了两个 HTTPS 端口,各自使用不同的证书,分别都可以通过 curl
命令获得正确结果了。
补充
tls.mode
实际还支持双向和透传两种方式,都可以在流量控制参考中找到相关内容。
涉及链接
Istio 流量控制参考:
https://istio.io/docs/reference/config/istio.networking.v1alpha3/
用 HTTPS 加密 Gateway:
https://istio.io/zh/docs/tasks/traffic-management/secure-ingress/
代码
Python 脚本
#!/usr/bin/env python3# -*- coding: UTF-8 -*-from flask import Flaskimport os app = Flask(__name__)@app.route("/")def hello_world(): return "Hello, World!"@app.route("/version")def getversion(): return os.getenv("VERSION")if __name__ == "__main__": app.run(host="0.0.0.0", port=80, debug=True)
Dockerfile
FROM alpine RUN apk add --update --no-cache python3 && \ mkdir /web && \ pip3 install Flask COPY server.py /web CMD "/web/server.py"
Kubernetes 工作负载
apiVersion: v1 kind: Service metadata: name: flask labels: app: flask spec: ports: - name: http port: 80 selector: app: flask --- apiVersion: extensions/v1beta1 kind: Deployment metadata: name: flask-v1 spec: replicas: 1 template: metadata: labels: app: flask version: v1 spec: containers: - image: 'abc:25000/python-flask-server:v2' imagePullPolicy: IfNotPresent name: flask env: - name: VERSION value: v1 ports: - containerPort: 80 --- apiVersion: extensions/v1beta1 kind: Deployment metadata: name: flask-v2 spec: replicas: 1 template: metadata: labels: app: flask version: v2 spec: containers: - image: 'abc:25000/python-flask-server:v2' imagePullPolicy: IfNotPresent name: flask env: - name: VERSION value: v2 ports: - containerPort: 80