Skip to content

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

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

はじめに — なぜ今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)のグラフです。すべてのエンティティは `apiVersion`、`kind`、`metadata`、`spec` を持つ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アノテーションがなければ検証エラー」をコードで表現できます。

// 組織ポリシー検証プロセッサの骨格

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分で、組織標準がすべて組み込まれた状態で作り出すメカニズムです。

参考資料

- [Backstage 公式ドキュメント](https://backstage.io/docs/overview/what-is-backstage)

- [Backstage Software Catalog ドキュメント](https://backstage.io/docs/features/software-catalog/)

- [Backstage System Model (エンティティモデル)](https://backstage.io/docs/features/software-catalog/system-model)

- [Descriptor Format (catalog-info.yaml 仕様)](https://backstage.io/docs/features/software-catalog/descriptor-format)

- [Well-known Annotations 一覧](https://backstage.io/docs/features/software-catalog/well-known-annotations)

- [GitHub Discovery 設定ガイド](https://backstage.io/docs/integrations/github/discovery)

- [Backstage 認証ガイド](https://backstage.io/docs/auth/)

- [CNCF Backstage プロジェクトページ](https://www.cncf.io/projects/backstage/)

- [Backstage GitHub リポジトリ](https://github.com/backstage/backstage)

- [Spotify Backstage 公式サイト](https://backstage.spotify.com/)

현재 단락 (1/392)

マイクロサービスが50個を超えたあたりから、組織には同じ質問が殺到し始めます。「このサービスの担当チームはどこですか」「決済APIの仕様書はどこにありますか」「このDBを使っているサービスは何ですか」...

작성 글자: 0원문 글자: 14,346작성 단락: 0/392