Powershellを使った、ServiceNowへの添付ファイル移行方法

# はじめに

この記事では、添付ファイルをServiceNowのチケットに紐づく形で移行する方法をご紹介します。

想定される状況としては、旧システムから新システム（ServiceNow）への移行で、旧システム側で持っている添付ファイルを旧システムのチケット移行と同時に移行するようなケースです。

ServiceNowでは標準でデータを取り込む仕組みであるImport SetやTransform mapが備わっていますが、あくまでテキストデータの移行に用いられる手法であり、添付ファイルなどのバイナリデータをUI上で移行する標準機能はありません。

そこで用いる方法が、ServiceNowのAPIとして提供されているAttachment APIです。

ServiceNowのREST API Explorer機能を利用することで、今回の要件である添付ファイルを移行するためのリクエストを実行するコードを生成してくれるので、それをベースに要件を満たすようなコードを書きました。

コードはServiceNow Script、cURL、Python、Ruby、JavaScript、Perl、Powershellから好きなものを選べますが、今回はWindowsにダウンロードしたローカルディレクトリの添付ファイルを移行することから、PowerShellを採用しました。

# 要件

- インシデントチケット数万件を移行する
- そのインシデントチケットに紐づく添付ファイル数万件を移行する
- 紐づきは対応表で確認する

添付ファイルを手動移行する方法では到底プロジェクト期間内に終わらない状況のため、スクリプト処理を利用した一括アップロードを検討しました。

# 課題

取り組むうえでいくつか課題がありました。参考のため記載します。

- 添付ファイル名の一意性
    - 対策：移行元システムから添付ファイルの抽出時に、添付ファイル名が一意となるよう識別子を振り分ける
- データ容量の大きさ
    - 対策：今回は実施しなかったが、検討したものの一例として、例えばファイルはAWS S3上で管理し、AWS Batch等でServiceNowにPOSTする処理を組む

# 実装方法

実装方法をご紹介します。

## ServiceNowのREST API Explorerを用いて基礎的な添付ファイルPOST用のPowerShell Scriptを生成する

1. ServiceNowにログインする
2. （英語環境）メニューで[REST API Explorer]と検索し、表示されたモジュールをクリックする
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/9d8bf9d9-a7ea-07ec-ff62-69e7a93e01a4.png)
        
3. API Nameから[Attachment API]を選択する
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/4692e58e-36a6-8127-b149-cc5ec51603cd.png)
    
4. Upload an attachment from a binary request (POST)を選択する
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/a5075b9c-fdc4-6cbd-cf83-534668fee1b5.png)
    
5. 画面右側の設定画面にて、[table_name]にincident、[table_sys_id], [file_name]に任意の値を入れ、画面下部の[PowerShell]をクリックする
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/31f7454e-d6f4-6fce-ed44-df087a6e8862.png)
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/40d7f88c-dd25-88ba-db0c-78d5a75ef2dc.png)
    
6. 表示されたコードをps1ファイルとして保存する
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/a4ade4f9-070e-ce3c-36c3-400d643a1567.png)
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/05c37e56-567c-d760-cd6f-afa8f79f3437.png)
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/141b1f2b-6167-9bc1-3880-8ddfad99f389.png)

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/e9123c79-12bc-1b1b-9e98-e1b72b7601a1.png)

    
    任意の名前で、任意の場所に保存する。
    

## データ準備・ServiceNow側チケット準備

データを準備します。実際のデータは見せられないため、検証用のダミーデータの作成方法をお見せしながら、何となくどのようなものが実際に使われたかを想像していただけますと幸いです。

### ServiceNowチケットインポート

1. インポートファイル準備
    
    まずはインポート用のファイルを準備します。今回はチケット10件で試してみましょう。
    
    Defaultビューで必須項目となっている3つの項目を抜き出して、インポート素材としました。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/70f3dc56-4323-2f00-7dee-547e20de8c9f.png)
    
2. Load Data （これ以降の項番はご存知の方は飛ばして、各自で実施してください）
    
    Load DataモジュールからLoad Data画面を開き、画像のようにセットアップしてください。LabelはIncident投入用であることが分かればよく、あとは任意でつけてください。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/0753d823-66ce-69d8-6701-d7587572c990.png)
    
    Submitをクリックしてください。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/ab60422e-732c-d91a-a195-537baf6f3f19.png)
    
    inserts 10と出れば成功です。
    
    別タブで[Import sets]リンクを開き、ISET番号を控えておきましょう。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/18789b8b-411f-f1bb-8fa5-27cfa98cd775.png)
    
    ISET0010002
    
3. Transform map
    
    [Create transform map]をクリックし、変換マップを設定します。（中間テーブルから実際のテーブルに移すためのマッピング）
    
    下記のように設定してください。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/8985ca78-5d59-d991-d8bc-de9687d159a9.png)
    
    ※[Run Business Rule]は、データ投入時にBusiness Ruleを実行するかどうかを設定する項目です。チェックを入れる前に、現行の仕様を十分確認してください。今回のインポート処理では、incidentテーブルへのinsert時にBusiness Ruleが起動します。また、連鎖的に他のBusiness Ruleが起動する可能性もあるため、事前に影響範囲を確認してください。状況によっては、Business Ruleの実行が必要と判断される場合もあります。
    
    問題なければ、画面下部Related Linksの[Auto Map Matching Fields]をクリックしてください。
    
4. 変換マッピングの実施
    
    画面中部Related Linksの[Transform]をクリックしてください。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/70a643d0-9202-2ff6-cec3-056d0beabdb4.png)
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/27b07cad-3104-5a68-5fc2-324d3be47450.png)
    
    Import set欄に間違いなく先ほど自分がインポートしたImport Setが入っていることを、ISET番号で確認してください。
    
    ※他の人が同時にインポート作業をしていると、ISET番号を誤る危険性があります
    
    Transformをクリックしてください。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/4f47cda5-29f8-fcde-1df1-db8e90de5299.png)
    
    一応成功ですが、行ごとに成功したかどうかを[ISET0010002]をクリックしたうえで確認します。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/4451340d-613e-9f3a-c042-b92aba13afff.png)
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/03fa77d5-ce4a-a781-c27c-14c4a6e88fdc.png)
    
    問題なく10行作られていることが分かります。
    
5. Incidentの一覧画面からも確認
    
    作成者は投入したユーザで登録されるため、作成者と作成日時でフィルタを掛ければ、インポートしたレコードに絞って確認できます。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/d5cabf03-ed2f-d21d-5fc2-2a63560352c9.png)
    

### 対応表

対応表は移行元システムのチケット番号と、ファイル名が対応しているシンプルな表です。

PowerShellで読み込むため、CSVで作成します。

※対応表に記載のファイルは事前に作成しておいてください。コマンドラインで下記のように打ち込むと、テキスト形式のダミーファイルを一括作成できます。

```
for /l %i in (1,1,10) do (echo Dummy content > dummy_%i.txt)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/ab33dba9-0ba3-6361-115e-f8efd7c48a2c.png)

### 対応表のsys_id変換

この対応表のチケット番号を、sys_idに変換していきます。

ServiceNowからsys_id付きのデータをExcelでエクスポートするには、URLを活用した方法があります。

[How to include SysID field in an Excel export file when exporting a table or a record? - Support and Troubleshooting](https://support.servicenow.com/kb?id=kb_article_view&sysparm_article=KB0691394)

ServiceNowのKnowledgeに記載のある通り、

https://<インスタンス名>.service-now.com/<テーブル名>_list.do?EXCEL&sysparm_default_export_fields=all

と入力すれば、対象のテーブルのチケット全件をsys_id付きでエクスポートすることができます。

ただし、全件取得する方法のため、すでに環境に多くのインシデントチケットがある場合は、対象のチケットを一覧画面でフィルタし、取得したEncoded Queryを下記のようにURLに付加することで、フィルタを掛けた状態でSys ID付きの一覧が取得できます。

https://<インスタンス名>.service-now.com/<テーブル名>_list.do?EXCEL&sysparm_default_export_fields=all&sysparm_query=<Encoded Query>

※PowerShellスクリプト内でServiceNowからSys IDをGETする方法もありますが、スクリプトの処理時間を考慮し、この方法を選択しています。

1. 対象環境にログイン済みのブラウザでURLを入力する
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/2c384ddc-26ed-40f3-05c0-eae22ea5c65c.png)
    
2. Enterを押下
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/1a2e80e6-7dc7-ef4b-322f-34c645019789.png)
    
    ダウンロードできました。
    
3. ファイルの中身を確認
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/bc571a4b-07dd-037f-04cc-daaf474e1bcd.png)
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/ab804248-547b-ae2c-08db-6109d4405249.png)
    
    先ほどのチケット番号やそれに紐づくsys_idが確認できます。
    
4. 対応表の修正
    
    チケット番号とsys_idを、対応表上で書き換えます。
    
    VLOOKUPなどで対応付けし、
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/853fd157-6f21-601f-f048-dfc5e8610afe.png)
    
    置き換える。
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/2d62f036-b668-4692-e3ae-964a640c730d.png)
    

## スクリプトの書き方を要件を満たすようにする

今回の要件から、大まかに「対応表を読み込む」「スクリプト内でチケットに紐づく実際のファイルのパスを特定する」「アップロードする」という作業があります。

これらに対し適切なコードとするための方法を考えます。

### 対応表を読み込む

```powershell
# Eg. User name="admin", Password="admin" for this code sample.
$user = "admin"
$pass = "admin" # 実際のパスワードに修正してください
$yourinstance = "yourinstance"

# Build auth header
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user, $pass)))

# ここまで元のスクリプト
# 対応表の格納先（例）
$MappingFilePath = "C:\Users\Batch\mapping.csv"

# 対応表読み込み
$Mapping = Import-Csv -Path $MappingFilePath
```

Import-CSVというコマンドレットを使い、CSVをカスタムオブジェクトとして取得します。

※ユーザIDやパスワードの取り扱いにはご注意ください。保存時は削除するなどの対応を各自で実施お願いいたします。

### スクリプト内でチケットに紐づく実際のファイルのパスを特定する

ファイルが格納されているパスが分かれば、指定のチケットにアップロードする情報として利用できます。

説明のためスクリプトを分割しております。

```powershell
（承前）
# 添付ファイルが保存されているディレクトリのパス
$AttachmentFolder = "C:\Users\Batch\files"

# 対応表の行ごとにパスを特定する処理を実行
foreach ($Row in $Mapping) {
    $TicketId = $Row.ticket_id
    $FileName = $Row.file_name

# ここで実際のファイルのパスを特定している（どのファイルをアップロードすれば良いか分かった状態）
    $FilePath = Join-Path -Path $AttachmentFolder -ChildPath $FileName
     （後続処理）
 }
```

### アップロードする

```powershell
（後続処理部分）

    if (Test-Path $FilePath) {
        # ServiceNowにファイルをアップロードするためのリクエストを作成
        
        # リクエストヘッダーを設定します
        # 元のHeaderオブジェクトの記述方式だと認証がうまくいかなかったため、下記の形式に修正しました
        # 拡張子の違いを吸収できるようContent-Typeの書き方を汎用的にしました（検証ではtxtのみを利用しています）
        
        $Headers = @{
            "Authorization" = "Basic $base64AuthInfo"
            "Accept" = "application/json"
            "Content-Type" = (Get-Item $FilePath).Extension.TrimStart('.') # ファイルの拡張子を使用
        }
        
        # anyとなっていた部分を、対応表から読み込んだsys_idとファイル名に置き換えています
        $Uri = "https://$yourinstance.service-now.com/api/now/attachment/file?table_name=incident&table_sys_id=$TicketId&file_name=$FileName"
        
        # 実行部分はファイルを特定のパスからアップデートするので、工夫が必要です
        try {
            # ファイルをバイト配列に変換する処理です
            $FileContent = [System.IO.File]::ReadAllBytes($FilePath)

            # ここでファイルをアップロードします
            $Response = Invoke-RestMethod -Uri $Uri -Method Post -Headers $Headers -Body $FileContent
            Write-Host "$FileName を$TicketId にアップロードできました。"
        }
        # try内の処理が失敗した際にエラーメッセージを表示します
        catch {
            Write-Host "$FileName を$TicketId にアップロードするのに失敗しました。$_"
        }
    }
    # そもそもファイルがディレクトリ内で見つからないときの処理です
    else {
        Write-Host "ファイルが見つかりませんでした: $FilePath"
    }
```

### 実施結果

1. Power Shell ISEを管理者モードで起動
2. コードを開く
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/54ddb5f5-18e8-1702-eaed-d244ad6cf59b.png)
    
3. 実行する
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/baadac5a-5770-10a7-1206-9c683eec0636.png)
    
    対応表に仕込んでいたエラーも正しく吐かれています。
    
4. 結果を確認する
    
    ★Incident一覧チェック
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/c39c774a-6485-3c8f-95e0-d1f781021258.png)
    
    ★Incidentフォームチェック
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/a16c09de-5d00-5346-cb50-9d384d859bf2.png)
    
    ★添付ファイルテーブル(sys_attachment)チェック
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/83c3b77a-6c96-c57b-a30a-d7c8a29fdf63.png)
    
5. ファイルの中身の確認
    
    ファイル名をServiceNow上でリネーム
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/04fb1485-7e39-e673-0e9f-3876421422ea.png)
    
    ダウンロード
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/89abaeb3-31e6-3228-a2fb-0b9799194efe.png)
    
    比較
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3959149/55dc1851-af19-1905-697a-0bf3fd80a0e9.png)
    

# 最後に

参考になりましたら、いいねボタンをクリックしていただけますと幸いです。

なお、検証で使ったファイルはtxtだけですが、検証時に49.5KBのxlsxファイルでもアップロードできることを確認しております。16バイトファイルを10ファイルアップロードするのに、おおよそ4秒かかっています。49.5KBのxlsxファイルをアップロードする際は、16バイトのtxtファイルと比べるとやや時間がかかりました。

このように、より容量が増えたり、ファイル数が増えたり、ファイル形式が多様化することにより、それぞれ個別の問題やエラーが発生するかもしれません。

課題に直面した場合は、ぜひ課題と解決策をコメントに記載いただけますと幸いです。