Serverless Functions
The serverless functions consist of two plugins, serverless-pre-function and serverless-post-function. These plugins enable the execution of user-defined logic at the beginning and end of the execution phases the functions hook to.
Tips for Writing Functions
Only Lua functions are allowed in the serverless plugins and not other Lua code.
For example, anonymous functions are legal:
return function()
ngx.log(ngx.ERR, 'one')
end
Closures are also legal:
local count = 1
return function()
count = count + 1
ngx.say(count)
end
But code other than functions are illegal:
local count = 1
ngx.say(count)
Examples
The examples below demonstrate how you can configure the serverless-pre-function and serverless-post-function plugins for different scenarios.
Log Information before and after a Phase
The example below demonstrates how you can configure the serverless plugins to execute custom logics to log information to error logs before and after the rewrite phase.
Create a route as such:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H 'X-API-KEY: ${ADMIN_API_KEY}' \
-d '{
"id": "serverless-pre-route",
"uri": "/anything",
"plugins": {
"serverless-pre-function": {
"phase": "rewrite",
"functions" : [
"return function()
ngx.log(ngx.ERR, \"serverless pre function\");
end"
]
},
"serverless-post-function": {
"phase": "rewrite",
"functions" : [
"return function(conf, ctx)
ngx.log(ngx.ERR, \"match uri \", ctx.curr_req_matched and ctx.curr_req_matched._path);
end"
]
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
services:
- name: httpbin
routes:
- name: serverless-pre-route
uris:
- /anything
plugins:
serverless-pre-function:
phase: rewrite
functions:
- |
return function()
ngx.log(ngx.ERR, "serverless pre function")
end
serverless-post-function:
phase: rewrite
functions:
- |
return function(conf, ctx)
ngx.log(ngx.ERR, "match uri ", ctx.curr_req_matched and ctx.curr_req_matched._path)
end
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: serverless-functions-plugin-config
spec:
plugins:
- name: serverless-pre-function
config:
phase: rewrite
functions:
- |
return function()
ngx.log(ngx.ERR, "serverless pre function")
end
- name: serverless-post-function
config:
phase: rewrite
functions:
- |
return function(conf, ctx)
ngx.log(ngx.ERR, "match uri ", ctx.curr_req_matched and ctx.curr_req_matched._path)
end
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: serverless-pre-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: serverless-functions-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: serverless-pre-route
spec:
ingressClassName: apisix
http:
- name: serverless-pre-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: serverless-pre-function
config:
phase: rewrite
functions:
- |
return function()
ngx.log(ngx.ERR, "serverless pre function")
end
- name: serverless-post-function
config:
phase: rewrite
functions:
- |
return function(conf, ctx)
ngx.log(ngx.ERR, "match uri ", ctx.curr_req_matched and ctx.curr_req_matched._path)
end
Apply the configuration:
kubectl apply -f serverless-functions-ic.yaml
❶ Hook the serverless pre-function logic to the rewrite phase.
❷ Define a Lua function that logs a message of serverless pre function in the error log.
❸ Hook the serverless post-function logic to the rewrite phase.
❹ Define a Lua function that logs the matched URI in the error log. conf and ctx can be passed as the first two arguments like other plugins, where conf is the plugin configurations and ctx is the request context.
Send the request to the route:
curl -i "http://127.0.0.1:9080/anything"
You should receive an HTTP/1.1 200 OK response and see the following entries in the error log:
2024/05/09 15:07:09 [error] 51#51: *3963 [lua] [string "return function() ngx.log(ngx.ERR, "serverles..."]:1: func(): serverless pre function, client: 172.21.0.1, server: _, request: "GET /anything HTTP/1.1", host: "127.0.0.1:9080"
2024/05/09 15:16:58 [error] 50#50: *9343 [lua] [string "return function(conf, ctx) ngx.log(ngx.ERR, "..."]:1: func(): match uri /anything, client: 172.21.0.1, server: _, request: "GET /anything HTTP/1.1", host: "127.0.0.1:9080"
The first entry is added by the pre-function and the second entry is added by the post-function.
Register Custom Variables
The following example registers a custom built-in variable with a serverless function and adds its value to syslog entries.
This example cannot be completed with the Ingress Controller because it does not support configuring route labels.
The pinned rsyslog collector image is published for amd64. Set the running APISIX or API7 Gateway container name:
export GATEWAY_CONTAINER=replace-with-gateway-container-name
Create a dedicated network:
docker network create gateway-rsyslog-net
Connect the gateway to the network:
docker network connect gateway-rsyslog-net "$GATEWAY_CONTAINER"
Start the collector with TCP reception enabled:
docker run -d \
--platform linux/amd64 \
--name rsyslog-collector \
--network gateway-rsyslog-net \
-e ENABLE_TCP=on \
-e ENABLE_UDP=off \
rsyslog/rsyslog-collector:2026-04
Create a service with a serverless function to register a custom variable a6_route_labels, enable a logging plugin to later log the custom variable, and configure an upstream:
- APISIX Admin API
- ADC
curl "http://127.0.0.1:9180/apisix/admin/services" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "srv_custom_var",
"plugins": {
"serverless-pre-function": {
"phase": "rewrite",
"functions": [
"return function() local core = require \"apisix.core\"; core.ctx.register_var(\"a6_route_labels\", function(ctx) local route = ctx.matched_route and ctx.matched_route.value; if route and route.labels then return route.labels end; return nil end) end"
]
},
"syslog": {
"host": "rsyslog-collector",
"port": 514,
"flush_limit": 1
}
},
"upstream": {
"nodes": {
"httpbin.org:80": 1
}
}
}'
services:
- name: srv-custom-var
labels:
docs-example: serverless-custom-variable
plugins:
serverless-pre-function:
phase: rewrite
functions:
- |
return function()
local core = require("apisix.core")
core.ctx.register_var("a6_route_labels", function(ctx)
local route = ctx.matched_route and ctx.matched_route.value
if route and route.labels then
return route.labels
end
return nil
end)
end
syslog:
host: rsyslog-collector
port: 514
flush_limit: 1
upstream:
nodes:
- host: httpbin.org
port: 80
weight: 1
Preview the changes to services with the example label:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=serverless-custom-variable
Synchronize the reviewed changes:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=serverless-custom-variable
❶ functions: register a custom variable a6_route_labels and fetch the variable value from the matched route's labels property.
❷ Configure the collector hostname and TCP port reachable from the gateway.
❸ flush_limit: set to 1 to push log to the syslog server immediately.
Next, update the log format for all syslog instances with the new variable by configuring the plugin metadata:
- APISIX Admin API
- ADC
curl "http://127.0.0.1:9180/apisix/admin/plugin_metadata/syslog" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"log_format_extra": {
"host": "$host",
"client_ip": "$remote_addr",
"labels": "$a6_route_labels"
}
}'
Plugin metadata is a global collection and cannot be isolated with a label selector. Export the complete collection before changing this entry:
adc dump -o adc-metadata.yaml --with-id \
--include-resource-type plugin_metadata
Add or update the syslog entry while preserving every other entry in adc-metadata.yaml:
plugin_metadata:
# Keep all other plugin metadata entries from the exported file.
syslog:
log_format_extra:
host: "$host"
client_ip: "$remote_addr"
labels: "$a6_route_labels"
Preview the complete metadata change and confirm that it contains no unintended updates or deletions:
adc diff -f adc-metadata.yaml \
--include-resource-type plugin_metadata
Synchronize the reviewed plugin metadata collection:
adc sync -f adc-metadata.yaml \
--include-resource-type plugin_metadata
❶ $host and $remote_addr: NGINX variables.
❷ $a6_route_labels: custom variable.
Finally, create a route:
- APISIX Admin API
- ADC
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "route_custom_var",
"uri": "/get",
"service_id": "srv_custom_var",
"labels": {
"key": "test_a6_route_labels"
}
}'
Update adc.yaml to add the route to the existing service while preserving its plugins and upstream:
services:
- name: srv-custom-var
labels:
docs-example: serverless-custom-variable
plugins:
serverless-pre-function:
phase: rewrite
functions:
- |
return function()
local core = require("apisix.core")
core.ctx.register_var("a6_route_labels", function(ctx)
local route = ctx.matched_route and ctx.matched_route.value
if route and route.labels then
return route.labels
end
return nil
end)
end
syslog:
host: rsyslog-collector
port: 514
flush_limit: 1
routes:
- name: route-custom-var
uris:
- /get
labels:
key: test_a6_route_labels
upstream:
nodes:
- host: httpbin.org
port: 80
weight: 1
Preview the changes to services with the example label:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=serverless-custom-variable
Synchronize the reviewed changes:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=serverless-custom-variable
❶ In the Admin API example, set service_id to associate the route with the existing service. In ADC, the route is nested under the service definition.
❷ Add route labels so the custom variable can log them.
In a separate terminal, follow the collector log:
docker exec rsyslog-collector tail -f /var/log/all.log
Send a request to verify the variable registration:
curl "http://127.0.0.1:9080/get"
The collector prefixes each received line with syslog metadata. The JSON message should be similar to the following:
{
"host": "127.0.0.1",
"route_id": "route_custom_var",
"client_ip": "192.168.155.1",
"labels": {
"key": "test_a6_route_labels"
},
"service_id": "srv_custom_var"
}
When configured with ADC, route_id and service_id are generated identifiers, and labels also contains the docs-example: serverless-custom-variable service-ownership label.
This verifies the custom variable was registered and it logs the labels information in a route successfully.
Modify a Specific Field in Response Body
The example below demonstrates how you can use the serverless plugins to remove a specific field from a JSON response body.
Before proceeding with the removal, first configure a route as follows to see the unmodified response:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H 'X-API-KEY: ${ADMIN_API_KEY}' \
-d '{
"id":"serverless-remove-body-info",
"uri": "/get",
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
services:
- name: httpbin
routes:
- name: serverless-remove-body-info
uris:
- /get
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: serverless-remove-body-info
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /get
backendRefs:
- name: httpbin-external-domain
port: 80
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: serverless-remove-body-info
spec:
ingressClassName: apisix
http:
- name: serverless-remove-body-info
match:
paths:
- /get
upstreams:
- name: httpbin-external-domain
Apply the configuration:
kubectl apply -f serverless-remove-body-ic.yaml
Send a request to the route:
curl "http://127.0.0.1:9080/get"
You should see a response similar to the following with your host and proxy's IP information:
{
"args": {},
"headers": {
"Accept": "*/*",
"Host": "127.0.0.1",
"User-Agent": "curl/8.4.0",
"X-Amzn-Trace-Id": "Root=1-663db30f-51448a1b635f2f4338a4fcfc",
"X-Forwarded-Host": "127.0.0.1"
},
"origin": "172.19.0.1, 43.252.208.84",
"url": "http://127.0.0.1/get"
}
To remove the origin field from the response, update the route with serverless plugins:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes/serverless-remove-body-info" -X PATCH \
-H 'X-API-KEY: ${ADMIN_API_KEY}' \
-d '{
"plugins": {
"serverless-pre-function": {
"phase": "header_filter",
"functions" : [
"return function(conf, ctx)
local core = require(\"apisix.core\")
core.response.clear_header_as_body_modified()
end"
]
},
"serverless-post-function": {
"phase": "body_filter",
"functions" : [
"return function(conf, ctx)
local cjson = require(\"cjson\")
local core = require(\"apisix.core\")
local body = core.response.hold_body_chunk(ctx)
if not body then
return
end
body = cjson.decode(body)
body.origin = nil
body = cjson.encode(body)
ngx.arg[1] = body
end"
]
}
}
}'
services:
- name: httpbin
routes:
- name: serverless-remove-body-info
uris:
- /get
plugins:
serverless-pre-function:
phase: header_filter
functions:
- |
return function(conf, ctx)
local core = require("apisix.core")
core.response.clear_header_as_body_modified()
end
serverless-post-function:
phase: body_filter
functions:
- |
return function(conf, ctx)
local cjson = require("cjson")
local core = require("apisix.core")
local body = core.response.hold_body_chunk(ctx)
if not body then
return
end
body = cjson.decode(body)
body.origin = nil
body = cjson.encode(body)
ngx.arg[1] = body
end
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
Synchronize the configuration to the gateway:
adc sync -f adc.yaml
- Gateway API
- APISIX CRD
# Other Configs
# ---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: serverless-remove-body-plugin-config
spec:
plugins:
- name: serverless-pre-function
config:
phase: header_filter
functions:
- |
return function(conf, ctx)
local core = require("apisix.core")
core.response.clear_header_as_body_modified()
end
- name: serverless-post-function
config:
phase: body_filter
functions:
- |
return function(conf, ctx)
local cjson = require("cjson")
local core = require("apisix.core")
local body = core.response.hold_body_chunk(ctx)
if not body then
return
end
body = cjson.decode(body)
body.origin = nil
body = cjson.encode(body)
ngx.arg[1] = body
end
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: serverless-remove-body-info
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /get
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: serverless-remove-body-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
# Other Configs
# ---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: serverless-remove-body-info
spec:
ingressClassName: apisix
http:
- name: serverless-remove-body-info
match:
paths:
- /get
upstreams:
- name: httpbin-external-domain
plugins:
- name: serverless-pre-function
config:
phase: header_filter
functions:
- |
return function(conf, ctx)
local core = require("apisix.core")
core.response.clear_header_as_body_modified()
end
- name: serverless-post-function
config:
phase: body_filter
functions:
- |
return function(conf, ctx)
local cjson = require("cjson")
local core = require("apisix.core")
local body = core.response.hold_body_chunk(ctx)
if not body then
return
end
body = cjson.decode(body)
body.origin = nil
body = cjson.encode(body)
ngx.arg[1] = body
end
Apply the configuration:
kubectl apply -f serverless-remove-body-ic.yaml
❶ Execute a pre-function in the header_filter phase.
❷ Execute a post-function in the body_filter phase.
The pre-function calls clear_header_as_body_modified to clear body-related response headers such as Content-Length. The post-function collects the response body with hold_body_chunk, decodes the JSON payload, removes the origin field, and writes the updated body back to the response.
Send another request to the route:
curl "http://127.0.0.1:9080/get"
You should see a response without the origin information:
{
"url":"http://127.0.0.1/get",
"args":{},
"headers":{
"X-Forwarded-Host":"127.0.0.1",
"Host":"127.0.0.1",
"Accept":"*/*",
"User-Agent":"curl/8.4.0",
"X-Amzn-Trace-Id":"Root=1-663db276-1c15276864294d963c6e1755"
}
}
For simpler response modifications, such as modifying HTTP status codes, request headers, or the entire response body, please use the response-rewrite plugin.