Skip to content
Published on

Backstage Scaffolder — ゴールデンパスをコードにする方法

Authors

はじめに — 新サービス一つに2週間かかる組織

新規マイクロサービスを一つ立ち上げるのに実際に必要な作業を列挙してみます。リポジトリ作成、ボイラープレートコード、CIパイプライン、Dockerfile、KubernetesマニフェストまたはHelmチャート、モニタリングダッシュボード、アラートルール、ロギング設定、シークレット管理の連携、カタログ登録。各ステップで「前回作ったもののコピー」が発生し、コピー元がチームごとに違うため、サービス間の標準のばらつきは時間とともに拡大します。ある組織ではこのプロセスに2週間かかり、その2週間の大半はWikiを漁り、他チームに質問する時間です。

ゴールデンパス(Golden Path)はこの問題への答えです。Spotifyが最初に使ったこの用語は「組織が支援する、摩擦が最小化された実証済みの道」を意味します。重要なのは強制ではなく誘因だという点です。ゴールデンパスは柵ではなく舗装道路です。外れることはできますが、舗装道路が十分に速くて快適なら、わざわざ外れる理由がなくなります。

Backstage Scaffolder(Software Templates)は、ゴールデンパスを実行可能なコードにするツールです。本記事ではtemplate.yamlの構造からカスタムアクション開発、ガバナンスと測定まで、Scaffolderを運用レベルで扱うために必要な内容を整理します。第1回で扱ったカタログが前提であることを覚えておいてください。Scaffolderの成果物は常にカタログへ流れ込みます。

Scaffolderの動作構造

Scaffolderはバックエンドプラグインであり、テンプレートの実行はタスク(task)単位で処理されます。

  開発者                  Scaffolder UI            Scaffolder Backend
    |                         |                          |
    |--- テンプレート選択 ----->|                          |
    |--- フォーム入力 --------->|                          |
    |                         |--- タスク生成 ------------>|
    |                         |                          |
    |                         |     [stepsを順次実行]      |
    |                         |      1. fetch:template    |
    |                         |      2. publish:github    |
    |                         |      3. catalog:register  |
    |                         |                          |
    |<-- リアルタイムログ -------|<-- ステップ別ログ ---------|
    |<-- outputリンク ---------|<-- 完了 ------------------|
    |                         |                          |
    v                         v                          v
  新リポジトリ + CI + カタログ登録完了 (数分以内)

テンプレート自体もカタログのエンティティ(kind: Template)です。つまりテンプレートにもownerがあり、ディスカバリで自動登録され、カタログガバナンスの対象になります。

template.yamlの解剖 — parameters, steps, output

完全な構造を持つ実践的なテンプレートをまず見て、部分ごとに解剖します。

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: springboot-service
  title: Spring Boot マイクロサービス
  description: 組織標準が組み込まれたSpring Bootサービスを生成します
  tags:
    - java
    - spring-boot
    - recommended
spec:
  owner: group:default/platform-team
  type: service

  parameters:
    - title: サービス基本情報
      required:
        - name
        - description
        - owner
      properties:
        name:
          title: サービス名
          type: string
          pattern: '^[a-z0-9-]+$'
          maxLength: 40
          description: 小文字、数字、ハイフンのみ許可
          ui:autofocus: true
        description:
          title: 説明
          type: string
        owner:
          title: 所有チーム
          type: string
          ui:field: OwnerPicker
          ui:options:
            catalogFilter:
              kind: Group
              spec.type: team
    - title: 技術オプション
      properties:
        javaVersion:
          title: Javaバージョン
          type: string
          default: '21'
          enum: ['17', '21']
        enableKafka:
          title: Kafkaコンシューマを含める
          type: boolean
          default: false

  steps:
    - id: fetch
      name: スケルトンのレンダリング
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          description: ${{ parameters.description }}
          owner: ${{ parameters.owner }}
          javaVersion: ${{ parameters.javaVersion }}
          enableKafka: ${{ parameters.enableKafka }}

    - id: publish
      name: GitHubリポジトリの作成
      action: publish:github
      input:
        repoUrl: github.com?owner=acme-corp&repo=${{ parameters.name }}
        defaultBranch: main
        repoVisibility: internal
        protectDefaultBranch: true
        requireCodeOwnerReviews: true

    - id: register
      name: カタログ登録
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}
        catalogInfoPath: /catalog-info.yaml

  output:
    links:
      - title: リポジトリを開く
        url: ${{ steps['publish'].output.remoteUrl }}
      - title: カタログで見る
        icon: catalog
        entityRef: ${{ steps['register'].output.entityRef }}

parametersはJSON Schemaベースのフォーム定義です。配列の各項目がウィザードの1ページになり、patternenummaxLength のようなJSON Schema検証がクライアントとサーバーの両側で動作します。ui: プレフィックスのフィールドはreact-jsonschema-formベースのUI拡張で、ui:field: OwnerPicker はカタログのGroupエンティティを検索する専用ウィジェットを表示します。

stepsは順次実行されるアクションのリストです。各stepの出力は後続のstepから参照できます。式の文法は、コードブロックの例のようにドル記号と二重中括弧を使うNunjucksベースの文法です。

outputは実行完了画面に表示するリンクです。新しいリポジトリとカタログページへ直接移動できるようにするのが慣例です。

ビルトインアクション — 組み合わせのビルディングブロック

よく使うビルトインアクションを整理すると次のとおりです。

アクション役割備考
fetch:templateスケルトンを取得しNunjucksでレンダリングゴールデンパスの本体
fetch:plainレンダリングなしでファイルコピーバイナリ、そのまま使うファイル
publish:githubリポジトリ作成 + コードのプッシュブランチ保護オプションを含む
publish:github:pull-request既存リポジトリへPRを作成既存コードへの標準注入時
catalog:registerカタログへエンティティ登録最終ステップの慣例
catalog:writecatalog-info.yamlファイルの生成fetch成果物に追加
fs:rename / fs:delete作業ディレクトリのファイル操作条件付きファイル整理
debug:logデバッグ出力テンプレート開発時

インストール済みインスタンスで使用可能なアクションの全リストと入力スキーマは、ポータルの /create/actions パスで確認できます。テンプレートを書く前にこのページを見る習慣が、試行錯誤を大きく減らしてくれます。

GitLab、Azure DevOps、Bitbucket用のpublishアクションもそれぞれのモジュールで提供されているため、VCSがGitHubでなくても同じパターンを適用できます。

Nunjucksテンプレーティング — skeletonディレクトリ

fetch:template アクションはskeletonディレクトリのすべてのテキストファイルをNunjucksテンプレートエンジンでレンダリングします。ディレクトリ構造の例です。

templates/springboot-service/
├── template.yaml
└── skeleton/
    ├── catalog-info.yaml
    ├── README.md
    ├── Dockerfile
    ├── .github/
    │   └── workflows/
    │       └── ci.yaml
    ├── k8s/
    │   ├── deployment.yaml
    │   └── service.yaml
    └── src/main/java/...

skeleton内のファイルには置換変数を使います。たとえばskeletonのcatalog-info.yamlは次のように書きます。

# skeleton/catalog-info.yaml (Nunjucksテンプレート)
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: ${{ values.name }}
  description: ${{ values.description | dump }}
  annotations:
    github.com/project-slug: acme-corp/${{ values.name }}
    backstage.io/techdocs-ref: dir:.
spec:
  type: service
  lifecycle: experimental
  owner: ${{ values.owner }}

条件分岐と繰り返しも可能です。Kafkaオプションによって依存関係を出し入れするbuild.gradleの断片です。

// skeleton/build.gradle (Nunjucks条件分岐)
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    {%- if values.enableKafka %}
    implementation 'org.springframework.kafka:spring-kafka'
    {%- endif %}
}

ファイル名やディレクトリ名にも同じ置換文法が使えるため、パッケージパスのようにサービス名がパスに入る構造も表現できます。一つ罠があります。GitHub Actionsのワークフローファイルのように、スケルトン自体に二重中括弧の文法が含まれるファイルはNunjucksが誤って解釈する可能性があります。その場合はrawブロックで囲むか、fetch:templatecopyWithoutTemplating オプションで該当パスをレンダリングから除外します。

- id: fetch
  action: fetch:template
  input:
    url: ./skeleton
    copyWithoutTemplating:
      - .github/workflows/*.yaml   # Actionsワークフローは置換なしでコピー
    values:
      name: ${{ parameters.name }}

カスタムアクション開発 — Jiraチケット自動作成の例

ビルトインアクションで足りないときは、TypeScriptでカスタムアクションを作ります。サービス作成時にJiraへ追跡チケットを作るアクションの例です。

// plugins/scaffolder-backend-module-acme/src/actions/createJiraTicket.ts
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';

export const createJiraTicketAction = () => {
  return createTemplateAction<{
    projectKey: string;
    summary: string;
    description: string;
  }>({
    id: 'acme:jira:create-ticket',
    description: '新規サービス追跡用のJiraチケットを作成します',
    schema: {
      input: {
        type: 'object',
        required: ['projectKey', 'summary'],
        properties: {
          projectKey: { type: 'string', title: 'Jiraプロジェクトキー' },
          summary: { type: 'string', title: 'チケットタイトル' },
          description: { type: 'string', title: 'チケット本文' },
        },
      },
      output: {
        type: 'object',
        properties: {
          ticketUrl: { type: 'string' },
        },
      },
    },
    async handler(ctx) {
      const { projectKey, summary, description } = ctx.input;
      ctx.logger.info(`Creating Jira ticket in project ${projectKey}`);

      const response = await fetch('https://jira.acme.io/rest/api/2/issue', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${process.env.JIRA_TOKEN}`,
        },
        body: JSON.stringify({
          fields: {
            project: { key: projectKey },
            summary,
            description,
            issuetype: { name: 'Task' },
          },
        }),
      });

      if (!response.ok) {
        throw new Error(`Jira API error: ${response.status}`);
      }
      const issue = await response.json();
      ctx.output('ticketUrl', `https://jira.acme.io/browse/${issue.key}`);
    },
  });
};

新しいバックエンドシステムではモジュールとして登録します。

// plugins/scaffolder-backend-module-acme/src/module.ts
import { createBackendModule } from '@backstage/backend-plugin-api';
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
import { createJiraTicketAction } from './actions/createJiraTicket';

export const scaffolderModuleAcme = createBackendModule({
  pluginId: 'scaffolder',
  moduleId: 'acme-actions',
  register(reg) {
    reg.registerInit({
      deps: { scaffolder: scaffolderActionsExtensionPoint },
      async init({ scaffolder }) {
        scaffolder.addActions(createJiraTicketAction());
      },
    });
  },
});

これでテンプレートのstepsから action: acme:jira:create-ticket で呼び出せます。カスタムアクションは組織固有のゴールデンパスを作る中核手段です。社内シークレット管理システムへの登録、内部DNSの申請、コストタグの付与など、「うちの会社にしかない手続き」はすべてアクションの候補です。

入力検証とUIカスタマイズ

フォームの品質はテンプレートの採用率に直接影響します。3つのレベルの仕組みがあります。

1) JSON Schema検証。 patternminLengthenum は基本です。サービス名のようにグローバルに一意であるべき値は正規表現だけでは不十分なので、カスタムフィールド拡張でカタログAPIを照会して重複を事前にブロックする方式が良いでしょう。

2) ビルトインのカスタムフィールド。 OwnerPicker(グループ選択)、EntityPicker(エンティティ選択)、RepoUrlPicker(リポジトリ位置選択)が最もよく使われます。RepoUrlPickerはallowedOwnersで組織を固定してミスを防げます。

repoUrl:
  title: リポジトリの場所
  type: string
  ui:field: RepoUrlPicker
  ui:options:
    allowedHosts:
      - github.com
    allowedOwners:
      - acme-corp

3) 自作のカスタムフィールド拡張。 フロントエンドでReactコンポーネントと検証関数を登録すれば、フォームフィールドを完全にカスタマイズできます。たとえば社内のコストセンターコードをERP APIから検索して選択させるフィールドなどです。

組織標準の注入 — スケルトンこそがポリシー

ゴールデンパスの価値はテンプレートエンジンではなく、スケルトンの中身から生まれます。新しいサービスが生まれる瞬間にすでに備えているべきものをスケルトンに埋め込んでください。

  • CIパイプライン: ビルド、テスト、静的解析(SonarQube)、コンテナイメージスキャン、SBOM生成まで含むワークフロー
  • 可観測性: メトリクスエンドポイントの公開、構造化ロギング設定、標準ダッシュボードの自動生成(ダッシュボードのプロビジョニングコードを含む)
  • セキュリティのデフォルト: シークレットは外部シークレットマネージャー参照のみ、コンテナのnon-root実行、NetworkPolicyを標準で含む
  • 運用標準: readiness/livenessプローブ、リソースのrequests/limits、PodDisruptionBudget、標準ラベル
  • ドキュメントとカタログ: TechDocsの骨格(mkdocs.yml + docsディレクトリ)、catalog-info.yaml

こうすれば「標準を守れ」と言う必要がなくなります。標準を守ることが最も楽な道になるからです。逆に言えば、スケルトンが貧弱なテンプレートはゴールデンパスではなく、ただのリポジトリ生成機にすぎません。

テンプレートのテスト、バージョン管理、dry-run

テンプレートもソフトウェアなので、テストとバージョン管理が必要です。

dry-run: Backstage UIのテンプレートエディタ(/create/edit)でテンプレートを読み込み、実際のリポジトリ作成なしにレンダリング結果をプレビューできます。フォーム入力に応じた成果物ディレクトリを確認する最速のループです。

CIでの検証: テンプレートリポジトリのPRパイプラインで、最低限次を自動化します。

# .github/workflows/template-ci.yaml
name: template-ci
on:
  pull_request:
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: template.yamlスキーマ検証
        run: npx @roadiehq/backstage-entity-validator validate template.yaml
      - name: スケルトンレンダリングのスモークテスト
        run: |
          # サンプル値でスケルトンをレンダリングした後、
          # 成果物のビルドが通るか確認する
          ./scripts/render-and-build.sh sample-values.json

レンダリングされた成果物が実際にビルドでき、テストが通るところまで確認することが重要です。「テンプレートで作ったサービスが最初のビルドから壊れている」という体験は、ゴールデンパスの信頼を一撃で崩します。

バージョン管理: テンプレートリポジトリにセマンティックバージョンのタグを付け、本番のBackstageはタグ/リリース基準でテンプレートを登録する方式が安全です。テンプレートの変更が既存の生成物に遡及適用されない点も忘れてはいけません。すでに作られたサービスに標準変更を伝播するには、publish:github:pull-request アクションベースの一括PRキャンペーンか、別の自動化が必要です。

ガバナンス — 誰がテンプレートを作るのか

テンプレートの所有モデルは、2つの極端の間でバランスを取る必要があります。

モデル長所短所
プラットフォームチームの独占品質の一貫性、標準の統制ボトルネック、現場ニーズへの反映遅延
全面開放速い拡散、現場への密着品質のばらつき、重複テンプレートの乱立

推奨する中間点は「開放 + 審査」モデルです。誰でもテンプレートを提案できますが、公式カタログで recommended タグ付きで露出されるには、プラットフォームチームのレビューを通す必要があります。レビュー基準をチェックリストとして公開してください。スケルトン標準の包含、CI検証の存在、ownerグループの指定、ドキュメント化の水準といった項目です。そしてテンプレートごとにownerを強制し(テンプレートもカタログエンティティなので、第1回のガバナンスがそのまま適用されます)、放置されたテンプレートが匿名のまま残らないようにします。

ゴールデンパスの測定 — 採用率とリードタイム

測定しないゴールデンパスは改善できません。中核となる指標を2つ追跡してください。

採用率(adoption rate): 一定期間内の新規作成サービスのうち、テンプレート経由の割合です。Scaffolderのタスク履歴とカタログの新規Component登録を突き合わせれば計算できます。採用率が低ければ、テンプレートが現場のニーズとずれているシグナルです。

新規サービスのリードタイム: 「作ると決定」から「本番初デプロイ」までの時間です。テンプレート導入前のベースラインを測定しておけば、効果を定量的に報告できます。2週間が2時間になる事例は実際に珍しくありません。

補助指標としては、テンプレート実行の失敗率(タスクログ基準)、テンプレート別の使用分布(使われないテンプレートの特定)、作成後30日時点の標準準拠率(生成されたサービスがスケルトンのCI/可観測性設定を維持しているか)が有用です。

アンチパターン — こう作ると使われない

過剰なオプション。 パラメータが20個もあるテンプレートは「選択の摩擦」をそのまま移し替えただけです。ゴールデンパスの本質は決定を代行することです。オプションが増えるほど決定の負担がユーザーに戻ります。オプションが5個を超えたらテンプレートを分割すべきシグナルと受け取ってください。「フレームワーク選択オプション3つ付きのJavaサービステンプレート」より、「Spring Bootテンプレート」と「Quarkusテンプレート」の2つのほうが優れています。

放置されたテンプレート。 6か月間更新のないテンプレートは、すでに組織標準とずれている可能性が高いです。古い依存関係、廃止されたCI文法、変わったセキュリティポリシー。テンプレートで作ったサービスが生成直後からセキュリティスキャンに引っかかった瞬間、信頼は終わります。テンプレートリポジトリにも依存関係自動更新ボットを付け、四半期ごとにスケルトン成果物のビルドを再検証する定期パイプラインを置いてください。

一回限りの思考。 テンプレートを「作成時に一度だけ使うツール」とみなす観点です。成熟した組織はday-2作業にもScaffolderを使います。既存サービスへのモニタリング追加、ライブラリ移行PRの生成、インフラリソースの申請といった作業をテンプレート化すれば、ポータルは「サービス生成機」から「セルフサービスハブ」へ進化します。

ドキュメントのないテンプレート。 テンプレートが何を作り、何を作らないのか、生成後にやるべきことは何かの説明がなければ、ユーザーは実行をためらいます。テンプレートのdescriptionとREADMEに成果物の一覧と生成後のステップを明示してください。

チェックリスト

  • テンプレートは専用リポジトリでバージョン管理 (タグベースの登録)
  • template.yamlスキーマ検証がPRパイプラインに存在
  • スケルトンレンダリング成果物のビルド/テストのスモークテスト自動化
  • スケルトンにCI、可観測性、セキュリティ、運用標準がすべて内蔵
  • catalog-info.yamlとTechDocsの骨格が成果物に含まれる
  • パラメータは5個以下、合理的なデフォルト値を提供
  • OwnerPicker / RepoUrlPickerで入力ミスをブロック
  • ワークフローファイルなど二重中括弧を含むファイルはcopyWithoutTemplatingで処理
  • テンプレートごとにownerグループを指定、レビュープロセスを文書化
  • 採用率と新規サービスリードタイムの測定体制を構築
  • 四半期ごとのテンプレート再検証(依存関係、セキュリティポリシー整合性)パイプライン
  • day-2作業テンプレートのロードマップ策定

おわりに

Scaffolderは技術的にはシンプルなツールです。フォームを描き、ファイルをレンダリングし、リポジトリを作り、カタログに登録します。しかし組織的には強力なテコです。組織の標準がドキュメントではなく実行可能なコードになり、新サービスの初日の品質が組織の最高水準に平準化されます。要点は3つです。スケルトンに標準を惜しみなく詰め込むこと、テンプレートをソフトウェアのようにテストしバージョン管理すること、採用率を測定してゴールデンパスが実際に歩かれる道なのかを確認すること。

次回の第3回では、TechDocsとプラグイン開発、そしてBackstageを本番で運用するための全体的なチェックリストを扱います。

参考資料