0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【連載 第8回】ServiceNow標準機能だけで作る「Pull型」自動化連携 ― E2Eテストスクリプトで通しで動かす

0
Last updated at Posted at 2026-10-27

本記事は連載「ServiceNow標準機能だけで作るPull型自動化連携」の第8回です。
第4回〜第7回で、取得・確保・実行開始・結果登録のAPIと、進捗を表示するBusiness Ruleを作りました。
今回は、これらを1本のシェルスクリプトにまとめ、依頼から完了までを通しで動かします。

この回で作るもの

自動化エンジンの役割を、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サービスユーザーの権限とセキュリティなどを整理します。

連載記事


※ 本記事の内容は個人の見解であり、所属する組織の公式な見解ではありません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?