Why do I need a deploy script
Grails applications are very easy to assemble in a WAR. This is done like this:
grails war
In addition to the fact that WAR is going, I really want this WAR to be installed on the server. In our case, this is Tomcat. Manual installation requires some fuss:
- Stop the server. Kill the process if he did not stop himself.
- Delete old application files (just in case)
- Copy the new WAR to the server. Sometimes it needs to be renamed (say, in ROOT.war)
In Maven, for example, cargo plugin can do this work. But there are a lot of adventures and settings with him, and he doesn’t really take into account the particularities of the server.
We can also use a shell script. But why write in the inconvenient shell language when there is a great cross-platform language Groovy?
')
Writing a Groovy Script
Grails makes it easy to create scripts on Groovy command-line that are added to the
scripts folder of our application. Any of these scripts can then be run using the
grails console
command . We will write a script called
Deploy.groovy .
It's nice that inside the script you can use the configuration data of our Grails-application. For example, the server name and user name are environment-specific. For Grails 1.3.7, we can access the configuration like this:
depends(compile) depends(createConfig) def host = ConfigurationHolder.config?.deploy.host def username = ConfigurationHolder.config?.deploy.username
It is assumed that where inside
grails-app / conf / Config.groovy there will be approximately the following lines:
... environments { production { ... deploy { host = 'www1.shards.intra' username = 'deployer' } } }
A little trick is that in order to load the configuration, you first need to compile the
Config.groovy file. To do this, we declared
depends (compile) , where
compile is the already known Gant command (task) to compile the project.
Using SSH
We need to perform many operations on the server with SSH access. For simplicity, I took the free JSch library and limited myself to the password access option. Therefore, our script starts like this:
@GrabResolver(name='jcraft', root='http://jsch.sourceforge.net/maven2') @Grab(group='com.jcraft', module='jsch', version='0.1.44') import com.jcraft.jsch.*
Further we will do some magic manipulations with JSch. We need two things:
- Run Unix commands on the server
- Copy files to server
We have the Groovy language at our disposal, so we will try to make mini DSL with the functions we need. Add a couple of new methods to the Session object (from JSch), which is an SSH session. First, the
exec method to execute the command on the server:
Session.metaClass.exec = { String cmd -> Channel channel = this.openChannel("exec") channel.command = cmd channel.inputStream = null channel.errStream = System.err InputStream inp = channel.inputStream channel.connect() int exitStatus = -1 StringBuilder output = new StringBuilder() try { while (true) { output << inp if (channel.closed) { exitStatus = channel.exitStatus break } try { sleep(1000) } catch (Exception ee) { } } } finally { channel.disconnect() } if (exitStatus != 0) { println output throw new RuntimeException("Command [${cmd}] returned exit-status ${exitStatus}") } output.toString() }
For reasons of brevity, I made it so that if successful, the method displays nothing, and when errors occur, prints the entire output stream of the executed command.
Now I would also write the file to the server:
Session.metaClass.scp = { sourceFile, dst -> ChannelSftp channel = (ChannelSftp) openChannel("sftp") channel.connect() println "${sourceFile.path} => ${dst}" try { channel.put(new FileInputStream(sourceFile), dst, new SftpProgressMonitor() { private int max = 1 private int points = 0 private int current = 0 void init(int op, String src, String dest, long max) { this.max = max this.current = 0 } boolean count(long count) { current += count int newPoints = (current * 20 / max) as int if (newPoints > points) { print '.' } points = newPoints true } void end() { println '' } }) } finally { channel.disconnect() } }
Actually, all the main stuffing of this method is an indicator of progress.
Finally, to make a full-fledged DSL, we need a closure to which we attach our structures. This is done, for example, as follows (we attach to the
doRemote method):
Session.metaClass.doRemote = { Closure closure -> connect() try { closure.delegate = delegate closure.call() } finally { disconnect() } }
The
doRemote method forms “brackets” within which we can use the
exec and
scp methods.
Finally, we write the procedure itself deploy
Actually, the body of our script will look something like this:
// . grails help. target(main: " WAR- .") { .. JSch JSch jsch = new JSch() Properties config = new Properties() config.put("StrictHostKeyChecking", "no") config.put("HashKnownHosts", "yes") jsch.config = config // , host username . ... String password = new String(System.console() .readPassword("Enter password for ${username}@${host}: ")) Session session = jsch.getSession(username, host, 22) session.setPassword(password) session.doRemote { exec "- " ... scp warFile, '/opt/tomcat/latest/webapps/ROOT.war' ... } }
Now, in fact, you need to understand where the WAR file is and how to tell the system that it would be good to assemble it before the deployment procedure.
This is done by the already known command Gant called
depends :
depends(clean) depends(war)
First we clean the project, then we assemble the WAR. Regarding access to a WAR file, nothing is impossible. All scripts have access to the
grailsSettings variable, from which, among other things, you can find out where it lies:
File warFile = grailsSettings.projectWarFile
Read more about
grailsSettings in the Grails documentation.
Actually, everything is ready, there was a final touch. We have only one task declared in the script (main), let's assign it to run by default:
setDefaultTarget(main)
In addition, we use the built-in scripts of Grails, such as:
compile ,
war , etc. To import them into our script (so that they can be referenced by the
depends command), add the following to the top of the script:
includeTargets << grailsScript("Clean") includeTargets << grailsScript("Init") includeTargets << grailsScript("War")
To save space, I do not publish the final script in its entirety. View the finished script for Tomcat
here .
Conclusion
We have compiled a small mini-framework for writing deploy-scripts with the ability to access servers over SSH. We can start it like this:
grails deploy
Running the same
grails help deploy
we can even get instructions on how to use the script :)
Compared to shell scripts, this gives us the following advantages:
- Significantly more powerful language for writing a script.
- The script is integrated into the Grails-project and has access to the configuration. This allows you to do the deployment differently depending on the current Grails environment, for example:
grails prod deploy
. - We get access to any means of Gant (and, accordingly, Ant).
- You can get access to the project code and even the GORM model from the script (this is called bootstrap and is not covered in this text).