Skip to content

Split View: Platform Engineering과 Backstage로 Internal Developer Platform 구축 실전 가이드

✨ Learn with Quiz
|

Platform Engineering과 Backstage로 Internal Developer Platform 구축 실전 가이드

Platform Engineering Backstage

들어가며 - Platform Engineering이 전통적 DevOps를 대체하는 이유

"You build it, you run it"이라는 DevOps 원칙은 개발자에게 자율성을 주었지만, 동시에 엄청난 인지 부하(Cognitive Load)를 안겼습니다. 개발자가 Kubernetes 매니페스트를 직접 작성하고, CI/CD 파이프라인을 설정하고, 모니터링 대시보드를 구성하고, 인프라 프로비저닝까지 담당해야 하는 상황이 벌어진 것입니다. Gartner는 2026년까지 소프트웨어 엔지니어링 조직의 80%가 Platform Engineering 팀을 구성할 것으로 전망했고, 실제로 이 흐름은 현실이 되고 있습니다.

Platform Engineering은 셀프서비스 역량을 갖춘 Internal Developer Platform(IDP)을 구축하여 개발자의 인지 부하를 줄이고, 조직 전체의 소프트웨어 딜리버리 속도를 높이는 규율(discipline)입니다. DevOps가 "문화와 실천"에 초점을 맞추었다면, Platform Engineering은 "제품으로서의 플랫폼"에 초점을 맞춥니다. 개발자는 IDP의 고객이고, 플랫폼 팀은 이 제품을 개발하고 운영하는 팀입니다.

이 글에서는 CNCF Incubating 프로젝트인 Backstage를 기반으로 IDP를 구축하는 전 과정을 다룹니다. Software Catalog 구성, Golden Path Template 설계, 플러그인 개발, 그리고 운영 시 마주치는 실패 사례와 복구 절차까지 실전 중심으로 정리했습니다.

Internal Developer Platform 개념과 구성 요소

IDP란 무엇인가

Internal Developer Platform(IDP)은 개발자가 인프라와 운영 복잡성을 신경 쓰지 않고 코드에 집중할 수 있도록, 셀프서비스 인터페이스를 통해 인프라 프로비저닝, 배포, 모니터링, 문서화 등을 통합 제공하는 플랫폼입니다.

IDP의 핵심 구성 요소는 다음과 같습니다:

  • Service Catalog: 조직 내 모든 서비스, API, 리소스, 팀의 메타데이터를 한곳에서 관리
  • Self-Service Portal: 개발자가 인프라 요청 없이 직접 환경을 프로비저닝
  • Golden Path Template: 조직 표준에 맞는 프로젝트 스캐폴딩 템플릿
  • Documentation Hub: 서비스별 기술 문서를 코드 저장소와 연동하여 자동 렌더링
  • Integration Layer: CI/CD, 모니터링, 인시던트 관리 도구와의 연동

IDP vs Developer Portal

IDP와 Developer Portal은 종종 혼용되지만 엄밀히 구분됩니다. Developer Portal은 IDP의 프론트엔드 레이어입니다. IDP는 Portal 뒤에 있는 자동화 계층, 인프라 추상화, 정책 엔진, 워크플로우 오케스트레이션까지 포함하는 더 넓은 개념입니다.

┌──────────────────────────────────────────────────────┐
Developer Portal (UI)│   ┌───────────┐ ┌───────────┐ ┌───────────────────┐  │
│   │  Catalog  │ │  Scaffolder│TechDocs         │  │
│   └───────────┘ └───────────┘ └───────────────────┘  │
├──────────────────────────────────────────────────────┤
Internal Developer Platform│   ┌───────────┐ ┌───────────┐ ┌───────────────────┐  │
│   │ IaC Engine│CI/CD     │ │ Policy Engine     │  │
 (Terraform│  (GitHub (OPA/Kyverno)     │  │
│   │  Crossplane│Actions) │ │                   │  │
│   └───────────┘ └───────────┘ └───────────────────┘  │
│   ┌───────────┐ ┌───────────┐ ┌───────────────────┐  │
│   │ K8s       │ │ Observ-   │ │ Secret Mgmt       │  │
│   │ Clusters  │ │ ability   │  (Vault)           │  │
│   └───────────┘ └───────────┘ └───────────────────┘  │
└──────────────────────────────────────────────────────┘

Backstage 아키텍처와 핵심 기능

Backstage란

Backstage는 Spotify가 내부에서 사용하던 Developer Portal을 2020년에 오픈소스로 공개한 프로젝트입니다. 2024년 CNCF Incubating 프로젝트로 승격되었으며, 2026년 현재 커뮤니티에서 가장 활발하게 사용되는 IDP 프레임워크입니다.

핵심 기능 4가지

  1. Software Catalog: 조직의 모든 소프트웨어 자산(서비스, 라이브러리, 파이프라인, 인프라 등)을 YAML 기반 메타데이터로 등록하고 검색합니다. 서비스 간 의존성, 소유권, API 명세까지 한눈에 파악할 수 있습니다.

  2. Software Templates (Scaffolder): Golden Path를 코드화합니다. 개발자가 UI에서 몇 가지 파라미터만 입력하면, 조직 표준에 맞는 프로젝트가 자동으로 생성되고, Git 저장소 생성, CI/CD 파이프라인 설정, K8s 네임스페이스 프로비저닝까지 일괄 수행됩니다.

  3. TechDocs: docs-as-code 방식으로 MkDocs 기반의 기술 문서를 서비스 저장소에서 자동 빌드하고 Backstage UI에서 렌더링합니다. 문서가 코드와 함께 관리되므로 최신 상태가 유지됩니다.

  4. Plugins: Backstage의 확장성 핵심입니다. 200개 이상의 커뮤니티 플러그인이 존재하며, GitHub, GitLab, PagerDuty, Datadog, ArgoCD, Kubernetes 등과 연동됩니다. 직접 플러그인을 개발할 수도 있습니다.

아키텍처 구조

┌─────────────────────────────────────────────────┐
Backstage App (React)│  ┌─────────┐ ┌──────────┐ ┌─────────────────┐   │
│  │ Catalog │ │ Scaffolder│TechDocs        │   │
│  │ Plugin  │ │ Plugin   │ │ Plugin          │   │
│  └─────────┘ └──────────┘ └─────────────────┘   │
│  ┌─────────┐ ┌──────────┐ ┌─────────────────┐   │
│  │ K8s     │ │ CI/CD    │ │ Custom Plugins  │   │
│  │ Plugin  │ │ Plugin   │ │                 │   │
│  └─────────┘ └──────────┘ └─────────────────┘   │
├─────────────────────────────────────────────────┤
Backstage Backend (Node.js)│  ┌──────────┐ ┌───────────┐ ┌────────────────┐  │
│  │ Catalog  │ │ Scaffolder│Auth Provider  │  │
│  │ Backend  │ │ Backend (GitHub/Okta)  │  │
│  └──────────┘ └───────────┘ └────────────────┘  │
├─────────────────────────────────────────────────┤
Database (PostgreSQL) / Search└─────────────────────────────────────────────────┘

Backstage는 **프론트엔드(React SPA)**와 **백엔드(Node.js)**로 분리된 구조입니다. 프론트엔드 플러그인은 React 컴포넌트로, 백엔드 플러그인은 Express 라우터 형태로 구현됩니다. 데이터 저장소로는 PostgreSQL(프로덕션)이나 SQLite(개발용)를 사용합니다.

Backstage 설치와 초기 설정

프로젝트 생성

Backstage 앱을 생성하는 첫 번째 단계입니다. Node.js 18 이상과 Yarn Classic(1.x)이 필요합니다.

# Backstage 앱 생성
npx @backstage/create-app@latest

# 프로젝트 디렉토리 구조
# my-backstage-app/
# ├── app-config.yaml           # 메인 설정 파일
# ├── app-config.production.yaml # 프로덕션 오버라이드
# ├── catalog-info.yaml         # Backstage 자체의 카탈로그 엔트리
# ├── packages/
# │   ├── app/                  # 프론트엔드 (React)
# │   └── backend/              # 백엔드 (Node.js)
# ├── plugins/                  # 커스텀 플러그인
# └── package.json

# 로컬 개발 서버 시작
cd my-backstage-app
yarn dev

핵심 설정 - app-config.yaml

app-config.yaml은 Backstage의 중앙 설정 파일입니다. 데이터베이스, 인증, 카탈로그 소스, 통합(Integration) 등을 정의합니다.

# app-config.yaml
app:
  title: 'ACME Developer Platform'
  baseUrl: http://localhost:3000

organization:
  name: 'ACME Corp'

backend:
  baseUrl: http://localhost:7007
  listen:
    port: 7007
  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}

# GitHub 통합 설정
integrations:
  github:
    - host: github.com
      token: ${GITHUB_TOKEN}

# 인증 프로바이더 설정
auth:
  environment: production
  providers:
    github:
      production:
        clientId: ${AUTH_GITHUB_CLIENT_ID}
        clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}

# 카탈로그 소스 등록
catalog:
  import:
    entityFilename: catalog-info.yaml
    pullRequestBranchName: backstage-integration
  rules:
    - allow: [Component, System, API, Resource, Location, Template, Group, User]
  locations:
    # 조직 전체 GitHub 저장소 자동 탐색
    - type: github-discovery
      target: https://github.com/acme-corp/*/blob/main/catalog-info.yaml
    # 템플릿 등록
    - type: file
      target: ../../templates/all-templates.yaml
    # 조직 구조 동기화
    - type: github-org
      target: https://github.com/acme-corp

주의: GITHUB_TOKEN에는 repo, read:org, read:user 권한이 필요합니다. Fine-grained Token을 사용할 경우, 대상 저장소에 대한 Contents, Metadata 읽기 권한을 명시적으로 부여해야 합니다. 토큰 권한이 부족하면 카탈로그 등록 시 NotFoundError가 발생합니다.

프로덕션 배포 설정

# app-config.production.yaml
app:
  baseUrl: https://developer.acme.com

backend:
  baseUrl: https://developer-api.acme.com
  cors:
    origin: https://developer.acme.com

# TechDocs를 외부 스토리지로 설정
techdocs:
  builder: 'external'
  generator:
    runIn: 'local'
  publisher:
    type: 'awsS3'
    awsS3:
      bucketName: 'acme-techdocs'
      region: 'ap-northeast-2'

Software Catalog 구성

catalog-info.yaml 작성

모든 소프트웨어 엔티티는 catalog-info.yaml 파일을 통해 Backstage에 등록됩니다. 이 파일은 서비스 저장소의 루트에 위치하며, 서비스의 메타데이터, 소유권, 의존성, API 명세를 정의합니다.

# catalog-info.yaml - 마이크로서비스 등록 예시
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: order-service
  title: '주문 서비스'
  description: '주문 생성, 결제 처리, 재고 차감을 담당하는 핵심 도메인 서비스'
  annotations:
    # GitHub 연동
    github.com/project-slug: acme-corp/order-service
    # CI/CD 연동
    backstage.io/techdocs-ref: dir:.
    github.com/workflows: build-and-deploy.yaml
    # Kubernetes 연동
    backstage.io/kubernetes-id: order-service
    backstage.io/kubernetes-namespace: order
    # PagerDuty 인시던트 연동
    pagerduty.com/service-id: PXXXXXX
    # Datadog 대시보드
    datadoghq.com/dashboard-url: https://app.datadoghq.com/dashboard/xxx
  tags:
    - java
    - spring-boot
    - grpc
  links:
    - url: https://grafana.acme.com/d/order-svc
      title: 'Grafana Dashboard'
      icon: dashboard
    - url: https://wiki.acme.com/order-domain
      title: 'Domain Wiki'
      icon: docs
spec:
  type: service
  lifecycle: production
  owner: team-order
  system: ecommerce-platform
  providesApis:
    - order-api
  consumesApis:
    - inventory-api
    - payment-api
  dependsOn:
    - resource:order-database
    - resource:order-redis
---
# API 명세 등록
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
  name: order-api
  description: '주문 처리 gRPC API'
spec:
  type: grpc
  lifecycle: production
  owner: team-order
  system: ecommerce-platform
  definition:
    $text: ./proto/order.proto
---
# 데이터베이스 리소스 등록
apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
  name: order-database
  description: '주문 서비스 PostgreSQL 데이터베이스'
spec:
  type: database
  owner: team-order
  system: ecommerce-platform

엔티티 관계 구조

Backstage의 엔티티 모델은 계층 구조를 갖습니다:

엔티티 Kind역할예시
Domain비즈니스 도메인 영역commerce, logistics
System관련 컴포넌트의 논리적 그룹ecommerce-platform
Component개별 소프트웨어 단위 (서비스, 라이브러리, 웹사이트)order-service
API컴포넌트가 제공하는 인터페이스order-api (gRPC/REST/GraphQL)
Resource인프라 리소스order-database, order-redis
Group팀/조직 단위team-order
User개별 사용자jane.doe

이 관계 구조를 통해 "이 서비스를 누가 소유하고, 어떤 API를 제공하며, 어떤 인프라에 의존하고, 장애 시 누구에게 연락해야 하는지"를 즉시 파악할 수 있습니다. 서비스가 수백 개로 늘어나는 마이크로서비스 환경에서 이런 가시성은 결정적입니다.

Golden Path Template 만들기

Golden Path는 조직이 권장하는 "올바른 시작 방법"을 코드화한 것입니다. 개발자가 새 서비스를 시작할 때 CI/CD, 테스트, 모니터링, 보안 설정이 기본으로 내장된 프로젝트를 자동 생성할 수 있습니다.

Scaffolder Template 작성

# templates/spring-boot-service/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: spring-boot-grpc-service
  title: 'Spring Boot gRPC 마이크로서비스'
  description: |
    Spring Boot 3.x + gRPC 기반 마이크로서비스 프로젝트를 생성합니다.
    포함 항목: Dockerfile, Helm Chart, GitHub Actions CI/CD,
    Prometheus 메트릭, Health Check, catalog-info.yaml
  tags:
    - java
    - spring-boot
    - grpc
    - recommended
spec:
  owner: platform-team
  type: service

  # 사용자 입력 파라미터 정의
  parameters:
    - title: '서비스 기본 정보'
      required:
        - serviceName
        - owner
        - system
      properties:
        serviceName:
          title: '서비스 이름'
          type: string
          description: '영문 소문자, 하이픈만 사용 (예: order-service)'
          pattern: '^[a-z][a-z0-9-]*$'
          ui:autofocus: true
        description:
          title: '서비스 설명'
          type: string
        owner:
          title: '소유 팀'
          type: string
          ui:field: OwnerPicker
          ui:options:
            catalogFilter:
              kind: Group
        system:
          title: '소속 시스템'
          type: string
          ui:field: EntityPicker
          ui:options:
            catalogFilter:
              kind: System

    - title: '기술 옵션'
      properties:
        javaVersion:
          title: 'Java 버전'
          type: string
          enum: ['17', '21']
          default: '21'
        database:
          title: '데이터베이스'
          type: string
          enum: ['postgresql', 'mysql', 'none']
          default: 'postgresql'
        enableKafka:
          title: 'Kafka 연동'
          type: boolean
          default: false

    - title: '인프라 설정'
      properties:
        namespace:
          title: 'K8s 네임스페이스'
          type: string
          default: 'default'
        cluster:
          title: '배포 클러스터'
          type: string
          enum: ['dev', 'staging', 'production']
          default: 'dev'

  # 실행 단계 정의
  steps:
    # 1. 템플릿에서 프로젝트 코드 생성
    - id: fetch-template
      name: '프로젝트 코드 생성'
      action: fetch:template
      input:
        url: ./skeleton
        values:
          serviceName: ${{ parameters.serviceName }}
          description: ${{ parameters.description }}
          owner: ${{ parameters.owner }}
          system: ${{ parameters.system }}
          javaVersion: ${{ parameters.javaVersion }}
          database: ${{ parameters.database }}
          enableKafka: ${{ parameters.enableKafka }}
          namespace: ${{ parameters.namespace }}

    # 2. GitHub 저장소 생성
    - id: publish-github
      name: 'GitHub 저장소 생성'
      action: publish:github
      input:
        allowedHosts: ['github.com']
        repoUrl: github.com?owner=acme-corp&repo=${{ parameters.serviceName }}
        description: ${{ parameters.description }}
        defaultBranch: main
        protectDefaultBranch: true
        requireCodeOwnerReviews: true
        repoVisibility: internal

    # 3. Backstage 카탈로그에 등록
    - id: register-catalog
      name: '카탈로그 등록'
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps['publish-github'].output.repoContentsUrl }}
        catalogInfoPath: /catalog-info.yaml

    # 4. ArgoCD Application 생성
    - id: create-argocd-app
      name: 'ArgoCD 배포 설정'
      action: argocd:create-resources
      input:
        appName: ${{ parameters.serviceName }}
        argoInstance: main
        namespace: ${{ parameters.namespace }}
        repoUrl: ${{ steps['publish-github'].output.remoteUrl }}
        path: deploy/helm

  # 완료 후 안내
  output:
    links:
      - title: 'GitHub 저장소'
        url: ${{ steps['publish-github'].output.remoteUrl }}
      - title: '카탈로그에서 보기'
        icon: catalog
        entityRef: ${{ steps['register-catalog'].output.entityRef }}

이 템플릿 하나로 개발자는 UI에서 서비스 이름과 몇 가지 옵션만 선택하면, GitHub 저장소 생성부터 CI/CD 파이프라인 설정, K8s 배포, 카탈로그 등록까지 5분 안에 완료됩니다. 기존에 평균 3일이 소요되던 신규 서비스 온보딩이 획기적으로 단축됩니다.

플러그인 개발과 통합

커스텀 플러그인 생성

Backstage CLI를 통해 프론트엔드 또는 백엔드 플러그인 스캐폴딩을 수행할 수 있습니다.

# 프론트엔드 플러그인 생성
cd my-backstage-app
yarn new --select plugin

# 백엔드 플러그인 생성
yarn new --select backend-plugin

# 생성된 플러그인 구조
# plugins/
# └── my-custom-plugin/
#     ├── src/
#     │   ├── components/
#     │   │   └── ExampleComponent/
#     │   ├── plugin.ts          # 플러그인 정의
#     │   ├── routes.ts          # 라우팅 설정
#     │   └── index.ts
#     ├── dev/                   # 독립 개발 환경
#     │   └── index.tsx
#     └── package.json

프론트엔드 플러그인 구현 예시

팀별 서비스 헬스 현황을 대시보드로 보여주는 플러그인 예시입니다.

// plugins/team-health-dashboard/src/plugin.ts
import { createPlugin, createRoutableExtension } from '@backstage/core-plugin-api'

export const teamHealthDashboardPlugin = createPlugin({
  id: 'team-health-dashboard',
  routes: {
    root: rootRouteRef,
  },
})

export const TeamHealthDashboardPage = teamHealthDashboardPlugin.provide(
  createRoutableExtension({
    name: 'TeamHealthDashboardPage',
    component: () => import('./components/DashboardPage').then((m) => m.DashboardPage),
    mountPoint: rootRouteRef,
  })
)
// plugins/team-health-dashboard/src/components/DashboardPage.tsx
import React, { useEffect, useState } from 'react';
import {
  Table,
  TableColumn,
  StatusOK,
  StatusError,
  StatusWarning,
  Progress,
  ResponseErrorPanel,
} from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';

interface ServiceHealth {
  name: string;
  owner: string;
  status: 'healthy' | 'degraded' | 'down';
  uptime: number;
  lastIncident: string;
  errorRate: number;
}

const columns: TableColumn<ServiceHealth>[] = [
  { title: '서비스', field: 'name' },
  { title: '소유팀', field: 'owner' },
  {
    title: '상태',
    field: 'status',
    render: (row: ServiceHealth) => {
      switch (row.status) {
        case 'healthy':
          return <StatusOK>정상</StatusOK>;
        case 'degraded':
          return <StatusWarning>성능 저하</StatusWarning>;
        case 'down':
          return <StatusError>장애</StatusError>;
        default:
          return null;
      }
    },
  },
  { title: 'Uptime (%)', field: 'uptime', type: 'numeric' },
  { title: '에러율 (%)', field: 'errorRate', type: 'numeric' },
  { title: '마지막 인시던트', field: 'lastIncident' },
];

export const DashboardPage = () => {
  const catalogApi = useApi(catalogApiRef);
  const [services, setServices] = useState<ServiceHealth[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error>();

  useEffect(() => {
    const fetchData = async () => {
      try {
        const entities = await catalogApi.getEntities({
          filter: { kind: 'Component', 'spec.type': 'service' },
        });

        // 각 서비스의 헬스 데이터를 Prometheus/Datadog API에서 가져오기
        const healthData = await Promise.all(
          entities.items.map(async entity => {
            const metrics = await fetchServiceMetrics(
              entity.metadata.name,
            );
            return {
              name: entity.metadata.name,
              owner:
                entity.spec?.owner?.toString() ?? 'unknown',
              status: metrics.status,
              uptime: metrics.uptime,
              lastIncident: metrics.lastIncident,
              errorRate: metrics.errorRate,
            };
          }),
        );

        setServices(healthData);
      } catch (e) {
        setError(e as Error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [catalogApi]);

  if (loading) return <Progress />;
  if (error) return <ResponseErrorPanel error={error} />;

  return (
    <Table
      title="서비스 헬스 대시보드"
      columns={columns}
      data={services}
      options={{
        sorting: true,
        paging: true,
        pageSize: 20,
        search: true,
      }}
    />
  );
};

주요 플러그인 통합 목록

프로덕션 IDP에서 자주 통합하는 플러그인과 역할입니다:

플러그인용도설정 포인트
@backstage/plugin-kubernetesPod 상태, 로그, 이벤트 실시간 조회ServiceAccount, RBAC 설정
@backstage/plugin-techdocsdocs-as-code 기반 문서 렌더링MkDocs 설정, S3/GCS 퍼블리셔
@roadiehq/backstage-plugin-github-insightsPR 현황, 기여자 통계GitHub App 토큰
@backstage/plugin-catalog-importUI에서 catalog-info.yaml 등록카탈로그 규칙
@backstage-community/plugin-cost-insights클라우드 비용 가시화비용 API 연동
@pagerduty/backstage-plugin온콜 현황, 인시던트 목록PagerDuty API 키
@backstage/plugin-scaffolder-backend-module-githubGitHub 저장소 자동 생성GitHub App 권한

IDP 도구 비교

Backstage 외에도 여러 IDP 도구가 시장에 존재합니다. 조직의 규모, 기술 수준, 예산에 따라 적합한 도구가 다릅니다.

항목BackstagePortCortexOpsLevel
라이선스Apache 2.0 (오픈소스)SaaS (무료 티어 있음)SaaSSaaS
호스팅셀프호스팅클라우드 관리형클라우드 관리형클라우드 관리형
커스터마이징매우 높음 (플러그인 개발)중간 (위젯/블루프린트)낮음중간
초기 설정 비용높음 (전담 팀 필요)낮음낮음낮음
운영 부담높음 (업그레이드, 보안)없음 (SaaS)없음없음
Service CatalogYAML 기반, 강력UI 기반, 직관적자동 디스커버리자동 디스커버리
Scaffolding내장 (Scaffolder)셀프서비스 액션제한적서비스 템플릿
기술 문서TechDocs 내장외부 연동외부 연동외부 연동
Scorecards플러그인으로 추가내장핵심 기능내장
적합 조직대규모, 기술력 높은 팀중소규모, 빠른 도입서비스 성숙도 중심중간 규모
가격 (50명 기준)무료 (인프라/인건비 별도)~$2,000/월~$5,000/월~$3,000/월

선택 기준 요약: 전담 플랫폼 엔지니어링 팀이 있고 높은 커스터마이징이 필요하면 Backstage, 빠른 도입과 낮은 운영 부담을 원하면 Port, 서비스 성숙도 측정과 표준 준수에 집중하고 싶다면 Cortex가 적합합니다.

운영 시 주의사항

Adoption 메트릭 추적

IDP를 구축하는 것보다 개발자들이 실제로 사용하게 만드는 것이 더 어렵습니다. 다음 메트릭을 추적하여 채택 현황을 측정하세요:

메트릭측정 방법목표
카탈로그 커버리지등록된 서비스 수 / 전체 서비스 수95% 이상
템플릿 사용률템플릿으로 생성된 서비스 / 전체 신규 서비스80% 이상
DAU/WAUBackstage 일간/주간 활성 사용자개발자의 60% 이상
온보딩 시간신규 서비스 생성 ~ 첫 배포까지 소요 시간1시간 이내
MTTR 개선장애 발생 ~ 담당팀 인지까지 시간기존 대비 50% 감소
TechDocs 최신성30일 내 업데이트된 문서 비율70% 이상

피해야 할 안티패턴

  1. Big Bang 출시: 모든 기능을 한 번에 출시하려 하면 실패합니다. MVP로 시작하세요. 첫 번째 단계는 Software Catalog만으로 충분합니다. 카탈로그에 조직의 모든 서비스를 등록하고, 소유권과 의존성을 시각화하는 것만으로도 큰 가치를 제공합니다.

  2. 플랫폼 = 포탈 착각: UI만 예쁘게 만들어놓고 뒤에 자동화가 없으면 개발자들은 금방 떠납니다. "클릭 한 번으로 K8s 네임스페이스와 CI/CD가 설정되는" 실질적 자동화가 핵심입니다.

  3. 개발자 피드백 무시: 플랫폼은 제품입니다. 개발자 설문, 사용 데이터 분석, 정기적 피드백 세션 없이 플랫폼 팀이 "좋을 것 같은" 기능만 만들면 채택률이 떨어집니다.

  4. Backstage 버전 업그레이드 방치: Backstage는 릴리스 주기가 빠릅니다(월 1~2회). 6개월 이상 업그레이드를 미루면 마이그레이션이 극도로 어려워집니다. backstage-cli versions:bump 명령을 정기적으로 실행하세요.

  5. 강제 채택: 관리 도구로 전락하면 개발자의 반감을 삽니다. IDP는 개발자 경험(DX)을 개선하는 도구여야 합니다. "이걸 안 쓰면 징계"가 아니라, "이걸 쓰면 3일 걸리던 게 5분이 된다"가 메시지여야 합니다.

Backstage 업그레이드 절차

# 1. 현재 버전 확인
yarn backstage-cli info

# 2. 버전 범프 (의존성 자동 업데이트)
yarn backstage-cli versions:bump

# 3. 변경사항 확인
git diff

# 4. 타입 체크
yarn tsc

# 5. 테스트 실행
yarn test

# 6. 빌드 확인
yarn build

# 7. 마이그레이션 가이드 확인
# https://backstage.io/docs/getting-started/keeping-backstage-updated

# 권장: CI에서 자동으로 업그레이드 PR 생성
# .github/workflows/backstage-upgrade.yaml
# 매주 월요일 자동 실행하여 버전 범프 후 PR 생성

경고: versions:bump 실행 전 반드시 현재 변경사항을 커밋하세요. 업그레이드 시 package.json, yarn.lock, 그리고 때로는 코드 마이그레이션이 필요한 breaking change가 포함될 수 있습니다. 스테이징 환경에서 먼저 검증한 후 프로덕션에 적용하는 것을 권장합니다.

실패 사례와 복구 절차

사례 1: 카탈로그 엔티티 동기화 실패

증상: GitHub에 catalog-info.yaml이 존재하지만 Backstage UI에 표시되지 않음

원인 분석:

  • GitHub 토큰 만료 또는 권한 부족 (가장 흔함)
  • YAML 구문 오류 (들여쓰기, 잘못된 kind 값)
  • catalog.rules에서 해당 Kind가 allow 목록에 없음
  • 네트워크 문제로 GitHub API 호출 실패

복구 절차:

  1. Backstage 로그 확인: kubectl logs -l app=backstage -c backstage --tail=100
  2. 토큰 유효성 검증: curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user
  3. YAML 검증: npx @backstage/cli catalog-info validate catalog-info.yaml
  4. 수동 새로고침: Backstage UI의 해당 엔티티 페이지에서 "Refresh" 버튼 클릭
  5. 캐시 초기화가 필요한 경우: Backstage Pod 재시작

사례 2: Scaffolder 템플릿 실행 중 GitHub 저장소 생성 실패

증상: 템플릿 실행 시 "publish:github" 단계에서 RequestError: HttpError: Resource not accessible by integration 에러

원인 분석:

  • GitHub App의 Repository: Administration 권한 누락
  • 조직의 저장소 생성 정책(repository creation policy)에 의해 차단
  • 이미 동일 이름의 저장소가 존재

복구 절차:

  1. GitHub App 권한 확인 및 업데이트
  2. 조직 설정에서 App이 저장소 생성을 허용하는지 확인
  3. 실패한 태스크의 로그를 Backstage UI에서 확인 (Scaffolder 태스크 로그 페이지)
  4. 부분적으로 생성된 리소스(빈 저장소 등)가 있다면 수동 정리 후 재실행

사례 3: PostgreSQL 연결 풀 고갈

증상: Backstage 응답 속도 급격히 저하, 간헐적 ConnectionTimeoutError

원인 분석:

  • 카탈로그 엔티티가 수천 개로 증가하면서 DB 연결이 부족
  • 기본 연결 풀 크기(10개)가 트래픽을 감당하지 못함

복구 절차 및 예방:

# app-config.yaml - 연결 풀 튜닝
backend:
  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}
    pool:
      min: 5
      max: 30
      acquireTimeoutMillis: 60000
      idleTimeoutMillis: 30000

사례 4: TechDocs 빌드 실패

증상: 서비스 페이지에서 TechDocs 탭이 비어 있거나 빌드 에러 표시

원인 분석:

  • mkdocs.yml 파일이 저장소에 없음
  • Python 의존성(mkdocs-techdocs-core) 설치 실패
  • S3 버킷 권한 문제 (외부 퍼블리셔 사용 시)

복구 절차:

  1. 저장소에 mkdocs.ymldocs/index.md가 존재하는지 확인
  2. 로컬에서 npx @techdocs/cli generate 명령으로 빌드 테스트
  3. 외부 퍼블리셔 사용 시 IAM 권한 확인
  4. techdocs.builderlocal로 변경하여 Backstage가 직접 빌드하도록 설정 변경 (소규모 환경)

사례 5: Backstage 업그레이드 후 플러그인 호환성 깨짐

증상: 업그레이드 후 특정 플러그인 페이지에서 화이트 스크린 또는 런타임 에러

원인 분석:

  • Backstage 코어 패키지와 플러그인 간 버전 불일치
  • API가 deprecated된 후 제거됨
  • 새로운 Backstage 시스템(New Backend System) 마이그레이션 필요

복구 절차:

  1. 즉시 이전 버전으로 롤백 (Helm rollback 또는 이전 이미지 태그로 배포)
  2. 브라우저 콘솔에서 에러 메시지 확인
  3. 문제 플러그인의 GitHub 이슈 검색
  4. 플러그인을 최신 호환 버전으로 업데이트하거나, 임시로 비활성화
  5. 스테이징에서 검증 후 다시 프로덕션 배포

체크리스트

IDP 구축 준비 체크리스트

  • 플랫폼 엔지니어링 전담 팀 구성 (최소 2~3명)
  • 개발자 페인포인트 서베이 실시 (인지 부하가 가장 큰 영역 파악)
  • IDP의 첫 번째 MVP 범위 결정 (권장: Software Catalog부터 시작)
  • Backstage vs SaaS 도구 비교 평가 완료
  • PostgreSQL 인스턴스 프로비저닝 (프로덕션)
  • GitHub App 또는 OAuth App 생성 및 권한 설정
  • SSO 프로바이더 연동 계획 (Okta, Azure AD, Google 등)

Backstage 운영 체크리스트

  • app-config.production.yaml 분리 및 시크릿 관리 (Vault, K8s Secret)
  • HTTPS/TLS 설정 (Ingress 또는 로드밸런서)
  • 데이터베이스 백업 스케줄 설정
  • 모니터링 구성 (Backstage 메트릭 + 인프라 메트릭)
  • 주간 버전 업그레이드 워크플로우 자동화
  • 카탈로그 엔티티 검증 CI (PR에서 catalog-info.yaml 유효성 검사)
  • 개발자 온보딩 가이드 문서 작성
  • 분기별 개발자 만족도 조사 계획

카탈로그 등록 체크리스트 (서비스별)

  • catalog-info.yaml 작성 및 저장소 루트에 배치
  • metadata.name 조직 네이밍 규칙 준수 확인
  • spec.owner 올바른 팀 그룹으로 설정
  • spec.system 올바른 시스템에 소속
  • API 명세(OpenAPI, gRPC, AsyncAPI) 등록
  • annotations에 CI/CD, 모니터링, 인시던트 도구 연동 정보 추가
  • TechDocs 설정 (mkdocs.yml + docs/ 디렉토리)
  • Kubernetes 어노테이션 설정 (클러스터 내 워크로드 매핑)

참고자료

Platform Engineering and Building an Internal Developer Platform with Backstage: A Practical Guide

Platform Engineering Backstage

Introduction - Why Platform Engineering Is Replacing Traditional DevOps

The DevOps principle of "You build it, you run it" gave developers autonomy, but also imposed an enormous cognitive load. Developers found themselves writing Kubernetes manifests, configuring CI/CD pipelines, setting up monitoring dashboards, and handling infrastructure provisioning all on their own. Gartner predicted that 80% of software engineering organizations would establish Platform Engineering teams by 2026, and that trend has become reality.

Platform Engineering is a discipline that builds Internal Developer Platforms (IDPs) with self-service capabilities to reduce developer cognitive load and accelerate software delivery across the organization. While DevOps focused on "culture and practices," Platform Engineering focuses on "platform as a product." Developers are the IDP's customers, and the platform team develops and operates this product.

This article covers the entire process of building an IDP based on Backstage, a CNCF Incubating project. It provides a practice-oriented guide from Software Catalog configuration, Golden Path Template design, and plugin development to failure cases and recovery procedures encountered during operations.

Internal Developer Platform Concepts and Components

What Is an IDP?

An Internal Developer Platform (IDP) is a platform that provides a self-service interface so developers can focus on code without worrying about infrastructure and operational complexity. It integrates infrastructure provisioning, deployment, monitoring, and documentation through a unified interface.

The core components of an IDP are:

  • Service Catalog: Centralized management of metadata for all services, APIs, resources, and teams in the organization
  • Self-Service Portal: Developers directly provision environments without infrastructure requests
  • Golden Path Template: Project scaffolding templates that conform to organizational standards
  • Documentation Hub: Auto-rendering of per-service technical docs linked to code repositories
  • Integration Layer: Integration with CI/CD, monitoring, and incident management tools

IDP vs Developer Portal

IDP and Developer Portal are often used interchangeably, but they are strictly different. A Developer Portal is the frontend layer of an IDP. The IDP is a broader concept that includes the automation layer, infrastructure abstraction, policy engine, and workflow orchestration behind the Portal.

┌──────────────────────────────────────────────────────┐
Developer Portal (UI)│   ┌───────────┐ ┌───────────┐ ┌───────────────────┐  │
│   │  Catalog  │ │  Scaffolder│TechDocs         │  │
│   └───────────┘ └───────────┘ └───────────────────┘  │
├──────────────────────────────────────────────────────┤
Internal Developer Platform│   ┌───────────┐ ┌───────────┐ ┌───────────────────┐  │
│   │ IaC Engine│CI/CD     │ │ Policy Engine     │  │
 (Terraform│  (GitHub (OPA/Kyverno)     │  │
│   │  Crossplane│Actions) │ │                   │  │
│   └───────────┘ └───────────┘ └───────────────────┘  │
│   ┌───────────┐ ┌───────────┐ ┌───────────────────┐  │
│   │ K8s       │ │ Observ-   │ │ Secret Mgmt       │  │
│   │ Clusters  │ │ ability   │  (Vault)           │  │
│   └───────────┘ └───────────┘ └───────────────────┘  │
└──────────────────────────────────────────────────────┘

Backstage Architecture and Core Features

What Is Backstage?

Backstage is a project that Spotify open-sourced in 2020 from the Developer Portal it used internally. It was promoted to a CNCF Incubating project in 2024, and as of 2026, it is the most actively used IDP framework in the community.

Four Core Features

  1. Software Catalog: Register and search all software assets in the organization (services, libraries, pipelines, infrastructure, etc.) using YAML-based metadata. You can see service dependencies, ownership, and API specifications at a glance.

  2. Software Templates (Scaffolder): Codify Golden Paths. When a developer enters a few parameters in the UI, a project conforming to organizational standards is automatically generated, including Git repository creation, CI/CD pipeline setup, and K8s namespace provisioning -- all in one step.

  3. TechDocs: Automatically build MkDocs-based technical documentation from service repositories using a docs-as-code approach and render it in the Backstage UI. Documentation stays up-to-date because it is managed alongside the code.

  4. Plugins: The core of Backstage's extensibility. Over 200 community plugins exist, integrating with GitHub, GitLab, PagerDuty, Datadog, ArgoCD, Kubernetes, and more. You can also develop your own plugins.

Architecture Structure

┌─────────────────────────────────────────────────┐
Backstage App (React)│  ┌─────────┐ ┌──────────┐ ┌─────────────────┐   │
│  │ Catalog │ │ Scaffolder│TechDocs        │   │
│  │ Plugin  │ │ Plugin   │ │ Plugin          │   │
│  └─────────┘ └──────────┘ └─────────────────┘   │
│  ┌─────────┐ ┌──────────┐ ┌─────────────────┐   │
│  │ K8s     │ │ CI/CD    │ │ Custom Plugins  │   │
│  │ Plugin  │ │ Plugin   │ │                 │   │
│  └─────────┘ └──────────┘ └─────────────────┘   │
├─────────────────────────────────────────────────┤
Backstage Backend (Node.js)│  ┌──────────┐ ┌───────────┐ ┌────────────────┐  │
│  │ Catalog  │ │ Scaffolder│Auth Provider  │  │
│  │ Backend  │ │ Backend (GitHub/Okta)  │  │
│  └──────────┘ └───────────┘ └────────────────┘  │
├─────────────────────────────────────────────────┤
Database (PostgreSQL) / Search└─────────────────────────────────────────────────┘

Backstage has a separated architecture of frontend (React SPA) and backend (Node.js). Frontend plugins are implemented as React components, and backend plugins as Express routers. PostgreSQL (production) or SQLite (development) is used as the data store.

Backstage Installation and Initial Setup

Project Creation

The first step is creating a Backstage app. Node.js 18 or higher and Yarn Classic (1.x) are required.

# Create Backstage app
npx @backstage/create-app@latest

# Project directory structure
# my-backstage-app/
# ├── app-config.yaml           # Main configuration file
# ├── app-config.production.yaml # Production overrides
# ├── catalog-info.yaml         # Backstage's own catalog entry
# ├── packages/
# │   ├── app/                  # Frontend (React)
# │   └── backend/              # Backend (Node.js)
# ├── plugins/                  # Custom plugins
# └── package.json

# Start local dev server
cd my-backstage-app
yarn dev

Core Configuration - app-config.yaml

app-config.yaml is Backstage's central configuration file. It defines the database, authentication, catalog sources, and integrations.

# app-config.yaml
app:
  title: 'ACME Developer Platform'
  baseUrl: http://localhost:3000

organization:
  name: 'ACME Corp'

backend:
  baseUrl: http://localhost:7007
  listen:
    port: 7007
  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}

# GitHub integration settings
integrations:
  github:
    - host: github.com
      token: ${GITHUB_TOKEN}

# Authentication provider settings
auth:
  environment: production
  providers:
    github:
      production:
        clientId: ${AUTH_GITHUB_CLIENT_ID}
        clientSecret: ${AUTH_GITHUB_CLIENT_SECRET}

# Catalog source registration
catalog:
  import:
    entityFilename: catalog-info.yaml
    pullRequestBranchName: backstage-integration
  rules:
    - allow: [Component, System, API, Resource, Location, Template, Group, User]
  locations:
    # Auto-discover all GitHub repos in the organization
    - type: github-discovery
      target: https://github.com/acme-corp/*/blob/main/catalog-info.yaml
    # Register templates
    - type: file
      target: ../../templates/all-templates.yaml
    # Sync organization structure
    - type: github-org
      target: https://github.com/acme-corp

Note: GITHUB_TOKEN requires repo, read:org, and read:user permissions. When using Fine-grained Tokens, you must explicitly grant Contents and Metadata read permissions for target repositories. Insufficient token permissions will cause NotFoundError during catalog registration.

Production Deployment Configuration

# app-config.production.yaml
app:
  baseUrl: https://developer.acme.com

backend:
  baseUrl: https://developer-api.acme.com
  cors:
    origin: https://developer.acme.com

# Configure TechDocs with external storage
techdocs:
  builder: 'external'
  generator:
    runIn: 'local'
  publisher:
    type: 'awsS3'
    awsS3:
      bucketName: 'acme-techdocs'
      region: 'ap-northeast-2'

Software Catalog Configuration

Writing catalog-info.yaml

All software entities are registered in Backstage through a catalog-info.yaml file. This file is placed at the root of the service repository and defines the service's metadata, ownership, dependencies, and API specifications.

# catalog-info.yaml - Microservice registration example
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: order-service
  title: 'Order Service'
  description: 'Core domain service responsible for order creation, payment processing, and inventory deduction'
  annotations:
    # GitHub integration
    github.com/project-slug: acme-corp/order-service
    # CI/CD integration
    backstage.io/techdocs-ref: dir:.
    github.com/workflows: build-and-deploy.yaml
    # Kubernetes integration
    backstage.io/kubernetes-id: order-service
    backstage.io/kubernetes-namespace: order
    # PagerDuty incident integration
    pagerduty.com/service-id: PXXXXXX
    # Datadog dashboard
    datadoghq.com/dashboard-url: https://app.datadoghq.com/dashboard/xxx
  tags:
    - java
    - spring-boot
    - grpc
  links:
    - url: https://grafana.acme.com/d/order-svc
      title: 'Grafana Dashboard'
      icon: dashboard
    - url: https://wiki.acme.com/order-domain
      title: 'Domain Wiki'
      icon: docs
spec:
  type: service
  lifecycle: production
  owner: team-order
  system: ecommerce-platform
  providesApis:
    - order-api
  consumesApis:
    - inventory-api
    - payment-api
  dependsOn:
    - resource:order-database
    - resource:order-redis
---
# API specification registration
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
  name: order-api
  description: 'Order processing gRPC API'
spec:
  type: grpc
  lifecycle: production
  owner: team-order
  system: ecommerce-platform
  definition:
    $text: ./proto/order.proto
---
# Database resource registration
apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
  name: order-database
  description: 'Order Service PostgreSQL database'
spec:
  type: database
  owner: team-order
  system: ecommerce-platform

Entity Relationship Structure

Backstage's entity model has a hierarchical structure:

Entity KindRoleExample
DomainBusiness domain areacommerce, logistics
SystemLogical group of related componentsecommerce-platform
ComponentIndividual software unit (service, library, website)order-service
APIInterface provided by a componentorder-api (gRPC/REST/GraphQL)
ResourceInfrastructure resourceorder-database, order-redis
GroupTeam/organizational unitteam-order
UserIndividual userjane.doe

Through this relationship structure, you can instantly determine "who owns this service, what APIs it provides, what infrastructure it depends on, and who to contact during an incident." In microservice environments where services grow to hundreds, this visibility is critical.

Creating Golden Path Templates

A Golden Path codifies the organization's recommended "right way to start." When developers begin a new service, they can automatically generate a project with CI/CD, testing, monitoring, and security settings built in by default.

Writing a Scaffolder Template

# templates/spring-boot-service/template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: spring-boot-grpc-service
  title: 'Spring Boot gRPC Microservice'
  description: |
    Creates a Spring Boot 3.x + gRPC-based microservice project.
    Includes: Dockerfile, Helm Chart, GitHub Actions CI/CD,
    Prometheus metrics, Health Check, catalog-info.yaml
  tags:
    - java
    - spring-boot
    - grpc
    - recommended
spec:
  owner: platform-team
  type: service

  # Define user input parameters
  parameters:
    - title: 'Service Basic Information'
      required:
        - serviceName
        - owner
        - system
      properties:
        serviceName:
          title: 'Service Name'
          type: string
          description: 'Lowercase letters and hyphens only (e.g., order-service)'
          pattern: '^[a-z][a-z0-9-]*$'
          ui:autofocus: true
        description:
          title: 'Service Description'
          type: string
        owner:
          title: 'Owning Team'
          type: string
          ui:field: OwnerPicker
          ui:options:
            catalogFilter:
              kind: Group
        system:
          title: 'Parent System'
          type: string
          ui:field: EntityPicker
          ui:options:
            catalogFilter:
              kind: System

    - title: 'Technical Options'
      properties:
        javaVersion:
          title: 'Java Version'
          type: string
          enum: ['17', '21']
          default: '21'
        database:
          title: 'Database'
          type: string
          enum: ['postgresql', 'mysql', 'none']
          default: 'postgresql'
        enableKafka:
          title: 'Kafka Integration'
          type: boolean
          default: false

    - title: 'Infrastructure Settings'
      properties:
        namespace:
          title: 'K8s Namespace'
          type: string
          default: 'default'
        cluster:
          title: 'Deployment Cluster'
          type: string
          enum: ['dev', 'staging', 'production']
          default: 'dev'

  # Define execution steps
  steps:
    # 1. Generate project code from template
    - id: fetch-template
      name: 'Generate Project Code'
      action: fetch:template
      input:
        url: ./skeleton
        values:
          serviceName: ${{ parameters.serviceName }}
          description: ${{ parameters.description }}
          owner: ${{ parameters.owner }}
          system: ${{ parameters.system }}
          javaVersion: ${{ parameters.javaVersion }}
          database: ${{ parameters.database }}
          enableKafka: ${{ parameters.enableKafka }}
          namespace: ${{ parameters.namespace }}

    # 2. Create GitHub repository
    - id: publish-github
      name: 'Create GitHub Repository'
      action: publish:github
      input:
        allowedHosts: ['github.com']
        repoUrl: github.com?owner=acme-corp&repo=${{ parameters.serviceName }}
        description: ${{ parameters.description }}
        defaultBranch: main
        protectDefaultBranch: true
        requireCodeOwnerReviews: true
        repoVisibility: internal

    # 3. Register in Backstage catalog
    - id: register-catalog
      name: 'Register in Catalog'
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps['publish-github'].output.repoContentsUrl }}
        catalogInfoPath: /catalog-info.yaml

    # 4. Create ArgoCD Application
    - id: create-argocd-app
      name: 'Configure ArgoCD Deployment'
      action: argocd:create-resources
      input:
        appName: ${{ parameters.serviceName }}
        argoInstance: main
        namespace: ${{ parameters.namespace }}
        repoUrl: ${{ steps['publish-github'].output.remoteUrl }}
        path: deploy/helm

  # Post-completion guidance
  output:
    links:
      - title: 'GitHub Repository'
        url: ${{ steps['publish-github'].output.remoteUrl }}
      - title: 'View in Catalog'
        icon: catalog
        entityRef: ${{ steps['register-catalog'].output.entityRef }}

With this single template, a developer can select a service name and a few options from the UI, and everything from GitHub repository creation to CI/CD pipeline setup, K8s deployment, and catalog registration is completed within 5 minutes. New service onboarding that previously took an average of 3 days is dramatically shortened.

Plugin Development and Integration

Creating Custom Plugins

Use the Backstage CLI to scaffold frontend or backend plugins.

# Create frontend plugin
cd my-backstage-app
yarn new --select plugin

# Create backend plugin
yarn new --select backend-plugin

# Generated plugin structure
# plugins/
# └── my-custom-plugin/
#     ├── src/
#     │   ├── components/
#     │   │   └── ExampleComponent/
#     │   ├── plugin.ts          # Plugin definition
#     │   ├── routes.ts          # Routing configuration
#     │   └── index.ts
#     ├── dev/                   # Standalone dev environment
#     │   └── index.tsx
#     └── package.json

Frontend Plugin Implementation Example

Here is an example plugin that shows a dashboard of per-team service health status.

// plugins/team-health-dashboard/src/plugin.ts
import { createPlugin, createRoutableExtension } from '@backstage/core-plugin-api'

export const teamHealthDashboardPlugin = createPlugin({
  id: 'team-health-dashboard',
  routes: {
    root: rootRouteRef,
  },
})

export const TeamHealthDashboardPage = teamHealthDashboardPlugin.provide(
  createRoutableExtension({
    name: 'TeamHealthDashboardPage',
    component: () => import('./components/DashboardPage').then((m) => m.DashboardPage),
    mountPoint: rootRouteRef,
  })
)
// plugins/team-health-dashboard/src/components/DashboardPage.tsx
import React, { useEffect, useState } from 'react';
import {
  Table,
  TableColumn,
  StatusOK,
  StatusError,
  StatusWarning,
  Progress,
  ResponseErrorPanel,
} from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';

interface ServiceHealth {
  name: string;
  owner: string;
  status: 'healthy' | 'degraded' | 'down';
  uptime: number;
  lastIncident: string;
  errorRate: number;
}

const columns: TableColumn<ServiceHealth>[] = [
  { title: 'Service', field: 'name' },
  { title: 'Owner', field: 'owner' },
  {
    title: 'Status',
    field: 'status',
    render: (row: ServiceHealth) => {
      switch (row.status) {
        case 'healthy':
          return <StatusOK>Healthy</StatusOK>;
        case 'degraded':
          return <StatusWarning>Degraded</StatusWarning>;
        case 'down':
          return <StatusError>Down</StatusError>;
        default:
          return null;
      }
    },
  },
  { title: 'Uptime (%)', field: 'uptime', type: 'numeric' },
  { title: 'Error Rate (%)', field: 'errorRate', type: 'numeric' },
  { title: 'Last Incident', field: 'lastIncident' },
];

export const DashboardPage = () => {
  const catalogApi = useApi(catalogApiRef);
  const [services, setServices] = useState<ServiceHealth[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error>();

  useEffect(() => {
    const fetchData = async () => {
      try {
        const entities = await catalogApi.getEntities({
          filter: { kind: 'Component', 'spec.type': 'service' },
        });

        // Fetch health data for each service from Prometheus/Datadog API
        const healthData = await Promise.all(
          entities.items.map(async entity => {
            const metrics = await fetchServiceMetrics(
              entity.metadata.name,
            );
            return {
              name: entity.metadata.name,
              owner:
                entity.spec?.owner?.toString() ?? 'unknown',
              status: metrics.status,
              uptime: metrics.uptime,
              lastIncident: metrics.lastIncident,
              errorRate: metrics.errorRate,
            };
          }),
        );

        setServices(healthData);
      } catch (e) {
        setError(e as Error);
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, [catalogApi]);

  if (loading) return <Progress />;
  if (error) return <ResponseErrorPanel error={error} />;

  return (
    <Table
      title="Service Health Dashboard"
      columns={columns}
      data={services}
      options={{
        sorting: true,
        paging: true,
        pageSize: 20,
        search: true,
      }}
    />
  );
};

Key Plugin Integration List

Frequently integrated plugins and their roles in production IDPs:

PluginPurposeConfiguration Point
@backstage/plugin-kubernetesReal-time Pod status, logs, event viewingServiceAccount, RBAC settings
@backstage/plugin-techdocsdocs-as-code based doc renderingMkDocs config, S3/GCS publisher
@roadiehq/backstage-plugin-github-insightsPR status, contributor statsGitHub App token
@backstage/plugin-catalog-importRegister catalog-info.yaml from UICatalog rules
@backstage-community/plugin-cost-insightsCloud cost visualizationCost API integration
@pagerduty/backstage-pluginOn-call status, incident listPagerDuty API key
@backstage/plugin-scaffolder-backend-module-githubAutomatic GitHub repository creationGitHub App permissions

IDP Tool Comparison

Besides Backstage, several IDP tools exist in the market. The right tool varies based on organizational size, technical capability, and budget.

ItemBackstagePortCortexOpsLevel
LicenseApache 2.0 (Open Source)SaaS (free tier available)SaaSSaaS
HostingSelf-hostedCloud managedCloud managedCloud managed
CustomizationVery high (plugin dev)Medium (widgets/blueprints)LowMedium
Initial Setup CostHigh (dedicated team needed)LowLowLow
Operations BurdenHigh (upgrades, security)None (SaaS)NoneNone
Service CatalogYAML-based, powerfulUI-based, intuitiveAuto-discoveryAuto-discovery
ScaffoldingBuilt-in (Scaffolder)Self-service actionsLimitedService templates
Technical DocsTechDocs built-inExternal integrationExternal integrationExternal integration
ScorecardsAdded via pluginBuilt-inCore featureBuilt-in
Best ForLarge, highly technical teamsSmall-medium, fast adoptionService maturity focusMid-size orgs
Price (50 users)Free (infra/personnel costs separate)~$2,000/month~$5,000/month~$3,000/month

Selection Criteria Summary: If you have a dedicated Platform Engineering team and need high customization, choose Backstage. For fast adoption and low operational burden, choose Port. If you want to focus on service maturity measurement and standards compliance, Cortex is the right fit.

Operational Considerations

Tracking Adoption Metrics

Building an IDP is easier than getting developers to actually use it. Track the following metrics to measure adoption:

MetricMeasurement MethodTarget
Catalog CoverageRegistered services / Total servicesOver 95%
Template Usage RateServices created via template / Total new servicesOver 80%
DAU/WAUBackstage daily/weekly active usersOver 60% of developers
Onboarding TimeTime from new service creation to first deploymentUnder 1 hour
MTTR ImprovementTime from incident to responsible team awareness50% reduction from baseline
TechDocs FreshnessPercentage of docs updated within 30 daysOver 70%

Anti-Patterns to Avoid

  1. Big Bang Launch: Trying to launch all features at once leads to failure. Start with an MVP. The first step is just the Software Catalog. Registering all services in the catalog and visualizing ownership and dependencies alone provides significant value.

  2. Platform = Portal Misconception: If you build a pretty UI with no automation behind it, developers will quickly leave. The key is practical automation -- "one click to set up a K8s namespace and CI/CD."

  3. Ignoring Developer Feedback: A platform is a product. Without developer surveys, usage data analysis, and regular feedback sessions, the platform team building features they think are "nice to have" will see adoption rates drop.

  4. Neglecting Backstage Version Upgrades: Backstage has a fast release cycle (1-2 times per month). Postponing upgrades for over 6 months makes migration extremely difficult. Run backstage-cli versions:bump regularly.

  5. Forced Adoption: If the IDP devolves into a management tool, it breeds developer resentment. An IDP should improve developer experience (DX). The message should not be "use this or face consequences" but rather "use this and what took 3 days now takes 5 minutes."

Backstage Upgrade Procedure

# 1. Check current version
yarn backstage-cli info

# 2. Version bump (auto-update dependencies)
yarn backstage-cli versions:bump

# 3. Review changes
git diff

# 4. Type check
yarn tsc

# 5. Run tests
yarn test

# 6. Verify build
yarn build

# 7. Check migration guide
# https://backstage.io/docs/getting-started/keeping-backstage-updated

# Recommended: Auto-generate upgrade PRs in CI
# .github/workflows/backstage-upgrade.yaml
# Runs weekly on Monday to bump versions and create PR

Warning: Always commit current changes before running versions:bump. Upgrades may include changes to package.json, yarn.lock, and sometimes code migration for breaking changes. It is recommended to verify in a staging environment first before applying to production.

Failure Cases and Recovery Procedures

Case 1: Catalog Entity Sync Failure

Symptom: catalog-info.yaml exists on GitHub but does not appear in the Backstage UI

Root Cause Analysis:

  • GitHub token expired or insufficient permissions (most common)
  • YAML syntax error (indentation, invalid kind value)
  • The Kind is not in the allow list in catalog.rules
  • Network issues causing GitHub API call failures

Recovery Procedure:

  1. Check Backstage logs: kubectl logs -l app=backstage -c backstage --tail=100
  2. Validate token: curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user
  3. Validate YAML: npx @backstage/cli catalog-info validate catalog-info.yaml
  4. Manual refresh: Click the "Refresh" button on the entity page in the Backstage UI
  5. If cache reset is needed: Restart the Backstage Pod

Case 2: Scaffolder Template Execution Fails During GitHub Repository Creation

Symptom: RequestError: HttpError: Resource not accessible by integration error at the "publish:github" step

Root Cause Analysis:

  • Missing Repository: Administration permission on the GitHub App
  • Blocked by the organization's repository creation policy
  • A repository with the same name already exists

Recovery Procedure:

  1. Check and update GitHub App permissions
  2. Verify the App is allowed to create repositories in the organization settings
  3. Check the failed task logs in the Backstage UI (Scaffolder task log page)
  4. If partially created resources (empty repos, etc.) exist, manually clean up and re-run

Case 3: PostgreSQL Connection Pool Exhaustion

Symptom: Backstage response time sharply degrades, intermittent ConnectionTimeoutError

Root Cause Analysis:

  • Catalog entities grew to thousands, exceeding available DB connections
  • Default connection pool size (10) cannot handle the traffic

Recovery Procedure and Prevention:

# app-config.yaml - Connection pool tuning
backend:
  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}
    pool:
      min: 5
      max: 30
      acquireTimeoutMillis: 60000
      idleTimeoutMillis: 30000

Case 4: TechDocs Build Failure

Symptom: TechDocs tab on the service page is empty or shows a build error

Root Cause Analysis:

  • mkdocs.yml file is missing from the repository
  • Python dependency (mkdocs-techdocs-core) installation failure
  • S3 bucket permission issues (when using external publisher)

Recovery Procedure:

  1. Verify that mkdocs.yml and docs/index.md exist in the repository
  2. Test the build locally with npx @techdocs/cli generate
  3. Check IAM permissions when using an external publisher
  4. Change techdocs.builder to local so Backstage builds directly (for small environments)

Case 5: Plugin Compatibility Break After Backstage Upgrade

Symptom: White screen or runtime error on specific plugin pages after upgrade

Root Cause Analysis:

  • Version mismatch between Backstage core packages and plugins
  • API was deprecated and then removed
  • Migration to the New Backend System is required

Recovery Procedure:

  1. Immediately roll back to the previous version (Helm rollback or deploy with the previous image tag)
  2. Check error messages in the browser console
  3. Search the plugin's GitHub issues
  4. Update the plugin to the latest compatible version, or temporarily disable it
  5. Re-deploy to production after verifying in staging

Checklists

IDP Build Preparation Checklist

  • Form a dedicated Platform Engineering team (minimum 2-3 people)
  • Conduct developer pain point survey (identify areas with highest cognitive load)
  • Decide on the first MVP scope for the IDP (recommended: start with Software Catalog)
  • Complete comparative evaluation of Backstage vs SaaS tools
  • Provision PostgreSQL instance (production)
  • Create and configure permissions for GitHub App or OAuth App
  • Plan SSO provider integration (Okta, Azure AD, Google, etc.)

Backstage Operations Checklist

  • Separate app-config.production.yaml and manage secrets (Vault, K8s Secret)
  • Configure HTTPS/TLS (Ingress or load balancer)
  • Set up database backup schedule
  • Configure monitoring (Backstage metrics + infrastructure metrics)
  • Automate weekly version upgrade workflow
  • Catalog entity validation CI (validate catalog-info.yaml in PRs)
  • Write developer onboarding guide documentation
  • Plan quarterly developer satisfaction surveys

Catalog Registration Checklist (Per Service)

  • Write catalog-info.yaml and place it at the repository root
  • Verify metadata.name follows organizational naming conventions
  • Set spec.owner to the correct team group
  • Assign spec.system to the correct system
  • Register API specifications (OpenAPI, gRPC, AsyncAPI)
  • Add CI/CD, monitoring, and incident tool integration info in annotations
  • Configure TechDocs (mkdocs.yml + docs/ directory)
  • Set up Kubernetes annotations (workload mapping within the cluster)

References

Quiz

Q1: What is the main topic covered in "Platform Engineering and Building an Internal Developer Platform with Backstage: A Practical Guide"?

A comprehensive Platform Engineering guide covering Backstage-based Internal Developer Platform construction, Software Catalog, Golden Path Templates, plugin development, and operations automation.

Q2: What is Internal Developer Platform Concepts and Components? What Is an IDP? An Internal Developer Platform (IDP) is a platform that provides a self-service interface so developers can focus on code without worrying about infrastructure and operational complexity.

Q3: Describe the Backstage Architecture and Core Features. What Is Backstage? Backstage is a project that Spotify open-sourced in 2020 from the Developer Portal it used internally. It was promoted to a CNCF Incubating project in 2024, and as of 2026, it is the most actively used IDP framework in the community.

Q4: What are the key steps for Backstage Installation and Initial Setup? Project Creation The first step is creating a Backstage app. Node.js 18 or higher and Yarn Classic (1.x) are required. Core Configuration - app-config.yaml app-config.yaml is Backstage's central configuration file.

Q5: What are the key steps for Software Catalog Configuration? Writing catalog-info.yaml All software entities are registered in Backstage through a catalog-info.yaml file. This file is placed at the root of the service repository and defines the service's metadata, ownership, dependencies, and API specifications.