こんにちは。テクマトリックスの橘です。
最近、お客様環境にJenkinsおよびパイプラインを構築するという案件があり、その中で、ユーザー入力を待つような対話型のバッチをJenkinsから実行したいとのご要望をいただきました。
開始から終了までが自動化されているバッチでないとJenkinsで実行するのは難しい、と私は認識していたのですが、実際にはそういった対話型のバッチであってもJenkinsから実行する方法はあります。
ただし、その方法は長期運用のCI/CD環境では事故要因になることもあるようなので、この記事で皆さんにも共有できればと思います。
目次
はじめに:対話型バッチの例
それでは、まず対話型のバッチについて例を見ていきましょう。
ビルドバッチ例
@echo off
echo App build
set /p "CFG=ビルド設定の入力 (Debug/Release): "
echo Building... CFG=%CFG%
call build_command_here --config "%CFG%"
exit /b 0デプロイバッチ例
@echo off
echo App deploy
set /p "ENV=デプロイ先の入力 (DEV/STG/PRD): "
set /p "VER=バージョンの入力 (e.g. 1.2.3): "
echo Deploying %VER% to %ENV%...
call deploy_command_here --env "%ENV%" --version "%VER%"
exit /b 0ターゲットやデプロイ先の環境、バージョンなどによってビルドやデプロイの内容を変更したい、というようなシーンで利用されると思います。
このようなバッチは、開発プロセスのあるタイミングでユーザーが実行し、そのまま実行したユーザーが入力値をセットしていく、というような使われ方になると思います。
Jenkins と “対話型バッチ” の相性問題
では、このような対話型のバッチをJenkinsのパイプラインから実行するとどうなるでしょう?
仮に上記のサンプルをパイプラインから実行した場合、3行目の set /p の部分で、入力待ちのまま停止してタイムアウトしてしまい、結果としてパイプラインはエラーで失敗する、ということになります。また、入力がEOF扱いになり空文字のまま進んでしまい、意図しない値で処理が走るという可能性もあります。
このようなバッチをCI/CDプロセスに組み込むと、そのバッチの実行途中でパイプラインが停止してしまう、あるいは意図しない結果となってしまい、プロセス全体を自動化できない、という問題に直面することになります。これでは、CI/CDのメリットを享受するのが難しくなってしまいます。
結論:対話は消す
このような対話型のバッチをJenkinsから正しく実行するための最も安全な手段は何か、それは非対話型にする、というものです。それでは、元も子もないじゃないか、と思われるかもしれませんが、対話型バッチをJenkinsに組み込むと、パイプライン運用上の事故確率が上がってしまうため、より安全で確実にパイプラインを実行するための恒久的な対応としては、バッチから対話部分を削除し、何かしら別の方法に置き換えるということが推奨されます。
上記のようなケースではターゲットやバージョンなどのユーザーが入力する値を、バッチの引数にする、という変更が最もわかりやすい例かと思います。
ビルドバッチの実行例/変更例
C:\> build.bat Release@echo off
echo App build
if "%~1"=="" (echo Usage: %~nx0 Debug^|Release & exit /b 2)
set "CFG=%~1"
echo Building... CFG=%CFG%
call build_command_here --config "%CFG%"
exit /b %ERRORLEVEL%デプロイバッチの実行例/変更例
C:\> deploy.bat DEV 1.2.3@echo off
echo App deploy
if "%~1"=="" (echo Usage: %~nx0 ENV VERSION & exit /b 1)
if "%~2"=="" (echo Usage: %~nx0 ENV VERSION & exit /b 1)
set "ENV=%~1"
set "VER=%~2"
echo Deploying %VER% to %ENV%...
call deploy_command_here --env "%ENV%" --version "%VER%"
exit /b %ERRORLEVEL%対話型バッチの救世主? echo <引数>|build.bat
上記のようにバッチファイルを変更することが簡単にできるなら、誰でもバッチを非対話型にするという選択肢を取ると思います。しかし、実際に開発現場で利用されているバッチはより複雑で、ドキュメントが整備されておらず処理を変更しづらい、また開発業務をしながらバッチの変更をしなければいけないといった状況の場合、すぐに変更することは難しいと思います。
そんな時に、バッチの変更をせずに手っ取り早い回避策として利用できるのが、パイプを利用したechoコマンドです。以下がその例です。
echo Release|build.bat
(echo DEV & echo 1.2.3) | deploy.bat
これはパイプ(|)で 標準入力(stdin) に文字列を流し込み、set /p などの入力待ちに「入力したことにする」方法です。これを利用することで、パイプラインから実行した場合であっても、set /p の部分で停止せず、バッチの処理は継続される、ということになります。
echo <引数>|build.bat が成功する例/失敗する例
この方法を救世主のように持ち上げましたが、実際はそうではありません。むしろ、成功するケースの方が少数というのが実態です。
パイプを利用した場合の成功例、失敗例を少しだけ見ていきましょう。
成功例
- set /p の回数とechoの入力回数が合致している
失敗例
- pauseによる任意キー待ち
- バッチから呼ばれる外部exeが独自対話型
- バッチから別プロセスが呼ばれ、入力先がずれる
- Windows のコンソール API(キーイベント)で入力を待つ処理
- 独自の対話UI
このようにパイプを利用しても自動実行ができないケースは多く、「パイプ=自動化可能」ではなく、「相手がどの入力経路を使っているか」で結果が変わってしまいます。またバッチ自体の変更にも堅牢とは言えないため、CI/CDシステムのような長期運用が想定された環境では事故要因になりがちです。対話前提の作り自体が事故につながりやすいため、echo パイプは一時しのぎと割り切り、恒久的な対応について検討を進めるべきです。
解決策:バッチを非対話化する設計
それでは、どのような対策を取るべきなのか、結論は既にお話ししている通り、対話を削除する、ということになります。
先程はユーザー入力部分をバッチの引数にする、という案をお見せしましたが、それ以外にも
- 環境変数化する
- 設定ファイルに外出しする
などの手段も考えられます。いずれもバッチファイル自体の改修が必要なことには変わりませんが、事故のないCI/CDを実現するためには必要なコストとして、修正を検討していただくのがよいでしょう。
非対話型にしたその後は:Jenkinsのパラメータを利用する
バッチを非対話化したら、次は Jenkins 側で値を安全に設定できるようにします。Choice/Boolean/String パラメータを使うと、環境(DEV/STG/PRD)、ビルド種別(Debug/Release)などをジョブ開始時に設定することができます。Choice にしておけば不正値を入力しづらく、実行ログにも「どの値で走らせたか」が残り監査性も上がります。Jenkins パラメータ → 環境変数 → バッチ引数、という流れにすると整理しやすく、対話部分を Jenkins 側で実現する、ということが可能になります。
pipeline {
agent any
parameters {
choice(name: 'CFG', choices: ['Debug', 'Release'], description: 'Build config')
}
stages {
stage('Build') {
steps {
bat label: 'ビルドの実行', script: "build.bat \"${params.CFG}\""
}
}
}
}非対話型にしたその後は:ユーザー承認を待つPipeline input
本番デプロイなど「人の最終承認が必要」な工程では、バッチの中で入力待ちを作るのではなく、Pipelineのinput で一時停止する方法が適しています。承認ボタンを押したら、その後はバッチを引数付きで実行します。こうすることで、承認のタイミングと責任の所在(誰が承認したか)が Jenkins に残り、操作は Web 画面に集約することができます。当然、バッチが途中でハングする、ということも防ぐことができます。
pipeline {
agent any
parameters {
choice(name: 'ENV', choices: ['DEV', 'STG', 'PRD'], description: 'Deploy environment')
string(name: 'VERSION', defaultValue: '1.2.3', description: 'Release version/tag')
}
stages {
stage('Confirm Deploy') {
when { expression { params.ENV == 'PRD' } } // PRDだけ手動承認
steps {
input message: "PRDへデプロイします。ENV=${params.ENV}, VERSION=${params.VERSION}",
ok: "Deploy実行"
}
}
stage('Deploy') {
steps {
bat label: 'デプロイの実行', script: "deploy.bat \"${params.ENV}\" \"${params.VERSION}\""
}
}
}
}まとめ
Jenkinsで対話型バッチをパイプラインに組み込む際の、推奨アプローチは以下の通りです。
恒久対応
- バッチを非対話化して引数/設定で動くようにする
- Jenkins パラメータで値を設定し、バッチ に渡す
- 本番デプロイなどは Pipeline
inputで承認を入れる
暫定対応
echo <引数>|を限定的に使う(ただし、環境依存のリスクあり)
推奨されるアプローチとしては、対話を削除する、すぐに対話を削除できないなら暫定的にechoパイプを利用する、となります。対話を「バッチの中」に残すほど、実行環境依存で止まりやすくなります。CI/CD の基本は人がいない前提で確実に動く形に寄せることであり、対話は Jenkins のワークフロー(パラメータや承認)に移すのが最善です。
弊社の開発基盤ソリューションチームでは、JenkinsやGitHub Actionsを利用したCI/CD環境の構築を行っています。今回のように対話型のバッチで運用している場合、そのままの状態でCI/CDに組み込むことは難しいですが、現在の開発プロセスをお聞きした上で、最適な自動化環境のご提案をすることができますので、ご興味頂けた方はぜひお問い合わせください。
