執筆者:柴原
家庭用ゲーム開発会社を経てモノリスソフトへ入社。 以来、開発環境エンジニアとして自動化、パイプラインの業務を担当。 好きな食べ物はラーメン。
TECH BLOG
こんにちは。モノリスソフト 開発環境エンジニアの柴原です。
今回は私がJenkinsでよく使っている実装について紹介をしたいと思います。この記事のエッセンスは以下の通りです。
ゲーム開発の現場では、チームメンバーが日々サーバーに製作したデータをアップロードしてくれるわけですが、新しいデータをゲーム上で確認するには、データのコンバートやゲーム実行ファイルの更新が必要です。
このデータのコンバートや実行ファイルの更新を、決められた順序で実行し、完成した最新バージョンのゲームを配布用サーバーに配置するまでを定期的に自動で行う仕組みをCI/CD(継続的インテグレーション / 継続的デリバリー※1)と呼び、Jenkinsはこうした仕組みを構築できるアプリケーションのひとつです。
※1 継続的デプロイとする説明もある
Jenkinsには、「データをコンバートする処理」や「ゲーム実行ファイルを配布する処理」といった処理のまとまりを「ジョブ」という単位で登録でき、定期的にジョブを実行させる機能があります。
さて、さっそくジョブ作成のために用意するスクリプトのサンプルを見てみましょう。
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つあることがお分かりいただけるでしょうか。
併記の日本語の通り、このジョブは大きく5つの処理の集まりになっています。
さらに、この5つの中でポイントになるのが、2つ目の ツールの呼び出し部分です。
ジョブのスクリプトとは別で用意した、convert_from_jenkins.bat というバッチファイルがあり、その呼び出し、および、実行後の終了コードの受け取りを25行目でおこなっています。
データのコンバートに関する処理は、バッチファイルの中で完結しており、ジョブのスクリプトにはコンバートの処理は何も書いてないということです。
こうすることの一番の理由は、ツールの管理を別の人と分業することにあります。
Jenkinsの管理者とJenkinsで動かしたいツールを準備する人は別にできた方がよいです。
開発現場には自動化可能なものがたくさんあり、それらすべてをJenkins管理者がケアするのは開発が大きくなるほど非効率になるでしょうから、Jenkins管理者しかジョブに関われないのはチームの弱点になる可能性があります。
こちらの図で言えば、赤色で示したツールを作成した人は、Jenkins管理者とは別の人です。
先のサンプルのように、ジョブからツールをシンプルに呼び出す構成になっていれば、ツール担当者はPipelineスクリプトを習得する必要はありません。
また、ジョブが失敗し、問題発生個所がツールの中だった場合でも、その問題がJenkinsの環境だけで起こる可能性は低く、ツール担当者の調査は手元で進められます。
ジョブを作る前の負担、作った後の負担、どちらも軽くしたいというのが、先のサンプルの意図だったわけです。
今回のジョブの運用イメージは以下のようになります。
分担がはっきりしていて分かりやすいですね。
次のサンプルを見てみましょう。
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 で始まるコードブロックを見ていきましょう。
ブロック数が6に増えていますが、1~5のブロックは先のサンプルとほぼ変わっていないことがお分かりいただけるでしょうか。
さらに、以下のブロックを見てみると、どちらのサンプルでも同じような関数を呼び出していることがわかります。
関数で呼び出しているのは、バージョン管理システムとのデータのやり取りや、チャットツールへの結果通知などですが、これらのサンプルに限らず、Jenkinsのジョブの事前準備、事後処理は、似たようなことをやりたいケースが多いため、共通の処理はユーティリティスクリプトに移します。
処理の共通化は基本的なことですが、Jenkinsにおいては、
ということにつながり、自動化を推し進めるうえで欠かせません。
その他、build で始まる行がいくつかありますが、これは別のジョブを呼び出すコマンドになります。
私の管理しているジョブは呼び出した先のジョブも含めて、同じようなブロック構成になっているものが多いです。
作業の分担、保守への取り組み方をどのジョブでも同じように行えることで、さらなる保守の効率化を目指しているというわけですね。
以上、2点のサンプルを通して、ジョブの運用を効率的に行うために、スクリプトにちりばめた工夫についてご紹介しました。
ツールの呼び出し側の話をしてきたので、ツール担当者向けに、呼び出される側のツールとしてスクリプトを使いたいときのサンプルをいくつかご紹介します。
vbs や powershell は Jenkinsのジョブから直接呼び出すことができます。
bat 'sample.vbs' powershell '."./sample.ps1"'
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内で何をかけばよいかの例を記載しています。
set PYTHON_EXE=python_install_path\python.exe "%PYTHON_EXE%" script_path\sample.py
※呼び出したい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/
執筆者:柴原
家庭用ゲーム開発会社を経てモノリスソフトへ入社。 以来、開発環境エンジニアとして自動化、パイプラインの業務を担当。 好きな食べ物はラーメン。