追加したバックエンドプラグインが401しか返さない
目標
VMの中で本物のBackstageバックエンドプロセスを起動し、バックエンドプラグインを自分で作って付けます。新しいパスがなぜ401を返すのか、公開パスをどう開けるのか、設定・ほかのプラグイン・拡張ポイントをプラグインのコードからどう使うのかを、HTTP応答とログで確認します。
なぜ重要なのか
Backstageを直すということは、大半がプラグインを追加するか、既存のプラグインを広げることです。新しいバックエンドプラグインを付けたら、すべてのリクエストが401になった場合、コードのバグを探して時間を使いがちです。実際には、デフォルトの認証ポリシーがルーターよりも前で止めています。同じ理由で、プラグインがカタログを呼び出すときにもトークンが必要で、カタログの動作を変えるときは、そのパッケージを直さずにモジュールを追加します。
このラボは、バックエンドだけを扱います。フロントエンドプラグイン(React・Material UI)は、アプリのバンドルをビルドする必要がありますが、このVMではそのビルドを行わないので、直接は確認しません。レポートで、2つの違いを整理します。
最初のVMの起動に4分ほどかかり、ステップ1のインストールは、インターネットから約30秒かかります。コードは、TypeScriptではなく、CommonJSのJavaScript 1ファイル(index.js)で書きます。
ステップ
/usr/local/cba-pluginに、バックエンドのパッケージ6つを、正確なバージョンでインストールします。- カタログプラグインだけを入れたバックエンドを、7007番ポートで起動します。
oncallプラグインを付けて、認証なしで呼び出した結果(401)を記録します。addAuthPolicyで、/pingの1つのパスだけを開けます。rootConfigで、設定値をリクエストごとに読み取り、再起動せずに設定を変えて、応答の変化を記録します。auth・discoveryサービスで、カタログAPIをサービス間で呼び出します。- カタログモジュール(
createBackendModule)で、エンティティプロバイダーを追加します。 - 401・404がどの層から出るのかと、バックエンドプラグインとフロントエンドプラグインの違いを報告します。
参考
- バックエンドの再起動(標準入出力を切り離さないと、シェルがぶら下がります):
cd /usr/local/cba-plugin && pkill -f 'node index.js'; setsid nohup node index.js > backend.log 2>&1 < /dev/null & - 準備の確認:
curl -s http://127.0.0.1:7007/.backstage/health/v1/readiness - ログの色文字を取り除く:
sed 's/\x1b\[[0-9;]*m//g' backend.log - ルートディスクは数百MBしかありません。インストールしたものとキャッシュは、
/usr/localの下に置いてください。 - バックエンドプラグインとモジュールの作り方: https://backstage.io/docs/backend-system/building-plugins-and-modules/index
- Http Routerサービス(addAuthPolicy): https://backstage.io/docs/backend-system/core-services/http-router
- Root Configサービス: https://backstage.io/docs/backend-system/core-services/root-config
- サービス間認証: https://backstage.io/docs/auth/service-to-service-auth
- モジュールと拡張ポイント: https://backstage.io/docs/backend-system/architecture/modules
- アーキテクチャの概要(フロントエンド・バックエンドプラグイン): https://backstage.io/docs/overview/architecture-overview
- Root Healthサービス: https://backstage.io/docs/backend-system/core-services/root-health
プラグインを載せるバックエンドの材料を、正確なバージョンで取得する
/usr/local/cba-pluginにnpmプロジェクトを作成し、次の6つのパッケージを範囲指定なしの正確なバージョンでインストールしてください。package.jsonのdependenciesにも、^なしでそのバージョンが書かれている必要があります。ルートディスクが小さいので、npmキャッシュはnpm_config_cache=/usr/local/cba-plugin/.npmcacheで、スクラッチディスクに置きます。@backstage/backend-defaults@0.17.8、@backstage/backend-plugin-api@1.10.0、@backstage/plugin-catalog-backend@3.9.1、@backstage/plugin-catalog-node@2.2.4、better-sqlite3@12.4.1、express@4.22.3です。
npm install --save-exact 패키지@버전 ...(プレースホルダーはパッケージ名とバージョンです)は、package.jsonに範囲なしで書きます。better-sqlite3はネイティブモジュールなので、最新の13.xには、このNode 20用のビルド済みバイナリがなく、コンパイルを試みて失敗します。backend-defaultsが要求する12.xから、固定バージョンを選ぶ理由です。インストールが終わったら、npm ls --depth=0でバージョンを確認してください。
カタログだけを入れたバックエンドを起動する
/usr/local/cba-plugin/app-config.yamlと/usr/local/cba-plugin/catalog/org.yaml、/usr/local/cba-plugin/index.jsを作成して、カタログプラグインだけを入れたバックエンドを、7007番ポートで起動してください。設定は、backend.baseUrl: http://localhost:7007、backend.listen.port: 7007、backend.databaseはclient: better-sqlite3・connection: ':memory:'、catalog.rulesは[{allow: [Component, Group, Location]}]、catalog.locationsにtype: file・target: ./catalog/org.yamlです。org.yamlには、Group team-paymentsと、そのチームが所有するComponent payments-api・refund-workerを入れます。index.jsは、createBackend()に@backstage/plugin-catalog-backendをaddして、startします。プロセスは、/usr/local/cba-pluginでnode index.jsとして起動し、出力を/usr/local/cba-plugin/backend.logに送ってください。/.backstage/health/v1/readinessが200なら、準備できています。
シェルを閉じても生き続ける必要があるので、setsid nohup node index.js > backend.log 2>&1 < /dev/null &のように、標準入出力をすべて切り離してください(切り離さないと、コマンドが終わらずにぶら下がります)。起動ログでPlugin initialization completeの行を探し、認証なしで/api/catalog/entitiesを呼び出すと何が返ってくるかも、見ておいてください。
新しいプラグインのパスが、すべて401になる
index.jsに、createBackendPluginでpluginId oncallのプラグインを作って、addしてください。coreServices.httpRouterでexpressルーターを登録し、GET /ping(→ {"ok":true})とGET /roster(当番のJSON)の2つのパスを置きます。認証ポリシーは、まだ追加しません。再起動したあと、認証なしでhttp://127.0.0.1:7007/api/oncall/pingを呼び出した結果を、curl -s -iの出力のまま、/root/cba-plugin/before-policy.txtに保存してください。
プラグインのルーターは、/api/<pluginId>の下に付きます。コードにバグがなくても401が出る理由を、ドキュメントのデフォルトの認証ポリシーから探してみてください。比較のために、/api/oncall/없는경로と/api/없는플러그인/x(プレースホルダーは存在しないパスと存在しないプラグインです)も呼び出してみると、401がどの層から出るのかが見えます。
状態確認のパス1つだけを、認証なしで開ける
oncallプラグインのinitで、http.addAuthPolicyを使い、1つのパス(/ping)だけをallow: 'unauthenticated'で開けてください。再起動のあと、認証なしで/api/oncall/pingは200 {"ok":true}、/api/oncall/rosterは、それでも401である必要があります。
ポリシーのpathは、ルーターに書いたパスと同じ形式(プラグインの接頭辞なし)です。プラグイン全体を開ける設定(backend.auth.dangerouslyDisableDefaultAuthPolicy)は、すべてのプラグインを認証なしにしてしまうので、使わないでください。
当番のチャンネルを、コードではなく設定から読み取る
app-config.yamlにoncall.channel: '#payments-oncall'を入れ、oncallプラグインにcoreServices.rootConfigを注入して、リクエストを受けるたびにconfig.getString('oncall.channel')を返すGET /channel(→ {"channel":"..."}、認証なしで許可)を追加してから、再起動してください。応答を確認したあと、再起動せずに設定ファイルの値を'#platform-oncall'に直し、応答が変わることを確認してください。/root/cba-plugin/channel.txtに、before=<처음 응답의 channel>とafter=<바뀐 응답의 channel>(プレースホルダーは、最初の応答のchannelと、変わった応答のchannelです)の2行を残します。
設定ファイルは、バックエンドが見守っていて、変わると再び読み取ります(ログにFound 0 new secrets in configが、もう一度出力されます)。それでも値が変わらないなら、initで一度読み取って、変数に入れたままにしていないかを見てください。応答が変わるまで、数秒ポーリングしてください。
プラグインがカタログを呼び出すときにも、トークンが必要
oncallプラグインに、GET /services(認証なしで許可)を追加してください。このハンドラーは、coreServices.authのgetPluginRequestToken(onBehalfOfはauth.getOwnServiceCredentials()、targetPluginIdはcatalog)でトークンを受け取り、coreServices.discoveryのgetBaseUrl('catalog')でアドレスを取得して、/entities?filter=kind=componentをAuthorization: Bearerヘッダーで呼び出したあと、{"catalogStatus": <카탈로그 응답 코드>, "names": [Component 이름 정렬]}(プレースホルダーは、カタログの応答コードと、Component名を並べ替えたものです)を返します。再起動のあと、応答のnamesにpayments-apiとrefund-workerがある必要があります。ステップ5で変えたチャンネルの設定は、そのままにしておきます。
プラグイン同士は、コードで互いを呼び出せず、HTTPでしか通信しません。そのため、同じプロセスの中でも、カタログのデフォルトの認証ポリシーを通過するトークンが必要です。起動の直後は、カタログがファイルをまだ処理できておらず、一覧が空になることがあるので、数秒後にもう一度呼び出してください。バックエンドのログで、/api/catalog/entitiesのリクエストのUser-Agentが何と記録されるかも、見てください。
カタログを直さずに、モジュールでエンティティを押し込む
createBackendModuleで、pluginId catalog、moduleId pager-providerのモジュールを作って、addしてください。@backstage/plugin-catalog-nodeのcatalogProcessingExtensionPointを注入して、addEntityProviderでprovider名pager-providerを登録し、connectでapplyMutation({type: 'full', ...})により、Component pager-bridge(owner team-payments、backstage.io/managed-by-location・backstage.io/managed-by-origin-locationアノテーションを含む)を入れます。org.yamlには入れません。再起動のあと、/api/oncall/servicesのnamesに、pager-bridgeがpayments-api・refund-workerと一緒に出る必要があります。
モジュールは、対象のプラグインが公開した拡張ポイントだけで、そのプラグインを広げます。カタログのパッケージ自体ではなく、-nodeライブラリのパッケージから拡張ポイントを取り込む理由を、モジュールのドキュメントで確認してください。providerが入れるエンティティも、locationアノテーションがなければ、処理の段階でふるい落とされます。
401と404がどの層から出るのかを報告する
/root/cba-plugin/report.mdの最初の5行に、今起動しているバックエンドで直接確認した値を、키=값(プレースホルダーはキーと値です)の形式で書いてください。mount_path=(oncallプラグインのルーターが付いたパス)、protected_route_status=(認証なしで/roster)、unknown_route_status=(認証なしで、oncallプラグインの下の存在しないパス)、unknown_plugin_status=(登録されていないpluginIdの下のパス)、catalog_direct_status=(認証なしで/api/catalog/entities)です。その下に、バックエンドプラグインとフロントエンドプラグインが、それぞれどこで動いて、どうつながるかの説明を書いてください。
数字は、記憶ではなく、curlで今測った値を移してください。存在しないパスが404ではない理由は、認証の検査がルーターよりも前にあるからです。説明には、フロントエンドプラグインがブラウザーで動き、backend.baseUrlの/api/<pluginId>を呼び出すという点を入れてください。