プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

kintone のデプロイに Ginue を試してみた

kintone で複数のアプリが連携して動作するような場合、開発環境をどうやって本番環境へ反映させるか?というのが結構な問題になると思います。

gusuku deploit を使っていれば、ある程度はアプリのコピーをカバーできるのですが、

  • ライセンス上、異なるドメインへアプリをコピーする場合はコピー先にもライセンスが必要になっている
  • 無料プランだと 10 アプリまでしか対応していない

という点でちょっと使いにくいです。潤沢に予算があればこれを使いましょうで終われるんですけどね・・・

なので、改めて、 kintone のデプロイ方法としていい方法がないか調べてみたところ、 Ginue というツールが結構良さそうだったので、試してみました。そのあたりの一連のことをメモっておきます。

  • node v20.10.0
  • Ginue 2.2.1

デプロイ方法について調査

まずは、 kintone が提供している標準機能についてです。

アプリテンプレート/スペーステンプレートは、アプリ/スペースの内容をテンプレートにして、そのテンプレートを元にアプリ/スペースを作成することで、元のアプリ/スペースを反映することができます。

この方法の難点としては、

  • アプリ/スペースの新規作成時にしか、テンプレートの指定ができない

というのにつきます。つまり、プロジェクト開始時にデプロイするときはうまく使えるのですが、あとからアプリの一部にフィールドを追加したとか、設定を変更した、というのを反映することができません。

デプロイ API を使えば、自作デプロイツールを作ることも不可能ではないのですが、なかなかそこまでの労力はかけられないので、最終手段になるでしょうね。ま、存在は知っていても損はないので、一応書いておきました。

オープンソースなどのツール

kintone がデプロイ API を提供しているので、先人がツールでも作ってくれていないかな?と思ってちょっと調べてみると、2つほど見つかりました。

リポジトリは次の通りです。

このうち、 kdx は 2020 年あたりでリリースやコミットが止まっているようです。一方、 Ginue は安定版のリリースこそ 2020 年で止まってますが、ベータ版のリリースや Github のコミットは最近(2023 年)まで続いているので、こちらはまだ動いている(と期待してもよい)ようです。

というわけで、今回は Ginue を試してみることにしました。あ、 kdx のほうは試用していないだけで、使えないというわけではありませんので、お間違いないように。興味を持った方はぜひ試してください。

試用

Ginue に絞ってさらに調べてみると、いくつかの紹介記事がありました。

あと、 Ginue のリポジトリの説明も結構充実しているので、これらを参考にして試用します。

アプリの作成

まずは、 kintone にログインして、サンプルとなるアプリを作成しておきます。

フィールドはテキストフィールドを一つだけ持って、jsカスタマイズファイルを指定しておきます。

次に ginue を使うのがデプロイのためなので、デプロイ先になるアプリも作成しておきます。こちらは、空っぽのアプリにしておきます。

これで、アプリの準備ができたので、次は Ginue の設定に移ります。

インストール

まずは、 Ginue をインストールします。

PS D:\work\tmp\kintone_tutorial\sample_ginue> npm install -D ginue

up to date, audited 473 packages in 1s

73 packages are looking for funding
  run `npm fund` for details

5 moderate severity vulnerabilities

To address issues that do not require attention, run:
  npm audit fix

Some issues need review, and may require choosing
a different dependency.

Run `npm audit` for details.
PS D:\work\tmp\kintone_tutorial\sample_ginue> 

設定ファイルを作成

ginue を動かすとき、コマンドライン引数であれこれを指定することもできますが、設定ファイルを作っておけば、それに従って動いてくるようです。設定ファイルは、 js, json, yaml の形式で作れるようです。今回は json の形式で作成します。

.ginuerc.json というファイルを下記のように作りました。

{
    "location": "kintone-settings/ginue-customize-js-test",
    "env": {
      "development": {
        "domain": "サブドメイン.cybozu.com",
        "username": "ユーザー名",
        "password": "パスワード",
        "app": {
            "test_ginue": nnn
        }
      },
      "ginue_test": {
        "domain": "サブドメイ.cybozu.com",
        "username": "ユーザー名",
        "password": "パスワード",
        "app": {
            "test_ginue": mmm
        }
      }
    }
}

env の下にある development と ginue_test というのが環境を表す名称になります。いくつでも作れるそうです。

先頭の location が kintone の設定内容を保存するフォルダの指定になります。上記のようにサブフォルダ指定もできます。

環境ごとに、サブドメイン名、ユーザー名、パスワードを指定しておくこともできます(指定がない場合は、コマンド実行時に聞かれるようです)。サブドメイン名を環境に応じて指定できるということは、自分の開発環境の内容をお客様開発の環境に移行するということも問題なくできそうですね。

app にはアプリを指定します。複数のアプリを指定することもできます。オブジェクト形式で記述しておくと、コマンドラインの引数としてアプリ名を指定することができるようになります。

既存アプリを取得

.ginuerc.jsonが作成できたら、既存アプリを取得してみます。

PS D:\work\tmp\kintone_tutorial\sample_ginue> npx ginue pull development
location: "kintone-settings/ginue-customize-js-test"
environment: "development"
domain: "サブドメイン.cybozu.com"
username: "ユーザー名"
password: [hidden]
app: {
  "test_ginue": nnn
}

kintone-settings/ginue-customize-js-test/development/test_ginue/field_acl.json
kintone-settings/ginue-customize-js-test/development/test_ginue/app_form_layout.json
kintone-settings/ginue-customize-js-test/development/test_ginue/record_acl.json
kintone-settings/ginue-customize-js-test/development/test_ginue/app.json
kintone-settings/ginue-customize-js-test/development/test_ginue/app_customize.json
kintone-settings/ginue-customize-js-test/development/test_ginue/app_acl.json
kintone-settings/ginue-customize-js-test/development/test_ginue/app_views.json
kintone-settings/ginue-customize-js-test/development/test_ginue/app_status.json
kintone-settings/ginue-customize-js-test/development/test_ginue/form.json
kintone-settings/ginue-customize-js-test/development/test_ginue/app_form_fields.json
kintone-settings/ginue-customize-js-test/development/test_ginue/app_settings.json
PS D:\work\tmp\kintone_tutorial\sample_ginue> 

こんな感じでフィールド設定や、アプリの設定内容が取得されます。

保存先は、.ginuerc.json で指定したディレクトリの配下に、環境名、アプリ名でディレクトリを分けてファイルを保存してくれます。なので、直接 json ファイルを編集することで設定を変更することもできますね。

デプロイのテスト

次に、取得したアプリを別の環境にデプロイするのを試します。デプロイは2段階に分かれており、最初は取得した設定内容をデプロイ先のアプリに反映する処理になります。

PS D:\work\tmp\kintone_tutorial\sample_ginue> npx ginue push development:ginue_test
location: "kintone-settings/ginue-customize-js-test"
environment: "development"
domain: "サブドメイン名.cybozu.com"
username: "ユーザー名"
password: [hidden]
app: {
  "test_ginue": nnn
}

domain: "サブドメイン名.cybozu.com"
username: "ユーザー名"
password: [hidden]
app: {
  "test_ginue": mmm
}
environment: "ginue_test"
location: "kintone-settings/ginue-customize-js-test"

? [push] Are you sure? Yes
app/form/fields.json
? Add field "文字列__1行_" to ginue_test.test_ginue? Yes
app/form/layout.json
[SKIP] app/acl.json
app/status.json
[SKIP] record/acl.json
[SKIP] field/acl.json
app/views.json
app/settings.json
PS D:\work\tmp\kintone_tutorial\sample_ginue> 

途中で、push してもよいか確認されるので、yを押すと処理が行われます。また、フィールドの追加や削除があった場合も、その都度確認されます。

次に、 push した内容(変更した内容)をデプロイしてアプリに反映させます。

PS D:\work\tmp\kintone_tutorial\sample_ginue> npx ginue deploy ginue_test          
location: "kintone-settings/ginue-customize-js-test"
environment: "ginue_test"
domain: "サブドメイン名.cybozu.com"
username: "ユーザー名"
password: [hidden]
app: {
  "test_ginue": mmm
}

? [deploy] Are you sure? Yes
PS D:\work\tmp\kintone_tutorial\sample_ginue> 

エラー等が出なければ、これで完了です。デプロイ先の kintone のアプリを開いてみると、確かにフィールドが追加されているのがわかります。

感想

Ginue 使ってみると、かなり便利そうです。細かいところは見ていませんが、多くの設定は正しくコピーされているようです。

ただ、今回試して感じたのは、先に、デプロイ先のアプリを用意しておく必要あるので、この部分はアプリ/スペーステンプレートを使って準備すると楽になりそうかな?というところです。それに、 push 時にフィールドの追加・削除があるとそのたびに確認されるので、空のアプリへの push だと結構確認作業だけでも大変かもしれません。

そういう意味ではやっぱり最初はアプリ/スペーステンプレートを使うのが無難そうですね。

制限事項

あと、今回試してみて気が付いたこととしては

  • アプリ名自体はコピーされない
  • カスタマイズの指定がデプロイ先に反映されない(URL, js ファイル指定とも)
  • アプリ/レコード/フィールドのアクセス権設定が反映されない

というのがありました。

1つ目については、上述したようにデプロイ先を準備する際にアプリ/スペーステンプレートを使えば大きな問題にはならなさそうです。

2つ目が実際に使おうと思うと、デプロイ時にはちょっと面倒かもしれません。自分でカスタマイズファイルのアップロードスクリプトを作成することでカバーすることになりそうです。

もっとも、2つ目の js ファイルについては、リポジトリを見ると js ファイルのダウンロード機能を追加しているコミットがあるので、次のリリース時にはカバーされるのかもしれません。

3つ目についてはどうすればいいのかな?ちょっとこの部分はネックになるかもしれませんが、複雑なアクセス権を設定していないようであれば、手作業での対応もありかもしれませんね。

補足

どうも、上記の反映されていないという内容は、 ginue pull で取得したファイルのうち、上記の push を実行した際に、画面に SKIP と表示されたファイルおよび何も表示されていないファイルに対応しているっぽいです。

具体的には

  • app_acl.json
  • app_customize.json
  • app.json
  • field_acl.json
  • form,json
  • record_acl.json

が該当しています。それぞれのファイル名は、 kintone REST API の各 URL に対応しているようです。せっかくなので、上記のファイルに対応したAPIは多分これだろうな、と推測したものを以下に載せておきます。

まとめ

上記の試用とは別に実際のプロジェクトを流用して試しても見たのですが、プロセス管理などもちゃんんとコピーされてました。もちろん、細かく見たわけではないので、他にも設定が反映されていないところなどもあるかもしれませんが、おおむね使えそうな印象です。

ということで、今回使った Ginue は実際のプロジェクトでも結構使えそうです。このまま開発が進んでくれるといいなー。

node の開発環境を ESModule に変更

kintone のアップロードツールがあるのはありがたいのですが、再度、開発環境でローカルサーバーを使おうとすると、いちいち設定を変更するのが、なかなかかったるいです。

ということで、こちらの記事で、ローカルサーバーへの切り替え処理を書いた(最終的には無駄になった)のですが、その際に package.json に type を追加してプロジェクト全体を ESModuleに指定しました。

package.json

{
  (略)
  "type": "module",
  (略)
}

すると、 webpack を実行した際に、

PS D:\work\tmp\kintone_tutorial\revert_live_server> npx webpack --mode development 
[webpack-cli] Failed to load 'D:\work\tmp\kintone_tutorial\revert_live_server\webpack.config.js' config
[webpack-cli] ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and 'D:\work\tmp\kintone_tutorial\revert_live_server\package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
    at file:///D:/work/tmp/kintone_tutorial/revert_live_server/webpack.config.js:1:14
    at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
    at async importModuleDynamicallyWrapper (node:internal/vm/module:431:15)
    at async WebpackCLI.tryRequireThenImport (D:\work\tmp\kintone_tutorial\revert_live_server\node_modules\webpack-cli\lib\webpack-cli.js:232:34)
    at async loadConfigByPath (D:\work\tmp\kintone_tutorial\revert_live_server\node_modules\webpack-cli\lib\webpack-cli.js:1406:27)
    at async WebpackCLI.loadConfig (D:\work\tmp\kintone_tutorial\revert_live_server\node_modules\webpack-cli\lib\webpack-cli.js:1515:38)
    at async WebpackCLI.createCompiler (D:\work\tmp\kintone_tutorial\revert_live_server\node_modules\webpack-cli\lib\webpack-cli.js:1781:22)
    at async WebpackCLI.runWebpack (D:\work\tmp\kintone_tutorial\revert_live_server\node_modules\webpack-cli\lib\webpack-cli.js:1877:20)
    at async Command.<anonymous> (D:\work\tmp\kintone_tutorial\revert_live_server\node_modules\webpack-cli\lib\webpack-cli.js:944:21)
PS D:\work\tmp\kintone_tutorial\revert_live_server> 

とエラーが出るようになりました。 エラーメッセージ見ても分かるように、 node が本来持っている CommonJS のモジュールと ESModule が混在しているのが原因でしょう。

ネットを調べると、メッセージの内容は異なりますが、似たような記事が見つかりました。

[0] Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: Module: /Users/....../webpack.config.js [0] require() of ES modules is not supported.と出た時の対処法 #package.json - Qiita

なので、こちらを参考にして、 webpack.config.js を webpack.config.cjs に替えてやれば問題なく動作しました。

なお、他にも webpack 実行時にいろいろとエラー(例えば import は拡張子まで含めて指定する必要がある、など)が出るので、エラーメッセージを読みながら修正しておきました。

(略)
ERROR in ./src/kintone-revert-test.js 1:0-33
Module not found: Error: Can't resolve './lib/util' in 'D:\work\tmp\kintone_tutorial\revert_live_server\src'
Did you mean 'util.js'?
BREAKING CHANGE: The request './lib/util' failed to resolve only because it was resolved as fully specified
(probably because the origin is strict EcmaScript Module, e. g. a module with javascript mimetype, a '*.mjs' file, or a '*.js' file where the package.json contains '"type": "module"').
The extension in the request is mandatory for it to be fully specified.
Add the extension to the request.
resolve './lib/util' in 'D:\work\tmp\kintone_tutorial\revert_live_server\src'
(略)

同様に ESLint と Prettier も拡張子を

  • .eslintrc.js -> .eslintrc.cjs
  • .prettierrc.js -> prettierrc.cjs

と変更したら、一応動いたようです。

ご参考までに。

kintone カスタマイズ設定を Live Server に戻す

こちらの記事でまとめたように、 kintone のアプリに対してカスタマイズファイルをアップロードできます。

ですが、自分の場合だと、普段は Live Server 設定で VSCode 上でデバッグして、ある程度開発が終わったら、一度カスタマイズファイルとしてアップロードして、動作確認&本番環境への反映、となるので、カスタマイズファイルアップロード後に不具合があると Live Server 設定で再度デバッグ等を行うことになります。

ということで、複数アプリのカスタマイズファイル設定を Live Server を使う設定に戻すための js を作ってみました。

という記事だったんですが、最終的な結論としては、 kintone の customize-uploader が対応してくれていたので、以下の作業は不要になりました。まあ、せっかくなので、残しておきますが、 customize-uploader を使った方法はこちらで紹介しておきます。

作成

アプリの設定を変更するのは kintone REST API client を利用します。

PS D:\work\tmp\kintone_tutorial\revert_live_server> npm install --save-dev @kintone/rest-api-client

up to date, audited 301 packages in 2s

71 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
PS D:\work\tmp\kintone_tutorial\revert_live_server> 

js ファイルの主な処理は、次のようになります。

  • 最初に app.getDeployStatus でアプリのステータスを取得します
  • ステータスが正しく取得されて、それらがすべて SUCCESS になっていることを確認します
  • 確認出来たら、app.updateAppCustomize でアプリのカスタマイズ設定を Live Server のURLに変更します。
  • 最後に、 app.deployApp を呼び出して、変更を反映します。

こういう流れになった理由については、後述してある補足もご覧ください。

最終的に作成した js ファイルはこんな感じです(こちらに書いたように ESModule にしています)。

#!/bin/bash/env node
"use strict";

import process from "node:process";
import {parseArgs} from "node:util";
import {KintoneRestAPIClient} from "@kintone/rest-api-client";
import {DEV_ENV_APP_ID} from "../src/lib/const/AppIdConfig.js";

const DEV_ENV_URL = "https://kintoneのドメイン名.cybozu.com";
const WEBPACK_DIST_URL = "https://localhost:5500/dist/";

// webpack.config.js の entry のキーに対応したファイル名を記述
const APP_JS_DEV = {
    sample_app: "kintone-revert-test.js",
};
function usage() {
    console.log("Usage: node  kintone_revert_localserver.js  [-h]  username  password");
    console.log("    -h:  display help");
    console.log("    username: kintone login username");
    console.log("    password: password for username");
}
function parseCliArgs() {
    const options = {
        help: {
            type: "boolean",
            short: "h",
        },
    };
    const {values, positionals} = parseArgs({
        options: options,
        allowPositionals: true,
    });
    // console.log(values);
    // console.log(positionals);
    if (values.help) {
        usage();
        return false;
    }
    if (positionals.length !== 2) {
        console.warn("invalid parameters");
        usage();
        return false;
    }
    return positionals;
}
/**
 * js カスタマイズ設定を、ローカルサーバーに戻す
 * @param {*} client
 */
async function revertCustomizeToLocalServer(client) {
    Object.entries(APP_JS_DEV).forEach(async (elm) => {
        const param = {
            app: DEV_ENV_APP_ID[elm[0]],
            desktop: {
                js: [
                    {
                        type: "URL",
                        url: WEBPACK_DIST_URL + elm[1],
                    },
                ],
            },
        };
        try {
            await client.app.updateAppCustomize(param);
        } catch (error) {
            console.error("failed to revert js customize to localserver, param:" + JSON.stringify(param));
            console.error(error);
        }
    });
}
/**
 * アプリの更新状況を取得
 * @param {*} client
 */
async function getDeployStatuses(client) {
    const app_ids = Object.keys(APP_JS_DEV).map((elm) => {
        return DEV_ENV_APP_ID[elm];
    });
    const param = {
        apps: app_ids,
    };

    try {
        const status = await client.app.getDeployStatus(param);
        return status;
    } catch (error) {
        console.error("failed to get app deploy status, param:" + JSON.stringify(param));
        console.error(error);
    }
    return undefined;
}
/**
 * 変更したアプリ設定を更新
 *
 * 変更した全てのアプリを同時に指定して、APIを呼び出す
 * @param {*} client
 */
async function updateAppSettings(client) {
    const app_ids = Object.keys(APP_JS_DEV).map((elm) => {
        return {
            app: DEV_ENV_APP_ID[elm],
        };
    });
    const param = {
        apps: app_ids,
        revert: false,
    };

    try {
        await client.app.deployApp(param);
    } catch (error) {
        console.error("failed to update app settings, param:" + JSON.stringify(param));
        console.error(error);
    }
}

/** ///////////////////////////// */
// 以下、メイン処理

const parsed = parseCliArgs();
if (!parsed) {
    process.exit(1);
}

const client = new KintoneRestAPIClient({
    baseUrl: DEV_ENV_URL,
    auth: {
        username: parsed[0],
        password: parsed[1],
    },
});

// アプリのデプロイ状況をチェック
console.log("deploy status, before update:");
const status = await getDeployStatuses(client);
console.log(JSON.stringify(status));

if (!status) {
    console.warn("status cannot get. so update process stopped.");
    process.exit(1);
}

if (
    status.apps.some((elm) => {
        return elm.status !== "SUCCESS";
    })
) {
    console.warn("some app is not deploied. so update process stopped.");
    process.exit(1);
}

console.log("start process to revert to localserver's URL");
// カスタマイズファイルの設定を localserver に戻す
await revertCustomizeToLocalServer(client);

// アプリの変更を反映
console.log("update app settings");
await updateAppSettings(client);
console.log("deploy status, after update:");
console.log(JSON.stringify(await getDeployStatuses(client)));

console.log("finish process to revert");

補足

実は上記の処理において、 getDeployStatus を呼ばずに、 deployApp を適用すると

KintoneRestAPIError: [520] [GAIA_DA02] データベースのロックに失敗したため、変更を保存できませんでした。時間をおいて再度お試しください。 (mlTC5KYXuKy0DOpUvvxs)

といったメッセージが出てきます。

でも、一度 getDeployStatus を実行後に、 deployApp を実行すると、問題なく完了します。 getDeployStatus を呼び出すタイミングは、updateAppCustomize 実行前でも後でも変わらず、大丈夫でした。

ドキュメントのどこを見ても、 deployApp の前に getDeployStatus を実行する必要がある、なんて記述はないようですが、何かあるんでしょうかね?

とりあえず実行できないと困るので、上記のような処理にしています。

試してみる

上記を作成したら、試してみます。

src/lib/const に AppIdConfig.js というファイルを用意して、

/**
 * アプリケーションID を定義するモジュール
 */

// 開発環境
const APP_ID_DEV = {
    sample_app: nnn,
};

// ステージング環境
const APP_ID_STAGE = {
    sample_app: nnn,
};

// 本番環境
const APP_ID_PROD = {
    sample_app: nnn,
};

/**
 * 複数環境におけるアプリ ID のオブジェクト
 */
export const environments = {
    "開発環境.cybozu.com": [APP_ID_DEV],
    "本番環境.cybozu.com": [APP_ID_STAGE, APP_ID_PROD],
};

/**
 * 開発環境のアプリ ID
 *
 * 開発環境には1つしかスペースがない前提
 */
export const DEV_ENV_APP_ID = environments["開発環境.cybozu.com"][0];

こんな感じに、アプリ名(sample_app)とそのアプリIDを対応させておきます。このようなファイルを利用している理由は、もともとこのファイルは、カスタマイズ時に環境ごとにアプリIDを切り替えるために定義していたものになります(こちらの記事で触れています)。

これを利用して、アプリIDを特定するということを行っています。

実行するには、

PS D:\work\tmp\kintone_tutorial\revert_live_server> node .\bin\kintone_revert_localserver.js ユーザー名 パスワード
deploy status, before update:
{"apps":[{"app":"nnn","status":"SUCCESS"}]}
start process to revert to localserver's URL
update app settings
deploy status, after update:
{"apps":[{"app":"nnn","status":"PROCESSING"}]}
finish process to revert
PS D:\work\tmp\kintone_tutorial\revert_live_server>

のように呼び出せばできます。

customize-uploader

さあ、これで Live Server に簡単に戻せるな、と思っていたところ、ふと気がついたのですが、 customize-uploader の js ファイルとして URL を指定すれば、切り替えられるんじゃないか?という疑問が出てきました。

早速、試してみます。

dest/customize-manifest-revert.json としてURLを記載しておきます。

{
    "app": "nnn",
    "scope": "ALL",
    "desktop": {
        "js": ["https://localhost:5500/dist/kintone-revert-test.js"],
        "css": []
    },
    "mobile": {
        "js": [],
        "css": []
    }
}

これをコマンドラインで指定します。

PS D:\work\tmp\kintone_tutorial\revert_live_server> npx kintone-customize-uploader --base-url https://kintoneのドメイン名.cybozu.com --username ユーザー名 --password パスワード dest/customize-manifest-revert.json
カスタマイズのアップロードを開始します
JavaScript/CSS ファイルをアップロードしました!
JavaScript/CSS カスタマイズの設定を変更しました!
運用環境への反映の完了を待っています...
運用環境への反映の完了を待っています...
運用環境への反映の完了を待っています...
運用環境に反映しました!
PS D:\work\tmp\kintone_tutorial\revert_live_server> 

あ、できてしまいました・・・。kintone 側のアプリの設定を見ても、ちゃんと切り替わっています。

ということで、上記のスクリプトは無駄になってしまいましたが、一応残しておきますね。

まとめ

上記のように customize-uploader で Live Server に戻せるのに気が付いた後、 customize-uploader のリポジトリをみたら、ちゃんとサンプルに URL を含んだものが載ってました。

ドキュメントはちゃんと確認しないといけませんね。