Skip to content
Published on

Backstage運用実践 — TechDocs、プラグイン開発、本番チェックリスト

Authors

はじめに — インストールは簡単、運用は難しい

Backstageをデモとして立ち上げるだけなら半日で十分です。しかし6か月後も生きているBackstage、つまりドキュメントが最新で、プラグインが安定して動作し、アップグレードが滞らず、数百人が毎日使うBackstageを作るのはまったく別の問題です。第1回でカタログを、第2回でScaffolderを扱いましたが、最終回となる第3回のテーマは運用です。TechDocsでドキュメントをコードのように管理するパイプライン、プラグインアーキテクチャとカスタムプラグイン開発、そして本番運用の全領域(権限、アップグレード、性能、観測、セキュリティ、組織モデル)をチェックリストとともに整理します。

TechDocs — ドキュメントをコードのように

TechDocsはBackstageのdocs-as-codeソリューションです。中心となるアイデアはシンプルです。ドキュメントをコードの隣にMarkdownとして置き、MkDocsでビルドし、カタログのエンティティに紐づいたドキュメントサイトとして配信することです。ドキュメントがコードと同じリポジトリ、同じPR、同じレビューを通るため、「コードは変わったのにドキュメントはそのまま」という問題が構造的に減ります。

リポジトリ側に必要なものは2つです。

# mkdocs.yml (リポジトリルート)
site_name: payment-api
site_description: 決済APIサービスのドキュメント

nav:
  - はじめに: index.md
  - アーキテクチャ: architecture.md
  - 運用ガイド: runbook.md
  - オンボーディング: onboarding.md

plugins:
  - techdocs-core
payment-api/
├── catalog-info.yaml      # backstage.io/techdocs-ref: dir:. アノテーション
├── mkdocs.yml
└── docs/
    ├── index.md
    ├── architecture.md
    ├── runbook.md
    └── onboarding.md

ビルド戦略 — ローカルビルド vs 外部パイプライン

TechDocsのビルドには2つのモードがあります。

戦略動作適した状況
ローカルビルド (out-of-the-box)Backstageバックエンドがリクエスト時に直接ビルドデモ、小規模、導入初期
外部パイプライン (recommended)CIがビルドしオブジェクトストレージへアップロード本番、大規模

本番の推奨構成は外部パイプラインです。Backstage本体からビルド負荷を切り離し、ドキュメントの更新をコードのマージ時点で起きるようにできるからです。

  開発者がPRをマージ
       |
       v
  CI (GitHub Actions)
       |  techdocs-cli generate   (MkDocsビルド)
       |  techdocs-cli publish    (S3アップロード)
       v
  S3バケット (techdocs-bucket)
       ^
       |  読み取り専用
  Backstage TechDocs Backend ---> ユーザーのブラウザ

CIパイプラインの例です。

# .github/workflows/techdocs.yaml
name: techdocs
on:
  push:
    branches: [main]
    paths: ['docs/**', 'mkdocs.yml']
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - name: Install tooling
        run: |
          pip install mkdocs-techdocs-core
          npm install -g @techdocs/cli
      - name: Generate docs
        run: techdocs-cli generate --no-docker
      - name: Publish to S3
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.TECHDOCS_AWS_KEY }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.TECHDOCS_AWS_SECRET }}
        run: |
          techdocs-cli publish \
            --publisher-type awsS3 \
            --storage-name techdocs-bucket \
            --entity default/Component/payment-api

Backstage側の設定は次のとおりです。

# app-config.yaml
techdocs:
  builder: 'external'        # バックエンドのビルドを無効化
  publisher:
    type: 'awsS3'
    awsS3:
      bucketName: techdocs-bucket
      region: ap-northeast-2

運用のヒントを一つ加えると、第2回で扱ったScaffolderのスケルトンにmkdocs.ymlとdocsディレクトリの骨格を含めてください。新しいサービスが「ドキュメントのある状態」で生まれるようにすることが、ドキュメント文化を作る最も現実的な方法です。

プラグインアーキテクチャ — すべてがプラグイン

Backstageの設計哲学は「すべての機能はプラグイン」です。カタログもScaffolderもTechDocsもすべてプラグインです。構造を理解すればカスタム開発が容易になります。

+------------------- Backstageモノレポ (yarn workspaces) --------------------------+
|                                                                                 |
|  packages/app  (フロントエンドシェル)      packages/backend (バックエンドシェル)      |
|  +--------------------------+           +-----------------------------+         |
|  | React SPA                |           | Node.js                     |         |
|  | - プラグインページのルーティング|  HTTP    | - プラグイン別ルーターをmount   |         |
|  | - エンティティページのタブ/カード| <-------> | - DB/キャッシュ/スケジューラ提供 |         |
|  +--------------------------+           +-----------------------------+         |
|                                                                                 |
|  plugins/                                                                       |
|  ├── my-plugin           (フロントエンドプラグイン: Reactコンポーネント)              |
|  ├── my-plugin-backend   (バックエンドプラグイン: REST API、DBアクセス)              |
|  └── my-plugin-common    (共有型、isomorphicコード)                               |
+---------------------------------------------------------------------------------+

バックエンドは「新バックエンドシステム(new backend system)」が標準です。依存性注入ベースで、プラグインが必要なインフラサービス(ロガー、DB、スケジューラ、ディスカバリ)を宣言すると、フレームワークが注入してくれます。バックエンドのエントリポイントが次のように宣言的になったことが核心です。

// packages/backend/src/index.ts (新バックエンドシステム)
import { createBackend } from '@backstage/backend-defaults';

const backend = createBackend();

backend.add(import('@backstage/plugin-app-backend'));
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(import('@backstage/plugin-scaffolder-backend'));
backend.add(import('@backstage/plugin-techdocs-backend'));
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-permission-backend'));
// 社内カスタムプラグイン
backend.add(import('@internal/plugin-deploy-board-backend'));

backend.start();

カスタムプラグイン実践 — 社内デプロイ状況ボード

「今どのサービスがどの環境にどのバージョンで稼働しているか」を表示するデプロイ状況ボードを作ると仮定し、フロントエンド/バックエンドの骨格を見てみます。プラグイン作成はCLIから始めます。

yarn new   # メニューから plugin / backend-plugin を選択

バックエンド — デプロイデータAPI

// plugins/deploy-board-backend/src/plugin.ts
import {
  coreServices,
  createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { createRouter } from './router';

export const deployBoardPlugin = createBackendPlugin({
  pluginId: 'deploy-board',
  register(env) {
    env.registerInit({
      deps: {
        logger: coreServices.logger,
        httpRouter: coreServices.httpRouter,
        database: coreServices.database,
      },
      async init({ logger, httpRouter, database }) {
        httpRouter.use(await createRouter({ logger, database }));
      },
    });
  },
});
// plugins/deploy-board-backend/src/router.ts
import { Router } from 'express';
import express from 'express';

export async function createRouter(options: {
  logger: any;
  database: any;
}): Promise<Router> {
  const router = Router();
  router.use(express.json());

  // デプロイイベントの収集 (CDパイプラインが呼び出す)
  router.post('/deployments', async (req, res) => {
    const { component, environment, version, status } = req.body;
    options.logger.info(`deployment: ${component} ${version} -> ${environment}`);
    // databaseハンドルからknexインスタンスを取得して保存
    const knex = await options.database.getClient();
    await knex('deployments').insert({
      component,
      environment,
      version,
      status,
      deployed_at: new Date(),
    });
    res.status(201).json({ ok: true });
  });

  // コンポーネント別の最新デプロイ照会 (フロントエンドが呼び出す)
  router.get('/deployments/:component', async (req, res) => {
    const knex = await options.database.getClient();
    const rows = await knex('deployments')
      .where({ component: req.params.component })
      .orderBy('deployed_at', 'desc')
      .limit(20);
    res.json(rows);
  });

  return router;
}

フロントエンド — エンティティページのカード

// plugins/deploy-board/src/components/DeployBoardCard.tsx
import React from 'react';
import { useEntity } from '@backstage/plugin-catalog-react';
import { useApi, fetchApiRef, discoveryApiRef } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import { InfoCard, Table } from '@backstage/core-components';

export const DeployBoardCard = () => {
  const { entity } = useEntity();
  const fetchApi = useApi(fetchApiRef);
  const discoveryApi = useApi(discoveryApiRef);

  const { value, loading, error } = useAsync(async () => {
    const baseUrl = await discoveryApi.getBaseUrl('deploy-board');
    const res = await fetchApi.fetch(
      `${baseUrl}/deployments/${entity.metadata.name}`,
    );
    return res.json();
  }, [entity.metadata.name]);

  if (error) return <InfoCard title="デプロイ状況">取得失敗</InfoCard>;

  return (
    <InfoCard title="デプロイ状況">
      <Table
        isLoading={loading}
        options={{ paging: false, search: false }}
        columns={[
          { title: '環境', field: 'environment' },
          { title: 'バージョン', field: 'version' },
          { title: 'ステータス', field: 'status' },
          { title: 'デプロイ時刻', field: 'deployed_at' },
        ]}
        data={value ?? []}
      />
    </InfoCard>
  );
};

このカードをエンティティページ(EntityPage)の概要タブに組み込めば、カタログでサービスを開くたびに環境別のデプロイ状態が見えます。カタログ(第1回)の上にデータを載せる典型的なパターンです。CDパイプライン側は、デプロイ完了時点で上記のPOSTエンドポイントを呼ぶ1行を追加するだけです。

主要なエコシステムプラグイン

自作する前にエコシステムを先に確認してください。実務で採用率の高いプラグインです。

プラグイン提供機能連携キー
Kubernetesエンティティ別のPod/Deployment状態kubernetes-id アノテーション
ArgoCD同期状態、デプロイ履歴argocd アプリセレクタのアノテーション
SonarQube品質ゲート、カバレッジsonarqube プロジェクトキー
Grafanaダッシュボード/アラートの埋め込みgrafana ダッシュボードセレクタ
PagerDutyオンコール表示、インシデント作成pagerduty インテグレーションキー
GitHub Actionsワークフロー実行状態project-slug アノテーション
Cost Insightsクラウドコストの可視化コストデータアダプタの実装

共通パターンが見えるはずです。すべてカタログのアノテーションで有効化されます。第1回で強調した「アノテーションガバナンス」がここで効いてきます。アノテーションが十分に埋まったカタログであれば、プラグインの追加は設定数行で終わります。

権限 — Permission Framework

デフォルトのBackstageでは、ログインしたすべてのユーザーがすべてを閲覧できます。組織が大きくなると「Scaffolderテンプレートの実行は正社員のみ」「特定システムのエンティティは該当部門のみ」といった要求が出てきます。permission frameworkはこれをポリシーコードで解決します。

構造は3つの部分から成ります。プラグインが権限クエリを発行し(例: catalog.entity.delete)、ポリシー(policy)が許可/拒否を決定し、決定はユーザーのアイデンティティと条件(conditional decision)を反映できます。

// packages/backend/src/extensions/permissionPolicy.ts
import { createBackendModule } from '@backstage/backend-plugin-api';
import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha';
import {
  PermissionPolicy,
  PolicyQuery,
  PolicyQueryUser,
} from '@backstage/plugin-permission-node';
import {
  AuthorizeResult,
  PolicyDecision,
  isPermission,
} from '@backstage/plugin-permission-common';
import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common/alpha';

class AcmePermissionPolicy implements PermissionPolicy {
  async handle(
    request: PolicyQuery,
    user?: PolicyQueryUser,
  ): Promise<PolicyDecision> {
    // カタログエンティティの削除はplatform-teamのみ許可
    if (isPermission(request.permission, catalogEntityDeletePermission)) {
      const isPlatformTeam = user?.info.ownershipEntityRefs.includes(
        'group:default/platform-team',
      );
      return {
        result: isPlatformTeam ? AuthorizeResult.ALLOW : AuthorizeResult.DENY,
      };
    }
    return { result: AuthorizeResult.ALLOW };
  }
}

export const permissionModuleAcmePolicy = createBackendModule({
  pluginId: 'permission',
  moduleId: 'acme-policy',
  register(reg) {
    reg.registerInit({
      deps: { policy: policyExtensionPoint },
      async init({ policy }) {
        policy.setPolicy(new AcmePermissionPolicy());
      },
    });
  },
});

有効化はapp-configで行います。

permission:
  enabled: true

ポリシー設計の原則は「デフォルト許可、危険な操作のみ制限」から始めることです。最初から緻密な拒否ポリシーを敷くと、ポータルの定着そのものが阻害されます。

本番運用 — アップグレード、モノレポ、DB

リリーストレインとアップグレード戦略

Backstageは毎月メインラインリリースを出し、その間にパッチが出ます。アップグレードを先送りにするほどマイグレーションコストが非線形に増大するというのが、実務の共通した経験です。推奨する運用方式は次のとおりです。

  • 四半期に最低1回、可能なら毎月のアップグレードスロットをカレンダーに固定します。
  • アップグレードは専用CLIで実施します。
# すべての@backstageパッケージを目標リリースへバンプ
yarn backstage-cli versions:bump

# 変更されたパッケージ間の重複/衝突を点検
yarn backstage-cli versions:check

# ビルドとテストで検証
yarn tsc && yarn build:all && yarn test:all
  • 各リリースの変更点はアップグレードヘルパー(Backstage Upgrade Helper)でdiffを確認しながら適用します。特にバックエンドシステムとフロントエンドシステムのマイグレーションガイドは、リリースノートと併せて必ず読むべきです。

yarnモノレポの管理

Backstageアプリはyarn workspacesのモノレポです。運用で頻繁に遭遇する問題は依存関係の重複です。同じ@backstageパッケージの異なるバージョンが共存すると、型エラーやランタイムの誤動作が発生し得ます。yarn backstage-cli versions:check --fix で解消し、カスタムプラグインの@backstage依存はpeerDependency的に管理して本体バージョンに追従させるのが安全です。

DBマイグレーション

各バックエンドプラグインは自分のスキーマをKnexマイグレーションで管理し、起動時に自動適用されます。運用上重要なのは2点です。第一に、アップグレード直後の初回起動はマイグレーションの時間分だけ遅くなり得るため、readinessプローブのタイムアウトに余裕を持たせてください。第二に、ロールバックのシナリオです。スキーマが前進した状態で旧バージョンに戻すと壊れる可能性があるため、アップグレード前のDBスナップショットを標準手順に含める必要があります。

性能 — カタログの大規模化とキャッシング

エンティティが数千を超えると、気を配るべきポイントが出てきます。

  • 処理ループの負荷: すべてのエンティティは周期的に再処理されます。エンティティ数が増えるとDBとCPUの負荷も増えます。プロセッサをカスタムした場合、外部API呼び出しのような遅い処理がループ内に入らないようにしてください。
  • DBチューニング: PostgreSQLのコネクションプールサイズ、そしてrefresh_stateテーブルを中心としたインデックスの挙動をモニタリングしてください。カタログ負荷の大半はDBに現れます。
  • 検索インデキシング: 検索プラグインのインデキシング周期をエンティティ規模に合わせて調整し、大規模なら検索バックエンドをPostgreSQL検索から専用エンジン(例: OpenSearch/Elasticsearch)へ切り替えます。
  • キャッシュ層: キャッシュストアをmemoryからRedisのような外部ストアに変えると、マルチレプリカでのキャッシュヒット率が安定します。
backend:
  cache:
    store: redis
    connection: redis://backstage-redis:6379
  • 水平スケーリング: Backstageバックエンドはステートレスに近い設計のため、レプリカ増設で読み取り負荷を分散できます。スケジュールタスクはDBコーディネーションで重複実行が防止されます。

観測 — ポータルもサービスである

Backstage自体もSLOを持つ本番サービスとして扱うべきです。

  • メトリクス: バックエンドはOpenTelemetryの計装をサポートしています。HTTPリクエストのレイテンシ/エラー率、カタログ処理キューの遅延、Scaffolderタスクの成功率を中核指標として収集し、Prometheusへエクスポートしてください。
  • ログ: 構造化JSONログを標準の収集パイプライン(例: Loki、CloudWatch)へ送ります。特にカタログ処理のエラーログは「どのエンティティがなぜ更新に失敗しているか」を教えてくれる一次シグナルです。
  • ダッシュボードとアラート: ポータルの可用性、ログイン成功率、カタログの鮮度(最後に成功した同期の時刻)、TechDocsビルドの失敗率があれば、初期の運用ダッシュボードとして十分です。

セキュリティ — プラグインのサプライチェーンとシークレット

  • プラグインのサプライチェーン: コミュニティプラグインはnpmパッケージです。導入前にメンテナンスの活発さと要求権限の範囲をレビューし、社内npmプロキシ/ロックファイル監査(yarn npm audit、socket系ツール)をCIに含めてください。信頼境界の内側に入るコードであることを忘れてはいけません。
  • シークレット管理: app-configのシークレットはすべて環境変数参照とし、実際の値はKubernetes Secretまたは外部シークレットマネージャー(Vault、AWS Secrets Manager)から注入します。GitHub Appのキーファイルのような資格情報は決してイメージに焼き込みません。
  • ネットワーク境界: Backstageバックエンドは社内システムの各所にアクセス権を持つハブになります。アウトバウンドのアクセス先をNetworkPolicyで明示し、トークンは最小権限で発行してください。
  • 認証トークンの検証: バックエンド間の呼び出しはサービストークンで保護されており、外部公開エンドポイントが認証ミドルウェアの背後にあるかを定期的に点検する必要があります。

組織運用モデル — プラットフォームチームと貢献モデル

Backstage運用の半分は組織設計です。実証済みのモデルは「中央プラットフォームチーム + 社内オープンソース貢献」の組み合わせです。

  • プラットフォームチームはコア運用(アップグレード、認証、カタログガバナンス、共通プラグイン)を所有します。2〜4名の専任で始めるケースが多いです。
  • ドメインチームは自分たちのツールのプラグイン/テンプレートを直接貢献します。このとき、貢献ガイドライン(コードレビュー基準、プラグイン品質基準、オーナーシップの約束)を文書化した社内オープンソースモデルが必要です。
  • RFCプロセス: ポータルに大きな変更(新しいコアプラグイン、権限ポリシーの変更、メタデータスキーマの変更)を導入する際、軽量なRFCドキュメントで意見を集める手順を置けば、プラットフォームチームの独断も現場の無秩序も防げます。

導入成熟度ロードマップ

これまでの3回を成熟度モデルとしてまとめると次のようになります。

レベル0  インストール済み   デモインスタンス、手動登録エンティティ数個
レベル1  カタログ稼働      ディスカバリ自動化、オーナーシップモデル、認証連携 [第1回]
レベル2  セルフサービス    ゴールデンパステンプレート、day-2アクション、
                         採用率測定                                  [第2回]
レベル3  ナレッジハブ      TechDocs全面展開、エコシステムプラグイン、検索定着 [第3回]
レベル4  運用成熟         権限ポリシー、SLO運用、貢献モデル、定期アップグレード [第3回]
レベル5  標準プラットフォーム ポータル経由がデフォルトの経路、指標ベースの継続改善

レベルを飛ばそうとする試み(カタログなしでプラグインから、定着なしで権限から)が失敗の主要因であることを改めて強調します。

本番チェックリスト

TechDocs

  • builder external + オブジェクトストレージのパブリッシャー構成
  • ドキュメントCIパイプライン (マージ時の自動ビルド/アップロード)
  • Scaffolderのスケルトンにmkdocsの骨格を含める

プラグイン

  • 新規要求はエコシステムプラグインを優先検討
  • カスタムプラグインはfrontend/backend/commonの3パッケージ構造
  • プラグインごとにownerとon-callを指定

権限/セキュリティ

  • permission frameworkの有効化、危険な操作を制限するポリシー
  • シークレットは外部注入、イメージに資格情報なし
  • プラグイン依存関係の監査がCIに含まれる

運用

  • 月次/四半期のアップグレードスロット固定、versions:bump手順の文書化
  • アップグレード前のDBスナップショット標準手順
  • OpenTelemetryメトリクス + 構造化ログの収集
  • ポータルSLOの定義 (可用性、カタログ鮮度)
  • Redisキャッシュ + マルチレプリカ構成
  • プラットフォームチームのオーナーシップと貢献ガイドラインの文書化
  • RFCプロセスの運用

おわりに

3回にわたってBackstageベースのIDPの骨格を見てきました。第1回のカタログは組織のソフトウェアの現実をデータにし、第2回のScaffolderは組織の標準を実行可能なコードにし、今回の第3回はその上にドキュメントとプラグインエコシステムを載せ、全体を本番品質で運用する方法を扱いました。最後に一つだけ残します。IDPはツールプロジェクトではなくプロダクトです。ユーザー(開発者)がいて、採用率という指標があり、ロードマップと運用責任が必要です。Backstageはそのプロダクトを作るための最も成熟したオープンソースの土台です。

参考資料