Create a Custom Plugin in Lua
One of the key features of APISIX is its extensibility through plugins. In addition to a wide range of existing plugins, APISIX also allows you to build custom plugins to add extra functionalities and manage API traffic with custom flow. Oftentimes, you use Lua programming language to implement new plugins. APISIX processes requests in phases and the relevant plugin logics get executed in each phase during the routing of requests.
This guide walks you through the example process of developing a new custom plugin in Lua for APISIX.
Prerequisite(s)
- Install Docker.
- Install cURL to send requests to the services for validation.
- Follow the Getting Started tutorial to start a new APISIX instance in Docker or on Kubernetes.
- If you use the Admin API examples and authentication is enabled, export an admin-role key as
ADMIN_API_KEY. Configure the key underdeployment.admin.admin_key. The Docker quickstart disables authentication for local evaluation; keep it enabled in production.
Develop File Proxy Plugin
In this section, you will create a new custom plugin called file-proxy for APISIX using Lua. This plugin will be used to expose the static files (YAML, JSON, JavaScript, CSS, or image files) through API and fetch a file from a specified URL.
For example, API users can access openapi.yaml file at the specified URL http://127.0.0.1:9080/openapi.yaml.
Create a Lua File
Create a new Lua file for the plugin source code named file-proxy.lua.
Import Modules
Import the necessary modules for the file-proxy plugin:
local core = require("apisix.core")
local io = require("io")
local ngx = ngx
Define Plugin Name
Declare a unique name for the plugin:
local plugin_name = "file-proxy"
Define Plugin Schema
Create a schema for plugin parameters. The schema defines the available parameters as well as their data types, properties, default values, valid values, and so on.
The file-proxy example accepts only /usr/local/apisix/openapi.yaml, the public specification created below. Restricting the schema avoids exposing arbitrary gateway files through route configuration. Do not use a tutorial plugin to serve credentials or other private files.
local plugin_schema = {
type = "object",
properties = {
path = {
type = "string",
enum = {"/usr/local/apisix/openapi.yaml"}
},
},
required = {"path"}
}
❶ The path of the file to be served.
❷ The path set be a required parameter.
Define Lua Module Table
Define properties version, priority, name, and schema for the plugin. The name and schema are the plugin's name and schema defined previously. The version and priority are used by APISIX to manage the plugin.
local _M = {
version = 1.0,
priority = 1000,
name = plugin_name,
schema = plugin_schema
}
❶ version: The field typically refers to the version that is currently in use. If you publish and update your plugin logic, it is going to be 1.1 (You can set any version you wish).
❷ priority: The field is used to sort plugins before executing each of their phases. Plugins with a higher priority are executed first. Make sure the priority for the custom plugin fit into that of existing plugins. The priorities of existing plugins are documented in the config.yaml.example file.
❸ name: The plugin's name.
❹ schema: The plugin's schema.
Define Schema Check Function
Define a schema checker to validate the user-input parameters against the defined schema:
function _M.check_schema(conf)
local ok, err = core.schema.check(plugin_schema, conf)
if not ok then
return false, err
end
return true
end
Define Custom Logic
APISIX allows you to inject custom logic in various phases. See Lua NGINX Module Directives to learn more about different phases.
Implement custom logic for the file-proxy plugin in the access function, which would be executed during the access phase. The logic should open the file specified in the plugin configuration, read its content, and return the content as the response. If the file cannot be opened, it should log an error and return a 404 Not Found.
function _M.access(conf, ctx)
local fd = io.open(conf.path, "rb")
if fd then
local content = fd:read("*all")
fd:close()
ngx.header.content_length = #content
ngx.say(content)
ngx.exit(ngx.OK)
else
ngx.exit(ngx.HTTP_NOT_FOUND)
core.log.error("File is not found: ", conf.path, ", error info: ", err)
end
end
Define Logging Logic
The log function is executed during the log phase. Logging is optional but helpful for debugging and checking if the plugin is working properly.
Log only this example's constrained configuration. Do not dump the request context, which can contain credentials and other private request data:
function _M.log(conf, ctx)
core.log.warn("conf: ", core.json.encode(conf))
end
Put Everything Together
When you put together all the code above, file-proxy.lua should look like the following:
-- Introduce the necessary modules/libraries we need for this plugin
local core = require("apisix.core")
local io = require("io")
local ngx = ngx
-- Declare the plugin's name
local plugin_name = "file-proxy"
-- Define the plugin schema format
local plugin_schema = {
type = "object",
properties = {
path = {
type = "string", -- The path of the file to be served
enum = {"/usr/local/apisix/openapi.yaml"}
},
},
required = {"path"} -- The path is a required field
}
-- Define the plugin with its version, priority, name, and schema
local _M = {
version = 1.0,
priority = 1000,
name = plugin_name,
schema = plugin_schema
}
-- Function to check if the plugin configuration is correct
function _M.check_schema(conf)
-- Validate the configuration against the schema
local ok, err = core.schema.check(plugin_schema, conf)
-- If validation fails, return false and the error
if not ok then
return false, err
end
-- If validation succeeds, return true
return true
end
-- Function to be called during the access phase
function _M.access(conf, ctx)
-- Open the file specified in the configuration
local fd = io.open(conf.path, "rb")
-- If the file is opened successfully, read its content and return it as the response
if fd then
local content = fd:read("*all")
fd:close()
ngx.header.content_length = #content
ngx.say(content)
ngx.exit(ngx.OK)
else
-- If the file cannot be opened, log an error and return a 404 Not Found status
ngx.exit(ngx.HTTP_NOT_FOUND)
core.log.error("File is not found: ", conf.path, ", error info: ", err)
end
end
-- Function to be called during the log phase
function _M.log(conf, ctx)
-- Log the constrained plugin configuration
core.log.warn("conf: ", core.json.encode(conf))
end
-- Return the plugin so it can be used by APISIX
return _M
Source Custom Plugin
There are two approaches to load the custom plugin code into APISIX:
- Place the plugin source code in the default APISIX plugins directory
/apisix/pluginsalong with the other APISIX plugins. - Place the plugin source code in a separate directory and specify the search path in
extra_lua_pathin theconfig.yaml.
The following sections provide instructions for the second approach as it is recommended to manage custom code in a separate directory.
Create Custom Plugin File
APISIX looks for L7 plugins within /apisix/plugins and L4 plugins within /apisix/stream/plugins in your custom directory.
For the custom plugin, create a directory with a customized name and /apisix/plugins within the directory:
docker exec apisix-quickstart /bin/sh -c "mkdir -p custom-plugin/apisix/plugins"
Save the plugin file to the directory:
docker exec apisix-quickstart /bin/sh -c "echo '
local core = require(\"apisix.core\")
local io = require(\"io\")
local ngx = ngx
local plugin_name = \"file-proxy\"
local plugin_schema = {
type = \"object\",
properties = {
path = {
type = \"string\",
enum = {\"/usr/local/apisix/openapi.yaml\"}
},
},
required = {\"path\"}
}
local _M = {
version = 1.0,
priority = 1000,
name = plugin_name,
schema = plugin_schema
}
function _M.check_schema(conf)
local ok, err = core.schema.check(plugin_schema, conf)
if not ok then
return false, err
end
return true
end
function _M.access(conf, ctx)
local fd = io.open(conf.path,\"rb\")
if fd then
local content = fd:read(\"*all\")
fd:close()
ngx.header.content_length = #content
ngx.say(content)
ngx.exit(ngx.OK)
else
ngx.exit(ngx.HTTP_NOT_FOUND)
core.log.error(\"File is not found: \", conf.path, \", error info: \", err)
end
end
function _M.log(conf, ctx)
core.log.warn(\"conf: \", core.json.encode(conf))
end
return _M
' > /usr/local/apisix/custom-plugin/apisix/plugins/file-proxy.lua"
Update APISIX Configuration File
Copy the complete active configuration file to the host and keep a backup:
docker cp apisix-quickstart:/usr/local/apisix/conf/config.yaml ./config.yaml
cp ./config.yaml ./config.yaml.backup
In config.yaml, set the custom plugin search path under the existing apisix mapping:
apisix:
extra_lua_path: "/usr/local/apisix/custom-plugin/?.lua"
Append file-proxy to the complete existing top-level plugins list. If the active file omits that list, copy the complete default list from this release's configuration example before appending file-proxy. Keep all other settings, especially the Admin API allowlist, authentication, and etcd connection.
Defining a top-level plugins list replaces the default list. Do not configure a one-plugin list containing only file-proxy.
Copy the candidate configuration into the container, test it, and reload APISIX only if validation succeeds:
docker cp ./config.yaml apisix-quickstart:/usr/local/apisix/conf/config.yaml
docker exec apisix-quickstart apisix test && \
docker exec apisix-quickstart apisix reload
If validation fails, restore the backup before making another change:
docker cp ./config.yaml.backup apisix-quickstart:/usr/local/apisix/conf/config.yaml
Test Custom Plugin
Store Test File
Store a static openapi.yaml file in the APISIX instance:
docker exec apisix-quickstart /bin/sh -c "echo '
openapi: 3.0.1
info:
title: OpenAPI Spec
description: OpenAPI Spec file description.
' > /usr/local/apisix/openapi.yaml"
Create a Route
To use the file-proxy custom plugin, you need to create a route in APISIX that uses the plugin:
- Admin API
- ADC
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id":"openapi-file-proxy",
"uri":"/openapi.yaml",
"plugins":{
"file-proxy":{
"path":"/usr/local/apisix/openapi.yaml"
}
}
}'
services:
- name: OpenAPI File Proxy Service
routes:
- uris:
- /openapi.yaml
name: openapi-file-proxy
plugins:
file-proxy:
path: /usr/local/apisix/openapi.yaml
Synchronize the configuration to APISIX:
adc sync -f adc.yaml
Validate the Plugin
Send a request to the route:
curl "http://127.0.0.1:9080/openapi.yaml"
The response should be the content of the file openapi.yaml:
openapi: 3.0.1
info:
title: OpenAPI Spec
description: OpenAPI Spec file description.
Next Steps
Developing custom plugins in Lua is one way to extend APISIX. From here:
- To inspect what your plugin is seeing at runtime in a live worker, without redeploying or restarting APISIX, see Set Breakpoints.
- To write a plugin in a language other than Lua, see Create a WASM Plugin. APISIX also supports plugins written in Go, Java, and Python through the external plugin runner.