TT Lab
はじめる
学ぶ 学習パス コース

CBA — Backstage認定アソシエイト

本番設定で起動したポータルのバックエンドが準備完了にならない

TT Labで続きを見る

目標

本物のBackstageバックエンドを、運用と同じように起動します。設定ファイル2つを--configで重ね、秘密情報は環境変数で入れ、ブラウザーの画面が呼び出せるようにCORSを開き、再起動してもデータが残るようにDBを移します。判定はファイルではなく、起動しているプロセスの動作(待ち受けるポート、準備状態、401/200、応答ヘッダー、再起動後に残ったデータ)で行います。

なぜ重要なのか

ローカルでは動いていたバックエンドが、運用に載せると、見当違いのポートで待ち受けたり、起動しているのに準備ができていなかったり、画面からだけAPI呼び出しが止まったり、再起動のたびに登録したものが消えたりします。原因は、ほとんどがコードではなく、設定が重なる順序、抜けた環境変数、オリジンが違うクライアントとサーバー、メモリDBです。マージのルールを頭で知っていることと、起動しているプロセスで、その結果を測ってみることは、別のものです。

最初のVMの起動に4分ほどかかり、ステップ1のインストールは、インターネットから約30秒かかります。このVMにはdockerもpodmanもないので、コンテナイメージのビルドは行いません(command -v dockerが空です)。フロントエンドのアプリもビルドせず、ブラウザーの代わりに、Originヘッダーを付けたcurlで、CORSの応答を確認します。

ステップ

  1. /usr/local/cba-prodに、バックエンドのパッケージを固定バージョンでインストールします。
  2. 基本と運用の設定ファイル2つを--configで重ねて起動し、順序をひっくり返すと何が変わるかを見ます。
  3. ${PORTAL_API_TOKEN}なしで起動したバックエンドが準備できないことを確認し、秘密情報を環境変数で入れます。
  4. backend.cors.originで、ポータルの画面のオリジンだけを許可します。
  5. APP_CONFIG_環境変数で、ファイルを直さずに許可するオリジンを広げます。
  6. :memory:のDBで再起動すると、登録したlocationが消えることを記録します。
  7. DBをディレクトリに移して、再起動のあとにも残るようにします。
  8. 今のプロセスで測った値で、運用設定の点検表を書きます。

参考

運用設定を試すバックエンドを、固定バージョンで取得する

/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で、ツールがあるかどうかを確認してください。