`
November 17, 2023 本文阅读量

在istio服务网格中扩展自定义功能

istio 作为当下火热的服务网格框架,其提供了丰富的功能,但是在实际的使用中,我们还是会遇到一些特殊的场景,需要我们自己基于自己的业务场景去扩展一些功能。本文就简单尝试下如何在 istio 中扩展自定义功能。

前提

本文假设你已经对 Kubernetes、istio 和 Envoy 有一定的了解,如果你还不了解,可以先阅读下面的文章:

当然不仅限于知道这些,还需要对其有一定的实践经验,这样才能更好的理解本文的内容。当然本文也不会涉及太深,只是作为 istio 的一个入门扩展教程。

为什么要扩展功能?#背景

用通俗的话去理解 istio 的作用就是,对我们部署在 kubernetes 上的应用进行流量控制(代理),给我们提供了:流量控制、流量监控、流量安全等功能。但是在实际的使用中,我们还是会遇到一些特殊的场景,需要我们自己基于自己的业务场景去扩展一些功能,比如:ip 白名单、ip 黑名单、统一认证等。这些功能往往和具体公司的业务场景有关,因此 istio 无法直接提供这些功能,需要我们自己去扩展。

传统的扩展方式

在传统架构中,常常通过 API 网关这一组件来实现这一些扩展能力,常用的 API 网关有:kong、apisix、openresty,而扩展的原理就是插件,或者像 nginx/openresty 在请求链中的不同阶段提供了不同的 hook,我们可以基于这些 hook 来实现扩展功能。

就我个人的经历来说,网关要么自己定制开发,要么基于openresty来实现,又或者使用一些开源的网关,如:kong、apisix,在此基础上进行二次开发。不过这些方式都是在传统API网关中去实现的,随着 Service Mesh 的发展,API网关的某些功能也被 Sidecar 代理所取代,比如:流量控制、流量监控、流量安全等。目前 Mesh 发展的趋势也是进一步在 Sidecar 代理中实现更多的功能,而不是在 API 网关中实现。

Envoy 的扩展能力

envoy 提供了丰富的扩展能力,Access Logger, Access Log Filter, Clusters, Listen Filter, Network Filter, HTTP Filter 等等。这一块内容非常多,如果想要了解更多,请参考官方文档。

istio 的扩展能力

https://istio.io/latest/docs/reference/config/networking/envoy-filter/

istio 作为一个 Service Mesh 框架,其底层的代理是 Envoy,因此 istio 也继承了 Envoy 的扩展能力。istio 提供了 EnvoyFilter 这样一种自定义 istio Pilot 生成的 Envoy 配置的机制。可以修改某些字段的值、添加特定过滤器。对于特定命名空间中的给定工作负载,可以存在任意数量的 EnvoyFilter。这些 EnvoyFilter 的应用顺序如下:配置根命名空间中的所有 EnvoyFilter,然后是工作负载命名空间中的所有匹配的 EnvoyFilter。

需要谨慎的使用这个功能,因为错误的配置会影响整个网格的稳定性。

实践:实现一个请求参数改写请求头的功能

假设我们有一个业务场景,需要将请求参数中的 cvn 参数的值,改写到请求头中的 x-istio-cvn 参数中,那我们应该怎么做呢?我们可以用伪代码来简单梳理下逻辑:

function rewrite_cvn_to_header()
    cvn = request.query_params.cvn
    request.headers.set("x-istio-cvn", cvn)
end

编写 EnvoyFilter

从 envoy 的扩展能力中,我们可以知道,我们需要编写一个 HTTP Filter,用于在请求到达 Sidecar 代理时,对请求进行处理,将请求参数中的 cvn 参数的值,改写到请求头中的 x-client-cvn 参数中。而在 istio 中,我们需要编写一个 EnvoyFilter 资源,用于将 HTTP Filter 注入到 Envoy 中。

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: cvn-rewrite       # EnvoyFilter 的名称
  namespace: istio-system # EnvoyFilter 所在的命名空间, 作用于 ingressgateway
spec:
    workloadSelector:
        labels:
          istio: ingressgateway
    configPatches:
        - applyTo: HTTP_FILTER # 应用于 HTTP_FILTER
          match:
            context: GATEWAY  # 作用于 GATEWAY
            listener:
              filterChain:
                  filter:
                    name: envoy.filters.network.http_connection_manager
                    subFilter:
                        name: envoy.filters.http.router
          patch:
            operation: INSERT_BEFORE # 在 envoy.router 之前插入
            value:
              name: envoy.lua
              typed_config:
                  "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
                  inlineCode: |
                    function envoy_on_request(request_handle)
                        local cvn = request_handle:headers():get(":path"):match("cvn=([%w%.%-]+)")
                        if cvn == nil then
                            cvn = "default"
                        end
                        request_handle:headers():add("x-client-cvn", cvn)
                    end                    

如果想要了解 envoy 中关于 lua 更多的扩展内容,一定要先通读下官方文档,地址在此:https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter.html#lua

部署

为了方便测试我们可以编写这么一个应用用于打印请求头:

app.py 服务代码:

from flask import Flask, request

app = Flask(__name__)

@app.route("/")
def index():
    print(request.headers)

    dict = {}
    for key, value in request.headers:
        dict[key] = value

    return dict

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

Dockerfile 文件进行打包:

nerdctl.lima build -t docker.io/yeqown/istio-envoy-filter-demo:0.0.1 . --build-arg VERSION=0.0.1

如果不想自己打包,可以直接用我的镜像:docker.io/yeqown/istio-envoy-filter-demo:0.0.1

FROM python:3.8-slim-buster

WORKDIR /app

COPY . .

RUN pip install flask

EXPOSE 8080

CMD ["python", "app.py"]

k8s 资源配置 deployment.yaml 文件:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: istio-envoy-filter-demo
  namespace: default
  labels:
    app: istio-envoy-filter-demo
spec:
    selector:
      matchLabels:
        app: istio-envoy-filter-demo
    replicas: 1
    template:
      metadata:
        labels:
          app: istio-envoy-filter-demo
      spec:
        containers:
            - name: istio-envoy-filter-demo
              image: docker.io/yeqown/istio-envoy-filter-demo:0.0.1
              imagePullPolicy: IfNotPresent
              ports:
                  - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: istio-envoy-filter-demo
  namespace: default
spec:
    selector:
        app: istio-envoy-filter-demo
    ports:
        - name: http-port
          port: 8080
          targetPort: 8080

deployment.networking.yaml istio 配置文件:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: istio-envoy-filter-demo
  namespace: default
spec:
  hosts:
    - "*"
  gateways:
    - istio-envoy-filter-demo-gateway
  http:
    - name: "istio-envoy-filter-demo"
      match:
        - uri:
            exact: /istio-envoy-filter-demo
      rewrite:
        uri: /
      route:
        - destination:
            host: istio-envoy-filter-demo.default.svc.cluster.local
---
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-envoy-filter-demo-gateway
  namespace: default
spec:
  selector:
    istio: ingressgateway
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - "*"

将这些文件都部署后,可以通过以下的命令来检查时链路是否通畅:

INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].port}')
curl -X GET -v http://$INGRESS_HOST:$INGRESS_PORT/istio-envoy-filter-demo
# > GET /istio-envoy-filter-demo HTTP/1.1
# > Host: 10.96.133.213
# > User-Agent: curl/8.4.0
# > Accept: */*
# >
# < HTTP/1.1 200 OK
# < server: istio-envoy
# < date: Wed, 27 Dec 2023 06:56:18 GMT
# < content-type: application/json
# < content-length: 691
# < x-envoy-upstream-service-time: 1
# <
# { [691 bytes data]
# 100   691  100   691    0     0   182k      0 --:--:-- --:--:-- --:--:--  224k
# * Connection #0 to host 10.96.133.213 left intact
# {
#   "Accept": "*/*",
#   "Host": "10.96.133.213",
#   "User-Agent": "curl/8.4.0",
#   "X-B3-Parentspanid": "914dc42200412837",
#   "X-B3-Sampled": "1",
#   "X-B3-Spanid": "8c40029a531c2e93",
#   "X-B3-Traceid": "c992738f6d8be43e914dc42200412837",
#   "X-Envoy-Attempt-Count": "1",
#   "X-Envoy-Internal": "true",
#   "X-Envoy-Original-Path": "/istio-envoy-filter-demo",
#   "X-Forwarded-Client-Cert": "By=spiffe://cluster.local/ns/default/sa/default;Hash=da144c5982372ecb6463dbfed384d2c4430f7ebfd5e4f5183238f19f8efb565a;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account",
#   "X-Forwarded-For": "10.244.0.1",
#   "X-Forwarded-Proto": "http",
#   "X-Request-Id": "f94024c6-8948-9a1c-af4c-aec40c395f8a"
# }

这里我们可以注意到,请求头中除了常见的 Accept, Host, User-Agent 还多了一些,如:X-B3-Parentspanid, X-Request-Id 等,这些都是 istio 为我们提供的功能,用于链路追踪,我们可以通过这些参数来追踪请求的链路。当然其中没有包含 x-client-cvn 因为我们还没有部署 EnvoyFilter。

部署并测试自定义 envoyFilter

将上面编写的 EnvoyFilter 部署到 k8s 中:

kubectl apply -f envoyfilter.yaml

将这个资源都应用到 k8s 之后,我们再次访问服务,可以看到请求头中多了一个 x-client-cvn 参数,这就是我们自定义的 EnvoyFilter 所做的事情。

curl -X GET -v http://$INGRESS_HOST:$INGRESS_PORT/istio-envoy-filter-demo?cvn=v1.2.3-hotfix
# * Connected to 10.96.133.213 (10.96.133.213) port 80
# > GET /istio-envoy-filter-demo?cvn=v1.2.3-hotfix HTTP/1.1
# > Host: 10.96.133.213
# > User-Agent: curl/8.4.0
# > Accept: */*
# >
# < HTTP/1.1 200 OK
# < server: istio-envoy
# < date: Wed, 27 Dec 2023 06:55:32 GMT
# < content-type: application/json
# < content-length: 715
# < x-envoy-upstream-service-time: 7
# <
# { [715 bytes data]
# 100   715  100   715    0     0  78857      0 --:--:-- --:--:-- --:--:-- 79444
# * Connection #0 to host 10.96.133.213 left intact
# {
#   "Accept": "*/*",
#   "Host": "10.96.133.213",
#   "User-Agent": "curl/8.4.0",
#   "X-B3-Parentspanid": "ea5a04a5505b30f2",
#   "X-B3-Sampled": "1",
#   "X-B3-Spanid": "579aefbe55ff1859",
#   "X-B3-Traceid": "22daad86fc4471d2ea5a04a5505b30f2",
#   "X-Client-Cvn": "v1.2.3-hotfix",
#   "X-Envoy-Attempt-Count": "1",
#   "X-Envoy-Internal": "true",
#   "X-Envoy-Original-Path": "/istio-envoy-filter-demo?cvn=v1.2.3-hotfix",
#   "X-Forwarded-Client-Cert": "By=spiffe://cluster.local/ns/default/sa/default;Hash=da144c5982372ecb6463dbfed384d2c4430f7ebfd5e4f5183238f19f8efb565a;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account",
#   "X-Forwarded-For": "10.244.0.1",
#   "X-Forwarded-Proto": "http",
#   "X-Request-Id": "c5041ac1-8b18-9999-be44-63bc6fbfa885"
# }

到这里,我们就完成了一个简单的自定义 EnvoyFilter 的功能:在网关处将查询参数 cvn 转请求头 X-Client-Cvn ,当然这只是一个简单的例子,实际的使用中,我们可以基于 Envoy 的扩展机制,实现更多的功能,比如:ip 白名单、ip 黑名单、统一认证等。

所有代码都可以在:https://github.com/yeqown/playground/tree/master/k8s/istio-envoy-filter 找到。

参考资料