We’ve recently jumped into the world of Jenkins Multibranch Pipelines for one of the products we build in CarTrawler, and we couldn’t be much happier with the speed and flexibility it provides - allowing us to test, build, and deploy branches asynchronously and at scale without having to rely on a particular “build” branch, enabling us to test and demo features on our staging environment in isolation from other features.
One aspect of this was that we needed to deploy multiple versions of the product to our staging environment and be capable of accessing each one based off the URL. We took the approach of using a JIRA ticket number to access the feature/story that was being tested and, in order to do this, we needed to get our Apache vhost to serve from a different directory dynamically.
Ultimately, the flow of it all goes like so:
- Deploy app to
/home/jenkins/projectname/[JIRA-ID]
. - Access app from
http://projectname.internal.test/[JIRA-ID]/projectname
. - Serve app from
/home/jenkins/projectname/[JIRA-ID]
, based on what the URL was.
If you’re wondering why the URL is structured so, this particular project runs in a
directory on top of another product, and is expecting to be on the /projectname
endpoint.
The subdomain is our way of directing requests to the relevant products on our staging
environment (such as product1.internal.test
, product2.internal.test
, etc.).
Writing our Apache config
So, let’s get on with writing our Apache vhost config. If you’re comfortable with Apache configs, then feel free to skip to the end for the full config.
Establish the core
<Virtualhost *:80>
ServerName projectname.internal.test
DocumentRoot /home/jenkins/projectname
</Virtualhost>
First off, we need to write the core of our Virtual Host by establishing the ServerName
and DocumentRoot
directives. This sets us up so that a request to
http://projectname.internal.test/index.html
trigger our Virtualhost and resolves to
/home/jenkins/projectname/index.html
Alias requests to the appropriate directory
Next, we make use of Apache’s AliasMatch
which is where the bulk of our work happens. AliasMatch
allows us to use a regex on the
URL that came in, capture our desired parts, and resolve to a directory of our choosing
if the URL matched that regex.
AliasMatch "^/([-\w]+)/projectname(?:/(.*))?$" "/home/jenkins/projectname/$1/$2"
Above, we match on /PR-123/projectname/foo/bar
and, since we make use of regex
capturing groups, we can use the
captured values as variables $1
and $2
allowing us to resolve to
/home/jenkins/projectname/PR-123/foo/bar
.
Making it work with Single-page Apps
Normally, that would be enough to get up and running, but this particular project is a
single-page application with its own internal router, which means we need to route all
URLs pointing to /projectname/ANYTHING
back to the project’s index without changing the
URL itself, but only when that URL isn’t a valid file or directory on the filesystem.
Let’s get to it!
<LocationMatch "^/.*/projectname/.*">
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-l
RewriteCond %{REQUEST_FILENAME} projectname\/([-\w]+)(?:\/(.*))?
RewriteRule ^(.*)$ /%1/projectname/index.html [L]
</LocationMatch>
First, we need to establish a LocationMatch
directive.
This directive takes a regex which, when matched, allows the directives contained inside
it to run. Once inside, we enable the RewriteEngine
and set the RewriteBase
to /
.
This establishes the base path that our future rewrites will work off.
Next, we head into our RewriteCond
directives. These directives allow us to define
conditions for whether our RewriteRule
actually triggers. So, in order, these are:
- The requested filename is not a regular file
- The requested filename is not a directory
- The requested filename is not a symlink
- The requested filename matches the desired regex pattern
Finally, our RewriteRule
will trigger only if its own rule is matched (^(.*)$
) and
all preceding RewriteCond
directives are true. Note the use of %1
in our rule - this
is a backreference exposed by the last RewriteCond
regex used, allowing us to use the
values of the regex capture groups from that rule.
The flow
So after all of that, the final flow of a request made to
http://projectname.internal.test/PR-123/projectname/subview
in our Apache vhost goes
like so:
- Matches our
AliasMatch
and becomes a request to load/home/jenkins/projectname/PR-123/projectname/subview
from here on out - Matches our
<LocationMatch>
and begins our Rewrite block - The request is not a file that exists at this location
- The request is not a directory that exists at this location
- The request is not a symlink that exists at this location
- The request matches our regex and all previous conditions, so becomes a new
request to
http://projectname.internal.test/PR-123/projectname/index.html
- The request comes back in from the top and matches our
AliasMatch
, resolving to/home/jenkins/projectname/PR-123/projectname/index.html
- Matches our
<LocationMatch>
and begins our Rewrite block - This time, the requested location is a file on the filesystem, so we return it to the browser
The final config
After all that, our final config looks like the following:
<Virtualhost *:80>
ServerName projectname.internal.test
DocumentRoot /home/jenkins/projectname
AliasMatch "^/([-\w]+)/projectname(?:/(.*))?$" "/home/jenkins/projectname/$1/$2"
<LocationMatch "^/.*/projectname/.*">
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-l
RewriteCond %{REQUEST_FILENAME} projectname\/([-\w]+)(?:\/(.*))?
RewriteRule ^(.*)$ /%1/projectname/index.html [L]
</LocationMatch>
</Virtualhost>