Multitenant Sites

Multitenant with Sinatra

Let's implement the above scenario using Sinatra, a very small and lightweight Web application framework written in the Ruby language. In the July 2014 issue of LJ, I covered a similarly small framework, known as Flask, written in Python. Such frameworks often are perfect for simple sites and example code.

There are a number of ways that you can create a Sinatra application. My preference is to do so in a directory, along with a Gemfile and a config.ru file. This took less than five minutes for me to set up on my own computer. First, I created a directory called "multiatf". In that directory, I created a file called "Gemfile", which is where I will name the Ruby gems I'll be using for this application:


source 'https://rubygems.org'
gem "sinatra", :require => "sinatra/base"
gem 'shotgun'

The first line says that I want to retrieve gems from Rubygems.org, the official and standard location. The second line says that I want to use the "sinatra" gem, but that I don't want to require "sinatra", but rather "sinatra/base". Finally, I name the "shotgun" gem, which provides for automatic reloading of Sinatra apps—precisely the sort of thing I want when I'm developing an application.

Before continuing, I then run bundle install, which ensures that all of the gems named in the Gemfile have been installed. It creates a file named "Gemfile.lock", which lists the precise names and versions of each gem I'll be using in my application. This list includes those gems I have named explicitly and those upon which my named gems depend. It is worth taking a look at Gemfile.lock sometime; it may well give you insights into how your Sinatra and Rails applications work.

Next, I write a "config.ru" file, sometimes known as a "rackup file", which tells Rack—Ruby's standard interface between HTTP servers and applications—where my application's code is located and how to execute it. The file looks like this:


require 'bundler'

Bundler.require

require './multiatf.rb'
run Sinatra::Application

The first line loads the "bundler" gem. Bundler is an increasingly indispensable gem in the Ruby world, in that it manages the versions of your gems for you, ensuring that they will not require conflicting versions of a gem. After loading Bundler, you then use the "require" class method, which reviews your Gemfile.lock and loads the gems named within.

Next, the "require" statement reads a Ruby file named "multiatf.rb" in the current directory. That is the actual application code, and it's the file I will be writing and modifying most of all. Loading it means that Ruby will read the contents of the code. In the case of my Sinatra app, that means taking the various "get" and "post" declarations and turning them into the appropriate routing map, such that the appropriate code block is executed for each URL.

Then, once the application has been loaded, config.ru invokes Sinatra::Application. That starts the application up and running.

The final step in putting the application together is the multiatf.rb file. This also consists of very little code, but potentially could be quite large:


require 'sinatra'

get '/' do
  "Hello from server '#{request.host}'"
end

The first line loads the Sinatra code. Next is something that looks vaguely like a method definition, but isn't. Rather, it tells Sinatra that if someone makes a request to the / URL, it should return a string. In this case, the string isn't static, but rather contains a dynamic portion, including the value of "request.host". As you can imagine, this value will vary according to the hostname you are using.

To start this up on my development machine, I ran:


shotgun multiatf.rb

This produces output telling me that Shotgun is now running my application on port 9393, using Ruby's built-in WEBrick server. I can now go to my Web browser, and load up http://localhost:9393, and because of the get / declaration in my Sinatra file, that method is fired. I get a nice message telling me:


"Hello from server 'localhost'"

But, what if it isn't localhost? What if I go to another server name? For example, I added the following two lines to my /etc/hosts file:


127.0.0.1 atf1
127.0.0.1 atf2

In other words, when I tell my Web browser to go to host "atf1", it'll go to 127.0.0.1, and it will send, in the "Host" HTTP request header, the server name "atf1". The output then will be:


Hello from server 'atf1'

The same will be true for "atf2".

Showing Different Content

Thus, you've seen how you can have different output, based on the value of the server name. This seemingly simple fact opens the door to the entire world of multitenant systems. For example, you could imagine a company doing business under a variety of names, which would want to have the same Web application running, but showing the current domain name. All you have to do is change your strings, or your templates, to reflect the current hostname.

In many cases, showing different hostnames isn't enough. You may want to show a different business name or a different address. In order for that to happen, you'll need some additional data. The best and most scalable way to do this is a relational database, but you can simulate one with a Ruby hash that will be good enough for the purposes of this article.

In this case, let's define the hash such that it contains two keys, one for each of the hosts to recognize. Then, let's pull out the company's name from the hash, based on the key.

I thus change multiatf.rb to read as follows:


require 'sinatra'

hosts = {'atf1' => {name: 'First ATF site',
                    address: '111 Main Street'},
         'atf2' => {name: 'Second ATF site',
                    address: '222 Elm Street'}
        }

get '/' do
  "Welcome to '#{hosts[request.host][:name]}', located at
    ↪'#{hosts[request.host][:address]}'!"
end

The idea here is simple, but the effects are profound. This is how each domain can appear different, even if the content is the same. You can imagine going even further than this, pulling in a different CSS stylesheet to an HTML page based on the hostname, or having it show different pictures.

If you are using a relational database, you can enter each new tenant site in a table, giving each a unique ID number. You then can use that ID as a foreign key, adding (for example) this "site_id" value in a table describing merchandise. For example, one of my clients manages about 30 different sites, each with its own set of real-estate offerings. These 30 sites are actually running on a single Web application, with a single database. However, based on the hostname through which the user enters the site, the software displays a different set of properties. This has made the site and the software easy to manage, scale and grow. Each time a new site needs to be added, the biggest task is to update the SSL certificate, such that it includes the new hostname. Otherwise, the system works automatically, with the (nontechnical) company managers able to create new sites within several minutes, merely by filling out an HTML form. That form allows them to add a new entry into the "sites" table. The hostname is used to look up the site ID, whose value is then used to display properties.

Next month, I'll continue with this topic, discussing not only how you can have the same site produce similar content, but how you can configure it such that different users can manage their own sites, without interfering with the overall software and functionality.

Resources

The Sinatra home page is at http://sinatrarb.com.

For information and ideas about multitenant sites in Ruby on Rails, you might want to read Multitenancy with Rails, an e-book written by Ryan Bigg and available at https://leanpub.com/multi-tenancy-rails. Although the book specifically addresses multitenancy with Rails, it offers many ideas and approaches that are appropriate for other software systems.

______________________

Reuven M. Lerner, Linux Journal Senior Columnist, a longtime Web developer, consultant and trainer, is completing his PhD in learning sciences at Northwestern University.