Mapping Subdomains to Document Roots Through Nginx HttpDrizzleModule

Mapping Subdomains to Document Roots Through Nginx HttpDrizzleModule

A few days ago, one of our clients presented an interesting problem. They had several editions of an application with each edition being served for a specific client through his own subdomain. There exists a client(subdomain)-edition(document root) mapping in the database. They need to be able to map each client to his edition of the application.

HttpDrizzleModule to the Rescue!

HttpDrizzleModule is a production ready Nginx upstream module that allows Nginx to talk to MySQL, Drizzle, as well as other RDBMS’s that support the Drizzle or MySQL wired protocol using libdrizzle. By using this module, we can make Nginx query the database for the document root of the client’s application edition. Once the document root is specified, Nginx will route the client to his own edition of the application.

We created a setup similar to the client’s to test the proposed solution. We installed MySQL server and created a database drizzledb with the table editions that stores the client-document root mapping

The table had  the following entries:

[code]
+——+———+——————-+
| id | name | docroot |
+——+———+——————-+
| 1 | client1 | /var/www/silver |
| 2 | client2 | /var/www/gold |
| 3 | client3 | /var/www/platinum |
+——+———+——————-+
[/code]

The name column refers to the client (subdomain) while the docroot column refers to the document root of the client’s edition of the application.

In each document root, we created an index.html file with a content showing which client it serves.

[code]

# cat /var/www/silver/index.html
HELLO FROM SILVER

# cat /var/www/gold/index.html
HELLO FROM GOLD

# cat /var/www/platinum/index.html
HELLO FROM PLATINUM

[/code]

We could have compiled HttpDrizzleModule with Nginx core along with the other modules that we will later need like HttpLuaModule that we will use to access ngx_drizzle module, LuaRdsParser that we will need in order to parse theHttpDrizzleModule output that is formatted in the Resty-DBD-Stream (RDS) binary format.

However, we chose to install OpenResty that bundles the standard Nginx core, with lots of powerful 3rd-party Nginx modules including HttpDrizzleModule, HttpLuaModule, HttpEchoModule and HttpRdsJsonModule  that can be used  as well to format the RDS output generated by ngx_drizzle into JSON.

Since libdrizzle C library is no longer bundled by OpenResty, we had to download the last version supporting building libdrizzle 1.0 separately. Installation steps are straight forward:

[code]
tar xzvf drizzle7-2011.07.21.tar.gz
cd drizzle7-2011.07.21/
./configure –without-server
make libdrizzle-1.0
make install-libdrizzle-1.0
[/code]

Then we built openresty with –with-http_drizzle_module:

[code]
./configure –with-http_drizzle_module && make && make install
[/code]

At this point, we had our environment ready to test our proposed solution.

We created a virtual host with the following configurations:

[code light=”true”]

upstream mysql_backend {
drizzle_server 127.0.0.1 dbname=drizzledb
password=DrIzzl3 user=drizzleuser
protocol=mysql;
drizzle_keepalive max=200 overflow=reject;
}
server {
listen 80;
server_name ~([^.]+)\.domain\.com$;
set $sbdomain $1;

location /query {
drizzle_query $echo_request_body;
drizzle_pass mysql_backend;
}

location / {
set $root ”;

rewrite_by_lua ‘
local subdomain = ngx.var.sbdomain
local quoted_subdomain = ngx.quote_sql_str(subdomain)
local sql = "select docroot from editions where name = " .. quoted_subdomain

local resp = ngx.location.capture("/query", {
method = ngx.HTTP_POST, body = sql
})

if resp.status ~= ngx.HTTP_OK or not resp.body then
error("failed to query mysql")
end

local parser = require "rds.parser"
local res, err = parser.parse(resp.body)
if res == nil then
error("failed to parse RDS: " .. err)
end

for i, row in ipairs(res.resultset) do
for col, val in pairs(row) do
if val ~= parser.null then
ngx.var.root=val
end
end
end

‘;

root $root;

}
}

[/code]

 

First we define an upstream mysql_backend for the drizzle_server that points to localhost 127.0.0.1 with the default port 3306. We specify the database name dbname, user, password as well as the protocol mysql (protocol defaults to drizzle).

We set drizzle_keepalive maximum capacity of the keep-alive connection pool to 200. We set overflow to reject so that it rejects the current request, and returns the 503 Service Unavailable error page if the connection pool is already full.

Next, we configure the virtual server. We extract the subdomain from the server_name and store it in the variable sbdomain.

We create the location /query which passes the SQL query drizzle_query ( Here it becomes the request body $echo_request_body) to the upstream mysql_backend for MySQL server to execute.

In the location / , we first initialize the $root variable that will eventually hold the required document root. This step is needed since only initialized Nginx variables can be accessed via Lua’s Nginx API package ngx.

Next, we use the ngx_lua’s equivalent of the HttpRewriteModule  rewrite_by_lua to execute some Lua code and issue API calls.

This is where all the magic happens.

We access the Nginx variable $sbdomain via Lua’s Nginx API ngx.var.sbdomain and store it into a local Lua variable subdomain. Caching an Nginx variable into a local Lua variable is particularly important when this variable is being accessed repeatedly since Nginx will allocate memory in the per-request memory pool which is freed only at request termination. We quote the subdomain variable into an SQL string, quoted_subdomain, in order to pass it as a parameter to the SQL query string sql. This query looks in the editions table for the document root docroot that matches the name quoted_subdomain. We next use ngx.location.capture to issue an internal non-blocking POST subrequest to the location /query to which we pass a table consisting of the request method and request body which is the required SQL query string sql.

ngx.location.capture returns a Lua table with four slots (resp.status, resp.header, resp.body, and resp.truncated). We make sure the response code is 200  (ngx.HTTP_OK) and the response body is not empty. We create an RDS parser module object parser. We use it to parse the response body resp.body or the query results returned from ngx_drizzle and we make sure that the parser’s output is not nil. Then we loop around the rows of parser’s resultset. We store the column’s value, the returned docroot, into the Nginx variable ngx.var.root which is then assigned to Nginx root directive.

The reason we used rewrite_by_lua and not content_by_lua for example is that rewrite_by_lua acts as a rewrite phase handler that can make dynamic routing decisions based on a specific response whereas content_by_lua acts as a content handler and cannot be used with other content handler directives in the same location such as proxy_pass directive or root directive.

Since we are not using the Nginx HttpRewriteModule in this location, we will not have to worry about the order of execution of Nginx’s rewrite and rewrite_by_lua which always executes after HttpRewriteModule regardless of the order.

This concludes the required configurations.

Now, if we requested any of the subdomains, we would get the correct document root :

[code]

# curl localhost -H "Host: client1.domain.com"
HELLO FROM SILVER
# curl localhost -H "Host: client2.domain.com"
HELLO FROM GOLD
# curl localhost -H "Host: client3.domain.com"
HELLO FROM PLATINUM

[/code]

mapping subdomains using Nginx HttpDrizzleModule