Objective
Our objective is to setup Apache httpd to work as a proxy in front of the Apache Tomcat application container.
Operating System and Software Versions
- Operating system: Red Hat Enterprise Linux 7.5
- Software: Apache httpd, Apache Tomcat
Requirements
Privileged access to the system
Difficulty
EASY
Conventions
- # – requires given linux commands to be executed with root privileges either directly as a root user or by use of
sudo
command - $ – given linux commands to be executed as a regular non-privileged user
Introduction
Using Apache httpd as a proxy to an Apache Tomcat application container is a common setup. It comes with many use cases, the most trivial is serving static content from httpd
, while providing services implementing heavy business logic from an application written in Java that resides in the Tomcat container.
By creating a proxy, we can create a kind of front-end to the application layer, where we can introduce security measures in the webserver, apply load balancing, use conditional redirect, or use any other functionality provided by the webserver. This way we don’t need to implement any of these features in our application, and can focus it’s capabilities to the service itself. We will have a full-featured webserver presented for the users, some of the urls forwarded silently to the application container that may be not accessible by itself. The application’s answers are forwarded back to the clients who will not know that they spoke anything else but the webserver – that is, if we take care not exposing any information (like unhandled error messages) from the application that can make them guess there are more than one layers.
We will use the AJP protocol that can be used between webservers and Java based application containers to provide the ability to balance the load between multiple application servers – however, to set up a load balancer is out of the scope of this tutorial.
We will configure our setup on Red Hat Linux 7.5, but the Apache webserver, the AJP module and the Apache Tomcat application container are available everywhere, and thus this setup is portable with small adjustments like filesystem paths or service names.
Installing required software
First we need to install the services we will use. In a load balanced setup Tomcat server(s) could be on different machines, and often they are, providing a farm of containers that build up a service.
# yum install httpd tomcat tomcat-webapps
We install the tomcat-webapps
for testing purposes, within this package is an examples web application deployed into our Tomcat server on installation. We’ll use this application to test that our setup is working as intended.
Now we can enable and start our Tomcat server:
# systemctl enable tomcat
# systemctl start tomcat
And our webserver:
# systemctl enable httpd
# systemctl start httpd
The default httpd
installation contains the proxy modules we need. To check that it is so, we can query the webserver with apachectl
:
# apachectl -M | grep ajp
proxy_ajp_module (shared)
Note: 1.x Apache versions use mod_jk
module instead of proxy_ajp
.
httpd configuration
The examples web application deployed into Tomcat are published after installation by default on server-url:8080/examples
. We will proxy requests coming to the server’s port 80 (the default http port) requesting something from the server-url/examples
to be served by the examples
web application deployed into Tomcat. Requests coming to any other URL on the server will be served by the web server. We’ll set up some static content to show this functionality.
In our example the server is called ws.foobar.com
. For the proxy to work create a text file with your favorite editor under the webserver’s drop-in configuration directory, which is /etc/httpd/conf.d
on Red Hat flavors, with the extension of .conf
. Our setup doesn’t need Tomcat to be reachable directly, so we use localhost
as target host in the /etc/httpd/conf.d/example_proxy.conf
file:
<VirtualHost ws.foobar.com:80>
ServerName ws.foobar.com
ProxyRequests Off
ProxyPass /examples ajp://localhost:8009/examples
ProxyPassReverse /examples ajp://localhost:8009/examples
</VirtualHost>
To be on the safe side, we can verify that our configuration is correct before applying with apachectl
:
# apachectl configtest
Syntax OK
If the configuration test returns an error like the following:
Could not resolve host name ws.foobar.com -- ignoring!
If means that our ServerName
directive is invalid, as it can’t be resolved by the webserver. Either we need to register it in the (local or global) DNS, or provide a line in the /etc/hosts
file that contains the host’s public IP address followed by the name we gave in the above configuration. If the hosts file already contains the IP with another name (maybe the real hostname), we can add the servername after the host’s name(s) in the same line, the setup will work.
After successful test we need to apply the new configuration by restarting the webserver:
# systemctl restart httpd
Tomcat configuration
With the default install the Tomcat container will listen to AJP requests on all interfaces on port 8009. This can be verified in the main configuration file:
# view /usr/share/tomcat/conf/server.xml
[..]
<!-- Define an AJP 1.3 Connector on port 8009 -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
[..]
If we don’t need the Tomcat container and the applications within to be reachable by themselves, we can set every connector to listen only on localhost:
Connector address="127.0.0.1" port=..."
To apply we can restart Tomcat with:
# systemctl restart tomcat
In our lab machine will not do this, as we need to see that we are served the same content on both port 80
and 8080
.
Testing
Our minimal AJP proxy setup is complete, we can test it. From the command line we can call the examples
application directly on port 8080
:
$ wget http://ws.foobar.com:8080/examples
--2018-09-13 11:00:58-- http://ws.foobar.com:8080/examples
Resolving ws.foobar.com (ws.foobar.com)... 10.104.1.165
Connecting to ws.foobar.com (ws.foobar.com)|10.104.1.165|:8080... connected.
HTTP request sent, awaiting response... 302 Found
Location: /examples/ [following]
--2018-09-13 11:00:58-- http://ws.foobar.com:8080/examples/
Reusing existing connection to ws.foobar.com:8080.
HTTP request sent, awaiting response... 200 OK
Length: 1253 (1.2K) [text/html]
Saving to: 'examples'
100%[=========================================================================================================================================================================>] 1,253 --.-K/s in 0s
2018-09-13 11:00:58 (102 MB/s) - 'examples' saved [1253/1253]
And see the contents provided:
$ tail examples
<h3>Apache Tomcat Examples</H3>
<p></p>
<ul>
<li><a href="servlets">Servlets examples</a></li>
<li><a href="jsp">JSP Examples</a></li>
<li><a href="websocket/index.xhtml">WebSocket (JSR356) Examples</a></li>
<li><a href="websocket-deprecated">WebSocket Examples using the deprecated
Apache Tomcat proprietary API</a></li>
</ul>
</body></html>
And if we call the same application trough our AJP proxy, we should also get an answer, while there isn’t any content in the webserver’s document root:
$ wget http://ws.foobar.com/examples
--2018-09-13 11:01:09-- http://ws.foobar.com/examples
Resolving ws.foobar.com (ws.foobar.com)... 10.104.1.165
Connecting to ws.foobar.com (ws.foobar.com)|10.104.1.165|:80... connected.
HTTP request sent, awaiting response... 302 Found
Location: /examples/ [following]
--2018-09-13 11:01:09-- http://ws.foobar.com/examples/
Reusing existing connection to ws.foobar.com:80.
HTTP request sent, awaiting response... 200 OK
Length: 1253 (1.2K) [text/html]
Saving to: 'examples.1'
100%[=========================================================================================================================================================================>] 1,253 --.-K/s in 0s
2018-09-13 11:01:09 (101 MB/s) - 'examples.1' saved [1253/1253]
If all works, we will get an answer with the same contents, as the final answer is provided by the same application within the container:
$ tail examples.1
<h3>Apache Tomcat Examples</h3>
[...]
We can also test our setup with a browser. We need to call all URLs with the server’s name as the host (at least the one that is proxied). For that the machine running the browser need to be able to resolve the server name, by means of DNS or hosts file.
In our lab environment we haven’t disabled Tomcat listening on the public interface, so we can see what is provided when asked directly on port 8080
:
We can get the same content trough the AJP proxy provided by the webserver on port 80
:
While acting as a proxy, httpd
can serve any other content. We can create static content that is reachable on some other URL on the same server:
# mkdir /var/www/html/static_content
# echo "<html><body>Static content</body></html>" > /var/www/html/static_content/static.html
By pointing our browser to this new resource, we are provided with the new static content.
If the Tomcat container would not be reachable, we would not know the answer coming somewhere other than the webserver. As we proxied only a specific application, the container’s default ROOT application is not reachable trough the proxy, thus hidden from everything beyond the webserver.
Conclusion
The Apache webserver is highly extendable by the means of modules, one of them is the AJP proxy module. The above guide uses one machine and exposes one application with the proxy, but the same webserver could provide a single entry to many applications, possibly on many hosts running application containers, while providing other web content as well.
Combined with other modules, like mod_security
, we can add many features to our service without the need to develop them within the application, or if the need arises, redirect the proxy to another endpoint with a single edition of the configuration file and the reload of the webserver, making a migration or the introduction of the application’s new release a matter of seconds. The same reload can lead the visitor to a page explaining planned downtime, while maintenance is performed on the application servers – the use cases of an AJP proxy are only limited by the imagination of the IT staff.