Зачем нужен deploy-скрипт

Grails-приложения очень легко собираются в WAR. Делается это так:

grails war

Помимо того, что WAR собирается, очень хочется этот WAR еще и установить на сервер. В нашем случае это Tomcat. Установка вручную требует некоторой возни:

  1. Остановить сервер. Убить процесс, если он не остановился сам.
  2. Удалить старые файлы приложения (на всякий случай)
  3. Скопировать новый WAR на сервер. Иногда его нужно переименовывать (скажем, в ROOT.war)

В Maven эту работу может проделать, например, cargo plugin. Но с ним много приключений и настройки, причем он не особо учитывает особенности сервере.

Мы также можем использовать shell-скрипт. Но зачем писать на неудобном языке shell, когда есть замечательный кроссплатформенный язык Groovy?

Пишем скрипт на Groovy

Grails позволяет легко создавать на Groovy command-line скрипты, которые складываются в папку scripts нашего приложения. Любой из этих скриптов затем можно запустить консольной командой grails. Мы будем писать скрипт под названием Deploy.groovy.

Приятно, что внутри скрипта можно использовать конфигурационные данные нашего Grails-приложения. Например, имя сервера и имя пользователя являются environment-specific. Для Grails 1.3.7 мы можем получить доступ к конфигурации так:

depends(compile)
depends(createConfig)          /* Магическая конструкция Gant, которая загружает Config.groovy */
def host = ConfigurationHolder.config?.deploy.host
def username = ConfigurationHolder.config?.deploy.username

Предполагается, что где внутри Config.deploy будут такие строчки:

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

Небольшая хитрость состоит в том, что для загрузки конфигурации сначала нужно скомпилировать файл Config.groovy. Для этого мы объявили depends(compile), где compile — уже известная Gant команда (задача) компиляции проекта.

Используем SSH

Нам нужно проделывать много всяких операций на сервере с SSH-доступом. Для простоты я взял бесплатную библиотеку JSch и ограничился вариантом доступа с паролем. Поэтому наш скрипт начинается так:

@GrabResolver(name='jcraft', root='http://jsch.sourceforge.net/maven2')
@Grab(group='com.jcraft', module='jsch', version='0.1.44')
import com.jcraft.jsch.*

Далее проделаем некие магические манипуляции с JSch. Нам нужно две вещи:

  • Запускать Unix-команды на сервере
  • Копировать файлы на сервер

У нас есть в распоряжении язык Groovy, поэтому попытаемся сделать мини-DSL с нужными нам функциями. Добавим пару новых методов в объект Session (из JSch), который представляет собой SSH-сессию. Для начала метод exec для выполнения команды на сервере:

Session.metaClass.exec = { String cmd ->
    Channel channel = 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()
}

Из соображений краткости я сделал так, что при успешном выполнении метод не выводит ничего, а при возникновении ошибок печатает весь выходной поток выполненной команды.

Теперь бы еще записать файл на сервер:

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()
    }
}

Собственно, вся основная начинка этого метода — индикатор прогресса.

Ну и наконец, для изготовления полноценного DSL нам нужен closure, к которому мы прицепим наши конструкции. Это делается, например, так (прицепляем к методу doRemote):

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

Метод doRemote образует «скобки», внутри которых мы сможем употреблять методы exec и scp.

Наконец, записываем саму процедуру deploy

Собственно, тело нашего скрипта будет выглядеть примерно так:

// Описание нашего скрипта. Оно появится в 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'
        ...
    }
}

Теперь, собственно, надо понять, где лежит WAR-файл и как сообщить системе, что перед процедурой развертывания хорошо бы его собрать.

Это делается уже известной нам командой Gant под названием depends:

depends(clean)
depends(war)

Сначала чистим проект, потом собираем WAR. Что касается доступа к WAR-файлу, нет ничего невозможного. Всем скриптам доступна переменная grailsSettings, у которой в том числе можно узнать и где он лежит:

File warFile = grailsSettings.projectWarFile

Подробнее про grailsSettings написано в документации по Grails.

Собственно, все готово, остался последний штрих. У нас в скрипте заявлена только одна задача (main), назначим ее для запуска по умолчанию:

setDefaultTarget(main)

Помимо этого, мы используем встроенные скрипты Grails, такие как: compile, war и т.п. Для их импорта внутрь нашего скрипта (чтобы на них можно было ссылаться командой depends) добавим в начало скрипта следующее:

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

Для экономии места я не публикую итоговый скрипт целиком. Посмотреть готовый скрипт для Tomcat можно здесь.

Заключение

Мы собрали небольшой мини-фреймворк для написания deploy-скриптов с возможность доступа к серверам по SSH. Запустить его мы можем так:

grails deploy

Запустив

grails help deploy

мы даже сможем получить инструкции по использованию скрипта 🙂

По сравнению с shell-скриптами это дает нам следующие преимущества:

  • Существенно более мощный язык для написания скрипта.
  • Скрипт интегрирован в Grails-проект и имеет доступ к конфигурации. Это позволяет делать развертывание по-разному в зависимости от текущего Grails environment, например: grails prod deploy.
  • Получаем доступ к любым средствам Gant (и, соответственно, Ant).
  • Sharing

    Facebooktwittergoogle_plusredditpinterestlinkedinmailby feather