3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PullRequestでRundeckジョブを作成しGUIからSpringBatchを実行可能にする。

Last updated at Posted at 2017-08-23

#背景
現在携わっているシステムは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 適用

pom.xml抜粋
<?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 のテンプレをあらかじめ配置しておきます。

src/main/resources/rundeck-job.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叩くのあまり見ないです。
ちょっと恥ずかしいですが、こんな感じです。

Jenkinsfile
#!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というジョブができて実行できるようになっているはずです。

リアクションがあれば、時間を見て追記していく心づもりではいますのでよろしうお願いいたします。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?