この回で作るもの
自動化エンジンの役割を、curl と jq だけで再現するテスト用スクリプトです。実際の自動化処理は行わず、処理キーに応じたダミーの結果を返します。
| ステップ | 使うAPI | 解説した回 | SCTASKの状態 |
|---|---|---|---|
| ① 取得 | Table API(GET) | 第4回 | Open + autoengine1 |
| ② 確保 | Table API(PATCH) | 第4回 | In Progress + autoengine1 |
| ③ 詳細取得 | Scripted REST API(GET /task) | 第5回 | 変化なし |
| ④ 実行開始 | Scripted REST API(GET /task + 条件付き更新) | 第5回 | In Progress + autoengine1running |
| ⑤ 実行 | (スクリプト内のダミー処理) | — | 変化なし |
| ⑥ 結果登録 | Scripted REST API(POST /task_result) | 第6回 | SUCCESS: Closed Complete / FAILED: 引継ぎ |
②・④・⑥のタイミングで、第7回のBusiness RuleがRITMのActivityに進捗を表示します。
1. 準備
1.1 必要なもの
- 第3〜7回で作成したServiceNow側の設定一式
-
curlとjqが使えるLinux環境 - Catalog Itemから出した依頼(
Open + autoengine1のSCTASKが1件以上ある状態)
1.2 設定値
スクリプトは、環境変数で設定を上書きできるようにしています。
| 環境変数 | 既定値 | 内容 |
|---|---|---|
SN_INSTANCE |
https://<instance>.service-now.com |
インスタンスのURL |
SN_USER |
svc_autoengine1_api |
APIサービスユーザー |
SN_PASSWORD |
<password> |
パスワード |
TABLE_API_PATH |
/api/now/table/sc_task |
Table APIのパス |
TASK_API_PATH |
/api/<namespace>/automation_integration/v1/task |
詳細取得・実行開始APIのパス |
RESULT_API_PATH |
/api/<namespace>/automation_integration/v1/task_result |
結果登録APIのパス |
PROCESS_INTERVAL_SECONDS |
30 |
SCTASKを1件処理するごとの待ち時間(秒) |
ASSIGNMENT_GROUP |
autoengine1group |
自動化のAssignment group |
WAITING_ASSIGNEE |
autoengine1 |
実行待ちを表すユーザー |
RUNNING_ASSIGNEE |
autoengine1running |
実行中を表すユーザー |
OPEN_STATE |
1 |
Openの内部値 |
IN_PROGRESS_STATE |
2 |
In Progressの内部値 |
export SN_INSTANCE="https://<instance>.service-now.com"
export SN_USER="svc_autoengine1_api"
export SN_PASSWORD='********'
export TASK_API_PATH="/api/<namespace>/automation_integration/v1/task"
export RESULT_API_PATH="/api/<namespace>/automation_integration/v1/task_result"
<instance> と <namespace> は、実際の値に置き換えてください。< や > が残っていると、スクリプトは設定漏れとみなしてエラーで終了します。
2. E2Eテストスクリプト
servicenow_pull_e2e_test.sh として保存します。
#!/usr/bin/env bash
set -Eeuo pipefail
# Connection settings
SN_INSTANCE="${SN_INSTANCE:-https://<instance>.service-now.com}"
SN_USER="${SN_USER:-svc_autoengine1_api}"
SN_PASSWORD="${SN_PASSWORD:-<password>}"
# API paths
TABLE_API_PATH="${TABLE_API_PATH:-/api/now/table/sc_task}"
TASK_API_PATH="${TASK_API_PATH:-/api/<namespace>/automation_integration/v1/task}"
RESULT_API_PATH="${RESULT_API_PATH:-/api/<namespace>/automation_integration/v1/task_result}"
# Processing settings
PROCESS_INTERVAL_SECONDS="${PROCESS_INTERVAL_SECONDS:-30}"
ASSIGNMENT_GROUP="${ASSIGNMENT_GROUP:-autoengine1group}"
WAITING_ASSIGNEE="${WAITING_ASSIGNEE:-autoengine1}"
RUNNING_ASSIGNEE="${RUNNING_ASSIGNEE:-autoengine1running}"
OPEN_STATE="${OPEN_STATE:-1}"
IN_PROGRESS_STATE="${IN_PROGRESS_STATE:-2}"
require_command() {
command -v "$1" >/dev/null 2>&1 || {
echo "ERROR: required command not found: $1" >&2
exit 1
}
}
require_setting() {
local name="$1"
local value="$2"
if [[ -z "$value" || "$value" == *"<"* || "$value" == *">"* ]]; then
echo "ERROR: set $name before running this script." >&2
exit 1
fi
}
api_get() {
local url="$1"
shift
curl --fail-with-body --silent --show-error \
--user "$SN_USER:$SN_PASSWORD" \
--header "Accept: application/json" \
--get "$url" "$@"
}
api_patch_json() {
local url="$1"
local payload="$2"
curl --fail-with-body --silent --show-error \
--user "$SN_USER:$SN_PASSWORD" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--request PATCH \
--data "$payload" \
"$url"
}
api_post_json() {
local url="$1"
local payload="$2"
curl --fail-with-body --silent --show-error \
--user "$SN_USER:$SN_PASSWORD" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--request POST \
--data "$payload" \
"$url"
}
get_open_tasks() {
local query
query="state=${OPEN_STATE}^assignment_group.name=${ASSIGNMENT_GROUP}^assigned_to.user_name=${WAITING_ASSIGNEE}^ORDERBYsys_created_on"
api_get "${SN_INSTANCE}${TABLE_API_PATH}" \
--data-urlencode "sysparm_query=${query}" \
--data-urlencode "sysparm_fields=sys_id,number" \
--data-urlencode "sysparm_limit=10000"
}
claim_task_via_table_api() {
local task_sys_id="$1"
local payload
payload="$(jq -n --arg state "$IN_PROGRESS_STATE" '{state:$state}')"
api_patch_json \
"${SN_INSTANCE}${TABLE_API_PATH}/${task_sys_id}" \
"$payload"
}
get_task_detail() {
local task_number="$1"
api_get "${SN_INSTANCE}${TASK_API_PATH}" \
--data-urlencode "sctask=${task_number}"
}
start_task_via_task_api() {
local task_number="$1"
api_get "${SN_INSTANCE}${TASK_API_PATH}" \
--data-urlencode "sctask=${task_number}" \
--data-urlencode "expected_current_assigned_to=${WAITING_ASSIGNEE}" \
--data-urlencode "assigned_to=${RUNNING_ASSIGNEE}"
}
execute_task() {
local task_name="$1"
local task_detail="$2"
local target_host log_path destination
target_host="$(jq -r '.result.task.variables.target_host // empty' <<<"$task_detail")"
log_path="$(jq -r '.result.task.variables.log_path // empty' <<<"$task_detail")"
destination="$(jq -r '.result.task.variables.destination // empty' <<<"$task_detail")"
case "$task_name" in
REQTYPE_LINUXLOG_ACTION1)
jq -n \
--arg target_host "$target_host" \
--arg log_path "$log_path" \
--arg destination "$destination" \
'{status:"SUCCESS",message:"Linux log retrieval test completed.",data:{target_host:$target_host,log_path:$log_path,destination:$destination}}'
;;
REQTYPE_LINUXLOG_ACTION2)
jq -n \
--arg target_host "$target_host" \
--arg log_path "$log_path" \
--arg destination "$destination" \
'{status:"SUCCESS",message:"Linux log archive test completed.",data:{target_host:$target_host,log_path:$log_path,destination:$destination}}'
;;
REQTYPE_LSOF_ACTION1)
jq -n \
--arg target_host "$target_host" \
'{status:"SUCCESS",message:"Linux lsof test completed.",data:{target_host:$target_host}}'
;;
*)
jq -n \
--arg task_name "$task_name" \
'{status:"FAILED",message:("Unsupported task type: " + $task_name),data:{error_code:"UNSUPPORTED_TASK_TYPE",task_name:$task_name}}'
;;
esac
}
upload_result() {
local task_number="$1"
local execution_result="$2"
local job_id payload
job_id="shell-$(date -u +%Y%m%dT%H%M%SZ)-${task_number}"
payload="$(jq -n \
--arg sctask "$task_number" \
--arg jobid "$job_id" \
--argjson execution "$execution_result" \
'{sctask:$sctask,result:{status:$execution.status,jobid:$jobid,message:$execution.message,data:$execution.data}}')"
api_post_json "${SN_INSTANCE}${RESULT_API_PATH}" "$payload"
}
main() {
local tasks_json total index task_sys_id task_number
local claim_response detail start_response task_name execution_result upload_response
require_command curl
require_command jq
require_setting SN_INSTANCE "$SN_INSTANCE"
require_setting SN_USER "$SN_USER"
# ignore PASSWORD validation due to > is mistakenly checked.
# require_setting SN_PASSWORD "$SN_PASSWORD"
require_setting TASK_API_PATH "$TASK_API_PATH"
require_setting RESULT_API_PATH "$RESULT_API_PATH"
tasks_json="$(get_open_tasks)"
total="$(jq '.result | length' <<<"$tasks_json")"
if [[ "$total" -eq 0 ]]; then
echo "No Open + ${WAITING_ASSIGNEE} SCTASK found."
exit 0
fi
echo "Found ${total} SCTASK(s). Processing sequentially with ${PROCESS_INTERVAL_SECONDS}s interval."
for ((index=0; index<total; index++)); do
task_sys_id="$(jq -r ".result[${index}].sys_id" <<<"$tasks_json")"
task_number="$(jq -r ".result[${index}].number" <<<"$tasks_json")"
echo "[$((index + 1))/${total}] Claiming ${task_number}: state ${OPEN_STATE} -> ${IN_PROGRESS_STATE}"
claim_response="$(claim_task_via_table_api "$task_sys_id")"
if [[ "$(jq -r '.result.state // empty' <<<"$claim_response")" != "$IN_PROGRESS_STATE" ]]; then
echo "ERROR: state update failed for ${task_number}." >&2
jq . <<<"$claim_response" >&2
exit 1
fi
detail="$(get_task_detail "$task_number")"
task_name="$(jq -r '.result.task.short_description // empty' <<<"$detail")"
if [[ -z "$task_name" ]]; then
echo "ERROR: short_description is empty for ${task_number}." >&2
exit 1
fi
echo "[$((index + 1))/${total}] Starting ${task_number}: ${WAITING_ASSIGNEE} -> ${RUNNING_ASSIGNEE}"
start_response="$(start_task_via_task_api "$task_number")"
if [[ "$(jq -r '.result.updated // false' <<<"$start_response")" != "true" ]]; then
echo "ERROR: assigned_to update failed for ${task_number}." >&2
jq . <<<"$start_response" >&2
exit 1
fi
echo "[$((index + 1))/${total}] Executing ${task_name}"
execution_result="$(execute_task "$task_name" "$detail")"
echo "[$((index + 1))/${total}] Uploading result for ${task_number}"
upload_response="$(upload_result "$task_number" "$execution_result")"
jq . <<<"$upload_response"
if [[ "$index" -lt $((total - 1)) ]]; then
echo "Waiting ${PROCESS_INTERVAL_SECONDS} seconds before the next task."
sleep "$PROCESS_INTERVAL_SECONDS"
fi
done
echo "Completed. Processed tasks: ${total}"
}
main "$@"
3. スクリプトのポイント
3.1 関数と連載の対応
| 関数 | 処理 | 解説した回 |
|---|---|---|
get_open_tasks |
実行待ちのSCTASKを取得 | 第4回 |
claim_task_via_table_api |
Stateを2にして確保 | 第4回 |
get_task_detail |
SCTASKの詳細と変数を取得 | 第5回 |
start_task_via_task_api |
条件付きで autoengine1running に変更 |
第5回 |
execute_task |
処理キーに応じてダミーの結果を作成 | — |
upload_result |
結果を登録 | 第6回 |
3.2 処理の振り分けは Short description で行う
execute_task では、詳細取得で受け取った short_description を case 文で振り分けます。実際の自動化エンジンでは、この部分がPlaybookの呼び出しなどに置き換わります。
対応する処理がないキーの場合は、FAILED を返します。結果登録APIがSCTASKを failure_assignment_group へ引き継ぐので、未対応の作業は自動的に人の対応へ回ります。
3.3 各段階で応答を確認する
- 確保の後は、レスポンスの
stateが2になっていることを確認します。 - 実行開始の後は、
updatedがtrueであることを確認します。409が返った場合(すでに他の処理が開始している場合)は--fail-with-bodyによりエラーで終了し、作業を実行しません。
3.4 jobid
テスト用に、shell-<UTC日時>-<SCTASK番号> の形式でジョブIDを作っています。AAPなどを使う場合は、実際のジョブIDに置き換えます。
3.5 待ち時間
SCTASKを1件処理するごとに、PROCESS_INTERVAL_SECONDS 秒待ちます。Execution Planが次のSCTASKを生成する様子や、RITMのActivityの変化を画面で追いやすくするためです。
このスクリプトは、1回の実行で「その時点で実行待ちのSCTASK」だけを処理して終了します。Execution Planが次のSCTASKを生成した後は、もう一度スクリプトを実行してください。実運用では、cronなどで定期的に実行します。
4. 実行する
chmod +x servicenow_pull_e2e_test.sh
./servicenow_pull_e2e_test.sh
4.1 出力例
Found 1 SCTASK(s). Processing sequentially with 30s interval.
[1/1] Claiming SCTASK0010006: state 1 -> 2
[1/1] Starting SCTASK0010006: autoengine1 -> autoengine1running
[1/1] Executing REQTYPE_LINUXLOG_ACTION1
[1/1] Uploading result for SCTASK0010006
{
"result": {
"updated": true,
"sctask": "SCTASK0010006",
"status": "SUCCESS",
"jobid": "shell-20261013T091500Z-SCTASK0010006",
"state": "Closed Complete",
"assignment_group": "autoengine1group"
}
}
Completed. Processed tasks: 1
5. 画面で確認する
5.1 SCTASK
| 確認項目 | 期待する状態 |
|---|---|
| State | Closed Complete |
| Assignment group / Assigned to |
autoengine1group / autoengine1running(SUCCESSでは変更しない) |
| Work Notes |
=== Automation Execution Result === から始まる結果が追記されている |
5.2 RITM
| 確認項目 | 期待する状態 |
|---|---|
| Activity |
Task claimed and queued. → Execution started. → Execution completed successfully. が順に表示されている |
execution_result_json |
SCTASKの結果が配列として追記されている |
execution_result_display |
結果のテキストが追記されている |
5.3 Execution Plan
複数の作業を設定している場合は、次のSCTASKが Open + autoengine1 で生成されていることを確認します。もう一度スクリプトを実行すると、そのSCTASKが処理され、execution_result_json に2件目の結果が追記されます。
6. 失敗のテスト
6.1 未対応の処理キー
Task TemplateのShort descriptionを、case 文にないキー(例: REQTYPE_UNKNOWN_ACTION1)にして依頼を出し、スクリプトを実行します。
- 結果登録APIに
FAILEDが送られる - SCTASKは In Progress のまま、
failure_assignment_groupのグループへ移り、Assigned toが空欄になる - RITMのActivityには、確保と実行開始のコメントだけが表示される(完了のコメントは出ない)
- Work Notesに
UNSUPPORTED_TASK_TYPEを含む結果が記録される
6.2 二重実行の防止
1件のSCTASKを確保・実行開始した状態で、第5回のcurlで同じ実行開始をもう一度呼ぶと、409 ASSIGNED_TO_MISMATCH が返ります。スクリプトの場合は、エラーで終了し、作業は実行されません。
7. 通しテストのチェックリスト
- 実行待ちのSCTASKが取得される
-
確保で
In Progress + autoengine1になる -
詳細取得で
short_descriptionと変数が返る -
実行開始で
In Progress + autoengine1runningになる - SUCCESSでClosed Completeになり、次のSCTASKが生成される
-
2つ目のSCTASKの結果が、1つ目の結果を残したまま
execution_result_jsonに追記される - RITMのActivityに、SCTASKごとの進捗が表示される
-
未対応の処理キーでは、FAILEDとして
failure_assignment_groupへ引き継がれる -
実行待ちがないときは
No Open + autoengine1 SCTASK found.と表示して終了する
次回予告
最終回となる次回は、この仕組みを本番で使うために検討すべきポイントをまとめます。複数ポーラーでの排他制御、放置されたSCTASKの検知、結果登録の途中で失敗した場合の扱い、結果JSONのサイズ、APIサービスユーザーの権限とセキュリティなどを整理します。
連載記事
- 第1回 なぜ今ServiceNow標準機能だけで作る自動化連携なのか
- 第2回 全体アーキテクチャと設計方針
- 第3回 Catalog Item・Variable Set・Execution Planの準備
- 第4回 Table APIで実行待ちタスクを取得・確保する
- 第5回 Scripted REST APIでタスク詳細取得と実行開始
- 第6回 Scripted REST APIで結果を登録する
- 第7回 Business Ruleで依頼者に進捗を見せる
- 第8回 E2Eテストスクリプトで通しで動かす
- 第9回 本番化に向けた検討ポイント
※ 本記事の内容は個人の見解であり、所属する組織の公式な見解ではありません。