kintoneを使ってSORACOM Air SIM自動管理アプリ作ってみた

## はじめに

案件開発をする際にほぼ必ずSORACOM Air SIMを使うのですが、社内で管理しているSIMの数が多すぎてどのSIMがどの認証情報を使っててどのサービス設定されてて...っていうのを把握しきれなくなり、パッと見てSIMの情報を把握できるアプリを作ろうと思いました。そこで今回AWS lambda, kintoneを使ってSIMの自動管理アプリを作りました。

## 完成品

今回作ったアプリはこんな感じです。表示内容を「すべて」にしているのでテーブルがはみ出してますが、
表示内容をカスタマイズして必要項目に絞ることもできます

![kintone_app_sample.png](https://qiita-image-store.s3.amazonaws.com/0/192406/ae429d71-147f-01d0-3ee4-52870658221e.png)

## 構成

今回のアーキテクチャは以下の図のようになってます。

![図1.png](https://qiita-image-store.s3.amazonaws.com/0/192406/fc823380-4c9b-b34d-15b4-3fea91d568a5.png)

毎日9:00にCloudWatch Eventからlambdaをinvokeし、SORACOM APIを使ってkintoneにSIMの情報を更新させています。

## kintoneアプリのフォーム設定

フォームの一覧としては以下の形で作成しました。

![FireShot Capture 177 - アプリの設定 - https___kyoso.cybozu.com_k_admin_app_flow_app=524#section=form.png](https://qiita-image-store.s3.amazonaws.com/0/192406/d6710cbe-f6d4-40e2-087f-16b16c9644c8.png)

今回一括で知りたかった情報をフォームに入れたため、IPアドレスなどの詳細に関しては扱っていません。
フォーム項目のそれぞれのキー名等は以下のようにしました。
※ `レコード番号` 等の自動設定のキー名の変更方法ご存知の方いらっしゃいましたら教えてください...

```json
"レコード番号": {
        "type": "RECORD_NUMBER",
        "value": "32"
    },
    "air_check": {
        "type": "CHECK_BOX",
        "value": ["VPG"]
    },
    "harvest_radio": {
        "type": "RADIO_BUTTON",
        "value": "off"
    },
    "group_name": {
        "type": "SINGLE_LINE_TEXT",
        "value": "グループ名"
    },
    "更新者": {
        "type": "MODIFIER",
        "value": {
            "code": "Administrator",
            "name": "Administrator"
        }
    },
    "作成者": {
        "type": "CREATOR",
        "value": {
            "code": "Administrator",
            "name": "Administrator"
        }
    },
    "imsi": {
        "type": "SINGLE_LINE_TEXT",
        "value": "imsi番号"
    },
    "$revision": {
        "type": "__REVISION__",
        "value": "1"
    },
    "make_date": {
        "type": "CREATED_TIME",
        "value": "YYYY-MM-DDTHH:mm:ssZ"
    },
    "beam_radio": {
        "type": "RADIO_BUTTON",
        "value": "on"
    },
    "krypton_radio": {
        "type": "RADIO_BUTTON",
        "value": "off"
    },
    "update_date": {
        "type": "UPDATED_TIME",
        "value": "YYYY-MM-DDTHH:mm:ssZ"
    },
    "shared_key": {
        "type": "SINGLE_LINE_TEXT",
        "value": "事前共有鍵名"
    },
    "sim_name": {
        "type": "SINGLE_LINE_TEXT",
        "value": "SIM名"
    },
    "auth_name": {
        "type": "SINGLE_LINE_TEXT",
        "value": "認証情報名"
    },
    "endorse_radio": {
        "type": "RADIO_BUTTON",
        "value": "off"
    },
    "funnel_radio": {
        "type": "RADIO_BUTTON",
        "value": "off"
    },
    "status": {
        "type": "SINGLE_LINE_TEXT",
        "value": "active"
    },
    "$id": {
        "type": "__ID__",
        "value": "1"
    }
}
```

## lambdaの構成

lambda内の主な処理の構成は

1. SORACOM APIを使用するのに必要なトークンを取得する
2. kintoneに現在保存されている情報一覧を取得する
3. SORACOM APIを使用してSIM情報とグループ情報の一覧を取得する
4. 3で取得したデータを前項のjson形式に合うように成形する
5. 2の情報と4の情報を比較して

  - 追加するデータ
  - 更新するデータ
  - 削除するデータ
  - 変更不要のデータ

  に分類し、配列に収める
6. 5で作成したデータをそれぞれに対応する処理にかけてアプリを更新する

です。

## 使用したSORACOM API

今回使用したSORACOM APIは以下の3つです

- [トークン取得](https://dev.soracom.io/jp/docs/api/#!/Auth/auth)
- [SIM情報取得](https://dev.soracom.io/jp/docs/api/#!/Subscriber/listSubscribers)
- [グループ情報取得](https://dev.soracom.io/jp/docs/api/#!/Group/listGroups)

私が説明するより[リファレンス](https://dev.soracom.io/jp/docs/api/#/)の方が100億倍わかりやすいのでリンクだけ貼っておきます...

## 使用したkintone API

今回使用したkintone APIは以下の4つです。

- [レコード一覧の取得](https://kintone.github.io/kintone-nodejs-sdk/reference/record/#getrecordsappid-query-fieldscode-isshowtotalcount)
- [レコードの追加](https://kintone.github.io/kintone-nodejs-sdk/reference/record/#addrecordsappid-recordsdata)
- [レコードの更新](https://kintone.github.io/kintone-nodejs-sdk/reference/record/#updaterecordsappid-recordsdata)
- [レコードの削除](https://kintone.github.io/kintone-nodejs-sdk/reference/record/#deleterecordsappid-recordids)

私が説明するより[リファレンス](https://kintone.github.io/kintone-nodejs-sdk/reference/record/)の方が(以下略)

## AWS環境の構築

同期が最近「terraform!terraform!」って言っていたのでそんなに推してるなら使ってみよう。と思い
今回lambdaとCloudWatch eventの設定はterraformを使ってみました。
今までserverless frameworkを使っていたので少し抵抗はあったのですが、
基本から教えてもらうとserverless frameworkより直感的でわかりやすかったです。
moduleさえ用意しておけば汎用的に使えるしファイル内に記載するキー名(?)も省略されてないので読めばterraform初心者でも何をしているのか大体理解することができました。
(個人の意見なので異論はもちろん認めます)

## 躓いたところ

### lambdaからkintone APIたたけない?!

kintone APIを使用するにあたって[公式サイト](https://developer.cybozu.io/hc/ja/articles/360000511243-kintone-API-SDK-β-for-Node-js)を参考に始めたんですけど、

```js
let basicUserName = 'xxxxx'; // basicAuth user name
let basicPassword = 'xxxxx'; // basicAuth password 
```

この`basicUserName`と`basicPassword`ってなに？！ってなりました...
自分のユーザーとパスワード載せてどうすんの...って思って...
ただ`APIToken`だけで**ローカル**で実行できたしいいやーって思ってそのままにしてたんですけど、
lambdaに乗せたら`TypeError: Cannot read property 'id' of null`って怒られてしまいました...
同期に言われて気づいたんですけど、普段社内ネットワークでつないでて気づかなかったんですが、
社内ネットワーク以外でログインしようとするとベーシック認証通さないといけなくて`basicUserName`と`basicPassword`はそのときに使うユーザー名とパスワードだったみたいです。
なるほど納得
lambdaなど外部のネットワークからAPIを利用する時はこの部分を設定しないとAPIを使えないみたいです。

### 1日に1回invokeされるように設定したはずなのに1分に1回invokeされてる?!

CloudWatch eventの設定をtfファイル上で[これを参考にして](https://www.terraform.io/docs/providers/aws/r/cloudwatch_event_rule.html)以下のように設定しました。(以下は一部)

```tf
resource "aws_cloudwatch_event_rule" "cloudwatch_event_rule_info" {
  name        = "soracom_sim_update_event"
  description = "SORACOM SIM管理アプリ(kintone)の更新lambdaをキックするイベント"

  event_pattern = <<PATTERN
{
  "detail-type": [
    "Scheduled Event"
  ]
}
PATTERN

  schedule_expression = "cron(0 0 * * ? *)"
}
```

この状態で実行し、コンソール上で確認してもクーロン式で毎日UTC00:00にinvokeされるな
と思っていたら、なんと1分に1回invokeするようになってました😱
同じような内容のルールと比較してみると

```json
{
  "detail-type": [
    "Scheduled Event"
  ]
}
```

期待通り動作しているルールには `detail-type`が存在していない...
試しにこれを削除して実装してみると無事に1日1回のinvokeに変わりました
根本原因はまだわからず...もしご存知の方がいらっしゃいましたら教えてください...

## kintoneアプリ内にある全レコードを取得できない？！(2019/02/28追記)

SIMの枚数が約150枚あるアカウントでこのシステムを動かすと[lambdaの構成](https://qiita.com/drafts/877ba987c29555d7c32b/edit#lambda%E3%81%AE%E6%A7%8B%E6%88%90)にある`追加するデータ`の中に既にアプリに存在するSIMの情報が...そしてkintoneのAPI側でも`すでに存在する値なので追加できません`とエラーが...
なぜ？？と思ってみてみたら、最初にkintoneアプリからレコードを全件取得していたつもりができていませんでした...取得件数を見てみると**100件**...
確かにアプリ上での表示件数は最大100件だしAPIでは1回に100件しか取得できないのか...と納得
どうやって残りの50件を取得したかというと、下の感じで全部取りきれてなかったら再帰で取得する...として解決しました。getRecordsのレスポンスの情報から再帰的に全件取得するの、私には難しかった...
だってgetRecordsのレスポンスってレコードの情報が連想配列で入ってるのと指定したqueryで取得できるレコードの数しかないんですよ？

```.js
  /**
   * kintoneアプリ上のレコード数を取得
   */
  static getTotalRecords() {
    return new Promise((resolve, reject) => {
      kintoneRecord
        .getRecords(appId, "", [], true)
        .then(res => {
          return resolve(Number(res.totalCount));
        })
        .catch(err => {
          return reject(err);
        });
    });
  }
  /**
   * kintoneアプリにスキャンをかける
   * 1度に100件までしか取得できないので
   * 全件が100件超える場合は再帰で取得する
   * @param {object[]} scanRecords
   * @param {string} query
   * @param {number} recordCounts
   */
  static getRecords(scanRecords, query, recordCounts) {
    return new Promise((resolve, reject) => {
      kintoneRecord
        .getRecords(appId, query, [], true)
        .then(res => {
          console.log("[SCAN KINTONE RECORDS] success!!");
          scanRecords = scanRecords.concat(res.records);
          if (scanRecords.length === recordCounts) {
            console.log("[SCAN KINTONE RECORDS] All done!!");
            return resolve(scanRecords);
          } else if (scanRecords.length < recordCounts) {
            const startRecordNum = Number(
              scanRecords[scanRecords.length - 1]["レコード番号"]["value"]
            );
            const query = `レコード番号 < ${startRecordNum}`;
            return resolve(
              KintoneController.getRecords(scanRecords, query, recordCounts)
            );
          }
        })
        .catch(err => {
          if (typeof err === Function) {
            return reject(err.getAll());
          }
          return reject(err);
        });
    });
  }
  /**
   * kintoneアプリ上にあるレコードを全件スキャン
   */
  static scanKintoneRecords() {
    return new Promise(async (resolve, reject) => {
      try {
        const recordCounts = await KintoneController.getTotalRecords();
        console.log("[SCAN RECORDS COUNT] ", recordCounts);
        let scanRecords = [];
        scanRecords = await KintoneController.getRecords(
          scanRecords,
          "",
          recordCounts
        );
        console.log("[SCAN KINTONE RECORDS] ", JSON.stringify(scanRecords));
        console.log("[SCANED KINTONE RECORDS COUNT] ", scanRecords.length);
        return resolve(scanRecords);
      } catch (err) {
        return reject(err);
      }
    });
  }

```

## まとめ

SORACOM APIもkintoneのアプリ作成もterraformも初めてだったんですけどどれも使いやすかったのでわりとすんなり(?)作成できました。SIM管理に困ってる方がいましたらぜひ。

## コード内容

[github](https://github.com/sunagawa-1219/soracom_sim_man)

あまり可読性に自信がないのですが参考にしていただければと思います...
PRお待ちしてます😓
nodeはv8.10を使用しています。
※今回メインはkintoneアプリ開発なのでlambdaのコードのみでterraformのコードは載せません。また別の機会に記事にできれば良いなぁと思います。