Skip to content
Published on

BackstageでIDPを構築する 1 — ソフトウェアカタログがすべて

Authors

はじめに — なぜ今IDPなのか

マイクロサービスが50個を超えたあたりから、組織には同じ質問が殺到し始めます。「このサービスの担当チームはどこですか」「決済APIの仕様書はどこにありますか」「このDBを使っているサービスは何ですか」「新入社員がシステム全体を把握するには誰に聞けばいいですか」。これらの質問に答えるコストこそが組織の認知負荷(cognitive load)であり、この負荷をシステマティックに減らすことがプラットフォームエンジニアリングの中心課題です。

Internal Developer Portal(IDP)はこの問題に対する業界の答えであり、その事実上の標準が、Spotifyが2020年にオープンソース化しCNCFに寄贈したBackstageです。BackstageはCNCFのSandbox(2020年)とIncubating(2022年)の段階を経て成熟し、公開されているアダプター一覧を基準に数千の組織が導入している、CNCFで最も活発なプロジェクトの一つです。GartnerやPuppetのプラットフォームエンジニアリングレポートも「大規模エンジニアリング組織の大多数が2026年までに社内プラットフォームチームを運営するようになる」という方向性を一貫して示してきました。

本シリーズはBackstageでIDPを構築する過程を3回に分けて扱います。第1回である本記事のテーマはただ一つ、ソフトウェアカタログです。結論から申し上げます。カタログが空のBackstageは何の価値もありません。 ScaffolderもTechDocsもKubernetesプラグインも、すべてカタログのエンティティに依存して動作します。カタログ設計がIDPプロジェクトの成否を分けます。

IDPにおけるBackstageの位置づけ

まず用語を整理します。IDPという略語は2つの意味で使われます。

用語意味焦点
Internal Developer Platformセルフサービスインフラ基盤の全体プロビジョニング、ゴールデンパス、環境管理
Internal Developer Portalプラットフォームへの単一エントリポイントUIカタログ、ドキュメント、セルフサービスUI

Backstageは厳密には後者、つまりポータルフレームワークです。インフラを直接プロビジョニングするエンジンではなく、組織のすべてのソフトウェア資産とツールを一つの画面に集約するフレームワークです。中核となる構成要素は4つです。

+---------------------------------------------------------------+
|                Backstage (ポータルフレームワーク)                  |
|                                                               |
|  +----------------+  +----------------+  +-----------------+  |
|  |   Software     |  |   Scaffolder   |  |    TechDocs     |  |
|  |   Catalog      |  | (ゴールデンパス)  |  |  (docs-as-code) |  |
|  |  (本記事の主題)   |  |    [第2回]      |  |     [第3回]     |  |
|  +-------+--------+  +-------+--------+  +--------+--------+  |
|          |                   |                    |           |
|  +-------v-------------------v--------------------v--------+  |
|  |        プラグインエコシステム (K8s, ArgoCD, ...)             |  |
|  +----------------------------------------------------------+  |
+---------------------------------------------------------------+

カタログは他のすべての機能のデータ基盤です。Scaffolderが新しいサービスを作ればカタログに登録し、TechDocsはカタログのエンティティにドキュメントを紐づけ、Kubernetesプラグインはカタログのアノテーションを読んでどのワークロードを表示するかを決めます。

カタログのデータモデル — 6つの中核Kind

Backstageのカタログはエンティティ(entity)のグラフです。すべてのエンティティは apiVersionkindmetadataspec を持つYAMLドキュメントとして表現され、Kubernetesのリソースモデルから意図的にインスピレーションを得ています。中核のKindは次のとおりです。

Kind説明
Componentコードから作られるソフトウェアの単位バックエンドサービス、Webアプリ、ライブラリ
APIコンポーネントが提供/消費するインターフェースOpenAPI、gRPC、GraphQL、AsyncAPI
Resourceコンポーネントが依存するインフラRDS、S3バケット、Kafkaトピック
System一つの機能を共に構成するまとまり決済システム、検索システム
Domainシステムを束ねるビジネス領域コマース、精算、会員
Group / Userオーナーシップの主体チーム、パート、個人

これらがリレーションで接続されグラフを構成します。

                      +------------------+
                      |     Domain       |   例: payments-domain
                      |  (ビジネス領域)    |
                      +--------^---------+
                               | partOf
                      +--------+---------+
                      |     System       |   例: payment-system
                      +--------^---------+
                               | partOf
          +--------------------+--------------------+
          |                    |                    |
  +-------+--------+   +------+---------+   +------+--------+
  |   Component    |   |   Component    |   |   Component   |
  |  payment-api   |   | payment-worker |   |  payment-web  |
  +---+-------+----+   +-------+--------+   +---------------+
      |       |                |
      |       | providesApi    | consumesApi
      |       v                v
      |    +--+----------------+--+
      |    |         API          |   例: payment-v1 (OpenAPI)
      |    +----------------------+
      | dependsOn
      v
  +---+------------+        +-----------------+
  |   Resource     |        |     Group       |
  |  payment-db    |        | team-payments   |
  +----------------+        +--------^--------+
                                     | ownedBy (すべてのエンティティから)

リレーションは双方向に自動生成されます。Componentが providesApis を宣言すると、API側には逆方向の apiProvidedBy リレーションが作られます。このグラフのおかげで「payment-dbを使うサービスすべて」や「team-paymentsが所有するすべての資産」といったクエリがワンクリックで可能になります。

catalog-info.yamlの実践的な書き方

各リポジトリのルートに catalog-info.yaml ファイルを置くのが標準的な慣行です。実践的な例を3つ見てみましょう。

バックエンドサービス (Component + 提供API)

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: payment-api
  title: Payment API Server
  description: 決済承認/キャンセルを処理する中核バックエンドサービス
  annotations:
    github.com/project-slug: acme-corp/payment-api
    backstage.io/techdocs-ref: dir:.
    backstage.io/kubernetes-id: payment-api
    pagerduty.com/integration-key: PD-INTEGRATION-KEY
    sonarqube.org/project-key: acme_payment-api
  tags:
    - java
    - spring-boot
    - payments
  links:
    - url: https://grafana.acme.io/d/payment-api
      title: Grafana Dashboard
      icon: dashboard
spec:
  type: service
  lifecycle: production
  owner: group:default/team-payments
  system: payment-system
  providesApis:
    - payment-v1
  dependsOn:
    - resource:payment-db
    - resource:payment-events-topic

注目すべきは annotations です。カタログ自体はこの値を解釈しませんが、各プラグインが自分のアノテーションを読んで動作します。backstage.io/kubernetes-id があって初めてKubernetesタブが表示され、pagerduty.com/integration-key があって初めてオンコール情報が出ます。アノテーションはプラグインの有効化スイッチそのものです。

APIエンティティ (OpenAPI仕様の紐づけ)

apiVersion: backstage.io/v1alpha1
kind: API
metadata:
  name: payment-v1
  title: Payment API v1
  description: 決済承認、キャンセル、照会のREST API
spec:
  type: openapi
  lifecycle: production
  owner: group:default/team-payments
  system: payment-system
  definition:
    $text: ./openapi/payment-v1.yaml

definition にOpenAPIドキュメントを紐づけると、BackstageはAPI定義タブでSwagger UIをレンダリングします。上記の例のtextローダーディレクティブは相対パスのファイルを読み込み、URLも指定できます。gRPCなら type: grpc にprotoファイルを、イベント駆動なら type: asyncapi を使います。

インフラリソースとシステム/ドメイン

apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
  name: payment-db
  description: 決済元帳PostgreSQL (AWS RDS)
  annotations:
    amazonaws.com/arn: arn:aws:rds:ap-northeast-2:111122223333:db:payment-db
spec:
  type: database
  owner: group:default/team-payments
  system: payment-system
---
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
  name: payment-system
  description: 決済承認から精算までを担当するシステム
spec:
  owner: group:default/team-payments
  domain: commerce
---
apiVersion: backstage.io/v1alpha1
kind: Domain
metadata:
  name: commerce
  description: コマースビジネスドメイン
spec:
  owner: group:default/commerce-tribe

一つのファイルに --- 区切りで複数のエンティティを宣言できます。SystemとDomainは通常、専用のガバナンスリポジトリ(例: acme-corp/software-catalog)にまとめて管理するほうが運用上すっきりします。

ディスカバリ — 手動登録はスケールしない

エンティティをUIで一つずつ登録する方式は、リポジトリが30個を超えただけで破綻します。カタログを満たすメカニズムを理解するには、2つの概念を区別する必要があります。

概念役割
Entity Provider外部ソースからエンティティをカタログに注入GitHubディスカバリ、LDAP、静的ファイル
Processor注入されたエンティティを検証/補強/リレーション生成スキーマ検証、リレーション構築、CODEOWNERS解釈
  GitHub Org          LDAP/AD           静的locations
      |                  |                    |
      v                  v                    v
+-----+------------------+--------------------+-----+
|            Entity Providers (注入レイヤ)            |
+--------------------------+-------------------------+
                           v
+--------------------------+-------------------------+
| 処理ループ: 検証 -> 変換 -> リレーション生成 -> 保存      |
| (Processorが各段階で介入)                            |
+--------------------------+-------------------------+
                           v
                 +---------+---------+
                 |  PostgreSQL (DB)  |
                 +---------+---------+
                           v
                 +---------+---------+
                 |  Catalog REST API |
                 +-------------------+

GitHub組織全体を自動スキャンするディスカバリ設定は次のとおりです。

# app-config.yaml
catalog:
  providers:
    github:
      acmeProvider:
        organization: 'acme-corp'
        catalogPath: '/catalog-info.yaml'
        filters:
          branch: 'main'
          repository: '.*'        # 正規表現で対象リポジトリをフィルタ
          topic:
            include: ['backstage-managed']
        schedule:
          frequency: { minutes: 30 }
          timeout: { minutes: 3 }

この設定一つで「mainブランチにcatalog-info.yamlがあり、backstage-managedトピックが付いたすべてのリポジトリ」が30分周期で自動同期されます。バックエンドには該当モジュールの登録が必要です。

// packages/backend/src/index.ts
backend.add(import('@backstage/plugin-catalog-backend-module-github/alpha'));

組織構造(Group/User)は、GitHub Teamsをそのまま取り込むorgディスカバリを使えば済みます。

catalog:
  providers:
    githubOrg:
      - id: acme-github-org
        githubUrl: https://github.com
        orgs: ['acme-corp']
        schedule:
          frequency: { hours: 1 }
          timeout: { minutes: 15 }

オーナーシップモデル — すべてのエンティティに持ち主を

カタログの最も重要な不変条件は「ownerのないエンティティは存在しない」です。オーナーシップの主体はGroupとUserエンティティです。

apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
  name: team-payments
  description: 決済プラットフォームチーム
spec:
  type: team
  profile:
    displayName: Payments Team
    email: team-payments@acme.io
  parent: commerce-tribe
  children: []
---
apiVersion: backstage.io/v1alpha1
kind: User
metadata:
  name: youngju.kim
spec:
  profile:
    displayName: Youngju Kim
    email: youngju.kim@acme.io
  memberOf:
    - team-payments

Groupは parent/children で組織ツリーを構成します。トライブ-スクワッド構造でも本部-チーム構造でもそのまま表現でき、このツリーはオーナーシップの集計(下位チームの資産を上位組織ビューで合算)に活用されます。

ownerの指定を忘れたリポジトリのためにCODEOWNERS連携も可能です。カタログバックエンドの CodeOwnersProcessor を有効化すると、spec.owner が空のエンティティに対してリポジトリのCODEOWNERSファイルを読み、所有チームを推論します。ただしこれは補助手段にとどめ、明示的なowner宣言を原則とすることを推奨します。CODEOWNERSは「コードレビューの責任」であり、カタログのownerは「運用の責任」なので、意味が微妙に異なるためです。

メタデータガバナンス — アノテーションポリシーとリント

カタログが大きくなるほど、メタデータの品質がそのままカタログの信頼性になります。ガバナンスの仕組みを最初から組み込んでおくのが得策です。

1) 必須メタデータポリシーをドキュメントとして宣言します。 たとえば次のような形です。

フィールド/アノテーション必須かどうか備考
spec.owner必須グループのみ許可、個人は禁止
spec.lifecycle必須experimental, production, deprecated から択一
description必須一文以上
github.com/project-slug必須ソースとの紐づけ
backstage.io/techdocs-ref推奨ドキュメント化対象サービスは必須
pagerduty.com/integration-key推奨productionサービスは必須

2) CIでcatalog-info.yamlをリントします。 エンティティ検証ツールをPRパイプラインに入れると、壊れたファイルがマージされる前に弾けます。

# .github/workflows/catalog-lint.yaml
name: catalog-lint
on:
  pull_request:
    paths:
      - 'catalog-info.yaml'
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Validate catalog entity
        run: npx @roadiehq/backstage-entity-validator validate catalog-info.yaml

3) カスタムプロセッサで組織ポリシーを強制します。 たとえば「productionライフサイクルなのにPagerDutyアノテーションがなければ検証エラー」をコードで表現できます。

// 組織ポリシー検証プロセッサの骨格
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
import { Entity } from '@backstage/catalog-model';

export class RequiredAnnotationsProcessor implements CatalogProcessor {
  getProcessorName(): string {
    return 'RequiredAnnotationsProcessor';
  }

  async validateEntityKind(entity: Entity): Promise<boolean> {
    if (entity.kind === 'Component' && entity.spec?.lifecycle === 'production') {
      const annotations = entity.metadata.annotations ?? {};
      if (!annotations['pagerduty.com/integration-key']) {
        throw new Error(
          `production component ${entity.metadata.name} requires pagerduty annotation`,
        );
      }
    }
    return false; // 他のプロセッサが処理を継続できるように
  }
}

本番デプロイ構成

ローカルデモならSQLiteで十分ですが、本番は必ずPostgreSQLを使う必要があります。中核となるapp-configは次のとおりです。

# app-config.production.yaml
app:
  baseUrl: https://backstage.acme.io

backend:
  baseUrl: https://backstage.acme.io
  listen:
    port: 7007
  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}
      ssl:
        rejectUnauthorized: true
  cache:
    store: memory

integrations:
  github:
    - host: github.com
      apps:
        - $include: github-app-credentials.yaml

catalog:
  rules:
    - allow: [Component, API, Resource, System, Domain, Group, User, Location]

GitHub連携は個人トークン(PAT)よりGitHub App方式を推奨します。レートリミットがインストール単位で分離され、権限スコープを細かく制御でき、人のアカウントに依存しないためです。

Kubernetesデプロイマニフェストの骨格は次のとおりです。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backstage
  namespace: backstage
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backstage
  template:
    metadata:
      labels:
        app: backstage
    spec:
      containers:
        - name: backstage
          image: ghcr.io/acme-corp/backstage:1.0.3
          ports:
            - containerPort: 7007
          envFrom:
            - secretRef:
                name: backstage-secrets
          readinessProbe:
            httpGet:
              path: /healthcheck
              port: 7007
          resources:
            requests:
              cpu: 500m
              memory: 1Gi
            limits:
              memory: 2Gi
---
apiVersion: v1
kind: Service
metadata:
  name: backstage
  namespace: backstage
spec:
  selector:
    app: backstage
  ports:
    - port: 80
      targetPort: 7007

レプリカを2以上にする場合、エンティティプロバイダのスケジュールタスクが重複実行されないように、BackstageがDBベースのコーディネーションを使うことも知っておくとよいでしょう。別途リーダー選出の設定なしで動作します。

認証連携 — GitHubログインとOIDC

社内ポータルなので認証は必須です。最もシンプルなGitHub OAuthの構成です。

auth:
  environment: production
  providers:
    github:
      production:
        clientId: ${AUTH_GITHUB_CLIENT_ID}
        clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}
        signIn:
          resolvers:
            - resolver: usernameMatchingUserEntityName

sign-in resolverは「外部IdPのアイデンティティ」を「カタログのUserエンティティ」にマッピングするルールです。つまりログインするにはカタログに該当Userエンティティが存在する必要があります。orgディスカバリでUserを自動注入しておけば自然に解決します。Okta、Keycloak、Azure ADのような社内IdPがあるならOIDCプロバイダを使います。

auth:
  environment: production
  providers:
    oidc:
      production:
        metadataUrl: https://keycloak.acme.io/realms/acme/.well-known/openid-configuration
        clientId: ${AUTH_OIDC_CLIENT_ID}
        clientSecret: ${AUTH_OIDC_CLIENT_SECRET}
        prompt: auto
        signIn:
          resolvers:
            - resolver: emailMatchingUserEntityProfileEmail

バックエンドには認証モジュールの追加が必要です。

backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-github-provider'));
// または OIDC モジュール
backend.add(import('@backstage/plugin-auth-backend-module-oidc-provider'));

カタログが生み出す価値 — 3つの場面

抽象的な効用ではなく、具体的な場面で整理してみます。

場面1: 深夜2時の障害対応でオンコールを探す。 注文サービスから決済APIタイムアウトのアラートが届きます。カタログでpayment-apiを開くと、ownerチーム、PagerDutyのオンコール担当者、Grafanaダッシュボードのリンク、直近のデプロイ履歴が一画面に揃っています。Slackで「これ誰の担当ですか」と叫びながら20分を浪費することがなくなります。

場面2: 依存関係の追跡と変更影響分析。 payment-dbのPostgreSQLメジャーバージョンアップグレードを計画します。カタログのdependsOn逆方向グラフを照会すれば、このDBに依存するコンポーネント全体とそれぞれの所有チームが即座にわかります。通知対象の選定が1分で終わります。

場面3: 新入社員のオンボーディング。 入社初週に、ドメイン → システム → コンポーネントの階層をたどりながら、組織のソフトウェア地図を一人で把握できます。「全体像を知るシニアの頭の中」がシステムとして外在化された状態だからです。

導入戦略 — パイロットチームと自動投入

カタログ導入で最も難しいのは技術ではなく定着です。実証済みの戦略は次のとおりです。

  1. パイロットチーム1〜2個から始めます。 全社ビッグバンはほぼ必ず失敗します。協力的でサービス数が手頃なチームを選び、catalog-info.yaml作成やオンボーディングの摩擦を直接観察してテンプレート化します。
  2. 初期投入は自動化します。 リポジトリ一覧をスクリプトで巡回し、ベースラインのcatalog-info.yamlを一括PRで生成する方式が効果的です。言語/フレームワークはリポジトリの内容から推論し、ownerはCODEOWNERSやコミット履歴から候補を抽出してPR本文で提案すれば、各チームは「レビューしてマージ」するだけで済みます。
  3. カタログを他のプロセスに接続して強制力を作ります。 たとえば「本番デプロイパイプラインはカタログに登録されたサービスのみ許可」というポリシーができれば、カタログ登録は選択ではなくデプロイの前提条件になります。
  4. 最初の90日以内に目に見える効用を一つ作りましょう。 オンコール情報の統合でもAPIドキュメント集約でも、「ポータルのおかげで時間が減った」という体験が一度生まれて初めて自発的な定着が始まります。

失敗パターン — こうすると失敗する

空のカタログ症候群。 Backstageをインストールしてデモエンティティを数個登録しただけで「チームが自分で登録するだろう」と放置するパターンです。空っぽのポータルに入った開発者は二度と戻ってきません。ディスカバリの自動化と初期一括投入なしで始めてはいけません。

手動管理の腐敗。 登録はしたが更新が手動の場合、メタデータは6か月以内に現実と乖離し始めます。カタログ情報が一度でも間違っていると判明した瞬間(見当違いのチームに障害問い合わせが行った瞬間)、信頼は急落します。解決策は一方向の原則です。真実の源泉(source of truth)をGitリポジトリのcatalog-info.yamlに固定し、UIでの手動登録を最小化し、組織図のようなデータはIdP/HRシステムから自動同期します。

オーナーシップのインフレ。 ownerを「platform-team」一つに集約したり、退職者個人に指定したままにするパターンです。ownerは必ず実在するチームグループでなければならず、組織改編時のグループエンティティ更新がプロセスに含まれている必要があります。

過剰モデリング。 初日からDomain/System/Component/API/Resourceの全階層を完璧に設計しようとする試みです。ComponentとGroupだけで始め、必要が生じたときにSystemとDomainを追加する漸進的アプローチが現実的です。

チェックリスト

導入段階で次の項目を点検してください。

  • PostgreSQLベースの本番構成 (SQLite禁止)
  • GitHub Appベースのインテグレーション (PATは避ける)
  • GitHubディスカバリ + org(Group/User)ディスカバリの有効化
  • 認証(GitHub OAuthまたはOIDC)とsign-in resolverの構成
  • catalog rulesで許可Kindを明示
  • 必須メタデータポリシーの文書化 (owner, lifecycle, description, 主要アノテーション)
  • PRパイプラインにcatalog-info.yamlリントを追加
  • 既存リポジトリの一括登録自動化スクリプトを実行
  • パイロットチームの選定とフィードバックループの構築
  • ownerは常にグループ、組織改編の反映プロセスを定義
  • 最初の90日で目に見える効用の目標設定 (例: オンコール情報の統合)

おわりに

本記事ではBackstage IDPの土台であるソフトウェアカタログを扱いました。エンティティモデルとリレーショングラフ、catalog-info.yamlの作成、ディスカバリの自動化、オーナーシップとガバナンス、本番デプロイまでが第1回の範囲でした。中心となるメッセージは一つです。カタログは機能ではなく基盤であり、自動的に満たされ自動的に更新されるよう設計されて初めて生き残ります。

次回の第2回では、カタログの上で動作するScaffolder、すなわちゴールデンパスをコードにするソフトウェアテンプレートを扱います。新しいサービスを5分で、組織標準がすべて組み込まれた状態で作り出すメカニズムです。

参考資料