Jenkinsでツールを定期実行しよう

INDEX

はじめに

こんにちは。モノリスソフト 開発環境エンジニアの柴原です。

今回は私がJenkinsでよく使っている実装について紹介をしたいと思います。この記事のエッセンスは以下の通りです。

  • Pipelineスタイルのジョブ作成の例に触れる
  • ジョブの運用をツール担当者と分担するためのスクリプトの書き方を知る
  • ツールの実行前後の処理はジョブ間でよく似ているものが多いので、ユーティリティを集めたライブラリを作って活用する

Jenkinsってなに?

ゲーム開発の現場では、チームメンバーが日々サーバーに製作したデータをアップロードしてくれるわけですが、新しいデータをゲーム上で確認するには、データのコンバートやゲーム実行ファイルの更新が必要です。

このデータのコンバートや実行ファイルの更新を、決められた順序で実行し、完成した最新バージョンのゲームを配布用サーバーに配置するまでを定期的に自動で行う仕組みをCI/CD(継続的インテグレーション / 継続的デリバリー※1)と呼び、Jenkinsはこうした仕組みを構築できるアプリケーションのひとつです。

※1 継続的デプロイとする説明もある

tech_10_02.jpg

Jenkinsには、「データをコンバートする処理」や「ゲーム実行ファイルを配布する処理」といった処理のまとまりを「ジョブ」という単位で登録でき、定期的にジョブを実行させる機能があります。

ジョブのスクリプトを見てみよう:ツール呼び出し編

さて、さっそくジョブ作成のために用意するスクリプトのサンプルを見てみましょう。

サンプル1:データコンバート

node('data_convert_agent')
{
    jobName = 'DataConvert'
 
    srcPath     = ''
    targetPath  = ''
    stage('前準備')
    {
        // ユーティリティ関数の読み込み
        fileUtil    = load 'D:/Jenkins/util/file.groovy'
        notifyUtil  = load 'D:/Jenkins/util/notification.groovy'
 
        // 関連パスの代入
        srcPath     = fileUtil.makePath('src_data/param')
        targetPath  = fileUtil.makePath('game_data/param')
 
        fileUtil.updateVcs(srcPath)
    }
 
    toolResult = 0
    stage('ツールの呼び出し')
    {
        // コンバートツールを呼び出し
        toolPath    = fileUtil.makeToolPath('converter/convert_from_jenkins.bat')
        toolResult  = bat returnStatus: true, script: toolPath
    }
    stage('エラー時処理')
    {
        if(toolResult != 0)
        {
            notifyUtil.notifyError(jobName, "エラーコード: $toolResult", $env.BUILD_URL)
            fileUtil.UndoChangesVcs(targetPath)
            error 'failed to convert'
        }
    }
    stage('成果物のアップロード')
    {
        fileUtil.uploadVcs(targetPath, 'Jenkinsによるデータコンバート', srcPath)
    }
    stage('結果の通知')
    {
        notifyUtil.notifyResult(jobName, $env.BUILD_URL)
    }
}

こちらは、Scripted Pipelineと呼ばれるスクリプトで、言語的にはGroovyで書かれているものです。

初めての方向けに詳細の説明は省略して、ざっくりと要点をお伝えしていきます。

まず、stage で始まるコードブロック(中カッコ {} でくくられた範囲)が5つあることがお分かりいただけるでしょうか。

  1. 前準備( 7行目~)
  2. ツールの呼び出し( 21行目~)
  3. エラー時処理(27行目~)
  4. 成果物のアップロード(36行目~)
  5. 結果の通知(40行目~)

併記の日本語の通り、このジョブは大きく5つの処理の集まりになっています。

さらに、この5つの中でポイントになるのが、2つ目の ツールの呼び出し部分です。

ジョブのスクリプトとは別で用意した、convert_from_jenkins.bat というバッチファイルがあり、その呼び出し、および、実行後の終了コードの受け取りを25行目でおこなっています。

データのコンバートに関する処理は、バッチファイルの中で完結しており、ジョブのスクリプトにはコンバートの処理は何も書いてないということです。

こうすることの一番の理由は、ツールの管理を別の人と分業することにあります。

Jenkinsの管理者とJenkinsで動かしたいツールを準備する人は別にできた方がよいです。

開発現場には自動化可能なものがたくさんあり、それらすべてをJenkins管理者がケアするのは開発が大きくなるほど非効率になるでしょうから、Jenkins管理者しかジョブに関われないのはチームの弱点になる可能性があります。

tech_10_03.jpg

こちらの図で言えば、赤色で示したツールを作成した人は、Jenkins管理者とは別の人です。

先のサンプルのように、ジョブからツールをシンプルに呼び出す構成になっていれば、ツール担当者はPipelineスクリプトを習得する必要はありません。

また、ジョブが失敗し、問題発生個所がツールの中だった場合でも、その問題がJenkinsの環境だけで起こる可能性は低く、ツール担当者の調査は手元で進められます。

ジョブを作る前の負担、作った後の負担、どちらも軽くしたいというのが、先のサンプルの意図だったわけです。

今回のジョブの運用イメージは以下のようになります。

  • 赤色のツール部分で問題が起きたら、ツール担当者に確認をしてもらいます。
  • 黄色い領域はツールの入出力物です。ここに原因がある場合は、以下の選択肢からよいものを検討します。
    • ツール管理者が問題を吸収できるようにツールを改造する
    • 素材データの作成者がデータを直す
    • ジョブの記述をJenkins管理者が修正する
  • それら以外の部分の問題はJenkins管理者が復旧を主導します。

分担がはっきりしていて分かりやすいですね。

ジョブのスクリプトを見てみよう:ユーティリティ活用編

次のサンプルを見てみましょう。

サンプル2:ゲームのビルド

node('build_agent')
{
    jobName = 'BuildAndDeploy'
 
    stage('前準備')
    {
        // ユーティリティ関数の読み込み
        fileUtil    = load 'D:/Jenkins/util/file.groovy'
        notifyUtil  = load 'D:/Jenkins/util/notification.groovy'
 
        // 関連パスの代入
        srcPath     = fileUtil.makePath('program')
        targetPath  = fileUtil.makeToolPath('build/metafiles')
 
        fileUtil.updateVcs(srcPath)
    }
 
    buildResult = 0
    stage('ビルドの呼び出し')
    {
        buildUtil   = load 'D:/Jenkins/util/build.groovy'
        slnPath     = srcPath + '/game.sln'
 
        buildResult = buildUtil.msbuild(slnPath, params.configuration, params.platform, params.rebuild)
    }
 
    stage('エラー時処理')
    {
        if (buildResult != 0 )
        {
            build job: 'ErrorReport', parameters: [
                text(name: 'buildUrl', value: env.BUILD_URL),
                text(name: 'configuration', value: params.configuration),
                text(name: 'platform', value: params.platform)
                ]
 
            error 'fail to build.'
        }
    }
     
    deployPath = ''
    stage('成果物のアップロード')
    {
        fileUtil.uploadVcs(targetPath, 'Jenkinsによるビルド記録', srcPath)
 
        deployPath = fileUtil.makeDeployPath('game_exec')
        build job: 'Deploy', parameters: [
                text(name: 'srcPath', value: srcPath)
                text(name: 'deployPath', value: deployPath)
            ]
    }
    stage('結果の通知')
    {
        notifyUtil.notifyResult(jobName, $env.BUILD_URL)
    }
    stage('ビルドしたゲームの自動テスト')
    {
        build job: 'Autoplay/SmokeTest', propagate: false, wait: false,
            parameters: [
                text(name: 'testDir', value: deployPath)
            ]
    }
}

こちらも、最初はstage で始まるコードブロックを見ていきましょう。

  1. 前準備( 5行目~)
  2. ビルドの呼び出し(19行目~)
  3. エラー時処理(27行目~)
  4. 成果物のアップロード(42行目~)
  5. 結果の通知(52行目~)
  6. ビルドしたゲームの自動テスト(56行目~)

ブロック数が6に増えていますが、1~5のブロックは先のサンプルとほぼ変わっていないことがお分かりいただけるでしょうか。

さらに、以下のブロックを見てみると、どちらのサンプルでも同じような関数を呼び出していることがわかります。

  • 1.前準備
    • fileUtil.updateVcs
  • 4.成果物のアップロード
    • fileUtil.uploadVcs
  • 5.結果の通知
    • notifyUtil.notifyResult

関数で呼び出しているのは、バージョン管理システムとのデータのやり取りや、チャットツールへの結果通知などですが、これらのサンプルに限らず、Jenkinsのジョブの事前準備、事後処理は、似たようなことをやりたいケースが多いため、共通の処理はユーティリティスクリプトに移します。

処理の共通化は基本的なことですが、Jenkinsにおいては、

  • 関数内の挙動を調整すれば多くのジョブの挙動を一括で調整できる
  • 新しいジョブを作る時の作成時間を短縮できる

ということにつながり、自動化を推し進めるうえで欠かせません。

その他、build で始まる行がいくつかありますが、これは別のジョブを呼び出すコマンドになります。

私の管理しているジョブは呼び出した先のジョブも含めて、同じようなブロック構成になっているものが多いです。

作業の分担、保守への取り組み方をどのジョブでも同じように行えることで、さらなる保守の効率化を目指しているというわけですね。

以上、2点のサンプルを通して、ジョブの運用を効率的に行うために、スクリプトにちりばめた工夫についてご紹介しました。

おまけ

ツールの呼び出し側の話をしてきたので、ツール担当者向けに、呼び出される側のツールとしてスクリプトを使いたいときのサンプルをいくつかご紹介します。

vbs, powershell は直接呼び出せる

vbs や powershell は Jenkinsのジョブから直接呼び出すことができます。

bat 'sample.vbs'
 
powershell '."./sample.ps1"'

エクセル内のvbaを呼び出すvbsをつくる

vba はcmdから直接呼び出せないので、vbs経由で呼び出します。以下は、vbsファイルの記述例です。

※sample.xlsm 内の VbaSampleFunc 関数を呼び出す例です。

Dim obj
Set obj=WScript.CreateObject("Excel.Application")
obj.Workbooks.Open "excel_path\sample.xlsm"
obj.Application.Run "VbaSampleFunc"
obj.Quit

以降のサンプルは、ジョブからcmdのバッチファイル(bat)を呼び出す想定で、そのbat内で何をかけばよいかの例を記載しています。

pythonスクリプトを実行する

set PYTHON_EXE=python_install_path\python.exe
"%PYTHON_EXE%" script_path\sample.py

mayaをバッチモードで起動しpythonスクリプトを呼び出す

※呼び出したいpythonモジュールは maya上からimportできるよう配置してある前提で。呼び出したい関数名は run とします。

set MAYA_BATCH_EXE=maya_install_path\bin\mayabatch.exe
"%MAYA_BATCH_EXE%" -command "python(\"import python_module_name; python_module_name.run()\")"

分業の上では、用意したツールはどのパス(フォルダ)から呼び出す想定なのかを伝えておいたり、エラー時の判定をエラーコードでやるのか、ログファイルの解析が必要なのか、などを決めておくと、さらに話がスムーズに進むと思います。

まとめ

今回は、私の管理しているJenkinsジョブの中身がどうなってるかをお伝えしました。

Jenkins側での操作の説明は省いているため、実際にジョブを作成するにはほかの資料もあたっていただく必要がありますが、この記事の内容を把握していると、Jenkins担当者にジョブを用意してもらう時のやりとりが円滑に進められるのではないかと思います。

参考・画像引用

Jenkins公式サイト
https://www.jenkins.io/

執筆者:柴原

家庭用ゲーム開発会社を経てモノリスソフトへ入社。 以来、開発環境エンジニアとして自動化、パイプラインの業務を担当。 好きな食べ物はラーメン。

ABOUT

モノリスソフト開発スタッフが日々取り組んでいる技術研究やノウハウをご紹介

RECRUIT採用情報