Tuesday, June 24, 2008

CFUnited: Automating the Build & Deploy Process with ANT

I was presenting on an old ghetto laptop that couldn't run connect (although in all fairness to the P.O.S. machine, connect is a total hog.). Anyway, the most important part other than my charm and grace is the code, right? So the presentation, with all promised code, jar file dependencies, etc is here at the bottom of the page.

My "wingman", the goatee'd fellow asking really good questions, asked me another great one the following day and I thought it bore mentioning here. During the 2nd part of the presentation, I demo'd macrodef and used an example of looping over a query of servers, and for each server, running a bat file that would open up a connection to that server. Then, it'd copy the code. Then, it'd run that same bat file and close the connection to the server.

Currently, our environment is completely locked out of the prod environment, rightfully so. And so in my example, I was saying that the bat file in question would temporariliy open up a connection to each production server and then close that connection.

My goatee'd wingman asked, "but that doesn't prevent you from running that bat file any other time, does it?" And he's absolutely right. Here's a case where I'm hoping that our network admins will work with us, understanding that we have nothing to gain by opening up those connections except during the deploy process. But that remains to be seen. This is definitely a case of "let's compromise".

So, thanks to all who showed up for the session, and extra special thanks for the brave folks who stuck around for the 2nd half of it!

For the curious and bored, here's the code in question. The contents of OpenOrClose.bat are irrelevant here... it's the concept we were discussing that matters. The "deployToAllServers" target is the big daddy.
<project name="CFUnited (j): SQL, for, MacroDef, and exec" basedir="." default="getServers">

<target name="init">

<property name="app.name" value="Client1App" />

<property name="dev.root" location="DEVSERVER" />
<property name="test.root" location="TESTSERVER" />
<property name="locations.dev.clientroot" location="${dev.root}\${app.name}" />
<property name="locations.test.clientroot" location="${test.root}\${app.name}" />

<property name="locations.dev.customtags" location="${dev.root}\CustomTags" />
<property name="locations.test.customtags" location="${test.root}\CustomTags" />

<property name="locations.test.deploy" location="${locations.test.clientroot}\deploy" />
<property name="locations.test.deployzip" location="${locations.test.deploy}\${app.name}.zip" />

<!-- read all our 'secure' properties from this file; this defines the sql.userid and sql.password properties -->
<property file="unames.properties" />

<!-- create a classpath for ANT to use for finding and running the jdbc driver(s) -->
<property name="jdbclibdir" location="lib" />
<path id="jdbc.classpath">
<fileset dir="${jdbclibdir}">
<include name="**/*.jar" />

<!-- these properties would be better placed in a .properties file -->
<!-- this will use the jtds.jar in the classpath -->
<property name="sqlserver.driver" value="net.sourceforge.jtds.jdbc.Driver" />
<property name="sqlserver.url" value="jdbc:jtds:sqlserver://localhost:1436/ANT;instance=NetSDK" />

<!-- this will use the mysql-connector jar in the classpath -->
<property name="mysql.driver" value="com.mysql.jdbc.Driver" />
<property name="mysql.url" value="jdbc:mysql://localhost/ANT" />

<!-- this is where the sql resultset will be stored -->
<property name="db.output" value="serverslist.txt" />

<!-- http://sourceforge.net/projects/ant-contrib -->
<taskdef resource="net/sf/antcontrib/antlib.xml" classpathref="jdbc.classpath" />


<target name="getServers" depends="init" description="Queries a db for a list of servers">

<sql driver="${mysql.driver}" url="${mysql.url}" userid="${sql.username}" password="${sql.password}" classpathref="jdbc.classpath" print="yes" output="${db.output}" showheaders="false" showtrailers="false">
select ServerIP from servers where ActiveFlag=1



IMAGINE: You have a database table of servers to which you'll deploy.
You want to query for the "active" servers, and for each server
Copy your code onto it. This would assume an active network connection between
the machine you're on and the servers to which you are deploying

<target name="loopOverServers" depends="init,getServers">
<loadfile srcFile="${db.output}" property="serverlist" />
<for list="${serverlist}" param="server" delimiter="${line.separator}">

<echo>Copying to @{server}\Apps\${app.name}</echo>

<copy toDir="@{server}\Apps\${app.name}" preserveLastModified="true" includeEmptyDirs="false">
<fileset dir="${locations.test.clientroot}" />

<format pattern="MM/dd/yyyy hh:mm aa" offset="-5" unit="year" property="customtagfilter" />
<copy toDir="@{server}\CustomTags" preserveLastModified="true" includeEmptyDirs="false">
<fileset dir="${locations.test.customtags}">
<date datetime="${customtagfilter}" when="after" />




You want to do the same as above, but you need to open and close the connection to each server
because your network people run a tight ship, and they don't want connections open
between your dev environment and your other environments except
during the brief time it takes to copy code


<target name="deployToAllServers" depends="init,getServers">
<!-- open the connections using a bat file provided to you by your network admins;
we'll call the bat file using a self-made task that we create with macrodef -->
<OpenOrCloseConnections action="open" />

<!-- call the loopOverServers target and copy all code -->
<antcall target="loopOverServers" />

<!-- close the connections -->
<OpenOrCloseConnections action="close" />

<!-- MACRODEF: this little gem lits you define your own tasks! -->

<macrodef name="OpenOrCloseConnections">
<attribute name="action" default="close" />
<!-- imagine: this OpenOrClose.bat is provided to you by your network people to open
up the appropriate connections, and then close them, for the life of your script.
In this way, you can directly copy files across the network during a brief window of
access and then automatically remove that access once the deployment is over -->
<exec executable="cmd">
<arg value="/c" />
<arg value="OpenOrClose.bat" />
<arg value="[email protected]{action}" />


Imagine a table called "Deployments" and a table called "J_Deployments_Servers". And
every time you deploy, you insert a row into the deployments table, get the ID
of that deployment, and for each server you copy code to, you insert a row
into J_Deployments_Servers with some audit information (IP address of machine
from which the deployment was run, the exact time of the copy, etc).

How would you do that using sql and macrodef up in your loopOverServers target?




At June 24, 2008 2:04 PM , Anonymous Jim Priest said...

That's cool.

At June 24, 2008 2:20 PM , Blogger Marc Esher said...

it'll be cooler if I can get them to agree to let us try it out. This one issue .... of being able to copy the code from A to B, could save sooo much time and aggravation when compared with zipping, uploading, unzipping, verification, etc.

now that I say that, i'm virtually certain that they won't let us precisely because i think network people like to know you're jumping through a whole lot of hoops to get things to work. it's like "how much pain did you cause today?" is the only question that matters.


Post a Comment

<< Home