Writing a deploy script for Grails

    Why do I need a deploy script


    Grails applications are very easy to build in WAR. It is done like this:

    grails war
    

    Besides the fact that the WAR is going, I really want to install this WAR on the server. In our case, this is Tomcat. Manual installation requires some fuss:
    1. Stop the server. Kill the process if it does not stop itself.
    2. Delete old application files (just in case)
    3. Copy the new WAR to the server. Sometimes it needs to be renamed (say, in ROOT.war)
    In Maven, this can be done, for example, by a cargo plugin. But with him a lot of adventure and customization, and he does not particularly take into account the features of the server.

    We can also use a shell script. But why write in an uncomfortable shell language when there is a wonderful cross-platform Groovy language?


    Writing a script in Groovy


    Grails makes it easy to create command-line scripts on Groovy that are added to our application's scripts folder . Any of these scripts can then be run with 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 username are environment-specific. For Grails 1.3.7, we can access the configuration like this:

    depends(compile)
    depends(createConfig)          /* Магическая конструкция Gant, которая загружает Config.groovy */
    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 something like this:

    ...
    environments {
        production {
            ...
            deploy {
                host = 'www1.shards.intra'
                username = 'deployer'
            }
        }
    }
    

    A little trick is that to download 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.

    We use SSH


    We need to do all sorts of operations on a 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.* 
    

    Next, 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 let's 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 upon successful execution, the method does not output anything, and when errors occur, it 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 filling of this method is an indicator of progress.

    And finally, to make a full-fledged DSL, we need a closure to which we attach our structures. This is done, for example, like this (hooked to the doRemote method ):

    Session.metaClass.doRemote = { Closure closure ->
        connect()
        try {
            closure.delegate = delegate
            closure.call()
        } finally {
            disconnect()
        }
    }
    

    The doRemote method forms “brackets” inside which we can use the exec and scp methods .

    Finally, write the deploy procedure itself


    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, we need to understand where the WAR-file lies and how to tell the system that it would be nice to collect it before the deployment procedure.

    This is done by the already known Gant command called depends :

    depends(clean)
    depends(war) 
    

    First we clean the project, then we collect WAR. Regarding access to the WAR file, nothing is impossible. All scripts have access to the grailsSettings variable , from which you can also find out where it lies:

    File warFile = grailsSettings.projectWarFile
    

    Read more about grailsSettings in the Grails documentation.

    Actually, everything is ready, the last touch remains. We have only one task declared in the script (main), assign it to run by default:

    setDefaultTarget(main)
    

    In addition, we use Grails built-in scripts, 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 beginning of the script:

    includeTargets << grailsScript("Clean")
    includeTargets << grailsScript("Init")
    includeTargets << grailsScript("War")
    

    To save space, I do not publish the final script as a whole. View the finished script for Tomcat here .

    Conclusion


    We have put together a small mini-framework for writing deploy scripts with the ability to access servers via SSH. We can run it like this:

    grails deploy
    
    Running
    grails help deploy
    we can even get instructions for using the script :)

    Compared to shell scripts, this gives us the following advantages:
    • A significantly more powerful scripting language.
    • 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 a script (this is called bootstrap and is not covered in this text).

    Also popular now: