概述
KtEnv 是一种基于ServiceMesh的微服务环境复用工具,源于阿里内部的“项目环境”实践。
通过识别Pod上的虚拟环境标签,KtEnv能够自动将测试环境网络动态隔离成多个虚拟隔离域,
同时以简单规则在隔离域间局部复用Pod实例,
从而达到只需很少资源成本即可创建大量不同微服务版本组合的独立测试环境的目的。
在本系列文章中,我们将会详细介绍 KT-env 的原理,使用,实战等等。
Mesh基础知识及请求路由
想要了解 KT-env 相关的功能和使用,首先需要了解一下什么是 mesh 以及请求路由相关的功能。
ServiceMesh 相关介绍
首先,我们来了解一下大家对 ServiceMesh 的定义:
服务网格(Service Mesh)是处理服务间通信的基础设施层。
它负责构成现代云原生应用程序的复杂服务拓扑来可靠地交付请求。
在实践中,Service Mesh 通常以轻量级网络代理阵列的形式实现,
这些代理与应用程序代码部署在一起,对应用程序来说无需感知代理的存在。**
ServiceMesh 通常有如下特点:
- 它是应用程序间通信的中间层。
- 轻量级网络代理。
- 应用程序无感知。
- 解耦应用程序的重试/超时、监控、追踪和服务发现。
ServiceMesh 仅仅是一种定义和概念,而关于 ServiceMesh 的具体实现,则有多种不同的服务实现方式,例如 Istio, Linkerd。
其中,Istio 是目前最流行 ServiceMesh 服务了,我们也将会继续对 Istio 来进行详细的说明。
Istio 概述
Istio 是 ServiceMesh 的一种实现。
Istio 服务网格从逻辑上分为数据平面和控制平面。
- 数据平面: 由一组智能代理(Envoy)组成,被部署为 Sidecar。这些代理负责协调和控制微服务之间的所有网络通信。它们还收集和报告所有网格流量的遥测数据。
- 控制平面: 管理并配置代理来进行流量路由。
下图展示了组成每个平面的不同组件:
Envoy
Istio 使用 Envoy 代理的扩展版本。Envoy 是用 C++ 开发的高性能代理,用于协调服务网格中所有服务的入站和出站流量。
Envoy 代理是唯一与数据平面流量交互的 Istio 组件。
Envoy 代理被部署为服务的 Sidecar,在逻辑上为服务增加了 Envoy 的许多内置特性,例如:
- 动态服务发现
- 负载均衡
- TLS
- HTTP/2 与 grpc 代理
- 熔断器
- 健康检查
- 基于百分比流量分割的分阶段发布
- 故障注入
- 丰富的监控指标
这种 Sidecar 部署允许 Istio 可以执行策略决策,并提取丰富的遥测数据,接着将这些数据发送到监视系统以提供有关整个网格行为的信息。
Sidecar 代理模型还允许您向现有的部署添加 Istio 功能,而不需要重新设计架构或重写代码。
由 Envoy 代理启用的一些 Istio 的功能和任务包括:
- 流量控制功能:通过丰富的 HTTP、gRPC、WebSocket 和 TCP 流量路由规则来执行细粒度的流量控制。
- 网络弹性特性:重试设置、故障转移、熔断器和故障注入。
- 安全性和身份认证特性:执行安全性策略,并强制实行通过配置 API 定义的访问控制和速率限制。
- 基于 WebAssembly 的可插拔扩展模型,允许通过自定义策略执行和生成网格流量的遥测。
Istiod
Istiod 提供服务发现、配置和证书管理。
Istiod 将控制流量行为的高级路由规则转换为 Envoy 特定的配置,并在运行时将其传播给 Sidecar。
Pilot 提取了特定平台的服务发现机制,并将其综合为一种标准格式,任何符合 Envoy API 的 Sidecar 都可以使用。
Istio 可以支持发现多种环境,如 Kubernetes 或 VM。
您可以使用 Istio 流量管理 API 让 Istiod 重新构造 Envoy 的配置,以便对服务网格中的流量进行更精细的控制。
Istiod 安全通过内置的身份和凭证管理,实现了强大的服务对服务和终端用户认证。
您可以使用 Istio 来升级服务网格中未加密的流量。
使用 Istio,运营商可以基于服务身份而不是相对不稳定的第 3 层或第 4 层网络标识符来执行策略。
此外,您可以使用 Istio 的授权功能控制谁可以访问您的服务。
Istiod 还充当证书授权(CA),并生成证书以允许在数据平面中进行安全的 mTLS 通信。
Istio 请求路由控制
Istio 提供的核心功能之一就是 服务发现 。
Istio 简单的规则配置和流量路由允许您控制服务之间的流量和 API 调用过程。
Istio 简化了服务级属性(如熔断器、超时和重试)的配置,并且让它轻而易举的执行重要的任务(如 A/B 测试、金丝雀发布和按流量百分比划分的分阶段发布)。
例如,在 Istio 中,有两个非常重要的概念,VirtualService 和 DestinationRule。
通过合理的 VirtualService 和 DestinationRule 的配置,可以实现根据指定 uri, 指定的 headers 信息等等来配置对应的下游地址。
可以参考如下示例: Istio 请求路由
Service 、 VirtualService 、 DestinationRule 关系介绍
Service 是 K8s 中的核心概念之一,它是 K8s 中默认的服务发现机制。
具体来说,K8s 中的 Service 对应于一组指定标签的 Pod 实例,发向该 Service 的请求会被自动转发到对应的 Pod 实例上。
虽然,K8s 中的 Service 已经具备了基本的服务发现的能力,但实际上这种基本的服务发现能力远远无法满足业务对流量治理相关能力,
例如,特定的路由策略、网络重试等功能。
因此,Istio 的出现就是为了进一步提升流量治理的能力,而在 Istio 中,最核心的两个概念就是 VirtualService 和 DestinationRule 了。
其中:
- VirtualService 的目的是定义一组要在访问 host 时应用的流量路由规则,每个路由规则定义了特定协议流量的匹配规则。如果流量的匹配规则满足的话,则将其发送到配置中定义的目标服务(或其子集/版本)中,此外,还包括了HTTP超时控制、重试、镜像、修改headers等。
- DestinationRule 的目的定义在路由发生后应用于服务流量的策略。 这些规则指定负载均衡的配置、来自 sidecar 的连接池大小和异常检测设置,以检测和从负载均衡池中驱逐不健康的实例。
VirtualService 故名思义就是虚拟服务,VirtualService 中定义了一系列针对指定服务的流量路由规则。
每个路由规则都是针对特定协议的匹配规则。
如果流量符合这些特征,就会根据规则发送到服务注册表中的目标服务(或者目标服务的子集或版本)。
DestinationRule 是基于已有的 K8s Service 进行 Pod 下的细粒度的分组。
例如,可以将一个 Service 下的 Pod 根据 label 等信息再次分为多个 subset,从而可以在 VirtualService 对流量进行 subset 级别的细粒度分发。
也就是说:VirtualService 和 DestinationRule 都是基于已有的 K8s Service 的条件下进行的功能扩展,必须保证 K8s Service 已经存在。
而在消息具体发送的过程中,envoy 会默认劫持流量并发送给对应的 Pod 实例,而非依赖 kube-proxy 进行请求下发。
示例的 DestinationRule 配置文件如下:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: flaskapp
spec:
host: flaskapp.books.svc.cluster.local
subsets:
- name: subset-v1
labels:
version: v1
- name: subset-v2
labels:
version: v2
它可以将 books namespace 下的 flaskapp 的 service 下关联的 Pod 进行细粒度的分组,
其中 subset-v1 中包含所有 labels 中 version 为 v1 的 Pod,subset-v2 中包含所有 labels 中 version 为 v2 的 Pod。
VirtualService 的配置文件如下:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: flaskapp-policy
spec:
hosts:
- flaskapp.books.svc.cluster.local
http:
- route:
- destination:
host: flaskapp.books.svc.cluster.local
subset: subset-v2
match:
- headers:
end-user:
exact: jason
- route:
- destination:
host: flaskapp.books.svc.cluster.local
subset: subset-v1
我们来看一下这个 VirtualService 的配置做了什么事情:
它扩展了 books namespace 下原有 K8s Service flaskapp 的路由策略,当请求的 headers 中包括 end-user 且值为 jason 的话,
则将对应请求发送给 subset-v2 分组的 Pod 实例,否则的话,则将对应请求发送给 subset-v1 分组的 Pod 实例。
KT-env目标及功能
对于微服务的开发者而言,拥有一套干净、独占的完整测试环境无疑能够提高软件研发过程中的功能调试和异常排查效率。
然而在中大型团队里,为每位开发者维护一整套专用测试服务集群,从经济成本和管理成本上考虑都并不现实。为此阿里巴巴的研发团队采用了基于路由隔离的"虚拟环境"方法。
“虚拟环境”的本质是基于服务实例(即Kubernetes的Pod实例)上的“环境标”(在Kubernetes表现为Label)属性,结合约定的路由复用规则,形成一个个开发者视角的专属测试环境。这些虚拟的专属环境从集群管理者的视角来看,称为“隔离域”,它具有如下特点:
- “隔离域”是由路由规则形成的虚拟边界
- 每个“环境标”都会形成一个独立的“隔离域”
- “隔离域”之间可以存在部分或完全重合
- “隔离域”的成员会随集群中服务实例所带“环境标”动态变化
路由规则
虚拟环境的路由规则很简单:**如果请求是来自一个带有环境标的服务实例,
它会优先寻找跟它具有相同环境标的实例,如果没有,则会寻找它上一级的环境标,直至到达顶级的默认服务实例**。
假设A、B、C、D四个微服务组成了一个完整的测试环境。
此时若为集群中的服务实例全部赋予环境标dev,就形成了一个隔离域,如下图蓝色部分所示。结合虚拟环境的实践,我们称这个隔离域为“公共基础环境”(也称默认环境)。
当进行特定项目开发的时候,开发者不需要重新部署整套微服务系统,而是单独部署需要修改的部分服务(如服务C和服务D),为它们赋予一个子级环境标,比如dev.proj,然后加入到测试环境中。
根据路由规则,当请求来自带有dev.proj环境标的实例C,需要调用服务B,但是发现没有带环境标dev.proj的服务B实例,于是会寻找带有上一级环境标dev的服务B实例。同理当链路到达服务C时,会由dev环境标的实例响应,而由于服务D存在dev.proj环境标的实例,所以这部分实例会接管到达服务D的请求。
因此dev.proj环境标所形成的隔离域边界下图红色区域所示,这便是dev.proj虚拟环境的服务实例集合。
不难看出,dev.proj虚拟环境复用了部分公共基础环境的资源(服务A和服务B)。这样做的好处是,第一不会占用大量的计算资源;第二,不会影响公共基础环境的稳定性。
开发者还可以将本地开发机加入虚拟环境。比如小明在本地启动了一个服务C的实例,他给这个服务实例打上环境标dev.proj.local,基于前面介绍的路由规则,带环境标的服务发出的请求会优先寻找带相同环境标的服务实例,如果找不到则会逐级寻找带有上一级环境标的实例。于是环境标为dev.proj.local的服务C、环境标为dev.proj服务D和公共基础环境dev中的服务A、服务B就组成了一个新的的虚拟环境,如下图红色部分所示。
这时小明的同事在本地启动了一个服务A,如果他没有对这个服务打环境标,则他的所有调用请求会默认使用“公共基础环境”进行测试。因此小明在自己的虚拟环境中的任何调试都不会影响到他的同事,反之亦然。如下图所示。
若小明的同事加入了小明所在的项目,他们之间需要进行“联调”。这时,他只需将本地的服务打上一个和小明相同的环境标即可,如下图红色部分所示。
总结而言,虚拟环境是通过在服务实例(以及从这些实例发出的请求)上携带约定标签,将个别需要调试或测试的特定版本服务实例与其他公共服务实例组成临时虚拟集群的一种环境管理实践。
KT-env部署
前置条件
KT-env 部署之前,首先需要有一个 Kubernetes 集群并且已经部署了 Istio 。
同时,本地还需要一个配置好的 kubectl 工具。
下载部署包
首先,我们需要从 发布页面 下载最新的部署文件包,并解压。
unzip kt-virtual-environment-v0.5.4.zip
cd v0.5.4/
安装相关组件
创建 CRD
在 KT-env 中,定义了一种新的资源对象: VirtualEnvironment 。
下面,我们第一步就来创建对应的 CRD 资源对象:
kubectl apply -f global/ktenv_crd.yaml
CRD组件会在Kubernetes集群内新增一种名为VirtualEnvironment的资源类型,在下一步我们将会用到它。可通过以下命令验证其安装状态:
kubectl get crd virtualenvironments.env.alibaba.com
若输出类似以下信息,则表明KtEnv的CRD组件已经正确部署:
NAME CREATED AT
virtualenvironments.env.alibaba.com 2020-04-21T13:20:35Z
部署 Webhook 组件
Webhook组件用于将Pod的虚拟环境标写入到其Sidecar容器的运行时环境变量内。
kubectl apply -f global/ktenv_webhook.yaml
Webhook组件默认被部署到名为kt-virtual-environment的Namespace中,包含一个Service和一个Deployment对象,以及它们创建的子资源对象,可用以下命令查看:
kubectl -n kt-virtual-environment get all
若输出类似以下信息,则表明KtEnv的Webhook组件已经部署且正常运行:
NAME READY STATUS RESTARTS AGE
pod/webhook-server-5dd55c79b5-rf6dl 1/1 Running 0 86s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/webhook-server ClusterIP 172.21.0.254 <none> 443/TCP 109s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/webhook-server 1/1 1 1 109s
NAME DESIRED CURRENT READY AGE
replicaset.apps/webhook-server-5dd55c79b5 1 1 1 86s
创建 Role 和 ServiceAccount
如果 K8s 集群中,已经开启了 RBAC 权限控制,那么,为了保证我们的 Operator 可以正常工作,还需要部署相应的Role和ServiceAccount。
kubectl apply -n default -f ktenv_service_account.yaml
部署 KT-env Operator
Operator是由CRD组件定义的虚拟环境管理器实例,需要在每个使用虚拟环境的Namespace里单独部署。
以使用default Namespace为例,通过以下命令完成部署:
kubectl apply -n default -f ktenv_operator.yaml
此外,为了让Webhook组件对目标Namespace起作用,还应该为其添加值为enabled的environment-tag-injection标签。
kubectl label namespace default environment-tag-injection=enabled
现在,Kubernetes集群就已经具备使用虚拟环境能力了。
KT-env实战
前置条件
KT-env 部署之前,首先需要有一个 Kubernetes 集群并且已经部署了 Istio 。
同时,本地还需要一个配置好的 kubectl 工具。
下载部署包
首先,我们需要从 发布页面 下载最新的部署文件包,并解压。
unzip kt-virtual-environment-v0.5.4.zip
cd v0.5.4/
安装相关组件
创建 CRD
在 KT-env 中,定义了一种新的资源对象: VirtualEnvironment 。
下面,我们第一步就来创建对应的 CRD 资源对象:
kubectl apply -f global/ktenv_crd.yaml
CRD组件会在Kubernetes集群内新增一种名为VirtualEnvironment的资源类型,在下一步我们将会用到它。可通过以下命令验证其安装状态:
kubectl get crd virtualenvironments.env.alibaba.com
若输出类似以下信息,则表明KtEnv的CRD组件已经正确部署:
NAME CREATED AT
virtualenvironments.env.alibaba.com 2020-04-21T13:20:35Z
部署 Webhook 组件
Webhook组件用于将Pod的虚拟环境标写入到其Sidecar容器的运行时环境变量内。
kubectl apply -f global/ktenv_webhook.yaml
Webhook组件默认被部署到名为kt-virtual-environment的Namespace中,包含一个Service和一个Deployment对象,以及它们创建的子资源对象,可用以下命令查看:
kubectl -n kt-virtual-environment get all
若输出类似以下信息,则表明KtEnv的Webhook组件已经部署且正常运行:
NAME READY STATUS RESTARTS AGE
pod/webhook-server-5dd55c79b5-rf6dl 1/1 Running 0 86s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/webhook-server ClusterIP 172.21.0.254 <none> 443/TCP 109s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/webhook-server 1/1 1 1 109s
NAME DESIRED CURRENT READY AGE
replicaset.apps/webhook-server-5dd55c79b5 1 1 1 86s
创建 Role 和 ServiceAccount
如果 K8s 集群中,已经开启了 RBAC 权限控制,那么,为了保证我们的 Operator 可以正常工作,还需要部署相应的Role和ServiceAccount。
kubectl apply -n default -f ktenv_service_account.yaml
部署 KT-env Operator
Operator是由CRD组件定义的虚拟环境管理器实例,需要在每个使用虚拟环境的Namespace里单独部署。
以使用default Namespace为例,通过以下命令完成部署:
kubectl apply -n default -f ktenv_operator.yaml
此外,为了让Webhook组件对目标Namespace起作用,还应该为其添加值为enabled的environment-tag-injection标签。
kubectl label namespace default environment-tag-injection=enabled
现在,Kubernetes集群就已经具备使用虚拟环境能力了。
前置条件
KT-env 部署之前,首先需要有一个 Kubernetes 集群并且已经部署了 Istio 。
同时,本地还需要一个配置好的 kubectl 工具。
下载部署包
首先,我们需要从 发布页面 下载最新的部署文件包,并解压。
unzip kt-virtual-environment-v0.5.4.zip
cd v0.5.4/
安装相关组件
创建 CRD
在 KT-env 中,定义了一种新的资源对象: VirtualEnvironment 。
下面,我们第一步就来创建对应的 CRD 资源对象:
kubectl apply -f global/ktenv_crd.yaml
CRD组件会在Kubernetes集群内新增一种名为VirtualEnvironment的资源类型,在下一步我们将会用到它。可通过以下命令验证其安装状态:
kubectl get crd virtualenvironments.env.alibaba.com
若输出类似以下信息,则表明KtEnv的CRD组件已经正确部署:
NAME CREATED AT
virtualenvironments.env.alibaba.com 2020-04-21T13:20:35Z
部署 Webhook 组件
Webhook组件用于将Pod的虚拟环境标写入到其Sidecar容器的运行时环境变量内。
kubectl apply -f global/ktenv_webhook.yaml
Webhook组件默认被部署到名为kt-virtual-environment的Namespace中,包含一个Service和一个Deployment对象,以及它们创建的子资源对象,可用以下命令查看:
kubectl -n kt-virtual-environment get all
若输出类似以下信息,则表明KtEnv的Webhook组件已经部署且正常运行:
NAME READY STATUS RESTARTS AGE
pod/webhook-server-5dd55c79b5-rf6dl 1/1 Running 0 86s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/webhook-server ClusterIP 172.21.0.254 <none> 443/TCP 109s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/webhook-server 1/1 1 1 109s
NAME DESIRED CURRENT READY AGE
replicaset.apps/webhook-server-5dd55c79b5 1 1 1 86s
创建 Role 和 ServiceAccount
如果 K8s 集群中,已经开启了 RBAC 权限控制,那么,为了保证我们的 Operator 可以正常工作,还需要部署相应的Role和ServiceAccount。
kubectl apply -n default -f ktenv_service_account.yaml
部署 KT-env Operator
Operator是由CRD组件定义的虚拟环境管理器实例,需要在每个使用虚拟环境的Namespace里单独部署。
以使用default Namespace为例,通过以下命令完成部署:
kubectl apply -n default -f ktenv_operator.yaml
此外,为了让Webhook组件对目标Namespace起作用,还应该为其添加值为enabled的environment-tag-injection标签。
kubectl label namespace default environment-tag-injection=enabled
现在,Kubernetes集群就已经具备使用虚拟环境能力了。
Kt-env实战
在之前的内容中,我们已经完成了 KT-env 的环境搭建。
同时,也了解了请求路由的基本原理。
下面,我们就来具体使用 KT-env 来实现对应的虚拟环境。
QuickStart
如果是一个新的 namespace,需要在新的 namespace 下部署 operator 和 role 相关信息,参考 deploy 。
Step1: 创建虚拟环境。
创建一个virtual-environment-cr.yaml
文件:
apiVersion: env.alibaba.com/v1alpha2
kind: VirtualEnvironment
metadata:
name: demo-virtualenv
spec:
envHeader:
name: ali-env-mark
autoInject: true
envLabel:
name: virtual-env
splitter: "."
defaultSubset: dev
实例创建后,会自动监听所在Namespace中的所有Service、Deployment和StatefulSet对象并自动生成路由隔离规则,形成虚拟环境。
kubectl apply -n default -f virtual-environment-cr.yaml
Step2: 拉取Git仓库,进入示例代码目录。
git clone https://github.com/alibaba/virtual-environment.git
cd virtual-environment/examples
Step3: 设置 default namespace 自动注入 sidecar 并使得 webhook 能够为 Envoy 自动注入环境变量。
kubectl label namespace default environment-tag-injection=enabled
kubectl label namespace default istio-injection=enabled
Step4: 在集群随意创建一个临时的Pod作为发送测试请求的客户端。
kubectl create deployment sleep --image=virtualenvironment/sleep --dry-run -o yaml | kubectl apply -n default -f -
Step5: 部署相关应用
KtEnv支持Deployment和StatefulSet对象的路由隔离,
在这个例子中将部署3种不同语言实现的示例应用,
其中app-js和app-java被部署为Deployment,而app-go被部署为StatefulSet。
修改 app.sh
脚本中 apply_pods 函数如下:
apply_pods() {
type=${1}
ee=`echo ${e} | sed -e "s/\./-/g"`
echo $s
cat ${basepath}/${type}.yaml | sed -e "s/service-name-env-placeholder/${s}-${ee}/g" \
-e "s/service-name-placeholder/${s}/g" \
-e "s/app-env-placeholder/${e}/g" \
-e "s/app-image-placeholder/`hget images ${s}`/g" \
-e "s#app-url-placeholder#`hget urls ${s}`#g" \
| kubectl ${action} --validate=false -n ${namespace} -f -
}
使用app.sh
脚本快速创建示例所需的VirtualEnvironment、Deployment、StatefulSet和Service资源:
# default namespace 下启动演示的服务实例
deploy/app.sh apply default
依次使用kubectl get virtualenvironment
、kubectl get deployment
、kubectl get statefulset
、kubectl get service
命令查看各资源的创建情况,等待所有资源部署完成。
Step6: 进入同Namespace的任意一个Pod,例如前面步骤创建的sleep容器。
# 进入集群中的容器
kubectl exec -n default -it $(kubectl get -n default pod -l app=sleep -o jsonpath='{.items[0].metadata.name}') -- /bin/sh
Step7: 实验一下
分别在请求头加上不同的虚拟环境名称,使用curl工具调用app-js服务。
3个服务的关系是: app-js -> app-go -> app-java
。
注意该示例创建的VirtualEnvironment实例配置使用.
作为环境层级分隔符,同时配置了传递标签Header的键名为ali-env-mark
。
已知各服务输出文本结构为[项目名 @ 响应的Pod所属虚拟环境]
<- 请求标签上的虚拟环境名称。
观察实际响应的服务实例情况:
# 使用dev.proj1标签
> curl -H 'ali-env-mark: dev.proj1' app-js:8080/demo
[springboot @ dev.proj1] <-dev.proj1
[go @ dev] <-dev.proj1
[node @ dev.proj1] <-dev.proj1
# 使用dev.proj1.feature1标签
> curl -H 'ali-env-mark: dev.proj1.feature1' app-js:8080/demo
[springboot @ dev.proj1.feature1] <-dev.proj1.feature1
[go @ dev] <-dev.proj1.feature1
[node @ dev.proj1] <-dev.proj1.feature1
# 使用dev.proj2标签
> curl -H 'ali-env-mark: dev.proj2' app-js:8080/demo
[springboot @ dev] <-dev.proj2
[go @ dev.proj2] <-dev.proj2
[node @ dev] <-dev.proj2
# 不带任何标签访问
# 由于启用了AutoInject配置,经过node服务后,请求自动加上了Pod所在虚拟环境的标签
> curl app-js:8080/demo
[springboot @ dev] <-dev
[go @ dev] <-dev
[node @ dev] <-empty
VirtualEnvironment 配置文件解读
虚拟环境实例通过VirtualEnvironment类型的Kubernetes资源定义。其内容结构示例如下:
apiVersion: env.alibaba.com/v1alpha2
kind: VirtualEnvironment
metadata:
name: demo-virtualenv
spec:
envHeader:
name: ali-env-mark
autoInject: true
envLabel:
name: virtual-env
splitter: "."
defaultSubset: dev
参数作用如下表所示:
配置参数 |
默认值 |
说明 |
envHeader.name |
X-Virtual-Env |
用于透传虚拟环境名的HTTP头名称(虽然有默认值,建议显性设置) |
envHeader.autoInject |
false |
是否为没有虚拟环境HTTP头记录的请求自动注入HTTP头(建议开启) |
envLabel.name |
virtual-env |
Pod上标记虚拟环境名用的标签名称(除非确实必要,建议保留默认值) |
envLabel.splitter |
. |
虚拟环境名中用于划分环境默认路由层级的字符(只能是单个字符) |
envLabel.defaultSubset |
无 |
请求未匹配到任何存在的虚拟环境时,进行兜底虚拟环境名(默认为随机路由) |
注意:VirtualEnvironment实例只对其所在的Namespace有效。如有需要,可以通过在多个Namespace分别创建相同配置的实例。
Kt-env源码解读
了解了 KT-env 的基本功能、原理和使用之后,我们最后再来了解一下 KT-env 相关的源码实现。
KT-env 的源码仓库地址: virtual-environment
其中,我们先来了解一下它们的目录结构和功能:
- cmd: 二进制程序的入口文件 - 核心目录。
- pkg: 相关功能模块的封装模块 - 核心目录。
- version: 版本信息。
- build: 编译脚本。
- deploy: 部署脚本。
- docs: 文档。
- examples: 示例相关代码。
- sdk: 透传 headers 相关的SDK。
其中,核心的源码目录是 cmd 和 pkg 两个目录。
查看 cmd 目录下的内容,可以很容易的看出,它一共涉及到三个二进制可执行程序,分别是 operator
,inspector
和 webhook
。
其中,inspector
的功能非常简单,就是一个 HTTP 客户端,可以调用 operator 的接口查询 operator 模块的版本信息、状态以及修改日志级别,不再赘述。
我们重点来分析 operator
和 webhook
两个程序的功能。
webhook
在 KT-env 中包含了一个全局的Mutating Admission Webhook组件,
它的主要作用是将Pod上的环境标信息通过环境变量注入到Sidecar容器里,便于Sidecar为出口流量的Header添加恰当的环境标。
那么什么是 Mutating Admission Webhook 呢?这是 K8s 中的一个特有的概念,我们先来了解一下。
Admission webhook 是一种用于接收准入请求并对其进行处理的 HTTP 回调机制。
K8s 中可以定义两种类型的 admission webhook,即 validating admission webhook 和 mutating admission webhook。
其中,Mutating admission webhook 会先被调用。
它们可以更改发送到 API 服务器的对象以执行自定义的设置默认值操作。
K8s的具体的处理流程如下图所示:
而 Kt-env 中的 Webhook 其实就是这么一个组件,它主要用于将Pod上的环境标信息通过环境变量注入到Sidecar容器里,
即在 Webhook 阶段,修改了 Sidecar 的配置,向其中设置了对应的环境变量。
了解了 webhook 的原理之后,我们就来看一下 KT-env 中的 webhook 是如何实现的吧。
首先,我们先来简单看一下 Webhook 对应的 yaml 配置文件:
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: virtual-environment-webhook
webhooks:
- name: webhook-server.kt-virtual-environment.svc
failurePolicy: Fail
clientConfig:
service:
name: webhook-server
namespace: kt-virtual-environment
path: "/inject"
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURYVENDQWtXZ0F3SUJBZ0lVZG42TEl2bDNaV2ltbndEVGwxS3U3ODhKcDBrd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1BqRThNRG9HQTFVRUF3d3pWbWx5ZEhWaGJDQkZiblpwY205dWJXVnVkQ0JCWkcxcGMzTnBiMjRnUTI5dQpkSEp2Ykd4bGNpQlhaV0pvYjI5cklFTkJNQjRYRFRJd01EZ3lOVEUxTURrMU5Wb1hEVE13TURneU16RTFNRGsxCk5Wb3dQakU4TURvR0ExVUVBd3d6Vm1seWRIVmhiQ0JGYm5acGNtOXViV1Z1ZENCQlpHMXBjM05wYjI0Z1EyOXUKZEhKdmJHeGxjaUJYWldKb2IyOXJJRU5CTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQwpBUUVBeDYrQkZIaXlkZk9uR3FMRmt4Y2lpL21ZMG9XU3dRV2krTHYwdmNqeklQTndKa2c0V0FJYTRWK0RqdTV2CmZ5NlE2RFhUaitlRnhlK212MmtVSEFtYjNsWS9iT3RaWGp2VnQ4bnBHS1VLdlBBN0hVRFdhUkZKOTR1eUJpQm8KQnlXTnZtTGNka0VFTjRVMVVGTlVIV3B1L1lHNXNaSC9ZQWZjZGEyMDhIUzVkQmllNTNYMmJjdjQrNGhzS3oyOAp0UUR2MmR3ZkxFT2crZ2lVUWRWRHUrN0lXUXpjRkp4NmdpdlBpVkl1UVRHVk04K0tya2dITlhXVjU4OGIwcTU1CnNPNjR2YWQ4cW5XT0t5bk5oSThyZzN0dGVJVkFHdkEvUnJGczFocytERHVBZlMxamhiRWUyUEJHSHVMeGx6TGIKNjR2NUV5VEhEdDVVOS93bFpxS0hMWlZQeFFJREFRQUJvMU13VVRBZEJnTlZIUTRFRmdRVVg1cEI4dkYzbnducQpUUjA1TlVhVlhaQzd4VjB3SHdZRFZSMGpCQmd3Rm9BVVg1cEI4dkYzbnducVRSMDVOVWFWWFpDN3hWMHdEd1lEClZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBbFU0YkxNMGlzbm5KaDA1dCs3TDkKWkFtS2M1eXhGdkUremlsK2Y5aEUzdzJpYjI0bVNYTVpOSWEvUWd6akxGK0owQVlIbUxXZTRQa2I4eXliQnVjRQp6d2VRNEc5Y3U2QWV5VFRHTk9zbU5lREx4WGRVOUJ1aElvRmZsR2ZDV1pudHA5ajZsbnFJbndqdjZJWDBEQmc1CkFEbXRNdVVrS0gyMXdUTHhXNVBWSmhQWnZiL3p1ZGNlNUxWRG1zT0Z5cjFkK2lnVnZPVzJJam51QUw3eGpXWFQKenVGYng5NnZBYTJjS0hWRjYyVzdoekp6NURiN0cwdWxJMXptOTF0ZXJaZjRYSHlUT3FzUC8wczFsYjV1V1k5cAo2NUZiZCtHMXJOL2NBcUM4NXo0K1Rrc2QrdTV1ZDFVREc5MEsvRG9UdS9vcis3U3o2bWNWdjVBejlLSHVxRzE2CnlBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
rules:
- operations: ["CREATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
namespaceSelector:
matchLabels:
environment-tag-injection: enabled
上述配置文件表示针对:
- 设置了
environment-tag-injection: enabled
的 namespace 下 - Pod 资源对象在创建时
- 会触发 Mutating Admission Webhook 调用
- 调用的请求地址是 webhook-server.kt-virtual-environment.svc/inject
而具体的注入环境变量的逻辑则在 webhook-server
模块内,
其核心代码见 main.go 。
我们选择其中部分的核心代码进行说明:
envLabels := os.Getenv("envLabel")
if envLabels == "" {
logger.Fatal("Cannot determine env label !!")
}
envLabelList := strings.Split(envLabels, ",")
envTag := ""
for _, label := range envLabelList {
if value, ok := pod.Labels[label]; ok {
envTag = value
break
}
}
从环境变量中读取 envLabel
的配置,并判断 Pod 上是否存在其中的某个 label,如果存在,则表示使用该 label 对应的 value 用于 headers 追加。
var patches []PatchOperation
if envVarIndex < 0 {
patches = append(patches, PatchOperation{
Op: "add",
Path: fmt.Sprintf("/spec/containers/%d/env/0", sidecarContainerIndex),
Value: corev1.EnvVar{Name: envVarName, Value: envTag},
})
} else {
patches = append(patches, PatchOperation{
Op: "replace",
Path: fmt.Sprintf("/spec/containers/%d/env/%d/value", sidecarContainerIndex, envVarIndex),
Value: envTag,
})
}
表示根据之前的计算规则,生成 patch 操作,用于向 Pod 中追加/修改对应的环境变量,
其中,key 为 VIRTUAL_ENVIRONMENT_TAG
,取值为之前步骤中从 Pod 上获取到的 label 对应的值。
通过上述步骤,就完成了将业务 Pod 上中指定的 Label 对应的值设置到 Sidecar 容器中的环境变量中了。
那么,Sidecar 中的环境变量是如何追加到发送请求的 header 中的呢?
这个其实是用到了 Istio 中的功能 EnvoyFilter
。
如上文所示,在 operator
模块中,会创建 EnvoyFilter
,在 EnvoyFilter
中,通过 lua
脚本,在出流量的阶段中,追加了 headers 信息。
示例 EnvoyFilter
配置如下:
kind: EnvoyFilter
apiVersion: networking.istio.io/v1alpha3
metadata:
name: demo-virtualenv
namespace: kt-env1
labels:
envHeader: ali-env-mark
envLabel: virtual-env
spec:
workloadSelector: ~
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_OUTBOUND
listener:
filterChain:
filter:
name: envoy.http_connection_manager
patch:
operation: INSERT_BEFORE
value:
name: virtual.environment.lua
typed_config:
'@type': type.googleapis.com/envoy.config.filter.http.lua.v2.Lua
inline_code: |-
local curEnv = os.getenv("VIRTUAL_ENVIRONMENT_TAG")
function envoy_on_request(req)
local env = req:headers():get("ali-env-mark")
if env == nil and curEnv ~= nil then
req:headers():add("ali-env-mark", curEnv)
end
end
其中,我们可以重点关注最下方的 lua 脚本,可以看出其基本的逻辑如下:
判断请求头部中是否包含 ali-env-mark
,如果没有包含,
则从环境变量中取出 VIRTUAL_ENVIRONMENT_TAG 的值并设置为 ali-env-mark
headers 的值。