Зачем нужен deploy-скрипт
Grails-приложения очень легко собираются в WAR. Делается это так:
grails war |
Помимо того, что WAR собирается, очень хочется этот WAR еще и установить на сервер. В нашем случае это Tomcat. Установка вручную требует некоторой возни:
- Остановить сервер. Убить процесс, если он не остановился сам.
- Удалить старые файлы приложения (на всякий случай)
- Скопировать новый 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).