# 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