在 Flomesh 服务网格 osm-edge 最新发布的 1.3.0 版本 中,我们推出了一项重要特性:插件(Plugin)。这项功能旨在为开发者提供途径对服务网格的功能进行扩展,今天这篇文章便为大家介绍一下该特性。
背景
向左走,向右走?
如今的服务网格貌似向着两个方向发展,一个是类似 Istio 的提供大量开箱即用功能,功能非常丰富;另一个是 Linkerd、Flomesh 等秉持着简单之上的原则,提供满足用户的最小功能集。二者无所谓优劣:前者功能丰富但不可避免代理额外的开销,不只是资源占用,还有学习、维护上的成本;后者学习、使用简单,资源占用少,但提供的功能无法满足用户对功能的追求。
在首届服务网格峰会中,几乎所有分享中都提及的内容是 扩展性,相信这也是已经采用或者观望中的用户非常关注的部分(对此感兴趣的同学,可以参考我的那篇 服务网格峰会回顾:服务网格的发展趋势)。即使像 Istio 这种提供了丰富的功能,用户仍需要对网格的功能进行扩展。
“魚,我所欲也;熊掌,亦我所欲也。” 有没有什么方案可以做到鱼与熊掌兼得呢?
不难想到,理想的方案是 最小功能集的低成本开销 + 灵活的扩展性。服务网格的核心在数据面,灵活的扩展性对 sidecar 代理的体质有着非常高的要求。这也正是 Flomesh 服务网格选择 可编程代理 Pipy 作为 sidecar 代理的原因。
可编程代理 Pipy
Pipy 是面向云、边缘和 IoT 的可编程网络代理。具有灵活多变、快、小、可编程、开放的特性。灵活多变的特性,也使其可以应用于多种场景。
Pipy 的核心使用了模块化设计,提供了大量可复用的小模块(过滤器)。这些过滤器可以组装成管道,网络数据流经这些管道并被处理。对外屏蔽了底层的细节,提供了类似拼图的编程方式来实现业务目标。
这里需要一提的是 Pipy 的脚本(实现功能逻辑的编程代码)可以通过网络动态下发到 Pipy 实例,做到无需编译无需重启为代理扩展新的功能。
Flomesh 服务网格的方案
osm-edge 提供了三个新的 CRD 来提供扩展性:
Plugin
:插件,包含了新增功能的代码逻辑,用于扩展功能。osm-edge 默认提供的功能也是通过插件方式存在的,但并不是以 Plugin
资源的形式提供。安装 osm-edge 时通过 helm values 文件对这些插件进行调整。更多内容,可以参考 Helm values.yaml 内置插件列表。
PluginChain
:插件链,插件的执行是循序执行的,通过插件链在组装插件的执行顺序。系统提供了 4 个插件链:inbound-tcp
、inbound-http
、outbound-tcp
、outbound-http
。分别对应着入站和出站流量的 4 层、7 层处理阶段。
PluginConfig
:插件配置,提供插件逻辑运行所需的配置,会以 JSON 的方式下发到边车代理。
有关插件 CRD 的详细说明,可以参考 Plugin API 文档,也将在下面的演示中结合具体示例进行解释。
演示
在这个演示中,我们将会为服务网格扩展 IAM(Identity and Access Management)功能,来提升服务的安全性:服务 A 访问服务 B 时会携带获得的令牌,服务 B 收到请求后通过身份验证服务来验证令牌,根据验证结果决定是否提供服务。
这里需要用到两个 Plugin
:为服务 A 的请求注入令牌的 token-injector
;对访问服务 B 的请求进行身份验证的 token-verifyer
。二者分别处理 outbound 和 inbound 的流量。
与之对应的是两个 PluginChain
:token-injector-chain
和 token-verifier-chain
。
安装集群
export INSTALL_K3S_VERSION=v1.23.8+k3s2
curl -sfL https://get.k3s.io | sh -s - --disable traefik --disable servicelb --write-kubeconfig-mode 644 --write-kubeconfig ~/.kube/config
复制代码
下载并安装 osm-edge
system=$(uname -s | tr [:upper:] [:lower:])
arch=$(dpkg --print-architecture)
release=v1.3.0
curl -L https://github.com/flomesh-io/osm-edge/releases/download/${release}/osm-edge-${release}-${system}-${arch}.tar.gz | tar -vxzf -
./${system}-${arch}/osm version
cp ./${system}-${arch}/osm /usr/local/bin/
复制代码
export osm_namespace=osm-system
export osm_mesh_name=osm
osm install \
--mesh-name "$osm_mesh_name" \
--osm-namespace "$osm_namespace"
复制代码
部署示例应用
kubectl create namespace curl
osm namespace add curl
kubectl apply -n curl -f https://raw.githubusercontent.com/flomesh-io/osm-edge-docs/main/manifests/samples/curl/curl.yaml
kubectl create namespace httpbin
osm namespace add httpbin
kubectl apply -n httpbin -f https://raw.githubusercontent.com/flomesh-io/osm-edge-docs/main/manifests/samples/httpbin/httpbin.yaml
sleep 2
kubectl wait --for=condition=ready pod -n curl -l app=curl --timeout=90s
kubectl wait --for=condition=ready pod -n httpbin -l app=httpbin --timeout=90s
curl_pod=`kubectl get pod -n curl -l app=curl -o jsonpath='{.items..metadata.name}'`
httpbin_pod=`kubectl get pod -n httpbin -l app=httpbin -o jsonpath='{.items..metadata.name}'`
复制代码
查看两个应用目前的插件链内容,这里 内置的插件 位于 modules
目录中。这些内置插件是服务网格原生提供的功能,并不是通过插件功能配置的,但是可以通过插件功能进行覆盖。
osm proxy get config_dump -n curl $curl_pod | jq '.Chains."outbound-http"'
[
"modules/outbound-http-routing.js",
"modules/outbound-metrics-http.js",
"modules/outbound-tracing-http.js",
"modules/outbound-logging-http.js",
"modules/outbound-circuit-breaker.js",
"modules/outbound-http-load-balancing.js",
"modules/outbound-http-default.js"
]
osm proxy get config_dump -n httpbin $httpbin_pod | jq '.Chains."inbound-http"'
[
"modules/inbound-tls-termination.js",
"modules/inbound-http-routing.js",
"modules/inbound-metrics-http.js",
"modules/inbound-tracing-http.js",
"modules/inbound-logging-http.js",
"modules/inbound-throttle-service.js",
"modules/inbound-throttle-route.js",
"modules/inbound-http-load-balancing.js",
"modules/inbound-http-default.js"
]
复制代码
测试下应用间的通信。
kubectl exec $curl_pod -n curl -c curl -- curl -Is http://httpbin.httpbin:14001/get
HTTP/1.1 200 OK
server: gunicorn/19.9.0
date: Sun, 05 Feb 2023 05:42:51 GMT
content-type: application/json
content-length: 304
access-control-allow-origin: *
access-control-allow-credentials: true
connection: keep-alive
复制代码
部署身份认证服务
部署独立的身份认证服务,对请求进行认证,返回 200
或者 401
。这里我们硬编码了有效令牌 2f1acc6c3a606b082e5eef5e54414ffb
。
kubectl create namespace auth
kubectl apply -n auth -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: ext-auth
name: ext-auth
spec:
replicas: 1
selector:
matchLabels:
app: ext-auth
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: ext-auth
spec:
containers:
- command:
- pipy
- -e
- |2-
pipy({
_acceptTokens: ['2f1acc6c3a606b082e5eef5e54414ffb'],
_allow: false,
})
// Pipeline layouts go here, e.g.:
.listen(8079)
.demuxHTTP().to($ => $
.handleMessageStart(
msg => ((token = msg?.head?.headers?.['x-iam-token']) =>
_allow = token && _acceptTokens?.find(el => el == token)
)()
)
.branch(() => _allow, $ => $.replaceMessage(new Message({ status: 200 })),
$ => $.replaceMessage(new Message({ status: 401 }))
)
)
image: flomesh/pipy:latest
name: pipy
resources: {}
---
apiVersion: v1
kind: Service
metadata:
labels:
app: ext-auth
name: ext-auth
spec:
ports:
- port: 8079
protocol: TCP
targetPort: 8079
selector:
app: ext-auth
EOF
复制代码
启用插件策略
默认情况下并未启用插件策略,需要修改 网格配置 来开启。
export osm_namespace=osm-system
kubectl patch meshconfig osm-mesh-config -n "$osm_namespace" -p '{"spec":{"featureFlags":{"enablePluginPolicy":true}}}' --type=merge
复制代码
声明插件
插件 token-injector
:
metadata.name
: 插件名称,也是插件脚本名称。比如这个插件将保存为 token-injector.js
存储在代码库的 plugins
目录中。
spec.pipyscript
:PipyJS 脚本 内容,是功能逻辑代码,保存在脚本文件 plugins/token-injector.js
中。在脚本中可以只用系统内置的上下文元数据(context metadata)。碍于篇幅,这里提供元数据变量列表的截图。
kubectl apply -f - <<EOF
kind: Plugin
apiVersion: plugin.flomesh.io/v1alpha1
metadata:
name: token-injector
spec:
priority: 115
pipyscript: |+
(
pipy({
_pluginName: '',
_pluginConfig: null,
_accessToken: null,
})
.import({
__service: 'outbound-http-routing',
})
.pipeline()
.onStart(
() => void (
_pluginName = __filename.slice(9, -3),
_pluginConfig = __service?.Plugins?.[_pluginName],
_accessToken = _pluginConfig?.AccessToken
)
)
.handleMessageStart(
msg => _accessToken && (msg.head.headers['x-iam-token'] = _accessToken)
)
.chain()
)
EOF
复制代码
插件 token-verifier
kubectl apply -f - <<EOF
kind: Plugin
apiVersion: plugin.flomesh.io/v1alpha1
metadata:
name: token-verifier
spec:
priority: 115
pipyscript: |+
(
pipy({
_pluginName: '',
_pluginConfig: null,
_accessToken: null,
_valid: false,
})
.import({
__service: 'inbound-http-routing',
})
.pipeline()
.onStart(
() => void (
_pluginName = __filename.slice(9, -3),
_pluginConfig = __service?.Plugins?.[_pluginName],
_accessToken = _pluginConfig?.AccessToken
)
)
.handleMessageStart(
msg => _valid = (_accessToken && msg.head.headers['accesstoken'] === _accessToken)
)
.branch(
() => _valid, (
$ => $.chain()
), (
$ => $.replaceMessage(
new Message({ status: 403 }, 'token verify failed')
)
)
)
)
EOF
复制代码
设置插件链
插件链 token-injector-chain
:
metadata.name
:插件链资源名称 token-injector-chain
spec.chains
name
:所处的插件链名称,4 个插件链之一,这里是 outbound-http
也就是出站流量的 HTTP 协议处理阶段。
plugins
:要插入到插件链的插件列表,这里将 token-injector
插入到插件链中。
spec.selectors
:插件链作用的目标,使用的是 Kubernetes 标签选择器 方案。
podSelector
:pod 选择器,选择标签 app=curl
的 pod。
namespaceSelector
:命名空间选择器,选择命名空间被网格纳管的命名空间,即 openservicemesh.io/monitored-by=osm
kubectl apply -n curl -f - <<EOF
kind: PluginChain
apiVersion: plugin.flomesh.io/v1alpha1
metadata:
name: token-injector-chain
spec:
chains:
- name: outbound-http
plugins:
- token-injector
selectors:
podSelector:
matchLabels:
app: curl
matchExpressions:
- key: app
operator: In
values: ["curl"]
namespaceSelector:
matchExpressions:
- key: openservicemesh.io/monitored-by
operator: In
values: ["osm"]
EOF
复制代码
插件链 token-verifier-chain
kubectl apply -n httpbin -f - <<EOF
kind: PluginChain
apiVersion: plugin.flomesh.io/v1alpha1
metadata:
name: token-verifier-chain
spec:
chains:
- name: inbound-http
plugins:
- token-verifier
selectors:
podSelector:
matchLabels:
app: httpbin
namespaceSelector:
matchExpressions:
- key: openservicemesh.io/monitored-by
operator: In
values: ["osm"]
EOF
复制代码
应用了插件链配置后,此时再查看两个应用的插件链。从结果可以看到两个位于 plugins
目录中的插件,我们声明的插件通过插件链的配置已经配置两个应用中了。
osm proxy get config_dump -n curl $curl_pod | jq '.Chains."outbound-http"'
[
"modules/outbound-http-routing.js",
"modules/outbound-metrics-http.js",
"modules/outbound-tracing-http.js",
"modules/outbound-logging-http.js",
"modules/outbound-circuit-breaker.js",
"plugins/token-injector.js",
"modules/outbound-http-load-balancing.js",
"modules/outbound-http-default.js"
]
osm proxy get config_dump -n httpbin $httpbin_pod | jq '.Chains."inbound-http"'
[
"modules/inbound-tls-termination.js",
"modules/inbound-http-routing.js",
"modules/inbound-metrics-http.js",
"modules/inbound-tracing-http.js",
"modules/inbound-logging-http.js",
"modules/inbound-throttle-service.js",
"modules/inbound-throttle-route.js",
"plugins/token-verifier.js",
"modules/inbound-http-load-balancing.js",
"modules/inbound-http-default.js"
]
复制代码
应用配置了插件之后,由于没有为插件进行配置,应用 curl
仍然可以访问 httpbin
。
kubectl exec $curl_pod -n curl -c curl -- curl -Is http://httpbin.httpbin:14001/get
HTTP/1.1 200 OK
server: gunicorn/19.9.0
date: Sun, 05 Feb 2023 06:34:33 GMT
content-type: application/json
content-length: 304
access-control-allow-origin: *
access-control-allow-credentials: true
connection: keep-alive
复制代码
设置插件配置
我们先应用插件 token-verifier
的配置,这里配置了身份认证服务 ext-auth.auth:8079
和需要进行认证的请求 /get
。
kubectl apply -n httpbin -f - <<EOF
kind: PluginConfig
apiVersion: plugin.flomesh.io/v1alpha1
metadata:
name: token-verifier-config
spec:
config:
Verifier: 'ext-auth.auth:8079'
Paths:
- "/get"
plugin: token-verifier
destinationRefs:
- kind: Service
name: httpbin
namespace: httpbin
EOF
复制代码
这时应用 curl
是无法访问 httbin
的 /get
路径,因为还没有为 curl
配置访问令牌。
kubectl exec $curl_pod -n curl -c curl -- curl -Is http://httpbin.httpbin:14001/get
HTTP/1.1 401 Unauthorized
content-length: 13
connection: keep-alive
复制代码
但访问 /headers
路径并不需要令牌。
kubectl exec $curl_pod -n curl -c curl -- curl -Is http://httpbin.httpbin:14001/headers
HTTP/1.1 200 OK
server: gunicorn/19.9.0
date: Sun, 05 Feb 2023 06:37:05 GMT
content-type: application/json
content-length: 217
access-control-allow-origin: *
access-control-allow-credentials: true
connection: keep-alive
复制代码
接下来应用插件 token-injector
的配置,为应用的请求配置访问令牌 2f1acc6c3a606b082e5eef5e54414ffb
。这个令牌,也是身份认证服务中硬编码的有效令牌。
kubectl apply -n curl -f - <<EOF
kind: PluginConfig
apiVersion: plugin.flomesh.io/v1alpha1
metadata:
name: token-injector-config
spec:
config:
AccessToken: '2f1acc6c3a606b082e5eef5e54414ffb'
plugin: token-injector
destinationRefs:
- kind: Service
name: httpbin
namespace: httpbin
EOF
复制代码
此时,再次访问 httpbin
的 /get
路径,请求通过认证后被 httpbin
接受。
kubectl exec $curl_pod -n curl -c curl -- curl -Is http://httpbin.httpbin:14001/get
HTTP/1.1 200 OK
server: gunicorn/19.9.0
date: Sun, 05 Feb 2023 06:39:54 GMT
content-type: application/json
content-length: 360
access-control-allow-origin: *
access-control-allow-credentials: true
connection: keep-alive
复制代码
总结
这篇文章,我们通过实际的场景来演示了 osm-edge 在 1.3.0 中的插件功能。插件功能的推出,为网格功能的扩展带来了方便快捷的机制。通过插件功能、Pipy 的可编程特性和多协议的支持,我们可以轻松为网格定制各种功能。
有了灵活扩展的能力,加上低成本的开销,相信服务网格会有更广阔的天地!
引用链接
[1]
osm-edge: https://github.com/flomesh-io/osm-edge
[2]
1.3.0 版本: https://github.com/flomesh-io/osm-edge/releases/tag/v1.3.0
[3]
可编程代理 Pipy: https://github.com/flomesh-io/pipy
[4]
Helm values.yaml 内置插件列表: https://github.com/flomesh-io/osm-edge/blob/45b05bd39dc0e8d1c28460622a4be2f92abdf28f/charts/osm/values.yaml#L84
[5]
Plugin API 文档: https://osm-edge-docs.flomesh.io/docs/api_reference/plugin/
[6]
网格配置: https://osm-edge-docs.flomesh.io/docs/api_reference/config/v1alpha2/
[7]
PipyJS 脚本: https://flomesh.io/pipy/docs/en/reference/pjs
[8]
Helm values.yaml 内置插件列表: https://github.com/flomesh-io/osm-edge/blob/45b05bd39dc0e8d1c28460622a4be2f92abdf28f/charts/osm/values.yaml#L84
[9]
Kubernetes 标签选择器: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
评论