Think In Geek

In geek we trust

Redirect parts of your website to different applications with Apache

Microservices everywhere. Those are the times we live in now. Everyone seems to be splitting monolithic web applications into smaller chunks. And that’s fine. However, setting up the local development environment for this can be sometimes a bit cumbersome.

It’s not an uncommon scenario when splitting parts of your site into standalone services to want to redirect certain paths of your URL to different back-end systems. There are multiple ways to do this, and I’ll show you how to set up a relatively painless Apache 2.4 setup where you can serve your monolith application as you used to, but at the same time point the new bits to the new services.

Let’s imagine the following scenario:

  • You’re developing a site http://www.acme.com
  • In your local development environment, you have a Rails app, serving the monolith, in port 3000
  • In order to make it all as close to production as possible, you develop in a special development domain dev.acme.com

Step 1: serving the monolith

The first thing you need to do is ensure your domain dev.acme.com points to your localhost:

/etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1	localhost dev.acme.com

At this point, with your Rails app up and running, if you point your browser to http://dev.acme.com:3000, you should see your monolith app. However, we want to get rid of the port in that URL. We can achieve this by using the Apache mod_proxy plugin.

Let’s start by creating our own Apache configuration file that we can mess with, so we don’t have to worry about modifying the ones that come with our OS.

dev.acme.com.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<VirtualHost *.dev.acme.com:80>
  ServerName www.dev.acme.com
  ServerAlias *.dev.acme.com
  ServerAlias dev.acme.com
  ProxyRequests Off
 
  # Monolith
  ProxyPass / http://0.0.0.0:3000/ retry=0
  ProxyPassReverse / http://0.0.0.0:3000/
 
  <Location "/">
    # Allow access to this proxied URL location for everyone.
    Order allow,deny
    Allow from all
  </Location>
<VirtualHost *.dev.acme.com:80>

The first thing we do is create a VirtualHost that will apply to any request coming to dev.acme.com on port 80 (the default HTTP port) We do that on lines 2, 3 and 4, where we set up the ServerName and a few ServerAlias. The ServerName directive will match requests coming to that domain into the VirtualHost. Since we usually want some other subdomains to be served by the same application, we can add aliases to the name via the ServerAlias directive. These lines will cover any request made to the dev.acme.com domain or any of its subdomains.

On line 5 we disable the forward proxy feature. No need to worry too much about this though, as it’s out of the scope of this post and this is just a local setup.

Then we make the magic happen in lines 8 and 9, using ProxyPass and ProxyPassReverse. ProxyPass / http://0.0.0.0:3000/ retry=0 will tell Apache to get any requests coming to the server and send them to our Rails application, which lives in port 3000 of our local machine. The retry=0 bit will ensure that requests will not be waiting for a timeout if for whatever reason our backend server was not available (e.g. our Rails app is starting, or has crashed).

Unfortunately this directive is not enough. When the request reaches your local Rails application, it is served back to Apache, but because it’s still being served at localhost:3000, some of its headers may contain references to this domain. One good example is a redirect response, which would be served back as a 302 or 301 with a Location header of http://localhost:3000/new_url.

Here’s where the ProxyPassReverse / http://0.0.0.0:3000/ comes into play. This line tells Apache to transform any response from the backend to modify the Location, Content-Location and URI headers and replace them by the host used in the original request (e.g. dev.acme.com). This way the redirect response from above would become a 302 or 301 with a Location header of http://dev.acme.com/new_url.

Step 2: splitting part of the site into its own service

Let’s say we want to change the way we serve part of our site. We’ve traditionally served our product category pages from the monolith, via controller requests making some MySQL queries and eventually rendering some ERB views. But now the information on these pages is actually coming from a API, and we want to render them via a new React Single Page Application. We therefore want all pages under the /categories path on our website to be served by this new nodejs server when doing local development, so everything looks as transparently and close to production as possible.

We will assume that our front-end development server lives on localhost:4000, so the setup we are after is the following:

  • Requests made to http://dev.acme.com/categories or any sub path of it need to be served by the nodejs front-end application on port 4000
  • Requests made elsewhere in the domain http://dev.acme.com/categories need to be served by our Rails application on port 3000

In order for us to achieve this we need to add another set of proxy rules to our Apache configuration file:

dev.acme.com.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<VirtualHost *.dev.acme.com:80>
  ServerName www.dev.acme.com
  ServerAlias *.dev.acme.com
  ServerAlias dev.acme.com
  ProxyRequests Off
 
  # Front-end app
  ProxyPass "^/categories.*$" http://0.0.0.0:4000/ retry=0
  ProxyPassReverse /categories http://0.0.0.0:4000/
 
  # Monolith
  ProxyPass / http://0.0.0.0:3000/ retry=0
  ProxyPassReverse / http://0.0.0.0:3000/
 
  <Location "/">
    # Allow access to this proxied URL location for everyone.
    Order allow,deny
    Allow from all
  </Location>
<VirtualHost *.dev.acme.com:80>

On lines 8 and 9, we tell Apache to look for any path matching the regular expression ^/categories.*$ and route it to the server listening on port 4000. You’ll have noticed that we added the rules for the category pages before the rules for the root path. This is important, as Apache will look at rules in order. If we were to swap lines 8 and 9 for lines 12 and 13, when visiting /categories, Apache would match that route to /, and it’d be served by our Rails app.

If you restart Apache now, fire up both your Rails and nodejs applications, and visit http://dev.acme.com/categories, you’ll see on your nodejs logs how the request has indeed been routed to the application on port 4000. However, you’ll probably notice that the application is not actually loading properly on your browser. If you go and then have a look at your Rails logs, you may notice a request being made to the path /bundle.js.

What’s happening here? Why is that React javascript file being routed to the Rails app? Let’s look at the whole process, step by step:

  1. A request is made to http://dev.acme.com/categories
  2. Apache routes it to your nodejs server
  3. Your nodejs server replies with an html response
  4. Apache receives this response and replaces its headers, ensuring any reference to localhost:4000 gets transformed into http://dev.acme.com
  5. Apache sends this modified response to the browser
  6. The browser renders the contents of the response

If you are using a typical React project, the HTML response that your browser renders will look like this:

response.html
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="/style/style.css">
  </head>
  <body>
    <div class="container"></div>
  </body>
  <script src="/bundle.js"></script>
</html>

Lines 5 and 10 are the issue here. Your browser will try to load the bundled assets referenced in your React app from the root of your path. This means the browser will make and HTTP request to load these files using the following URLs:

  • http://dev.acme.com/style/style.css
  • http://dev.acme.com/bundle.js

When Apache gets the requests, it will try to match them against the regular expression ^/categories.*$, there will be no match, and therefore it will route the request to your Rails app. And your Rails app knows nothing about these files, as they don’t belong to it.

We could now add more proxy rules to our configuration, but this doesn’t scale very well the moment we need to either add more applications, or our React app needs to serve more assets. Luckily for us, there’s another tool we can use to help us: Apache’s mod_rewrite plugin. mod_rewrite provides us with directives that can change the paths of the requested URLs based on rules. We can do this with RewriteCond and RewriteRule. This is what our final Apache configuration file will look like:

dev.acme.com.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<VirtualHost *.dev.acme.com:80>
  ServerName www.dev.acme.com
  ServerAlias *.dev.acme.com
  ServerAlias dev.acme.com
  ProxyRequests Off
  RewriteEngine On
 
  # Front-end app
  ProxyPass "^/categories.*$" http://0.0.0.0:4000/ retry=0
  ProxyPassReverse /categories http://0.0.0.0:4000/
  RewriteCond "%{HTTP_REFERER}" http://dev.acme.com/categories.*
  RewriteRule .(js|css)$ http://localhost:4000/%{REQUEST_URI} [R=301,L]
 
  # Monolith
  ProxyPass / http://0.0.0.0:3000/ retry=0
  ProxyPassReverse / http://0.0.0.0:3000/
 
  <Location "/">
    # Allow access to this proxied URL location for everyone.
    Order allow,deny
    Allow from all
  </Location>
<VirtualHost *.dev.acme.com:80>

On line 6 we enable the rewrite engine, so we can use the mod_rewrite directives. And then on lines 11 and 12 we make it all work. Whenever a request to load any file coming from the nodejs server is sent to Apache, it has an HTTP header called HTTP_REFERRER. This header contains the URL that made the request. In our case, any requests coming from the nodejs app will be coming from the /categories path, as it’s the only path our front-end application is serving. Let’s have a closer look at lines 11 and 12.

RewriteCond "%{HTTP_REFERER}" http://dev.acme.com/categories.* will add a Rewrite Condition. Rewrite Conditions are evaluated by Apache, and if they match, then a Rewrite Rule gets applied. We only have one condition in our case, and the condition is to check if the HTTP_REFERER header of the request matches the regular expression http://dev.acme.com/categories.*.

RewriteRule (.js|css)$ http://localhost:4000/%{REQUEST_URI} [R=301,L] is the actual rule that will get applied if the conditions before are met. The rule gets any file ending in js or css, and then issues a redirect response to a modified URL so that file is requested from the root of http://localhost:4000. This way, a request to http://dev.acme.com/bundle.js gets a 301 redirect response to http://localhost:4000/bundle.js, which our nodejs server will happily serve back.

Summary

We can use Apache to help us develop complex multi service applications locally. In order to do that, we need to spin up our different services to listen to different ports, and tell Apache to route requests to the correct application via ProxyPass and ProxyPassReverse directives, ensuring each path of our URL gets served by the right service. For more complex applications, where individual services need to load assets over HTTP, we can use the RewriteCond and RewriteRule directives so these assets get served by the correct services as well. Finally, we can wrap it all up inside a VirtualDomain and an extra DNS line in our /etc/hosts file, to be able to develop in an environment that is very close to what our production setup will look like.

,

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.