Docs
Ingress ControllerManage traffic and APIsCustom PluginsDeploy Lua Custom Plugins

Deploy Lua Custom Plugins

Load a constrained Lua plugin into APISIX or API7 Gateway on Kubernetes, apply it with the Ingress Controller, and verify its response header.

Applies to:API7 Ingress Controller (drives API7 Gateway)APISIX Ingress Controller (drives Apache APISIX)

Goal

Load a custom Lua plugin named guide-response-header into the gateway and apply it to a route. The plugin adds a configured response header while the request still reaches a local upstream.

The example deliberately exposes no filesystem path, command, or arbitrary Lua expression through Route configuration. Treat every custom plugin as gateway code: review it, test it outside production, and restrict who can deploy or configure it.

Before you begin

  • Complete Set Up Ingress Controller and Gateway.
  • You have kubectl configured against the cluster used in the setup guide.
  • Install Helm if you use the Apache APISIX path.
  • Choose a free local port for the gateway port-forward. The examples use 9080.

Set the variables this guide uses:

export GATEWAY_SERVICE_NAME="apisix-gateway"   # your gateway's Service name in namespace aic
export GATEWAY_LOCAL_PORT="9080"
export GATEWAY_URL="http://127.0.0.1:${GATEWAY_LOCAL_PORT}"

Confirm that this guide will not overwrite existing Kubernetes resources:

for resource in configmap/guide-response-header deployment/lua-plugin-guide-upstream service/lua-plugin-guide-upstream; do
  ! kubectl get --namespace aic "${resource}" >/dev/null 2>&1 || { echo "Stop: ${resource} already exists"; exit 1; }
done

Steps

Step 1: Write the Lua plugin

Save the following constrained plugin as guide-response-header.lua:

guide-response-header.lua
local core = require("apisix.core")
local ngx = ngx

local plugin_name = "guide-response-header"
local schema = {
    type = "object",
    properties = {
        value = {
            type = "string",
            minLength = 1,
            maxLength = 64,
        },
    },
    required = {"value"},
}

local _M = {
    version = 0.1,
    priority = 0,
    name = plugin_name,
    schema = schema,
}

function _M.check_schema(conf)
    return core.schema.check(schema, conf)
end

function _M.header_filter(conf, ctx)
    ngx.header["X-Guide-Plugin"] = conf.value
end

return _M

check_schema rejects missing, empty, or oversized values. The plugin cannot read files or execute user-supplied code.

Step 2: Load the plugin into the gateway

Every gateway pod that can serve the route must load the same reviewed plugin code. Use the workflow for your gateway product.

Create a ConfigMap in the gateway namespace:

kubectl create configmap guide-response-header \
  --namespace aic \
  --from-file=guide-response-header.lua

Save the exact effective Helm values with restricted permissions, then make a working copy. These files can contain secrets; do not commit or retain them after the test.

umask 077
helm get values --namespace aic apisix --all -o yaml > apisix-values.before-lua-guide.yaml
cp apisix-values.before-lua-guide.yaml apisix-values.lua-guide.yaml

In apisix-values.lua-guide.yaml, enable the custom-plugin mount and append guide-response-header to the existing apisix.plugins list. Preserve every existing list entry and every unrelated value:

apisix-values.lua-guide.yaml
apisix:
  customPlugins:
    enabled: true
    plugins:
      - name: guide-response-header
        configMap:
          name: guide-response-header
          mounts:
            - key: guide-response-header.lua
              path: /opts/custom_plugins/apisix/plugins/guide-response-header.lua
  plugins:
    # Keep every plugin already present in this list.
    - ...
    - guide-response-header

If customPlugins.plugins already contains entries, append this item instead of replacing them. If the plugin name is already present, stop and use the deployment's source-controlled configuration rather than taking ownership of it in this guide.

Apply the working values and wait for the gateway rollout:

helm upgrade apisix apisix/apisix \
  --namespace aic \
  -f apisix-values.lua-guide.yaml
kubectl rollout status deployment/apisix --namespace aic --timeout=5m

If the upgrade or rollout fails, restore immediately:

helm upgrade apisix apisix/apisix \
  --namespace aic \
  -f apisix-values.before-lua-guide.yaml
kubectl rollout status deployment/apisix --namespace aic --timeout=5m

Step 3: Create a local upstream

Save the following manifest as lua-plugin-upstream.yaml:

lua-plugin-upstream.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: aic
  name: lua-plugin-guide-upstream
spec:
  replicas: 1
  selector:
    matchLabels:
      app: lua-plugin-guide-upstream
  template:
    metadata:
      labels:
        app: lua-plugin-guide-upstream
    spec:
      containers:
        - name: echo
          image: hashicorp/http-echo:1.0.0
          args: ["-listen=:5678", "-text=lua-plugin-upstream"]
          ports:
            - name: http
              containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
  namespace: aic
  name: lua-plugin-guide-upstream
spec:
  selector:
    app: lua-plugin-guide-upstream
  ports:
    - name: http
      port: 80
      targetPort: 5678

Apply it and require the Deployment to become available:

kubectl apply -f lua-plugin-upstream.yaml
kubectl rollout status deployment/lua-plugin-guide-upstream --namespace aic --timeout=2m

Step 4: Create a route with the custom plugin

Create lua-plugin-route.yaml for the API used by your Ingress Controller:

lua-plugin-route.yaml
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
  namespace: aic
  name: guide-response-header
spec:
  plugins:
    - name: guide-response-header
      config:
        value: enabled
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  namespace: aic
  name: lua-plugin-guide
spec:
  parentRefs:
    - name: apisix
  rules:
    - matches:
        - path:
            type: Exact
            value: /lua-plugin-guide
      filters:
        - type: ExtensionRef
          extensionRef:
            group: apisix.apache.org
            kind: PluginConfig
            name: guide-response-header
      backendRefs:
        - name: lua-plugin-guide-upstream
          port: 80

Apply the route:

kubectl apply -f lua-plugin-route.yaml

Validate

Expose the gateway Service on your local machine:

kubectl port-forward --namespace aic "svc/${GATEWAY_SERVICE_NAME}" "${GATEWAY_LOCAL_PORT}:80" &
PORT_FORWARD_PID=$!

Capture the response and require both the plugin header and the exact upstream body:

curl --fail-with-body -sS --connect-timeout 3 --max-time 15 \
  -D lua-guide-headers.txt -o lua-guide-body.txt \
  "${GATEWAY_URL}/lua-plugin-guide"
tr -d '\r' < lua-guide-headers.txt | grep -Fqx 'X-Guide-Plugin: enabled'
test "$(cat lua-guide-body.txt)" = "lua-plugin-upstream"

Both assertions must succeed. The header proves that guide-response-header ran; the exact body proves that the request still reached the controlled upstream.

Roll back / Clean up

Delete the route and upstream first:

kubectl delete -f lua-plugin-route.yaml
kubectl delete -f lua-plugin-upstream.yaml

For Apache APISIX, restore the exact values captured before the guide, wait for the rollout, and then remove the ConfigMap:

helm upgrade apisix apisix/apisix \
  --namespace aic \
  -f apisix-values.before-lua-guide.yaml
kubectl rollout status deployment/apisix --namespace aic --timeout=5m
kubectl delete configmap guide-response-header --namespace aic

For API7 Gateway, remove the custom plugin from the gateway group only if this guide created it and no other route uses it.

Stop the port-forward and remove local files, including the sensitive Helm values:

kill "${PORT_FORWARD_PID}" 2>/dev/null || true
rm -f guide-response-header.lua lua-plugin-route.yaml lua-plugin-upstream.yaml \
  lua-guide-headers.txt lua-guide-body.txt \
  apisix-values.before-lua-guide.yaml apisix-values.lua-guide.yaml
unset GATEWAY_SERVICE_NAME GATEWAY_LOCAL_PORT GATEWAY_URL PORT_FORWARD_PID

Troubleshoot

SymptomCauseFix
The gateway rollout fails.The values file replaced an existing plugin entry, or the module path and plugin name differ.Restore the pre-guide values immediately, then compare the custom-plugin mount and the complete plugin list.
The Route is rejected.The gateway has not loaded guide-response-header, or value violates its schema.Confirm that every gateway pod completed its rollout and use a non-empty string no longer than 64 characters.
The response has no X-Guide-Plugin header.The plugin is not attached, another Route matched, or the API7 gateway group does not contain the plugin.Inspect the applied Route and PluginConfig, confirm the exact path, and verify the plugin registration for the gateway group.
The body assertion fails.The route points to another backend or the sample Deployment is not ready.Check the backend Service name and require the sample Deployment rollout to succeed before retrying.
Cleanup rollout fails.The backup is missing or belongs to another Helm release.Keep the ConfigMap in place until the correct pre-guide values are recovered and the gateway rolls out successfully.