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