实验16:使用 Lua 脚本扩展 Envoy

在这个实验中,我们将编写一个 Lua 脚本,为响应头添加一个头,并使用一个文件中定义的全局脚本。

我们将创建一个 Envoy 配置和一个 Lua 脚本,在响应句柄上添加一个头。由于我们不会使用请求路径,所以我们不需要定义 envoy_on_request 函数。响应函数看起来像这样。

function envoy_on_response(response_handle)
  response_handle:headers():add("hello", "world")
end

我们在从 headers() 函数返回的 header 对象上调用 add(<header-name>, <header-value>) 函数。

让我们在 Envoy 配置中内联定义这个脚本。为了简化配置,我们将使用 direct_response,而不是集群。

static_resources:
  listeners:
  - name: main
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 10000
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          route_config:
            name: some_route
            virtual_hosts:
            - name: some_service
              domains:
              - "*"
              routes:
              - match:
                  prefix: "/"
                direct_response:
                  status: 200
                  body:
                    inline_string: "200"
          http_filters:
          - name: envoy.filters.http.lua
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
              inline_code: |
                function envoy_on_response(response_handle)
                  response_handle:headers():add("hello", "world")
                end                
          - name: envoy.filters.http.router

将上述 YAML 保存为 8-lab-1-lua-script.yaml 并运行它。

func-e run -c 8-Lab-1-lua-script.yaml &

为了测试这个功能,我们可以向 localhost:10000 发送一个请求,并检查响应头。

$ curl -v localhost:10000
...
> GET / HTTP/1.1
> Host: localhost:10000
> User-Agent: curl/7.64.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-length: 3
< content-type: text/plain
< hello: world
< date: Tue, 23 Nov 2021 21:37:01 GMT
< server: envoy
<
200

输出应该包括我们在其他标准头文件中添加的 hello: world header。

让我们来看看一个更复杂的情况。对于传入的请求,我们要检查它们是否有一个叫做 my-request-id 的头,如果这个头不存在,那么我们要为所有 GET 请求添加一个 my-request-id 头。

因为我们想在 envoy_on_response 函数中检查方法和一个头,我们将使用动态元数据在 envoy_on_request 函数中存储这些值。然后,在响应函数中,我们可以读取元数据,检查头是否被设置,方法是否为 GET,并添加 my-request-id 头。

下面是代码的样子。

function envoy_on_request(request_handle)
  local headers = request_handle:headers()
  local metadata = request_handle:streamInfo():dynamicMetadata()
  metadata:set("envoy.filters.http.lua", "requestInfo", {
      requestId = headers:get("my-request-id"),
      method = headers:get(":method"),
    })
end
function envoy_on_response(response_handle)
  local requestInfoObj = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["requestInfo"]

  local requestId = requestInfoObj.requestId
  local method = requestInfoObj.method
  if (requestId == nil or requestId == '') and (method == 'GET') then
    response_handle:logInfo("Adding request ID header")
    response_handle:headers():add("my-request-id", "some_id_here")
  end
end

注意,目前我们使用 some_id_here 作为 my-request-id的值,以后我们会创建一个函数,为我们生成一个 ID。下面是完整的 Envoy 配置的样子。

static_resources:
  listeners:
  - name: main
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 10000
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          route_config:
            name: some_route
            virtual_hosts:
            - name: some_service
              domains:
              - "*"
              routes:
              - match:
                  prefix: "/"
                direct_response:
                  status: 200
                  body:
                    inline_string: "200"
          http_filters:
          - name: envoy.filters.http.lua
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
              inline_code: |
                function envoy_on_request(request_handle)
                  local headers = request_handle:headers()
                  local metadata = request_handle:streamInfo():dynamicMetadata()
                  metadata:set("envoy.filters.http.lua", "requestInfo", {
                      requestId = headers:get("my-request-id"),
                      method = headers:get(":method"),
                    })
                end
                function envoy_on_response(response_handle)
                  local requestInfoObj = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["requestInfo"]

                  local requestId = requestInfoObj.requestId
                  local method = requestInfoObj.method
                  if (requestId == nil or requestId == '') and (method == 'GET') then
                    response_handle:logInfo("Adding request ID header")
                    response_handle:headers():add("my-request-id", "some_id_here")
                  end
                end                
          - name: envoy.filters.http.router

将上述 YAML 保存为 8-lab-1-lua-script-1.yaml 并运行它。

func-e run -c 8-Lab-1-lua-script-1.yaml &

让我们试一试几种情况。首先,我们将发送一个没有设置 my-request-id 头的 GET 请求。

$ curl -v localhost:10000
...
[2021-11-23 22:59:35.932][2258][info][lua] [source/extensions/filters/http/lua/lua_filter.cc:795] script log: Adding request ID header
< HTTP/1.1 200 OK
< content-length: 3
< content-type: text/plain
< my-request-id: some_id_here
< date: Tue, 23 Nov 2021 22:59:35 GMT
< server: envoy
<
* Connection #0 to host localhost left intact
200

我们知道 Lua 代码运行了,因为我们看到了日志条目和 my-request-id 头的设置。

让我们试着发送一个 POST 请求。

$ curl -X POST -v localhost:10000
...
> POST / HTTP/1.1
> Host: localhost:10000
> User-Agent: curl/7.64.0
> Accept: */*
>
< HTTP/1.1 200 OK
< content-length: 3
< content-type: text/plain
< date: Mon, 29 Nov 2021 23:49:58 GMT
< server: envoy

注意到头信息中没有包括 my-request-id 头信息。最后,让我们也尝试发送一个 GET 请求,但也提供 my-request-id 头。在这种情况下,my-request-id 头信息也不应该被包含在响应中。

$ curl -v -H "my-request-id: something" localhost:10000
...
> GET / HTTP/1.1
> Host: localhost:10000
> User-Agent: curl/7.64.0
> Accept: */*
> my-request-id: something
>
< HTTP/1.1 200 OK
< content-length: 3
< content-type: text/plain
< date: Mon, 29 Nov 2021 23:51:57 GMT
< server: envoy
<
* Connection #0 to host localhost left intact

作为最后的练习,我们将创建一个单独的.lua 脚本,生成一个简单的随机字符串,可以用于请求 ID。我们将加载该脚本,然后在响应函数中调用它来获得请求 ID。

让我们创建一个 library.lua 文件,内容如下。

LIBRARY = {}

function LIBRARY.RandomString()
  local result = ""
  for i = 1, 24 do
    result = result .. string.char(math.random(97, 122))
  end
  return result
end

return LIBRARY

我们正在声明一个名为 LIBRARY的表和一个名为 RandomString的函数。

将上述 Lua 脚本保存为一个名为 library.lua的文件,并将其放在你的 Envoy 进程将要运行的同一个文件夹中。

Luajit 运行时在进程的工作目录和 /usr/local/share/lua/5.1 文件夹中寻找 Lua 模块。

我们在 Envoy 配置中的现有代码将基本保持不变。我们只需要加载 library.lua 和调用 RandomString 函数。

这是更新后 Envoy 配置。

static_resources:
  listeners:
  - name: main
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 10000
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          route_config:
            name: some_route
            virtual_hosts:
            - name: some_service
              domains:
              - "*"
              routes:
              - match:
                  prefix: "/"
                direct_response:
                  status: 200
                  body:
                    inline_string: "200"
          http_filters:
          - name: envoy.filters.http.lua
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
              inline_code: |
                local library = require("library")
                function envoy_on_request(request_handle)
                  local headers = request_handle:headers()
                  local metadata = request_handle:streamInfo():dynamicMetadata()
                  metadata:set("envoy.filters.http.lua", "requestInfo", {
                      requestId = headers:get("my-request-id"),
                      method = headers:get(":method"),
                    })
                end
                function envoy_on_response(response_handle)
                  local requestInfoObj = response_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.lua")["requestInfo"]

                  local requestId = requestInfoObj.requestId
                  local method = requestInfoObj.method
                  if (requestId == nil or requestId == '') and (method == 'GET') then
                    response_handle:logInfo("Adding request ID header")
                    response_handle:headers():add("my-request-id", library.RandomString())
                  end
                end                
          - name: envoy.filters.http.router

将上述 YAML 保存为 8-lab-1-lua-script-2.yaml,然后用 func-e 运行它。

为了试用它,让我们向 localhost:10000 发送一个请求。

$ curl -v localhost:10000
...
[2021-11-23 23:14:18.206][2526][info][lua] [source/extensions/filters/http/lua/lua_filter.cc:795] script log: Adding request ID header
< HTTP/1.1 200 OK
< content-length: 3
< content-type: text/plain
< my-request-id: usptcritlocbzsezhjmroule
< date: Tue, 23 Nov 2021 23:14:18 GMT
< server: envoy
<
* Connection #0 to host localhost left intact
200

输出将包括 my-request-id 和我们生成的、从 library.lua 文件调用的随机字符串。