← Back to list

Extending Consul API Gateway with Lua: A Practical Guide

Introduction

Nick Wales · 2025-12-05 21:11 · 0 claps · 4.3 min read
#consul #lua #nomad #routing
Open on Medium ↗

Extending Consul API Gateway with Lua: A Practical Guide

Introduction

Consul API Gateway provides a powerful abstraction for managing north–south traffic in modern service‑oriented architectures. While the gateway has a large feature set out of the box, many organizations need additional request and response processing, including executing custom logic, enriching headers, validating tokens, or transforming payloads.

This is where Lua filters come in. With the Consul API Gateway’s filter chain model, you can inject Lua scripts directly into the request path to shape traffic before it reaches upstream services.

In this post, we’ll assume you have Consul and Nomad deployed and a running API gateway:

  • All configurations will be with Terraform
  • Writing and attaching Lua filters
  • Testing with fake-service

Architecture Overview

For this walkthrough, the environment consists of:

  • Nomad — orchestrates the API Gateway and fake-service jobs
  • Consul — holds gateway configuration, service registrations, and the filter chain
  • Terraform — provisions both Nomad jobs and Consul config entries
  • Consul API Gateway — routes traffic and executes Lua scripts
  • Fake service — accepts traffic and returns useful information in the response

The traffic path will look like this:

Client → Consul API Gateway → (Lua Filter) → fake-service

Using Terraform to Deploy the Gateway and Lua Filters

The Consul API Gateway uses Consul configuration entries for routes and filter chains. Terraform keeps the configuration declarative and consistent.

Request and Response rules

In this case we are deploying two rules in the service defaults configuration for the api-gateway, one each on the request and response. Of note the name must match the registered service name of the deployed api-gateway. We are using the envoy_extensions block to deploy the lua script, we have to be specific about the ProxyType to differentiate it from sidecar proxies.

The specific rules are managing routing based on regex rules and changing the response.

On the request we are stripping “/public” from the path that the API gateway uses to call the upstream service while maintaining the rest of the URI.

On the response we are returning custom output when we see a 404 from the upstream.

Other examples could let us validate the request, configure authorization using an external service or investigate a JWT token.

resource "consul_config_entry_service_defaults" "api-gateway" {

  name = "api-gateway"
  expose {
    checks = false
  }
  protocol = "http"

  envoy_extensions {  
    name = "builtin/lua"
    arguments = {
      ProxyType = "api-gateway"
      Listener  = "outbound"
      Script = <<EOF

local function strip_public(path)
  local out = path:gsub("^/public", "")
  if out == "" then out = "/" end
  return out
end

function envoy_on_request(request_handle)
  if request_handle:headers():get("x-remove-prefix") == "1" then
    local original_path = request_handle:headers():get(":path")
    if string.find(original_path, "/public/.+") then
      local new_path = strip_public(original_path)
      request_handle:headers():replace(":path", new_path)
    end  
  end
end  

function envoy_on_response(response_handle)
  if response_handle:headers():get(":status") == "404" then
    local json = '{"message":"Modified by Lua script","status":"success"}'
    response_handle:body():setBytes(json)
    response_handle:headers():remove("content-length")
    response_handle:headers():replace("content-encoding", "identity")
    response_handle:headers():replace("content-type", "application/json")
  end
end
EOF
    }    
  }
}

Deploying Nomad Jobs

You can deploy both the gateway service and fake-service with Nomad. The gateway deployment is well documented in this project so I will not cover it here.

Deploying the fake-service

The fake service is deployed in the mesh, there are a couple of application specific environment variables we’re using to test the response. Setting the ERROR_CODE to 404 and the ERROR_RATE to 20%

job "frontend" {
  datacenters = ["dc1"]
  type = "service"
  namespace = "test"

  group "frontend" {
    count = 1

    network {
      mode = "bridge"
      port "expose" {}
    }        

    service {
      name = "frontend"
      port = "9090"

      check {
        expose   = true
        type     = "http"
        path     = "/health"
        interval = "30s"
        timeout  = "5s"       
      }

      connect {
        sidecar_service {
          proxy {
            transparent_proxy {}              
          }
        }  
      }
    }          

    task "frontend" {
      driver = "docker"

      config {
        image = "nicholasjackson/fake-service:v0.26.2"
      }

      env {
        NAME = "frontend"
        ERROR_CODE = "404"
        ERROR_RATE = "0.2"
      }                   
    }
  }
}

With the gateway, configuration and the upstream service deployed as show in the UI we can now use curl to run our tests:

Testing the rewrite

Here we request /public/service/endpoint and can see that the uri the upstream service is responding to is /service/endpoint. We’re passing the x-remove-prefix header to make this happen otherwise the lua script will not execute.

curl -H 'x-remove-prefix: 1' localhost:8090/public/service/endpoint
{
  "name": "frontend",
  "uri": "/service/endpoint",
  "type": "HTTP",
  "ip_addresses": [
    "172.26.64.21"
  ],
  "start_time": "2025-12-05T16:37:18.123124",
  "end_time": "2025-12-05T16:37:18.123365",
  "duration": "241.405µs",
  "body": "Hello World",
  "code": 200
}

Error handling

Every fifth response will be a 404, with a verbose curl call we can see the headers and body have been altered as we have configured.

❯ curl --verbose -H 'x-remove-prefix: 1' localhost:8090/public/service/endpoint
...
> GET /public/service/endpoint HTTP/1.1
> Host: localhost:8090
> User-Agent: curl/8.7.1
> Accept: */*
> x-remove-prefix: 1
> 
* Request completely sent off
< HTTP/1.1 404 Not Found
< date: Fri, 05 Dec 2025 16:41:36 GMT
< x-envoy-upstream-service-time: 3
< server: envoy
< content-encoding: identity
< content-type: application/json
< transfer-encoding: chunked
< 
* Connection #0 to host localhost left intact
{"message":"Modified by Lua script","status":"success"}%  

Layer 7 Intentions

We can also test the validity of the requests using intentions. In this case we are allowing access only to the rewritten path and specifically denying the original.

resource "consul_config_entry_service_intentions" "frontend" {
  name = "frontend"

  sources {
    name   = "api-gateway"
    type   = "consul"
    namespace = "default"
    partition = "default"
    precedence = 9

    permissions {
      action = "allow"
      http {
        path_exact = "/service/endpoint"
      }
    }
    permissions {
      action = "deny"
      http {
        path_exact = "/public/service/endpoint"
      }
    }
  } 
}

Now requests that are not rewritten get blocked by the intention.

❯ curl --verbose -H 'x-remove-prefix: 0' localhost:8090/public/service/endpoint
> GET /public/service/endpoint HTTP/1.1
> Host: localhost:8090
> User-Agent: curl/8.7.1
> Accept: */*
> x-remove-prefix: 0
> 
* Request completely sent off
< HTTP/1.1 403 Forbidden
< content-length: 19
< content-type: text/plain
< date: Fri, 05 Dec 2025 19:12:54 GMT
< server: envoy
< x-envoy-upstream-service-time: 1
< 
* Connection #0 to host localhost left intact
RBAC: access denied

Conclusion

This example shows how Consul API Gateway, Nomad, and Terraform form a clean and powerful workflow for building a programmable edge.

If you’re a platform or DevOps team evaluating how to adopt Consul as a traffic management layer, this approach provides:

  • repeatable deployments
  • consistent API governance
  • customizable request processing
  • tight integration with existing HashiCorp tooling

메타데이터
post_id
d3d15c4e3e20
slug
extending-consul-api-gateway-with-lua-a-practical-guide-d3d15c4e3e20
url
https://medium.com/@nickjrwales/extending-consul-api-gateway-with-lua-a-practical-guide-d3d15c4e3e20
canonical_url
https://medium.com/@nickjrwales/extending-consul-api-gateway-with-lua-a-practical-guide-d3d15c4e3e20
author_url
https://medium.com/@nickjrwales
status
ok
fetched_at
2026-07-14 12:23:01