Better SOA Development With Foreman and NGINX
Give Me More!
Everyone knows more is better. More kittens, more money, more apps. Why settle for one Ruby project, when you can have three? We’ll take one Rails app for authorization and one to serve an API. Hey, let’s throw in a Sinatra proxy server serving up an AngularJS app to while we’re at it! Now we’re cookin’!
There are many ways organizations stand to gain by splitting their application into multiple projects running in symphony. If we’re being good programmers and following the Single Responsibility Principle (SRP), it makes sense to embrace it at all levels of organization, from our methods and classes up through our project structure. To organize this on the macro level, we can use a Service-Oriented Architecture (SOA) approach. In this article, we’ll explore some patterns that make it easier to develop SOA apps with a minimum of headaches.
Service Oriented Architecture
In the mid-2000s, some programmers began to organize their applications in a new way. Led by enterprise behemoths like Microsoft and IBM, the programming community saw a rise in the use of web services: applications that provide data or functionality to others. When you stick a few of these services together, and coordinate them in some kind of client-facing interface, you’ve built an application using SOA. The benefits of this approach remain relevant in modern web development:
- Data storage is encapsulated
- You can reuse services between multiple applications (e.g. authentication)
- You can monitor messages sent between services for business intelligence
- Services are modular, and can be composed into new applications without repetition of common functionality
A Sample Project
To illustrate some processes that make it easier to develop an SOA project, we’ll imagine that we’re building a project called Panda
, that is composed of three services:
- PandaAPI (a RESTful API that serves data about Giant Pandas)
- PandaAuth (a login page and user authentication service)
- PandaClient (an AngularJS app sitting atop a Sinatra proxy server)
Setting Up GitHub
To deal with an SOA project like this, it’s helpful to make sure you have everything well-structured on GitHub, so that all developers working on it can get up to speed quickly, and stay in sync with each other. I recommend creating an organization, and setting up all of the service project repositories under that organiztion. For Panda, I would start with a structure that looks like this:
panda-org (organization)
|- panda-auth (repo)
|- panda-api (repo)
|- panda-client (repo)
|- processes (repo)
The first three repos will hold the actual service projects, and the processes
repo will hold scripts and processes that are shared between them.
Keeping Everything In Sync
It can be pretty annoying to have your projects all out of sync. To make it easier to keep things up to date, here’s a shell script you can use to pull all of your projects down and update them in one go.
Inside of the processes folder, create a git_update.sh
file.
It should look like this:
#!/bin/bash
BRANCH=$1
: ${BRANCH:="master"}
cd ../panda-auth && git checkout $BRANCH && git pull origin $BRANCH
cd ../panda-api && git checkout $BRANCH && git pull origin $BRANCH
cd ../panda-client && git checkout $BRANCH && git pull origin $BRANCH
When executing this script, you can specify the branch by running:
sh ./git_update <feature-name>.
We can do something similar for bundling and running migrations.
Create the bundle_and_migrate.sh
file.
It should look like this:
#!/bin/bash
# Load RVM
[[ -s "$HOME/.rvm/scripts/rvm" ]] && source "$HOME/.rvm/scripts/rvm" # Load RVM into a shell session *as a function*
export PATH="/usr/local/bin:/usr/local/sbin:~/bin:$PATH"
[[ -s "$HOME/.rvm/scripts/rvm" ]] && . "$HOME/.rvm/scripts/rvm" # Load RVM function
cd ../panda-auth && bundle && bundle exec rake db:migrate
cd ../panda-api && bundle && bundle exec rake db:migrate
cd ../panda-client && bundle && bundle exec rake db:migrate
Now the projects are all updated and ready to go, and we want to begin developing some features. We could write another script to go start rails server
in each of our directories, but there is a better way.
Call In The Foreman
The foreman gem is my favorite way to manage multiple applications: it runs them all in a single terminal session. It’s pretty simple to get set up, and saves you having to run a lot of terminal sessions (and a lot of headaches).
First, we’ll need to gem install foreman
, to make sure we have the global executable available. Then, we’ll set up a Procfile
to tell it which processes we want it to run. We’ll create ours in the processes
directory, since that’s where we’re keeping things that pertain to all of the projects in our SOA app.
The Procfile
should look like this:
auth: sh -c 'cd ../panda-auth && bundle exec rails s -p 3000'
api: sh -c 'cd ../panda-api && bundle exec rails s -p 3001'
client: sh -c 'cd ../panda-client && bundle exec rails s -p 3002'
This will work great as long as all of your apps are running on the same gemset. If not, you will need to check out the subcontractor gem.
From the processes
folder, run foreman start
. Sweet. Now everything is all set up. Just open up your browser and navigate to http://localhost:3000
. Oh, and pop open two more tabs for http://localhost:3001
and http://localhost:3002
in them.
But wouldn’t it be nice if we could just run everything under a single host name?
NGINX To The Rescue!
To get around the problem of having three different localhosts, we can use NGINX. If you’re not familiar with NGINX, it’s an Apache httpd alternative that acts as “a high performance HTTP server, a web accelerator, an application gateway, and a load balancer”. We can use it to serve up all three of our apps from the same host, and make things a whole lot easier on ourselves.
To install NGINX, I recommend using Homebrew. If you have Homebrew, installation is as simple as brew install nginx
. If you don’t, you can try one of the alternatives.
Once NGINX is installed, we’ll want to locate our nginx.conf
. If you installed using Homebrew, it will be located at /usr/local/etc/nginx/nginx.conf
. Otherwise, you’ll want to use ack, mdfind
, or another search tool to locate it.
Once you’ve located it, open it in your text editor and locate the server
section. Find the block that starts with location /
(line 43 for me) and replace it with the following:
http {
...
server {
listen 8080;
...
# Client
location / {
proxy_pass http://127.0.0.1:3002;
proxy_set_header X-Real-IP $remote_addr;
}
# Auth
location /auth {
proxy_pass http://127.0.0.1:3000;
proxy_set_header X-Real-IP $remote_addr;
}
# API
location /api {
proxy_pass http://127.0.0.1:3001;
proxy_set_header X-Real-IP $remote_addr;
}
...
}
...
}
Now start NGINX with the nginx
command. With these proxy_pass
settings in place, we should be able to visit see all of our apps from http://localhost:8080
:
/
takes us to the client app/auth
takes us to the auth appapi
takes us to the API app
Dealing With Redirects
One last tricky part of developing SOA apps is figuring out how to deal with URL redirects between our apps. Let’s say that you want the client app to redirect users to the auth app if they haven’t logged in yet.
You would probably want to do something like this in the client app:
class ApplicationController < ActionController::Base
before_action :check_user
private
def check_user
if !session[:user_id]
redirect_to '/auth'
end
end
end
Looks good, and it should work locally.
But it could be a problem if some of your apps are served from subdomains in production. Fortunately, there’s an easy way to get around this.
Create a config/service_urls.yml
file in each project.
Inside it, define the URL for each app:
defaults: &defaults
service-urls:
panda-api: 'localhost:8080/api'
panda-auth: 'localhost:8080/auth'
panda-client: 'localhost:8080'
development:
<<: *defaults
test:
<<: *defaults
production:
service-urls:
panda-api: 'TBD'
panda-auth: 'TBD'
panda-client: 'TBD'
We’ll also need to register this in the config/application.rb
file:
module PandaClient
class Application < Rails::Application
...
# In Rails 4.2, you can use:
Rails.configuration.urls = config_for(:service_urls)[Rails.env]
# For older Rails versions, use:
Rails.configuration.urls = YAML.load_file(Rails.root.join('config', 'service_urls.yml'))[Rails.env]
end
end
With this configuration in place, we can now update the URL redirect in our application controller so it will work in all environments.
That will look something like this:
class ApplicationController < ActionController::Base
...
def check_user
if !session[:user_id]
redirect_to auth_url
end
end
def auth_url
@auth_url ||= Rails.application.config.urls['service-urls']['panda-auth']
end
end
With these changes in place, our applications will now redirect to the appropriate url in all environments.
Conclusion
By now, you sould have a better idea of what it takes to develop an application using SOA principals. We’ve taken a look at using shell scripts to keep our files in sync, foreman to run several servers at once, and NGINX to pull everything together into a single host address that makes it easier to work with all our services in the browser.
Juggling several services can be pretty confusing, but if you start with the right set up, it makes things a lot easier. All your apps will be under control if you manage them from a central place, and using the strategies discussed in this article should help make the process less painful. Good luck!
P.S. What tricks do you use when you’re developing SOA apps? Did I leave anything out? If you think so, leave us a comment.
Share your thoughts with @engineyard on Twitter
OR
Talk about it on reddit