commit
718f529192
10 changed files with 401 additions and 0 deletions
@ -0,0 +1,21 @@ |
|||||
|
The MIT License (MIT) |
||||
|
|
||||
|
Copyright (c) 2016 Nicolas MASSE |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in all |
||||
|
copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
|
SOFTWARE. |
||||
@ -0,0 +1,47 @@ |
|||||
|
# Dynamic Routing for Apicast |
||||
|
|
||||
|
## Introduction |
||||
|
|
||||
|
This project provides a dynamic routing module for Apicast. It routes the |
||||
|
API request to the appropriate backend, based on an HTTP Header of the request. |
||||
|
|
||||
|
A sample use case could be: |
||||
|
- A Load Balancer in front of Apicast identifies the source of the request (`internal`/`external` or `dev`/`prod`) |
||||
|
- The LB add the corresponding header (`x-env: dev` or `x-env: prod` for instance) |
||||
|
- Based on this header, the Apicast routes the API request to the corresponding backend |
||||
|
|
||||
|
The API Backends are discovered by querying a Service Catalog. A sample service |
||||
|
catalog is given with this project. |
||||
|
|
||||
|
It is designed to be hosted on Apicast itself (or any nginx instance) in order |
||||
|
to simplify the deployment. |
||||
|
|
||||
|
## Deployment |
||||
|
|
||||
|
Put `dynamic-router.conf` in `/opt/app-root/src/apicast.d/dynamic-router.conf`: |
||||
|
``` |
||||
|
oc create configmap apicast.d --from-file=apicast-module/dynamic-router.conf |
||||
|
oc volume dc/apicast-staging --add --name=apicastd --mount-path /opt/app-root/src/apicast.d/ --type=configmap --configmap-name=apicast.d |
||||
|
``` |
||||
|
|
||||
|
Put `dynamic-router-upstream.conf` and `catalog.conf` in `/opt/app-root/src/sites.d/`: |
||||
|
``` |
||||
|
oc create configmap sites.d --from-file=apicast-module/dynamic-router-upstream.conf --from-file=catalog/catalog.conf |
||||
|
oc volume dc/apicast-staging --add --name=sitesd --mount-path /opt/app-root/src/sites.d/ --type=configmap --configmap-name=sites.d |
||||
|
``` |
||||
|
|
||||
|
Put `catalog.lua` and `dynamic-router.lua` in `/opt/app-root/src/src/custom/`: |
||||
|
``` |
||||
|
oc create configmap apicast-custom-module --from-file=apicast-module/dynamic-router.lua --from-file=catalog/catalog.lua |
||||
|
oc volume dc/apicast-staging --add --name=apicast-custom-module --mount-path /opt/app-root/src/src/custom/ --type=configmap --configmap-name=apicast-custom-module |
||||
|
``` |
||||
|
|
||||
|
Set the configuration required by the catalog and the dynamic routing module as environment variables and re-deploy apicast: |
||||
|
``` |
||||
|
oc env dc/apicast-staging APICAST_CUSTOM_CONFIG=custom/dynamic-router |
||||
|
oc env dc/apicast-staging DYNAMIC_ROUTER_CATALOG_URL=http://127.0.0.1:8082 |
||||
|
oc env dc/apicast-staging DYNAMIC_ROUTER_ENVIRONMENT_HEADER_NAME=x-env |
||||
|
oc rollout latest apicast-staging |
||||
|
``` |
||||
|
|
||||
|
Once, you get it to work on `apicast-staging`, you can do the same on `apicast-production`. |
||||
@ -0,0 +1,54 @@ |
|||||
|
# A dynamic routing module for Apicast |
||||
|
|
||||
|
## Introduction |
||||
|
|
||||
|
This project provides a dynamic routing module for Apicast. It routes the |
||||
|
API request to the appropriate backend, based on an HTTP Header of the request. |
||||
|
|
||||
|
A sample use case could be: |
||||
|
- A Load Balancer identifies the source of the request (internal / external for instance) |
||||
|
- The LB add the corresponding header (`x-env: dev` or `x-env: prod`) |
||||
|
- Based on this header, the Apicast routes the API request to the corresponding backend |
||||
|
|
||||
|
The API Backends are discovered by querying a Service Catalog. A sample service |
||||
|
catalog is given with this project. |
||||
|
|
||||
|
## Development |
||||
|
|
||||
|
First of all, setup your development environment as explained [here](https://github.com/3scale/apicast/tree/master#development--testing). |
||||
|
|
||||
|
Then, issue the following commands: |
||||
|
``` |
||||
|
git clone https://github.com/nmasse-itix/apicast-dynamic-router.git |
||||
|
git clone https://github.com/3scale/apicast.git |
||||
|
cd apicast |
||||
|
luarocks make apicast/*.rockspec --local |
||||
|
ln -s $PWD/../apicast-dynamic-router/apicast-module/dynamic-router.conf apicast/apicast.d/dynamic-router.conf |
||||
|
ln -s $PWD/../apicast-dynamic-router/apicast-module/dynamic-router-upstream.conf apicast/sites.d/dynamic-router-upstream.conf |
||||
|
mkdir -p custom |
||||
|
ln -s $PWD/../apicast-dynamic-router/apicast-module/dynamic-router.lua custom/dynamic-router.lua |
||||
|
``` |
||||
|
|
||||
|
Configure your apicast as explained [here](https://github.com/3scale/apicast/blob/master/doc/parameters.md) |
||||
|
and [here](https://github.com/3scale/apicast/blob/master/doc/configuration.md). |
||||
|
``` |
||||
|
export APICAST_CUSTOM_CONFIG=custom/dynamic-router |
||||
|
export DYNAMIC_ROUTER_CATALOG_URL=http://127.0.0.1:8082 |
||||
|
export DYNAMIC_ROUTER_ENVIRONMENT_HEADER_NAME=x-env |
||||
|
``` |
||||
|
|
||||
|
Finally, launch apicast: |
||||
|
``` |
||||
|
bin/apicast -i 0 -m off |
||||
|
``` |
||||
|
|
||||
|
## Testing |
||||
|
|
||||
|
The default catalog (`catalog.lua`) and the default configuration (`config.json`) |
||||
|
provide a few examples that you can test: |
||||
|
``` |
||||
|
curl -D - http://localhost:8080/echo |
||||
|
curl -D - http://localhost:8080/echo -H "x-env: prod" |
||||
|
curl -D - http://localhost:8080/echo -H "x-env: dev" |
||||
|
curl -D - http://localhost:8080/echo -H "x-env: bogus" |
||||
|
``` |
||||
@ -0,0 +1,11 @@ |
|||||
|
|
||||
|
upstream catalog_upstream { |
||||
|
server 0.0.0.1:1; |
||||
|
|
||||
|
balancer_by_lua_block { |
||||
|
local balancer = require "balancer" |
||||
|
balancer.call() |
||||
|
} |
||||
|
|
||||
|
keepalive 1024; |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
location = /dummy { |
||||
|
set $catalog_url "http://catalog_upstream"; |
||||
|
set $catalog_host "catalog_upstream"; |
||||
|
set $service_name "dummy"; |
||||
|
set $environment "bogus"; |
||||
|
} |
||||
|
|
||||
|
location = /dynamic-router { |
||||
|
internal; |
||||
|
|
||||
|
set $path /catalog/services/$service_name/environments/$environment/target; |
||||
|
proxy_pass $catalog_url$path; |
||||
|
proxy_pass_request_headers off; |
||||
|
proxy_pass_request_body off; |
||||
|
proxy_http_version 1.1; |
||||
|
proxy_set_header Host "$catalog_host"; |
||||
|
proxy_set_header Connection ""; |
||||
|
proxy_set_header Content-Length ""; |
||||
|
|
||||
|
#rewrite_by_lua_block { |
||||
|
# ngx.log(ngx.WARN, "service_name = " .. ngx.var.service_name) |
||||
|
# ngx.var.real_url = ngx.var.catalog_url .. ngx.var.path |
||||
|
#} |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
local resty_resolver = require 'resty.resolver' |
||||
|
local resty_url = require 'resty.url' |
||||
|
|
||||
|
local _M = { } |
||||
|
|
||||
|
local service_catalog_url |
||||
|
local environment_header_name |
||||
|
|
||||
|
function _M.setup(proxy) |
||||
|
-- In case of error during initialization, we will fallback to the default behavior |
||||
|
local error = false |
||||
|
|
||||
|
-- Get configuration from Environment Variables |
||||
|
service_catalog_url = os.getenv('DYNAMIC_ROUTER_CATALOG_URL') |
||||
|
if (service_catalog_url == nil) then |
||||
|
ngx.log(ngx.ERR, "No environment variable DYNAMIC_ROUTER_CATALOG_URL.") |
||||
|
error = true |
||||
|
else |
||||
|
ngx.log(ngx.INFO, "Using the catalog at " .. (service_catalog_url or "nil")) |
||||
|
end |
||||
|
|
||||
|
environment_header_name = os.getenv('DYNAMIC_ROUTER_ENVIRONMENT_HEADER_NAME') |
||||
|
if (environment_header_name == nil) then |
||||
|
ngx.log(ngx.ERR, "No environment variable DYNAMIC_ROUTER_ENVIRONMENT_HEADER_NAME.") |
||||
|
error = true |
||||
|
else |
||||
|
ngx.log(ngx.INFO, "Using the header " .. (environment_header_name or "nil") .. " as environment") |
||||
|
end |
||||
|
|
||||
|
if not error then |
||||
|
-- Update the Proxy Metatable with our custom function |
||||
|
proxy.get_upstream = get_upstream |
||||
|
else |
||||
|
ngx.log(ngx.ERR, "Errors during initialization. Dynamic Routing disabled.") |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
function get_upstream(service) |
||||
|
service = service or ngx.ctx.service |
||||
|
ngx.log(ngx.DEBUG, "Dynamically routing service " .. (service.id or "none")) |
||||
|
|
||||
|
local environment = ngx.req.get_headers()[string.lower(environment_header_name)] |
||||
|
if (environment == nil) then |
||||
|
ngx.log(ngx.WARN, "No header " .. environment_header_name .. " found, defaulting to '_default'.") |
||||
|
environment = "_default" |
||||
|
end |
||||
|
|
||||
|
-- Split the Catalog URL into components |
||||
|
local url = resty_url.split(service_catalog_url) |
||||
|
local scheme, _, _, server, port, path = |
||||
|
url[1], url[2], url[3], url[4], url[5] or resty_url.default_port(url[1]), url[6] or '' |
||||
|
|
||||
|
-- Resolve the DNS name of the Catalog Server |
||||
|
ngx.ctx.catalog_upstream = resty_resolver:instance():get_servers(server, { port = port }) |
||||
|
|
||||
|
-- Share those variables with the Nginx sub-request |
||||
|
local subrequest_vars = { |
||||
|
catalog_url = service_catalog_url, |
||||
|
catalog_host = server, |
||||
|
service_name = service.id, |
||||
|
environment = environment |
||||
|
} |
||||
|
ngx.log(ngx.INFO, 'Querying the Service Catalog at ', service_catalog_url, " for service id ", service.id, " and environment ", environment) |
||||
|
local res = ngx.location.capture("/dynamic-router", { vars = subrequest_vars, ctx = { catalog_upstream = ngx.ctx.catalog_upstream } }) |
||||
|
|
||||
|
local new_backend |
||||
|
if (res.status == 200) then |
||||
|
new_backend = res.body |
||||
|
ngx.log(ngx.INFO, "Found a backend for service " .. (service.id or "none") .. ": " .. new_backend) |
||||
|
else |
||||
|
-- In case we cannot get a positive answer from the Service Catalog, use the default API Backend. |
||||
|
new_backend = service.api_backend |
||||
|
ngx.log(ngx.ERR, "Could not get a positive response from the service catalog for service " .. (service.id or "none") .. ": HTTP " .. res.status) |
||||
|
end |
||||
|
|
||||
|
-- Split the new Backend URL into components |
||||
|
local url = resty_url.split(new_backend) |
||||
|
local scheme, _, _, server, port, path = |
||||
|
url[1], url[2], url[3], url[4], url[5] or resty_url.default_port(url[1]), url[6] or '' |
||||
|
|
||||
|
return { |
||||
|
server = server, |
||||
|
host = service.hostname_rewrite or server, |
||||
|
uri = scheme .. '://upstream' .. path, |
||||
|
port = tonumber(port) |
||||
|
} |
||||
|
end |
||||
|
|
||||
|
return _M |
||||
@ -0,0 +1,65 @@ |
|||||
|
# A "Self-Contained" Service Catalog for Apicast |
||||
|
|
||||
|
## Introduction |
||||
|
|
||||
|
This project provides a Service Catalog (list of services with their associated |
||||
|
environments and the matching URLs. |
||||
|
|
||||
|
It is designed to be hosted on Apicast itself (or any nginx instance) in order |
||||
|
to simplify the deployment. |
||||
|
|
||||
|
## How it works |
||||
|
|
||||
|
API Services, Environments and their backends are declared in the `catalog.lua`. |
||||
|
The service catalog listens on port 8082. |
||||
|
|
||||
|
You can query the Service Catalog by issuing an HTTP Request: |
||||
|
``` |
||||
|
GET /catalog/services/{service_id}/environments/{environment}/target |
||||
|
``` |
||||
|
|
||||
|
For instance, to query the `prod` environment of service `123`: |
||||
|
``` |
||||
|
curl -D - http://localhost:8082/catalog/services/123/environments/prod/target |
||||
|
``` |
||||
|
|
||||
|
## Development |
||||
|
|
||||
|
First of all, setup your development environment as explained [here](https://github.com/3scale/apicast/tree/master#development--testing). |
||||
|
|
||||
|
Then, issue the following commands: |
||||
|
``` |
||||
|
git clone https://github.com/nmasse-itix/apicast-dynamic-router.git |
||||
|
git clone https://github.com/3scale/apicast.git |
||||
|
cd apicast |
||||
|
luarocks make apicast/*.rockspec --local |
||||
|
ln -s $PWD/../apicast-dynamic-router/catalog/catalog.conf apicast/sites.d/catalog.conf |
||||
|
ln -s $PWD/../apicast-dynamic-router/catalog/config.json config.json |
||||
|
mkdir -p custom |
||||
|
ln -s $PWD/../apicast-dynamic-router/catalog/catalog.lua custom/catalog.lua |
||||
|
``` |
||||
|
|
||||
|
Configure your apicast as explained [here](https://github.com/3scale/apicast/blob/master/doc/parameters.md) |
||||
|
and [here](https://github.com/3scale/apicast/blob/master/doc/configuration.md). |
||||
|
``` |
||||
|
export THREESCALE_DEPLOYMENT_ENV=sandbox |
||||
|
export THREESCALE_CONFIG_FILE=config.json |
||||
|
export APICAST_LOG_LEVEL=debug |
||||
|
``` |
||||
|
|
||||
|
Finally, launch apicast: |
||||
|
``` |
||||
|
bin/apicast -i 0 -m off |
||||
|
``` |
||||
|
|
||||
|
## Testing |
||||
|
|
||||
|
The default catalog (`catalog.lua`) provides a few examples that you can test: |
||||
|
``` |
||||
|
curl -D - http://localhost:8082/catalog/services/123/environments/prod/target |
||||
|
curl -D - http://localhost:8082/catalog/services/123/environments/dev/target |
||||
|
curl -D - http://localhost:8082/catalog/services/123/environments/bogus/target |
||||
|
curl -D - http://localhost:8082/catalog/services/456/environments/prod/target |
||||
|
curl -D - http://localhost:8082/catalog/services/456/environments/dev/target |
||||
|
curl -D - http://localhost:8082/catalog/services/456/environments/bogus_but_should_work/target |
||||
|
``` |
||||
@ -0,0 +1,35 @@ |
|||||
|
server { |
||||
|
listen 8082; |
||||
|
|
||||
|
location ~ ^/catalog/services/([^/]+)/environments/([^/]+)/target$ { |
||||
|
content_by_lua_block { |
||||
|
local service = ngx.var[1] |
||||
|
local variant = ngx.var[2] |
||||
|
ngx.log(ngx.DEBUG, "Service Catalog request for service = " .. (ngx.var[1] or "<NONE>") .. " and variant = " .. (ngx.var[2] or "")) |
||||
|
|
||||
|
local catalog = require "custom/catalog" |
||||
|
if (catalog[service] ~= nil) then |
||||
|
local variants = catalog[service] |
||||
|
if (variants[variant] ~= nil) then |
||||
|
ngx.print(variants[variant]) |
||||
|
ngx.exit(ngx.HTTP_OK) |
||||
|
else |
||||
|
-- is there a default environment ? |
||||
|
if (variants._default ~= nil) then |
||||
|
ngx.print(variants._default) |
||||
|
ngx.exit(ngx.HTTP_OK) |
||||
|
else |
||||
|
ngx.status = 404 -- Status needs to be set before the body |
||||
|
ngx.print("") -- Prevent default NGINX Error Page |
||||
|
ngx.exit(ngx.HTTP_NOT_FOUND) |
||||
|
end |
||||
|
end |
||||
|
else |
||||
|
ngx.status = 404 -- Status needs to be set before the body |
||||
|
ngx.print("") -- Prevent default NGINX Error Page |
||||
|
ngx.exit(ngx.HTTP_NOT_FOUND) |
||||
|
end |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
return { |
||||
|
-- First service |
||||
|
["123"] = { |
||||
|
-- Prod environment |
||||
|
prod = "https://echo-api.3scale.net", |
||||
|
|
||||
|
-- Dev environment |
||||
|
dev = "http://echo-api.3scale.net" |
||||
|
}, |
||||
|
|
||||
|
-- Second service |
||||
|
["456"] = { |
||||
|
-- Prod environment |
||||
|
prod = "http://prod.myservice.corp", |
||||
|
|
||||
|
-- Dev environment |
||||
|
dev = "http://dev.myservice.corp", |
||||
|
|
||||
|
-- Default variant (if nothing matches): sandbox environment |
||||
|
_default = "http://sandbox.myservice.corp" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
{ |
||||
|
"id": 1234567890987, |
||||
|
"provider_key": "provider-key", |
||||
|
"services": [ |
||||
|
{ |
||||
|
"id": 123, |
||||
|
"backend_version": "1", |
||||
|
"proxy": { |
||||
|
"api_backend": "http://127.0.0.1:8081", |
||||
|
"hosts": [ |
||||
|
"localhost", |
||||
|
"127.0.0.1" |
||||
|
], |
||||
|
"backend": { |
||||
|
"endpoint": "http://127.0.0.1:8081", |
||||
|
"host": "backend" |
||||
|
}, |
||||
|
"auth_user_key": "user_key", |
||||
|
"credentials_location": "query", |
||||
|
"proxy_rules": [ |
||||
|
{ |
||||
|
"http_method": "GET", |
||||
|
"pattern": "/", |
||||
|
"metric_system_name": "hits", |
||||
|
"delta": 1, |
||||
|
"parameters": [], |
||||
|
"querystring_parameters": {} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
Loading…
Reference in new issue