ソラコムで新規注文したSIMをグループに追加する作業をGoogle Spread sheet とGoogle Apps Scriptで処理する

# はじめに

よくある相談の一つで、注文した複数のSIMを簡単に操作したいというものがあります(tagを付けたり、グループの追加をしたり）。また、この作業の場合担当者が購買担当やバックオフィス的な支援業務を行うノンテクな人が担当することが多いということをお聞きします。
ソラコムのSIM操作はAPIから簡単に行える様にも作られていますが、やはり、ノンテクなAPIに慣れていない方はコンソール/GUIで対応頂いているケースが多いようです。

バックオフィス用にサーバやクラウドの準備をするのも準備に時間がかかると思いますし、APIを定義したりするのもちょっと時間がかかる作業になることはよくあります。なるべく簡単かつ、受けいれる側の人も理解やしやすいと思われるGoogle Spread Sheet + Apps Scriptで注文IDをもとに注文の中にあるSIMを特定のSIMグループに追加する設定のサンプルを本記事ではご紹介します。

# 利用するSORACOM API

今回の作業で利用するのは以下のAPIです。

- 認証API： [auth](https://users.soracom.io/ja-jp/tools/api/reference/#/Auth/auth)
- 注文IDから注文に含まれるsubscriber情報と取得するAPI：[listOrderedSubscribers](https://users.soracom.io/ja-jp/tools/api/reference/#/Order/listOrderedSubscribers)
- IMSI情報をもとにグループへ追加するAPI : [setGroup](https://users.soracom.io/ja-jp/tools/api/reference/#/Subscriber/setGroup)

# 事前準備

## Google spread sheetの前提
１シート目に設定情報を書くようにしています。認証情報や、グループ情報 
設定シートは以下の様になっています。（プログラムからの読み出しの行列の位置B3-B7は決め打ちしているので配置を変える場合はプログラム側の認証情報の読み出し部分や、オーダーID取得、グループID取得取得の位置を修正してください)

"設定" というシートにしてあります。
![スクリーンショット 2021-12-02 22.00.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100893/9db24472-4925-c245-668d-5edb6ef455a5.png)

## オーダーIDの確認の仕方

コンソールの画面、左上の "Menu"を差選択し、以下になります。

メニューから発注を選択
![order_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100893/6471645e-d27d-a339-7932-8122d0f3bc37.jpeg)

発注押下後の発注リスト画面
(コンソールのSIM一覧からCSVとして取得することも可能です)
![order-ID.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100893/8e680d35-d2fe-71cf-7874-8aab60669c3d.jpeg)


## グループIDの確認の仕方

コンソールの画面、左上の "Menu"を差選択し、以下になります。

メニューからSIMグループを選択
![group-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100893/2facd578-7ae4-b7ef-9989-1b77b366feaa.jpeg)

グループ一覧からIDを確認
![スクリーンショット_2021-12-03_14_43_12.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100893/ccfa20f6-043b-a0a2-eb13-f12eaebb0c7c.jpeg)


２シート目にはIMSIと実行結果を残すようにしています。 

アウトプット出力イメージ （1行目ヘッダ情報は手書きです)
”SIM情報" というシートにしています。 IMSIや処理結果はプログラムが書き出すので、出力先シートの指定ができていれば特に操作や事前に書き込むべき情報はありません。（必要があればヘッダを書き出すコードはご自身で追記ください）
![スクリーンショット_2021-12-02_22_02_04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100893/728d6454-4faf-03bf-a19b-e96800b0fba1.jpeg)


#Apps Engineのコード

Google spread sheet メニューの拡張機能からApps Scriptを選択します。
mainの処理部 、 SoracomのAPI認証機能の分割をしています。Apps Scriptでは並列にファイルを作っても、特にimport/requireなどの外部処理読み込みを指定しなくても他のファイルにある関数を呼び出すことが出来ます。
本プログラムは `設定シートに必要情報が記入されていることを前提` にしています。

＊：本書は日本向けのSIM操作を対象としています。グローバルSIMを対象とされる場合は、エンドポイントを変更ください。
[APIカバレッジタイプによる違い](https://users.soracom.io/ja-jp/tools/api/coverage-type/)

main処理を記述している .gsファイル

```javascript:main.gs
const soracomUrl = "https://api.soracom.io/v1/";
const configSheetName = "設定";
const simInfosheetName = "SIM情報";
const configSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(configSheetName);
const workingSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(simInfosheetName);

function main() {
  console.log("start script");
  let configJson = readConfig();
  configJson["authHeader"] = soracomAuth();

  //オーダーIDからIMSI情報を取得する
  getOrderInfo(configJson);

  //IMSIを指定されたグループへ追加する
  setGroup(configJson);
}

// 設定シートの読み込み
function readConfig() {
  let configJson = {
    "orderId": configSheet.getRange(6, 2).getValue(),
    "groupId": configSheet.getRange(7, 2).getValue(),
  }
  return configJson;
}

// step2 オーダーIDからIMSIを取得する関数部 
function getOrderInfo(configJson) {
  console.log("getOrderInfo() start")
  // order info url
  const orderIdUrl = soracomUrl + "orders/" + configJson["orderId"] + "/subscribers"
  const options = {
    "method": "get",
    "headers": configJson["authHeader"],
    "methodExceptions": true
  }

  const res = UrlFetchApp.fetch(
    orderIdUrl,
    options
  )
  const resBodyJson = httpResCheck(res);
  if (resBodyJson != null) {
    const subscribersList = resBodyJson.orderedSubscriberList
    console.log(typeof(subscribersList));
    for (var i=0; i < subscribersList.length; i++) {
      console.log(subscribersList[i].imsi);
      var cellPos = i + 2;
      workingSheet.getRange(cellPos, 1).setValue(subscribersList[i].imsi)

    }
  }
  console.log("getOrderInfo() end");
}

// step3 SIMグループへの追加処理部
function setGroup(configJson) {
  console.log("setGroup() satart")
  const payload = {
    "groupId" : configJson["groupId"]
  };
  //const header = createAuthHeader();
  const options ={
    "method": "post",
    "headers": configJson["authHeader"],
    "payload": JSON.stringify(payload),
    "methodExceptions": true
  }
  let i = 2;
  while (true) {
    const imsi = workingSheet.getRange(i,1).getValue();
    if (imsi == "") {
      break;
    }

    let cmdRes = "success";
    const setGroupUrl = soracomUrl + "subscribers/" + imsi + "/set_group"
    const res = UrlFetchApp.fetch(
      setGroupUrl,
      options
    )
    const resBodyJson = httpResCheck(res);
    if (resBodyJson == null) {
      cmdRes = "false"
    }
    // 処理結果書き込み
    workingSheet.getRange(i,2).setValue(cmdRes);
    Utilities.sleep(600) //sleep setting by ms = 0.6s = 100 req/min
    i++;
  }
  console.log("setGroup() end")
  return;
}

function httpResCheck(httpRes){
    var httpCode = httpRes.getResponseCode();
    console.log("http res code:" + httpCode);
    if (200 != httpCode) {
       console.log("HTTP req error, response code:" + httpCode.toString());
       return null;
    }
    var resBody = httpRes.getContentText();
    // string -> json object
    return JSON.parse(resBody);
}


```

ソラコムの認証APIを実行して、HTTPヘッダの形まで作る.gsファイル。他のことをやりたい場合にも認証の方法は同一なので切り出して別ファイルにしています。

```javascript:soracomAuth.gs
function soracomAuth(configJson) {
  const payload = createAuthPayload();
  const authInfo = getAuth(payload);
  const headerInfo = createAuthHeader(authInfo);
  return headerInfo;
}

// auth setting
// email or samでの認証情報取得 key名を変更
function createAuthPayload() {
    console.log("createAuthPayload() start");
    const type = configSheet.getRange(3, 2).getValue();
    const key = configSheet.getRange(4, 2).getValue();
    const seacret = configSheet.getRange(5, 2).getValue();

    let payload = {}; 
    if (type != "email") {
        payload = {
            "authKeyId": key,
            "authKey": seacret 
        }
    } else {
        payload = {
            "email": key,
            "password": seacret
        }
    }
    console.log("createAuthPayload() end");
    return payload;
}


// auth token 取得
function getAuth(payload) {
    //auth_header json
    const authUrl = soracomUrl + "auth";
    const options = {
        "method": "post",
        "payload": JSON.stringify(payload),
        "headers": { "Content-Type": "application/json" },
        "muteHttpExceptons": true
    };
    var res = UrlFetchApp.fetch(
          authUrl,
          options,
    )
    const authJson = httpResCheck(res);
    //console.log("resBody:" + resBody);
    return authJson;
}

function createAuthHeader(authJson) {
  console.log("createAuthHeader() start");
  const apiKey = authJson.apiKey;
  const authToken = authJson.token;
  const operatorId = authJson.operatorId;

  console.log("createAuthHeader() end");
  return {
    "X-Soracom-API-Key":apiKey,
    "X-Soracom-Token":authToken,
    "Content-Type": "application/json"
  }
}
```

#本記事でカバーができていない部分

- SIMの枚数が多い場合に発生しそうな事
    - listOrderedSubscribers がpagingされる可能性がありますので、必要に応じてpaginationの対応をご検討ください。(Apps Scriptの実行時間制限にかかる可能性もあります。）
    - APIのToo many request（発生しにくいようにwaitをおいて実行していますが、too many requestが発生した場合にはAPIの呼び出しにsleep時間の延長を考慮ください）

- Apps Scriptの制約については調べきれておりませんが、実行回数制限などの確認などは各自でお願い致します。

- 同様に[Apps Scriptのdeployや管理](https://developers.google.com/apps-script/concepts/deployments)についてはご自身でご検討をお願いします。

- 複雑化、長時間化するようタスクの検討をされている方には、個人的に[AWS Lambda](https://aws.amazon.com/jp/lambda/) + [AWS Step Functions(Express)](https://aws.amazon.com/jp/step-functions/?step-functions.sort-by=item.additionalFields.postDateTime&step-functions.sort-order=desc)のご利用をおすすめしたいです。AWS Lambdaも1関数だと実行時間に制約があります。また、関数の稼働時間で課金対象にもなるためにLambdaの中でのsleepはコスト的に非効率となります。 AWS Step Functionsを利用するとそういったsleep時間をStep Functionsで受けたり、特定条件での分岐も出来るので通常のsleepと too many requestsを受けたときのsleepタイムを変えたりすることができたり、大規模化や複雑化したバックエンド処理を検討する場合には相性がよいサービスかなと思います。

- HTTPリクエストのレスポンスとしての200かそれ以外程度のエラー系しか設定していません。また網羅的なテストケースなども実施していないのであくまでも実装例としての参考として読んでいただければと思います。

- application のセキュリティ観点から必要最小限の権限セットのSAMユーザを作成して実行頂くことを推奨します。


# その他ご参考


- CLIをベースに類似のオペレショーン業務の例：[APIを利用 SIM の発注から管理を自動化する](https://users.soracom.io/ja-jp/tools/api/automate-ordering/)
- APIでこんなことができますというユースケースを紹介している資料： [SORACOM API使いこなしレシピ集](https://www.slideshare.net/SORACOM/a2-soracom-api-soracom-technology-camp-2020/)
- API実行の練習環境として[sandbox](https://users.soracom.io/ja-jp/tools/api-sandbox/)も提供しています。

# 最後に

よくあるお問い合わせや相談から 注文->グループ登録を支援する方法についてGoogle spread sheetで簡単に自動化してみました。同様にある程度定型化された簡単な業務であればバックオフィス機能を作らなくてもGoogle spread sheetから解決できることがありそうです。お困りの方にとって少しでも助けになれば幸いです。