#はじめに
ウェブアプリケーションで実行するには時間がかかりすぎる処理は、cronやバッチ処理などの非同期処理と分離することがあります。cronにしてしまうとアプリケーションからコントロールが効かず、待つしかない状況に陥ります。
Rundeckに重たい処理を置いて単独で動く状況を用意しつつ、ウェブアプリケーションからRundeckのAPIを使いRundeck上のジョブを制御してみます。
#使ったもの
rundeck-3.0.6-20180917.war (Open source windows版)
#RundeckのAPIを使えるようにするまで
Rundeckは標準でRESTなAPIを装備しています。簡単な設定をするだけで使えるようになります。
##API tokenを発行する
Rundeckにログインし右上のアイコンProfileから設定する。
API tokenの有効期限を変更する
インストール直後の状態だとAPIのトークンは最大で30日間しか有効にできません。システムに組み込むには不便なので、30日以上指定可能にしたい場合には、以下の設定をしておきます。
rundeck.api.tokens.duration.max=1y
0にすると無制限になりますが、今回は1y(1年間)にしておきました。
Duration string indicating maximum lifetime of API Tokens. If unset, the value will be "30d" (30 days). Format: "##{ydhms}" (years, days, hours, minutes, seconds). If you want to disable the max expiration you can set it to 0 and create token with 0 duration that don't expire.
設定を変更しRundeckを再起動すると、365dまで設定できるようになっています。
ここまでで前準備完了です。API Tokenはメモしておきます。
triEfS4Qjc3ymqG8plKqPvxBKE7rtJxe
#APIにアクセスしてみる
RundeckのAPIは、http://hostname:4440/api/Version/
でアクセスできます。呼び出しが簡単なsystem/infoにアクセスして動作確認してみます。hostnameとport番号は環境に合わせて書き換えてください。
URL
http://hostname:4440/api/2/system/info?authtoken=triEfS4Qjc3ymqG8plKqPvxBKE7rtJxe
API Tokenはパラメータに付ける形か、ヘッダーに含める形かどちらかです。
https://rundeck.org/docs/api/index.html#authentication
ブラウザでアクセスしてみて、XMLのメッセージがつらつらと返ってくればOKです。
RundeckにログインしているブラウザでAPIの確認するとNot Foundの画面が出ます。この場合はRundeckにログインしていない別のブラウザを使うか、コマンドラインベースのツール(curl, wgetなど)で確認してください。
APIにアクセスできる環境が整いました。
#Rundeckをアプリケーションに組み込む準備
ここから本題。
オンラインで時間のかかる処理をバックエンドのRundeckに依頼し、結果を待つアプリケーションを想定します。
やること
- Rundeckにジョブを作成(GUI)
- ジョブが次にいつ動くのか確認(API)
- Rundeck APIでジョブの状態確認(API)
- Rundeck APIでジョブの起動(API)
##jobを作成
RundeckのGUIにログインしジョブを作ります。ジョブ自体もAPIで作れますが、設定済みのジョブをウェブアプリケーションと連携することに絞ります。今回は、時間のかかるジョブをエミュレートするだけなので、「30秒waitして正常終了するジョブ」を作成しました。
UUIDは最下部にあります。一度ジョブ作成を完了させた後にジョブ編集すると見えます。
98d2c286-3165-403a-b2c0-90dddc3f2205
##次に動く時間の確認
次回ジョブが動く時間をアプリケーション側から見えるようにします。時間が見えていれば待つべきか手動実行するべきか判断付きやすくなります。特定のジョブがRundeck側でいつ動くのか調べるには、job/info
を使います。
curl --verbose
-H 'Accept:application/json '
-H 'X-Rundeck-Auth-Token:triEfS4Qjc3ymqG8plKqPvxBKE7rtJxe'
http://hostname:4440/api/27/job/98d2c286-3165-403a-b2c0-90dddc3f2205/info
実際は1行で書きます
{
"href":"http://hostname:4440//api/27/job/98d2c286-3165-403a-b2c0-90dddc3f2205",
"averageDuration":21872,
"id":"98d2c286-3165-403a-b2c0-90dddc3f2205",
"scheduleEnabled":true,
"scheduled":true,
"enabled":true,
"nextScheduledExecution":"2018-09-29T16:30:00Z",
"permalink":"http://hostname:4440//project/API_test/job/show/98d2c286-3165-403a-b2c0-90dddc3f2205",
"group":null,
"description":"",
"project":"API_test",
"name":"job 30 second"
}
次回実行時刻は、nextScheduledExecution
に入っています。
##今、ジョブが動いているか確認
アプリケーション側からジョブを動かす前に、そのジョブが動いているか確認する必要があります。複数人でブラウザを見ているケースや、Rundeckのスケジューラ側の起動とのバッティングを想定しています。
jobのUUIDを指定してexecutions
のAPIを実行すると取得できます。動いているジョブがあるか確認したいだけなのでstatus=runningで絞り込りをします。
curl
-H 'Accept:application/json '
-H 'X-Rundeck-Auth-Token:triEfS4Qjc3ymqG8plKqPvxBKE7rtJxe'
http://hostname:4440/api/27/job/98d2c286-3165-403a-b2c0-90dddc3f2205/executions?status=running
ジョブが動いていないときレスポンス
{
"paging":{"count":0,"total":0,"offset":0,"max":20},
"executions":[]
}
ジョブが動いているときレスポンス
{
"paging":{"count":1, "total":1, "offset":0,"max":20},
"executions":[{
"id":45,
"href":
"http://hostname:4440//api/27/execution/45",
"permalink":"http://hostname:4440//project/API_test/execution/show/45",
"status":"running",
"project":"API_test",
"executionType":"user",
"user":"admin",
"date-started":{"unixtime":1538237400000,"date":"2018-09-29T16:10:00Z"},
"job":{
"id":"98d2c286-3165-403a-b2c0-90dddc3f2205",
"averageDuration":21872,
"name":"job 30 second",
"group":"",
"project":"API_test",
"description":"",
"href":"http://hostname:4440//api/27/job/98d2c286-3165-403a-b2c0-90dddc3f2205",
"permalink":"http://hostname:4440//project/API_test/job/show/98d2c286-3165-403a-b2c0-90dddc3f2205"
},
"description":"powershell sleep 30",
"argstring":null
}]
}
このAPIを叩いて、countが0か確認すればよさそうです。
##ジョブの起動
ウェブアプリケーション側からバッチジョブを実行したいときに、job/run
を実行します。
curl
-H 'Accept:application/json '
-H 'X-Rundeck-Auth-Token:triEfS4Qjc3ymqG8plKqPvxBKE7rtJxe'
-X POST
http://hostname:4440/api/27/job/98d2c286-3165-403a-b2c0-90dddc3f2205/run
jobのrun はPOSTなので注意です。
起動に成功したときのレスポンス
{
"id":46,
"href":"http://hostname:4440//api/27/execution/46",
"permalink":"http://hostname:4440//project/API_test/execution/show/46",
"status":"running",
"project":"API_test",
"executionType":"user",
"user":"admin from api",
"date-started":{"unixtime":1538237862120,"date":"2018-09-29T16:17:42Z"},
"job":{
"id":"98d2c286-3165-403a-b2c0-90dddc3f2205",
"averageDuration":22907,
"name":"job 30 second",
"group":"",
"project":"API_test",
"description":"",
"href":"http://hostname:4440//api/27/job/98d2c286-3165-403a-b2c0-90dddc3f2205",
"permalink":"http://hostname:4440//project/API_test/job/show/98d2c286-3165-403a-b2c0-90dddc3f2205"
},
"description":"powershell sleep 30",
"argstring":null
}
起動に失敗した時のレスポンス
{
"error":true,
"apiversion":27,
"errorCode":"api.error.execution.conflict",
"message":"Execution had a conflict: Job \"job 30 second\" {{Job 98d2c286-3165-403a-b2c0-90dddc3f2205}} is currently being executed {{Execution 46}}"
}
#アプリケーションに組み込む
HTMLで画面を作り、jQueryでRundeckのAPIを実行するものを作ってみました。
##やっていること
10秒に一度、RundeckのAPIを実行し、次のジョブ実行時間、最終のジョブ実行時間、今動いているかを取ってくる。状態は画面に表示する。
ステータス確認ボタンは、10秒間隔で実行しているチェックを手動で実行するだけ。
ジョブ実行ボタンは、Rundeck側のジョブを呼び出し、時間のかかる処理をバックグラウンドで実行する
Rundeckのスケジュール時間まで5分を切ると、手動実行はできないようにする。
状態によって、ボタンの色(disabled)とアイコンが変わります。
##コード
Rundeck APIのURLとAuthKeyがHTML上に露出していますが、ウェブアプリケーション経由でRundeckにアクセスする(上手に隠す)のが正しい設計です。
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
</head>
<body>
<div style='margin:20px'>
<p>次のジョブ実行時間:<span id="next_execution">-</span></p>
<p>最後のジョブ実行時間:<span id="last_execution">-</span></p>
<p>チェック時間:<span id="last_check">-</span></p>
<button id="b2" type="button" class="btn btn-primary" onclick="check()">ステータス確認</button>
<button id="b3" type="button" class="btn btn-primary" onclick="executeJob()">
<i class="far fa-play-circle" id="available" style='display:none'></i>
<i class="far fa-pause-circle" id="not_available" style='display:none'></i>
<i class="fas fa-sync fa-spin" id="running" style='display:none'></i> ジョブ実行</button>
</div>
</body>
<script>
var jobid='98d2c286-3165-403a-b2c0-90dddc3f2205';
var apitoken='triEfS4Qjc3ymqG8plKqPvxBKE7rtJxe';
var apiheader = {'X-Rundeck-Auth-Token':apitoken,'Accept':'application/json'};
var running = false;
var shceduled = true;
var next_execution_time = 0;
var last_execution_time = 0;
var last_check_time = 0;
var job_execution_limit = 5*60; //sec
var job_interval = 0;
var auto_check_interval = 1*60; //sleep(sec)
function check(){
//非同期処理を順番に
checkNextExecution().then(checkJobStatus).then(checkLastExecution).then(setButtonStatus);
}
function checkNextExecution(){
dfd = new $.Deferred;
//jobがスケジュールされない状況はこれ以上やらない。
if( !shceduled ){
return dfd.resolve();
}
//rundeckの次回起動時間が来ていなければチェックしない
last_check_time = Date.now();
if( next_execution_time > last_check_time ){
return dfd.resolve();
}
//rundeck APIで次の動作時間を調べる
$.ajax({
url:'/rundeck-api/27/job/' + jobid + '/info',
type:'GET',
dataType:'json',
data:{},
headers:apiheader
})
.done( (data) => {
//APIから次実行時間が取れたときだけ、現在時間との時間差を計算
if( data.nextScheduledExecution){
scheduled=true;
d = new Date(data.nextScheduledExecution);
next_execution_time = d.getTime();
console.log('next=%s, now=%s', next_execution_time, last_check_time);
job_interval = (next_execution_time-last_check_time)/1000;
}else{
//取れないときは、スケジュールされていないので二度とチェックしないようにfalse
scheduled=false;
}
dfd.resolve();
})
.fail( (data) => {
console.log(data);
dfd.resolve();
})
.always( (data) => {
});
return dfd.promise();
}
function checkJobStatus(){
dfd = new $.Deferred;
$.ajax({
url:'/rundeck-api/27/job/' + jobid + '/executions',
type:'GET',
dataType:'json',
data:{'status':'running'},
headers:apiheader
})
.done( (data) => {
// console.log(data);
//status=runningで1行取れれば動いてるとみなす
running = data.paging.count>0;
dfd.resolve();
})
.fail( (data) => {
console.log(data);
dfd.resolve();
})
.always( (data) => {
});
return dfd.promise();
}
function checkLastExecution(){
dfd = new $.Deferred;
//executionsのAPIは新しい順に返してくる。並び順指定するパラメータはない
$.ajax({
url:'/rundeck-api/27/job/' + jobid + '/executions',
type:'GET',
dataType:'json',
data:{'max':'1'},
headers:apiheader
})
.done( (data) => {
//取れたら1個目が最終起動ログになるはず
last_execution_time = data.executions[0]['date-started'].unixtime;
dfd.resolve();
})
.fail( (data) => {
console.log(data);
dfd.resolve();
})
.always( (data) => {
});
return dfd.promise();
}
//ジョブの手動実行
function executeJob(){
$.ajax({
url:'/rundeck-api/27/job/' + jobid + '/run',
type:'POST',
dataType:'json',
data:{},
headers:apiheader
})
.done( (data) => {
console.log(data);
running = true;
setButtonStatus();
})
.fail( (data) => {
console.log(data);
})
.always( (data) => {
});
}
//画面のステータスを変更する関数
function setButtonStatus(){
console.log('scheduled=%s, interval=%s, limit=%s, running=%s',scheduled,job_interval,job_execution_limit,running);
next_date = getFormattedDate(next_execution_time);
last_date = getFormattedDate(last_execution_time);
last_check = getFormattedDate(last_check_time);
$('span#next_execution').text(next_date);
$('span#last_execution').text(last_date);
$('span#last_check').text(last_check);
if( running ){
$('i#available').hide();
$('i#not_available').hide();
$('i#running').show();
$('button#b3').prop('disabled', true);
}else{
$('i#running').hide();
if( scheduled && job_interval > job_execution_limit ){
$('i#available').hide();
$('i#not_available').show();
$('button#b3').prop('disabled', true);
}else{
$('i#available').show();
$('i#not_available').hide();
$('button#b3').prop('disabled', false);
}
}
}
function getFormattedDate(time){
if( time == 0){
return '--';
}
date = new Date(time);
y = date.getFullYear();
m = padZ(date.getMonth()+1);
d = padZ(date.getDate());
h = padZ(date.getHours());
mi = padZ(date.getMinutes());
s = padZ(date.getSeconds());
return y + '/' + m + '/' + d + ' ' + h + ':' + mi + ':' + s;
}
function padZ(a){
if( a < 10 ){
return '0' + a;
}
return a;
}
$(function(){
check();
setInterval(check,auto_check_interval*1000);
});
</script>
</html>
##おまけ
javascriptでRundeck APIへアクセスすると、CORSの設定がないためAPI接続エラーになります。Rundeckにbuild-inされているWebサーバーがAccess-Control-Allow-Origin
ヘッダーを返していないので、ブラウザがRundeckへのリクエストを止めてしまいます。ドキュメントを探してみたのですが、Access-Control-Allow-Originヘッダーを追加で設定するような項目は見つかりませんでした。
今回は、HTMLを配信したホストの/rundeck-api/以下にAPIのリクエストを送り、ApacheからProxyしてRundeckのポート付きのEndpointに転送する形で凌ぎました。
ProxyPass /rundeck-api/ http://hostname:4440/api/
Rundeckのサーバーがインターネット側からアクセス可能になっていることは少ないと思います。サーバーの位置やAPIのトークンが見えないように、アプリケーションとRundeckの間にプロキシ役のアプリケーションを置くのが正しい選択です。
#まとめ
今回は、バックエンドにRundeckを置きウェブアプリアプリケーションの一部として組み込むサンプルを作りました。ウェブとバッチで処理を分離するとき、バッチの状態を管理するテーブルを作ったり、同時起動しないように配慮したりと、いろいろと頭を悩ます部分があると思いますが、ひとつの解決法になるのではないでしょうか。