In this post, I’m going to discuss the setup I have for deploying an ASP.NET application in Linux.
The application is actually my pet project BuzzStats, so that name appears all over the place here. Feel free to think of it as ‘HelloWorldApp’ or ‘MyWebApp’.
Here’s a nice diagram I made that illustrates my current deployment process:
The basic points are:
- the build server has the concept of a 'version number'
- it is possible to version the assemblies with that version number
- it is possible to use the same version number to version the deployed application
- it is possible to switch between these deployed versions on the web server
The setup:
- Developer's workstation:
- doesn't matter, as long as he can push changes to GIT/SSH
- Code server:
- Ubuntu with SSH enabled to serve GIT repository
- Build server:
- Ubuntu
- Jenkins
- Mono
- Web server:
- Ubuntu
- Apache
- Mono
- Mod_Mono
In my current setup, the build server and the web server are actually the same physical machine. My next step would be to separate that also.
Versioning deployed web application
To support versions of deployed web applications, I use symbolic links.
Typically, you would find the application under /var/www/BuzzStats
.
I follow that convention, but I make a symbolic link to the actual place where
the application lives.
/var/www/BuzzStats -> /opt/BuzzStats/current
and that one is also a symbolic link:
ngeor@box:~$ ls -l /opt/BuzzStats
drwxrwsr-x 11 jenkins www-data 4096 Nov 7 21:14 1.5.4.161
drwxrwsr-x 11 jenkins www-data 4096 Nov 7 21:28 1.5.4.162
drwxrwsr-x 11 jenkins www-data 4096 Nov 8 21:45 1.5.4.164
lrwxrwxrwx 1 www-data www-data 9 Nov 8 21:45 current -> 1.5.4.164
You can see that there are some folders here, e.g. 1.5.4.161
. You
might recognize the format of a .NET assembly version number. This version
number is generated by the build server. The build server also creates this
version folder here and deploys the corresponding version.
Security note: Please notice that for that to work, the folder needs to be writable by the group, which can be a security issue. Additionally, to automatically remove version folders automatically (not discussed in this blog post), you also need the version folders themselves to be writable.
So the application is served from /var/www/BuzzStats
, as one would
typically expect to avoid surprises. That one is a symbolic link to
/opt/BuzzStats/current
. And finally that one in turn is a symbolic
link pointing to the active version folder.
Switching between versions is as easy as changing the symbolic link of
current
to point to a different version folder. Activate the latest
and greatest, figure our there’s a missing image? Rollback immediately and
easily to the previous version.
Automatic build
Let’s have a look at how the version number can be generated on the build server.
Version Number
In Jenkins, I use the Version Number Plugin to create a formatted version number. These are my settings:
- Environment Variable Name :
ASSEMBLY_VERSION_NUMBER
- Version Number Format String :
1.5.4.${BUILD_NUMBER}
- Build Display Name : Check the 'Use the formatted version number for build display name' option.
- Project Start Date : 2010-11-27 (not important, but it's been almost three years! :-) )
With this setup, I have an environment variable called
ASSEMBLY_VERSION_NUMBER
which will contain a value like
1.5.4.42
that will be incremented automatically every time Jenkins
bakes a new build.
A note on version semantics: for now I’m using a simple
solution. The first part of the version is hard-coded in my Jenkins settings
(1.5.4
). The last part is Jenkins’ own build number which is
automatically incremented. You can chose a different strategy for the version,
e.g. based on the date of the build. I will probably also change this in the
future so that only the 1.5
part is hard-coded. If you like this
topic (I do), you can further read Semantic
Versioning 2.0.0 and
Which
Version of Version?.
Build Steps
Now that we have the version number as an environment variable, we can use it during the build process.
- Pre-Build: Modify AssemblyInfo.cs files to use the
ASSEMBLY_VERSION_NUMBER
.This will make the built assemblies use the same version number. I've used various ways in the past, for now I'm doing it with a simple shell script: ``` #!/bin/sh # patch assemblies if [ -z "${ASSEMBLY_VERSION_NUMBER}" ]; then echo "Missing ASSEMBLY_VERSION_NUMBER" exit 1 fi for FILE in `find . -name AssemblyInfo.cs` do echo "Patching $FILE" sed -i -r "s/[0-9]+.[0-9]+.[0-9]+.[0-9]+/${ASSEMBLY_VERSION_NUMBER}/g" $FILE done ``` - Build: Simply run
xbuild
, mono's msbuild equivalent. This actually works (TM) these days, also with full NuGet support for package restore. - Package the web site into a folder, preparing for deployment. I currently do this with a very old NAnt script. The result is a temporary folder ready to be deployed (as in copy-pasted) to the final deployment location.
- Deploy to a local folder. Again with the same old NAnt script:
- create the folder
/opt/BuzzStats/${ASSEMBLY_VERSION_NUMBER}
- copy the package into that folder
rsync
), I will be able to deploy automatically to my actual web server where this blog is hosted. - create the folder
Activating and rolling-back
With this setup, after each build we have a new version folder under the main
folder /opt/BuzzStats
. The version is not activated;
remember that the web application is still pointing to
/var/www/BuzzStats
which in turn points to
/opt/BuzzStats/current
which still points to the old version.
Activation could also be part of deployment:
- remove the current link (
unlink /opt/BuzzStats/current
) - create a new symbolic link (
ln -s /opt/BuzzStats/${ASSEMBLY_VERSION_NUMBER} /opt/BuzzStats/current
)
One caveat here is that mono doesn’t detect the change and still continues to
run on the old folder. The only way I’ve found so far to “fix” this is to kill
the mono process with pkill mono
. Obviously this will kill all mono
processes which could be a bit more than what you want…
So, for now, I’ve left that as a manual step. I’ve written a small web application to manage activation and rolling-back using a simple UI and I’ll show/share that on a follow-up post.