本番設定で起動したポータルのバックエンドが準備完了にならない
目標
本物のBackstageバックエンドを、運用と同じように起動します。設定ファイル2つを--configで重ね、秘密情報は環境変数で入れ、ブラウザーの画面が呼び出せるようにCORSを開き、再起動してもデータが残るようにDBを移します。判定はファイルではなく、起動しているプロセスの動作(待ち受けるポート、準備状態、401/200、応答ヘッダー、再起動後に残ったデータ)で行います。
なぜ重要なのか
ローカルでは動いていたバックエンドが、運用に載せると、見当違いのポートで待ち受けたり、起動しているのに準備ができていなかったり、画面からだけAPI呼び出しが止まったり、再起動のたびに登録したものが消えたりします。原因は、ほとんどがコードではなく、設定が重なる順序、抜けた環境変数、オリジンが違うクライアントとサーバー、メモリDBです。マージのルールを頭で知っていることと、起動しているプロセスで、その結果を測ってみることは、別のものです。
最初のVMの起動に4分ほどかかり、ステップ1のインストールは、インターネットから約30秒かかります。このVMにはdockerもpodmanもないので、コンテナイメージのビルドは行いません(command -v dockerが空です)。フロントエンドのアプリもビルドせず、ブラウザーの代わりに、Originヘッダーを付けたcurlで、CORSの応答を確認します。
ステップ
/usr/local/cba-prodに、バックエンドのパッケージを固定バージョンでインストールします。- 基本と運用の設定ファイル2つを
--configで重ねて起動し、順序をひっくり返すと何が変わるかを見ます。 ${PORTAL_API_TOKEN}なしで起動したバックエンドが準備できないことを確認し、秘密情報を環境変数で入れます。backend.cors.originで、ポータルの画面のオリジンだけを許可します。APP_CONFIG_環境変数で、ファイルを直さずに許可するオリジンを広げます。:memory:のDBで再起動すると、登録したlocationが消えることを記録します。- DBをディレクトリに移して、再起動のあとにも残るようにします。
- 今のプロセスで測った値で、運用設定の点検表を書きます。
参考
- 運用のポートは7300です。準備の確認:
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:7300/.backstage/health/v1/readiness - ログは、
/usr/local/cba-prod/backend.logに追記します。起動のたびにLoading config fromの行で始まり、NODE_ENV=productionなら、その後はJSONが1行ずつです。 - CORSの確認:
curl -s -D - -o /dev/null -H 'Origin: http://portal.example.test:3000' http://127.0.0.1:7300/.backstage/health/v1/readiness - プロセスの環境変数の確認:
tr '\0' '\n' < /proc/$(pgrep -f 'node index.js' | head -1)/environ | grep -E 'NODE_ENV|APP_CONFIG|PORTAL' - 設定ファイルの書き方(--configの順序・APP_CONFIG_・${VAR}): https://backstage.io/docs/conf/writing
- 外部呼び出し用の静的トークン(externalAccess): https://backstage.io/docs/auth/service-to-service-auth
- プラグインのDB設定(better-sqlite3): https://backstage.io/docs/tutorials/configuring-plugin-databases
- 運用デプロイの概要: https://backstage.io/docs/deployment/
- Dockerイメージでのデプロイ(このVMでは扱いません): https://backstage.io/docs/deployment/docker
- アーキテクチャの概要(フロントエンドのアプリとバックエンド): https://backstage.io/docs/overview/architecture-overview
- backend.corsの設定スキーマ(config.d.ts): https://github.com/backstage/backstage/blob/master/packages/backend-defaults/config.d.ts
運用設定を試すバックエンドを、固定バージョンで取得する
/usr/local/cba-prodに、npmプロジェクト(package.jsonを含む)を作成し、3つのパッケージを範囲指定なしの正確なバージョンでインストールしてください。npmキャッシュは、npm_config_cache=/usr/local/cba-prod/.npmcacheに置きます。@backstage/backend-defaults@0.17.8、@backstage/plugin-catalog-backend@3.9.1、better-sqlite3@12.4.1です。
npm init -yのあとに、npm install --save-exact ...です。package.jsonは、依存関係の記録だけでなく、バックエンドが起動するときにプロジェクトのルートを探す基準でもあります(なければ、起動がNoPkgJsonFoundで落ちます)。
運用ファイルを重ねたら、別のポートで待ち受ける
/usr/local/cba-prod/app-config.yaml(基本)と/usr/local/cba-prod/app-config.production.yaml(運用)を作成してください。基本は、app.baseUrl: http://localhost:3000、backend.baseUrl: http://localhost:7007、backend.listen.port: 7007、DBはbetter-sqlite3・':memory:'、catalog.rules: [{allow: [Component, Location]}]、locationはfile・./catalog/portal.yaml(Component portal-web)です。運用は、backend.baseUrl: http://localhost:7300、backend.listen.port: 7300だけです。カタログプラグインだけを入れたindex.jsと、バックエンドをNODE_ENV=productionでnode index.js --config app-config.yaml --config app-config.production.yamlとして再び起動する/usr/local/cba-prod/start.shを作成し(出力は/usr/local/cba-prod/backend.logに追記)、実行してください。比較のために、1回は--configの順序をひっくり返して起動し、待ち受けるポートを見て、/root/cba-prod/order.txtにreversed_listen_port=<그때 포트>(プレースホルダーはそのときのポートです)を1行残したあと、start.shに戻します。
--configを1つでも与えると、基本ファイルの自動ロードはオフになり、与えた順序で重なり、あとのものが勝ちます。ログの最初の行Loading config from MergedConfigSource{...}で、実際に読んだファイルと順序が見えます。ss -ltnpで、待ち受けるポートを確認してください。start.shは、シェルが終了しても生き続けるように、setsid nohup ... >> backend.log 2>&1 < /dev/null &で起動します。
秘密情報が抜けたまま起動したバックエンドは、準備ができない
運用ファイルに、backend.auth.externalAccessとしてtype static、token ${PORTAL_API_TOKEN}、subject ops-cliを追加してください。まず環境変数なしで起動して、/.backstage/health/v1/readinessが何を返すかを見て、ログからMissing required config valueを含むメッセージを、/root/cba-prod/missing-env.txtに保存してください。そのあと、/root/cba-prod/secrets.envにPORTAL_API_TOKEN=<직접 만든 24자 이상 무작위 값>(プレースホルダーは、自分で作成した24文字以上のランダムな値です)を、権限600で置き、start.shがそのファイルを読み取って環境変数として渡すように直して、再び起動してください。トークンなしで/api/catalog/entitiesは401、Authorization: Bearer <그 값>(プレースホルダーはそのトークンの値です)なら200である必要があり、トークンの値は、どのYAMLにも書かれてはいけません。
${VAR}の置換に使われた環境変数がなければ、その値はまるごとなくなり、必須の値を読み取るサービスが、開始するときに失敗します。プロセスは落ちず、ポートは開いているので、「起動しているのに動かない」状態になります。NODE_ENV=productionのログはJSONなので、jq -r .messageで読みやすくなります。値は、openssl rand -hex 16やnode -p "require('crypto').randomBytes(24).toString('base64')"で作成してください。
ブラウザーのポータル画面だけが、バックエンドを呼び出せる
運用ファイルに、app.baseUrl: http://portal.example.test:3000とbackend.cors.origin: http://portal.example.test:3000を追加して、start.shで再び起動してください。Origin: http://portal.example.test:3000ヘッダーでリクエストすると、応答に同じ値のAccess-Control-Allow-Originが返り、Origin: http://evil.example.testでは、そのヘッダーがない必要があります。OPTIONSのプリフライトリクエスト(Access-Control-Request-Method: GET)も1回送って、応答コードを見てください。
フロントエンドのアプリは、ブラウザーでapp.baseUrlとして開き、backend.baseUrlのAPIを呼び出します。2つのオリジン(スキーム・ホスト・ポート)が違えば、ブラウザーがバックエンドのCORS応答ヘッダーを見て、許可するかどうかを決めます。curlはCORSを強制しないので、ヘッダーが返ってくるかで確認してください: curl -s -D - -o /dev/null -H 'Origin: ...' URL。
ファイルを直さずに、このデプロイだけでOriginを広げる
ローカル開発用の画面http://localhost:3000も、このバックエンドを呼び出せるようにしてください。ただし、YAMLは直さないでください。start.shで、APP_CONFIG_backend_cors_origin環境変数に、JSON配列["http://portal.example.test:3000","http://localhost:3000"]を入れて、再び起動します。2つのOriginのどちらもAccess-Control-Allow-Originを受け取り、http://evil.example.testは、それでも受け取れない必要があります。
APP_CONFIG_のあとの名前で、_が.に変わって設定のキーになり、値はJSONとして先に解釈されます。環境変数は、すべての設定ファイルよりも優先されます。ログの最初の行のEnvConfigSource{count=...}が、いくつに変わるかを見てください。シェルの引用符の中に、JSONのダブルクォートが生きている必要があります。
再起動したら、登録したlocationが消えた
トークンでPOST /api/catalog/locationsに{"type":"url","target":"https://git.example.test/portal/catalog-info.yaml"}を送って登録し(201)、GET /api/catalog/locationsの個数を数えてから、start.shで再起動して、もう一度数えてください。/root/cba-prod/memory.txtに、id=<등록 응답의 location.id>、before_restart=<개수>、after_restart=<개수>(プレースホルダーは登録応答のlocation.idと個数です)の3行を残します。
登録の応答が201でも、そのurlを実際に読み取るのは、あとの処理の段階です(このホストは存在しないアドレスなので、読み取りは失敗します。ここでは、登録の記録が残るかどうかだけを見ます)。DBの設定が何だったかを、思い出してください。
DBをディスクに移せば、再起動しても残る
運用ファイルに、backend.database.connection.directory: /usr/local/cba-prod/dbを追加してください(clientは、基本ファイルのbetter-sqlite3がマージされます)。start.shで再び起動し、同じurlをもう一度登録して、そのPOST応答のJSONを/root/cba-prod/persist.jsonに保存したあと、もう一度再起動して、GET /api/catalog/locations/<id>が200であることを確認してください。
オブジェクトはキー単位で深くマージされるので、運用ファイルにはconnectionだけを書けばよいです。directoryを与えると、プラグインごとにSQLiteのファイルができます。lsで、どんなファイルができたかを見てください。運用では、通常はPostgreSQLを使います。
運用設定の点検表を、今のプロセスで埋める
/root/cba-prod/report.mdの最初の5行に、今起動しているバックエンドで測った値を書いてください。listen_port=(nodeが待ち受けるポート)、base_port_open=(7007に接続できるかどうか。yes/no)、evil_origin_allowed=(ACAOを受け取るかどうか。yes/no。対象はhttp://evil.example.test)、env_config_count=(最後の起動ログのEnvConfigSource count)、locations_now=(個数。GET /api/catalog/locations)です。その下に、ブラウザーの画面(app.baseUrl)とバックエンド(backend.baseUrl)がどのように通信するか、NODE_ENV=productionでログに出た警告、このVMでDockerイメージのビルドをしなかった理由を、書いてください。
数字とyes/noは、記憶ではなく、今測った値です。ss -ltnp、curl、grep 'Loading config from' backend.log | tail -1です。警告は、JSONログで"level":"warn"の行を探してください。command -v dockerで、ツールがあるかどうかを確認してください。