#背景
現在携わっているシステムはWEB画面系は Pull Request をJenkinsでビルドして開発機のtomcatにデプロイ→SlackでURL通知→即レビュー、という流れができており、重宝しています。
ところがバッチ系(SpringBatch)はさすがにそれは難しい。 java ...
とかコマンド打つの面倒くさいしそもそもCUI怪しい人もいるので。
でもビルド結果をコンソールではなくRundeckから実行できればコマンド打てない人でもレビューできるなと思いやってみました。
完全なハンズオンにはできないですが、迷っているorハマっている方の一助になれば幸いです。
前提
Maven/Jenkins/Rundeckをまたいだ作業になるので、下記の前提をクリアしている方がいいです。
・Maven がそこそこ使える
・GitHub Hook で Jenkins PR-HEAD ビルド走らせる環境がある.
http://qiita.com/quattro_4/items/6b1962909ce868f12e5a
・Jenkinsfile が怖くない. Groovy の Sandbox も対処できる。
http://qiita.com/miyasakura_/items/9d9a8873c333cb9e9f43
http://qiita.com/yyYank/items/e591750c67f3a0bef221
・Rundeck で Job 動かせる。Rundeckと実行環境が別の場合はSSH経由で実行できる環境がある。
http://qiita.com/komeda-shinji/items/76c1c30f37ab5f81c441
実行時のパーミッションなど考慮すると jenkins ユーザがよいですよ。
http://qiita.com/toyottoyo/items/0ceb96c1454bc16d20ed
環境
Rundeck 2.9.2-1 (Windows Server) on Azure
Jenkins ver. 2.52 (Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-75-generic x86_64) on Azure
GitHub
spring-batch 3.0.7.RELEASE
作業項目
Rundeckのインストールとプロジェクト作成
上記リンク参照
JenkinsとGitHubの設定
・PRビルドできるように設定をする。
上記リンク参照
SpringBatchのexecutable-jar化
この記事のメインその1。
現在の環境がローカルWindows 検証機ubuntu、ステージング・本番Windowsといういびつな構成のため、shやbatでラップしようとすると単純に2倍の手間が掛かりそうなので、実行可能なjarを作り
Jar内の任意のjobを
> java -jar path/to/jar/somebuild.jar some-job-context.xml some-job param1=value1 ...
というように実行できるようにしました。
以降、サンプル用の諸元もろもろ
・groupId:jp.co.icsoft
・artifactId:ics-bat
・version:0.1-SNAPSHOT
・spring-batch job-context: src/main/resources/META-INF/job/some-job-context.xml
・spring-batch job: somejob
・Rundesc URL: http://yourrundeck:4440/
・Rundeck ジョブ名: ics-bat
・Rundeck ジョブ実行ノード: ubuntu-node
pom.xmlに shade plugin と resource filtering 適用
<?xml version="1.0" encoding="UTF-8"?>
<project ...>
<groupId>jp.co.icsoft</groupId>
<artifactId>ics-bat</artifactId>
<version>0.1-SNAPSHOT</version>
...
<build>
<resources>
<!-- for rundeck-job.xml -->
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
...
<plugins>
...
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>${project.artifactId}-${project.version}</finalName>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.springframework.batch.core.launch.support.CommandLineJobRunner
</mainClass>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>
</transformers>
<shadedArtifactAttached>true</shadedArtifactAttached>
<!-- filename will be '<project.artifact>-<project.version>-executable.jar'-->
<shadedClassifierName>executable</shadedClassifierName>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
<!-- filtering only specific (illegal!) jar if you need -->
<filter>
<artifact>org.bouncycastle:*</artifact>
<excludes>
<exclude>org/bouncycastle/jce/**/*</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
...
</build>
...
</project>
Spring の Namespace Handler を置く
spring.schemas
spring.handlers
をsrc/resources/META-INF
に置きましょう。
自分は本家から拾ってきましたよ。
https://github.com/spring-projects/spring-framework/tree/master/spring-beans/src/main/resources/META-INF
チェック
ここでmvn package
してみましょう。
ics-bat-0.1-SNAPSHOT-executable.jar
ができており、同じディレクトリにいる
ics-bat-0.1-SNAPSHOT.jar
より、dependency など取り込んだ分容量が大きくなっていればうまくいってます。
それができたら
> java -jar /home/ore/ics-bat/target/ics-bat-0.1-SNAPSHOT-executable.jar classpath:META-INF/job/some-job-context.jar somejob -next
がローカルで動く(少なくともspring-batchが起動する)ようになってるとよいです。
Rundeck用のjobテンプレートを置く
この記事のメインその2、自分が調べた範囲ですが、あまりやってる例を見ない。
のちの Jenkinfile でゴリゴリしてもいいのかもしれませんが、開発者に step 追記してもらいたかったりもするので、import させる xml のテンプレをあらかじめ配置しておきます。
<joblist>
<job>
<description>for ${env.BRANCH_NAME}</description>
<dispatch>
<excludePrecedence>true</excludePrecedence>
<keepgoing>false</keepgoing>
<rankOrder>ascending</rankOrder>
<successOnEmptyNodeFilter>false</successOnEmptyNodeFilter>
<threadcount>1</threadcount>
</dispatch>
<executionEnabled>true</executionEnabled>
<loglevel>DEBUG</loglevel>
<name>ics-bat-${env.BRANCH_NAME}"</name>
<nodeFilterEditable>false</nodeFilterEditable>
<nodefilters>
<filter>hostname: ubuntu-node</filter>
</nodefilters>
<nodesSelectedByDefault>true</nodesSelectedByDefault>
<scheduleEnabled>true</scheduleEnabled>
<sequence keepgoing='false' strategy='node-first'>
<command>
<!-- ここに開発担当者が別のcommandを追加可能 -->
<description>step1</description>
<exec>java -jar ${env.WORKSPACE}/target/${pom.artifactId}-${pom.version}-executable.jar classpath:META-INF/job/some-job-context.xml somejob -next
</exec>
</command>
</sequence>
</job>
</joblist>
Jenkinsfile置く
この記事のメインその3、JenkinsfileでRundeckのapi叩くのあまり見ないです。
ちょっと恥ずかしいですが、こんな感じです。
#!groovy
RUNDECK_URL = "http://yourrundeck:4440"
RUNDECK_IMPORT_URL = "api/20/jobs/import"
RUNDECK_TOKEN = "XXXXX"
def rundeckPermaLink = ""
node {
step([$class: 'GitHubSetCommitStatusBuilder'])
echo "Running ${env.BUILD_ID} on ${env.JENKINS_URL}"
def pom
stage('Checkout') {
checkout scm
}
stage('Build and test') {
env.PATH = "${tool 'maven-3.3.9'}/bin;${env.PATH}"
pom = readMavenPom file: 'pom.xml'
def version = pom.version.replace("-SNAPSHOT", ".${currentBuild.number}")
//deciding develop OR pull request
def mvnGoals = " clean"
mvnGoals += (env.BRANCH_NAME.equals("develop") ? " deploy" : " package -Dpullrequest=${env.BRANCH_NAME}")
mvnGoals += " -P develop"
// directory to deploy.
mvnGoals += " -DfileRootDir=${env.WORKSPACE}/target/temp" //この辺はお好みでどうぞ
mvnGoals += " -DbranchName=${env.BRANCH_NAME}"
try {
echo "Build and Test ${env.BRANCH_NAME}"
mvn "${mvnGoals}"
currentBuild.result = 'SUCCESS'
} catch (err) {
echo err.message
currentBuild.result = 'FAILURE'
}
echo "RESULT = ${currentBuild.result}"
}
stage('Rundeck') {
echo "WORKSPACE:${env.WORKSPACE}"
try {
rundeckPermaLink = postJobToRundeck()
} catch (err) {
echo err.message
currentBuild.result = 'FAILURE'
}
}
stage('Notify') {
notifySlack("${pom.artifactId}-${pom.version} on ${env.BRANCH_NAME} : ${currentBuild.result} : ${rundeckPermaLink}",
"esys-bat-sts")
notifyO365Teams("${pom.artifactId}-${pom.version} on ${env.BRANCH_NAME} : ${currentBuild.result}",
"esys-bat-sts")
step([$class: 'GitHubCommitNotifier', resultOnFailure: 'FAILURE'])
def color = isOK()? 'GREEN' : 'RED'
}
}
def mvn(String goals) {
def mvnHome = tool "maven-3.3.9"
def javaHome = tool "java-1.8.0-openjdk-amd64"
withEnv(["PATH+MAVEN=${mvnHome}/bin"]) {
wrap([$class: 'ConfigFileBuildWrapper', managedFiles: [
[fileId: 'XXXXX', targetLocation: "settings.xml", variable: '']
]]) {
sh "mvn -s settings.xml -B ${goals}"
}
}
}
def isOK() {
return "SUCCESS".equals(currentBuild.result)
}
@NonCPS
def echoVariables(val) {
val.properties.each { it ->
echo "$it.key -> $it.value"
}
}
def notifyO365Teams(text, deployedUrl) {
office365ConnectorSend message: "${text} (${env.BUILD_URL})",
status: currentBuild.result,
webhookUrl: 'https://outlook.office.com/webhook/XXXXXXX'
}
def notifySlack(text, deployedUrl) {
slackSend color: isOK() ? '#0000FF' : '#FF0000',
failOnError: true,
message: "${text} (${env.BUILD_URL})",
teamDomain: 'yourteam',
tokenCredentialId: 'Slack'
}
def String postJobToRundeck() {
//URLBuilderなどで爽やかに書きたいところ
String params = "dupeOption=update&project=ics-bat&format=xml&xmlBatch="
String jobXml = readFile("${env.WORKSPACE}/target/classes/rundeck-job.xml")
byte[] postData = (params + jobXml).getBytes("UTF-8")
echo(jobXml)
def req = new URL(RUNDECK_URL + "/" + RUNDECK_IMPORT_URL).openConnection();
req.setRequestMethod("POST")
req.setDoOutput(true)
req.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
req.setRequestProperty("Content-Length", String.valueOf(postData.length))
req.setRequestProperty("X-Rundeck-Auth-Token", RUNDECK_TOKEN)
req.getOutputStream().write(postData)
def resCd = req.getResponseCode()
if (resCd.equals(200)) {
String resXml = req.getInputStream().getText()
echo(resXml)
//Sandbox の制限で Closure 使えないので荒々しい書きぶり
return resXml.find(/<permalink>(.*)<\/permalink>/).replaceAll('<permalink>|</permalink>', '');
}
}
いやーん恥ずかしい。
適宜、in process script approvalなどで、sandboxが抑止してるメソッド解放してあげてください。
Sandobox自体を回避した方がいいくらい怒られます。
いきなり結論
これでビルドが完了すると、Rundeckのics-bat
プロジェクトにics-bat-PR-1
というジョブができて実行できるようになっているはずです。
リアクションがあれば、時間を見て追記していく心づもりではいますのでよろしうお願いいたします。