본문 바로가기
envoy

Envoy Lua 필터

by Misan Kim 2026. 4. 4.

# envoy 와 envoy lua 필터

 

오픈소스 프록시인 Envoy 는 클라우드 및 Kubernetes 환경에서 최근 몇 년 사이 가장 활발하게 사용되는 프록시 중 하나라고 생각한다. Envoy 의 강점은 xDS를 이용한 유연한 설정이라고 생각하는데, 다른 강점 중 하나는 다양한 Filter 를 제공하여 트래픽을 컨트롤하는 다양한 옵션을 제공한다는 것이 될 것 같다.

 

envoy 에서 lua 필터를 사용하면 lua 스크립트를 작성하여 envoy 에서 cluster(백엔드)로 보내는 요청과 응답을 중간에서 조작할 수 있다. 단순히 요청/응답 헤더를 조작하는 것 뿐만 아니라 별도의 cluster(제 3의 엔드포인트)를 호출하여 응답에 따라 추가적인 분기(if 문)를 만들고, 직접 응답 혹은 클러스터로 요청 전송 등을 구성할 수 있다.

 

하지만 스크립트의 구문 오류가 있는 경우 (단순히 스크립트만 동작하지 않고 envoy 는 정상 실행되는 경우도 있으나) envoy 가 실행되지 않을 수 있고, 스크립트 내용의 복잡도나 스크립트 내용에 포함된 추가 http 호출 대상(httpCall())과의 통신 상태에 따라 envoy 성능이나 레이턴시에 영향을 줄 수 있으니 lua 필터를 사용할 때에는 철저한 테스트와 검증은 반드시 필요하다고 할 수 있겠다.

 

 

# envoy config 작성

envoy_on_request(request_handle) 는 envoy 에서 cluster 로 전송되는 요청을 조작
envoy_on_response(response_handle) 는 cluster 에서 envoy 로 수신되는 응답을 조작

 

아래 샘플 config 는 envoy 로 들어오는 요청을 httpbin.org 백엔드로 프록시하는 샘플이다.

실제 활용하기 위한 목적이 아닌 lua 필터로 가능한 것이 무엇인지 테스트하기 위한 설정이니 이 점은 참고하면 좋겠다.

 

 

1. "/headers" 경로로 들어오는 요청은 백엔드로부터의 응답 본문을 로깅한다.

                        function envoy_on_response(response_handle)
                          -- 응답 body 를 로그로 출력
                          local body = response_handle:body(true)
                          local size = body:length()
                          response_handle:logInfo(size)
                          local msg = body:getBytes(0, size)
                          response_handle:logInfo(msg)
                        end

 

 

2. 그 외의 경로로 들어오는 요청의 경우 좀 더 다양한 액션을 구성해봤다. 먼저 envoy 로 들어온 요청을 백엔드로 전달하기 전에 ifconfig.me 로 추가적인 요청을 보낸다.

 

                        ...
                        function envoy_on_request(request_handle)
                          -- 별도의 cluster 로 http 요청을 추가 생성
                          local headers, body = request_handle:httpCall(
                            "service_ifconfig_me",
                            {
                              [":method"] = "GET",
                              [":path"] = "/",
                              [":authority"] = "ifconfig.me",
                              ["content-type"] = "application/json",
                              ["user-agent"] = "curl",
                            },
                            nil,
                            1000
                          )
                          ...
                        end

 

 

3. 2번 요청으로 보낸 응답 헤더 및 응답 바디를 로그로 출력한다.

                          ...
                          -- httpCall() 으로 받은 응답 헤더를 출력
                          for k, v in pairs(headers) do
                            request_handle:logInfo(k .. ": " .. v)
                          end
                          request_handle:logInfo(headers[":status"])
                          request_handle:logInfo(body)
                          ...

 

 

4. 2번 요청에 대한 status code 가 200이 아니면, 백엔드로 요청을 전달하지 않고 envoy 가 403 응답을 바로 클라이언트로 전송한다. 이 때 응답 헤더나 응답 본문을 원하는대로 구성 가능하다.

                          ...
                          -- 직접 응답을 클라이언트로 리턴
                          if headers[":status"] ~= "200"
                          then
                            request_handle:respond(
                              {
                                [":status"] = "403",
                                ["ifconfig_date"] = headers["date"],
                                ["ifconfig_status"] = headers[":status"],
                                ["ifconfig_body"] = body,
                              },
                              "test"
                            )
                          end
                          ...

 

 

5. 백엔드로 전달할 요청 헤더를 추가하고, 백엔드로 전달할 요청 경로(path)를 재작성(rewrite)한다.

                          ...
                          -- cluster 로 요청 헤더를 추가
                          request_handle:headers():add("ifconfig_date", headers["date"])
                          request_handle:headers():add("ifconfig_status", headers[":status"])
                          request_handle:headers():add("ifconfig_body", body)

                          -- cluster 로 요청 헤더(path)를 변경(rewriter)
                          request_handle:logInfo("request path: " .. request_handle:headers():get(":path"))
                          request_handle:headers():replace(":path", "/headers")
                          request_handle:logInfo("rewrite path: " .. request_handle:headers():get(":path"))
                          ...

 

 

아래는 전체 envoy config 파일의 내용이다.

vim envoy-lua.yaml

admin:
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 9901

static_resources:

  listeners:
  - name: listener_0
    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
          access_log:
          - name: envoy.access_loggers.stdout
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
          http_filters:
          - name: lua_filter_with_custom_name_1
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match:
                  prefix: "/headers"
                route:
                  host_rewrite_literal: httpbin.org
                  cluster: service_httpbin_org
                typed_per_filter_config:
                  lua_filter_with_custom_name_1:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute
                    source_code:
                      inline_string: |
                        function envoy_on_request(request_handle)
                        end
                        
                        function envoy_on_response(response_handle)
                          -- 응답 body 를 로그로 출력
                          local body = response_handle:body(true)
                          local size = body:length()
                          response_handle:logInfo(size)
                          local msg = body:getBytes(0, size)
                          response_handle:logInfo(msg)
                        end
              - match:
                  prefix: "/"
                route:
                  host_rewrite_literal: httpbin.org
                  cluster: service_httpbin_org
                typed_per_filter_config:
                  lua_filter_with_custom_name_1:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.LuaPerRoute
                    source_code:
                      inline_string: |
                        function envoy_on_request(request_handle)
                          -- 별도의 클러스터로 http 요청을 추가 생성
                          local headers, body = request_handle:httpCall(
                            "service_ifconfig_me",
                            {
                              [":method"] = "GET",
                              [":path"] = "/",
                              [":authority"] = "ifconfig.me",
                              ["content-type"] = "application/json",
                              ["user-agent"] = "curl",
                            },
                            nil,
                            1000
                          )

                          -- httpCall() 으로 받은 응답 헤더를 출력
                          for k, v in pairs(headers) do
                            request_handle:logInfo(k .. ": " .. v)
                          end
                          request_handle:logInfo(headers[":status"])
                          request_handle:logInfo(body)

                          -- 직접 응답을 클라이언트로 리턴
                          if headers[":status"] ~= "200"
                          then
                            request_handle:respond(
                              {
                                [":status"] = "403",
                                ["ifconfig_date"] = headers["date"],
                                ["ifconfig_status"] = headers[":status"],
                                ["ifconfig_body"] = body,
                              },
                              "test"
                            )
                          end

                          -- 클러스터로 요청 헤더를 추가
                          request_handle:headers():add("ifconfig_date", headers["date"])
                          request_handle:headers():add("ifconfig_status", headers[":status"])
                          request_handle:headers():add("ifconfig_body", body)

                          -- 클러스터로 요청 헤더(path)를 변경(rewriter)
                          request_handle:logInfo("request path: " .. request_handle:headers():get(":path"))
                          request_handle:headers():replace(":path", "/headers")
                          request_handle:logInfo("rewrite path: " .. request_handle:headers():get(":path"))
                        end

                        function envoy_on_response(response_handle)
                          -- 응답 body 를 로그로 출력
                          local body = response_handle:body(true)
                          local size = body:length()
                          response_handle:logInfo(size)
                          local msg = body:getBytes(0, size)
                          response_handle:logInfo(msg)
                        end

  clusters:
  - name: service_httpbin_org
    type: LOGICAL_DNS
    dns_lookup_family: V4_ONLY
    load_assignment:
      cluster_name: service_httpbin_org
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: httpbin.org
                port_value: 443
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
        sni: httpbin.org
  - name: service_ifconfig_me
    type: LOGICAL_DNS
    dns_lookup_family: V4_ONLY
    load_assignment:
      cluster_name: service_httpbin_org
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: ifconfig.me
                port_value: 443
    transport_socket:
      name: envoy.transport_sockets.tls
      typed_config:
        "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
        sni: ifconfig.me

 

 

# 컨테이너 실행하여 접속 테스트

docker run --rm -it \
      -v $(pwd)/envoy-lua.yaml:/envoy-custom.yaml \
      -p 9901:9901 \
      -p 10000:10000 \
      envoyproxy/envoy:v1.35.0 \
          -c /envoy-custom.yaml

호출 테스트
curl -v http://127.0.0.1:10000/
curl -v http://127.0.0.1:10000/headers

 

 

(참고) 출력되는 envoy 로그

...
[2026-04-04 02:52:27.731][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: date: Sat, 04 Apr 2026 02:52:27 GMT
[2026-04-04 02:52:27.731][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: access-control-allow-origin: *
[2026-04-04 02:52:27.731][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: x-envoy-upstream-service-time: 229
[2026-04-04 02:52:27.731][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
[2026-04-04 02:52:27.731][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: :status: 200
[2026-04-04 02:52:27.731][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: content-type: text/plain
[2026-04-04 02:52:27.731][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: via: 1.1 google
[2026-04-04 02:52:27.731][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: content-length: 13
[2026-04-04 02:52:27.731][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: 200
[2026-04-04 02:52:27.731][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: xxx.xxx.xxx.xxx
[2026-04-04 02:52:27.731][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: request path: /
[2026-04-04 02:52:27.731][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: rewrite path: /headers
[2026-04-04 02:52:28.490][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: 394
[2026-04-04 02:52:28.490][10][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: {
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "Ifconfig-Body": "xxx.xxx.xxx.xxx",
    "Ifconfig-Date": "Sat, 04 Apr 2026 02:52:27 GMT",
    "Ifconfig-Status": "200",
    "User-Agent": "curl/8.7.1",
    "X-Amzn-Trace-Id": "Root=1-69d07cec-3aa017f467a2a9a727a5577b",
    "X-Envoy-Expected-Rq-Timeout-Ms": "15000",
    "X-Envoy-Original-Host": "127.0.0.1:10000"
  }
}

[2026-04-04T02:52:27.500Z] "GET /headers HTTP/1.1" 200 - 0 394 990 757 "-" "curl/8.7.1" "fe228c43-8669-407e-b2b9-9a4cc2ded092" "httpbin.org" "52.6.211.202:443"

[2026-04-04 02:52:56.899][12][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: 269
[2026-04-04 02:52:56.899][12][info][lua] [source/extensions/filters/common/lua/lua.cc:26] script log: {
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "curl/8.7.1",
    "X-Amzn-Trace-Id": "Root=1-69d07d08-14d5e0147f4b136f41525bc7",
    "X-Envoy-Expected-Rq-Timeout-Ms": "15000",
    "X-Envoy-Original-Host": "127.0.0.1:10000"
  }
}

[2026-04-04T02:52:56.248Z] "GET /headers HTTP/1.1" 200 - 0 269 650 649 "-" "curl/8.7.1" "3f54233f-199c-4a71-a586-4d293417c5dd" "httpbin.org" "52.6.211.202:443"
...

 

 

 

# 참고 사이트

https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter
https://github.com/envoyproxy/examples/tree/main/lua
https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/http_conn_man
https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage
 https://www.envoyproxy.io/docs/envoy/latest/configuration/advanced/substitution_formatter