Skip to main content
Version: 3.19.0

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 under deployment.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:

  1. Place the plugin source code in the default APISIX plugins directory /apisix/plugins along with the other APISIX plugins.
  2. Place the plugin source code in a separate directory and specify the search path in extra_lua_path in the config.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:

config.yaml (custom plugin search path)
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.

caution

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:

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"
}
}
}'

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.