Split View: Platform Engineering과 Backstage로 Internal Developer Platform 구축 실전 가이드
Platform Engineering과 Backstage로 Internal Developer Platform 구축 실전 가이드
- 들어가며 - Platform Engineering이 전통적 DevOps를 대체하는 이유
- Internal Developer Platform 개념과 구성 요소
- Backstage 아키텍처와 핵심 기능
- Backstage 설치와 초기 설정
- Software Catalog 구성
- Golden Path Template 만들기
- 플러그인 개발과 통합
- IDP 도구 비교
- 운영 시 주의사항
- 실패 사례와 복구 절차
- 체크리스트
- 참고자료

들어가며 - 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가지
-
Software Catalog: 조직의 모든 소프트웨어 자산(서비스, 라이브러리, 파이프라인, 인프라 등)을 YAML 기반 메타데이터로 등록하고 검색합니다. 서비스 간 의존성, 소유권, API 명세까지 한눈에 파악할 수 있습니다.
-
Software Templates (Scaffolder): Golden Path를 코드화합니다. 개발자가 UI에서 몇 가지 파라미터만 입력하면, 조직 표준에 맞는 프로젝트가 자동으로 생성되고, Git 저장소 생성, CI/CD 파이프라인 설정, K8s 네임스페이스 프로비저닝까지 일괄 수행됩니다.
-
TechDocs: docs-as-code 방식으로 MkDocs 기반의 기술 문서를 서비스 저장소에서 자동 빌드하고 Backstage UI에서 렌더링합니다. 문서가 코드와 함께 관리되므로 최신 상태가 유지됩니다.
-
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-kubernetes | Pod 상태, 로그, 이벤트 실시간 조회 | ServiceAccount, RBAC 설정 |
@backstage/plugin-techdocs | docs-as-code 기반 문서 렌더링 | MkDocs 설정, S3/GCS 퍼블리셔 |
@roadiehq/backstage-plugin-github-insights | PR 현황, 기여자 통계 | GitHub App 토큰 |
@backstage/plugin-catalog-import | UI에서 catalog-info.yaml 등록 | 카탈로그 규칙 |
@backstage-community/plugin-cost-insights | 클라우드 비용 가시화 | 비용 API 연동 |
@pagerduty/backstage-plugin | 온콜 현황, 인시던트 목록 | PagerDuty API 키 |
@backstage/plugin-scaffolder-backend-module-github | GitHub 저장소 자동 생성 | GitHub App 권한 |
IDP 도구 비교
Backstage 외에도 여러 IDP 도구가 시장에 존재합니다. 조직의 규모, 기술 수준, 예산에 따라 적합한 도구가 다릅니다.
| 항목 | Backstage | Port | Cortex | OpsLevel |
|---|---|---|---|---|
| 라이선스 | Apache 2.0 (오픈소스) | SaaS (무료 티어 있음) | SaaS | SaaS |
| 호스팅 | 셀프호스팅 | 클라우드 관리형 | 클라우드 관리형 | 클라우드 관리형 |
| 커스터마이징 | 매우 높음 (플러그인 개발) | 중간 (위젯/블루프린트) | 낮음 | 중간 |
| 초기 설정 비용 | 높음 (전담 팀 필요) | 낮음 | 낮음 | 낮음 |
| 운영 부담 | 높음 (업그레이드, 보안) | 없음 (SaaS) | 없음 | 없음 |
| Service Catalog | YAML 기반, 강력 | UI 기반, 직관적 | 자동 디스커버리 | 자동 디스커버리 |
| Scaffolding | 내장 (Scaffolder) | 셀프서비스 액션 | 제한적 | 서비스 템플릿 |
| 기술 문서 | TechDocs 내장 | 외부 연동 | 외부 연동 | 외부 연동 |
| Scorecards | 플러그인으로 추가 | 내장 | 핵심 기능 | 내장 |
| 적합 조직 | 대규모, 기술력 높은 팀 | 중소규모, 빠른 도입 | 서비스 성숙도 중심 | 중간 규모 |
| 가격 (50명 기준) | 무료 (인프라/인건비 별도) | ~$2,000/월 | ~$5,000/월 | ~$3,000/월 |
선택 기준 요약: 전담 플랫폼 엔지니어링 팀이 있고 높은 커스터마이징이 필요하면 Backstage, 빠른 도입과 낮은 운영 부담을 원하면 Port, 서비스 성숙도 측정과 표준 준수에 집중하고 싶다면 Cortex가 적합합니다.
운영 시 주의사항
Adoption 메트릭 추적
IDP를 구축하는 것보다 개발자들이 실제로 사용하게 만드는 것이 더 어렵습니다. 다음 메트릭을 추적하여 채택 현황을 측정하세요:
| 메트릭 | 측정 방법 | 목표 |
|---|---|---|
| 카탈로그 커버리지 | 등록된 서비스 수 / 전체 서비스 수 | 95% 이상 |
| 템플릿 사용률 | 템플릿으로 생성된 서비스 / 전체 신규 서비스 | 80% 이상 |
| DAU/WAU | Backstage 일간/주간 활성 사용자 | 개발자의 60% 이상 |
| 온보딩 시간 | 신규 서비스 생성 ~ 첫 배포까지 소요 시간 | 1시간 이내 |
| MTTR 개선 | 장애 발생 ~ 담당팀 인지까지 시간 | 기존 대비 50% 감소 |
| TechDocs 최신성 | 30일 내 업데이트된 문서 비율 | 70% 이상 |
피해야 할 안티패턴
-
Big Bang 출시: 모든 기능을 한 번에 출시하려 하면 실패합니다. MVP로 시작하세요. 첫 번째 단계는 Software Catalog만으로 충분합니다. 카탈로그에 조직의 모든 서비스를 등록하고, 소유권과 의존성을 시각화하는 것만으로도 큰 가치를 제공합니다.
-
플랫폼 = 포탈 착각: UI만 예쁘게 만들어놓고 뒤에 자동화가 없으면 개발자들은 금방 떠납니다. "클릭 한 번으로 K8s 네임스페이스와 CI/CD가 설정되는" 실질적 자동화가 핵심입니다.
-
개발자 피드백 무시: 플랫폼은 제품입니다. 개발자 설문, 사용 데이터 분석, 정기적 피드백 세션 없이 플랫폼 팀이 "좋을 것 같은" 기능만 만들면 채택률이 떨어집니다.
-
Backstage 버전 업그레이드 방치: Backstage는 릴리스 주기가 빠릅니다(월 1~2회). 6개월 이상 업그레이드를 미루면 마이그레이션이 극도로 어려워집니다.
backstage-cli versions:bump명령을 정기적으로 실행하세요. -
강제 채택: 관리 도구로 전락하면 개발자의 반감을 삽니다. 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 호출 실패
복구 절차:
- Backstage 로그 확인:
kubectl logs -l app=backstage -c backstage --tail=100 - 토큰 유효성 검증:
curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user - YAML 검증:
npx @backstage/cli catalog-info validate catalog-info.yaml - 수동 새로고침: Backstage UI의 해당 엔티티 페이지에서 "Refresh" 버튼 클릭
- 캐시 초기화가 필요한 경우: Backstage Pod 재시작
사례 2: Scaffolder 템플릿 실행 중 GitHub 저장소 생성 실패
증상: 템플릿 실행 시 "publish:github" 단계에서 RequestError: HttpError: Resource not accessible by integration 에러
원인 분석:
- GitHub App의
Repository: Administration권한 누락 - 조직의 저장소 생성 정책(repository creation policy)에 의해 차단
- 이미 동일 이름의 저장소가 존재
복구 절차:
- GitHub App 권한 확인 및 업데이트
- 조직 설정에서 App이 저장소 생성을 허용하는지 확인
- 실패한 태스크의 로그를 Backstage UI에서 확인 (Scaffolder 태스크 로그 페이지)
- 부분적으로 생성된 리소스(빈 저장소 등)가 있다면 수동 정리 후 재실행
사례 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 버킷 권한 문제 (외부 퍼블리셔 사용 시)
복구 절차:
- 저장소에
mkdocs.yml과docs/index.md가 존재하는지 확인 - 로컬에서
npx @techdocs/cli generate명령으로 빌드 테스트 - 외부 퍼블리셔 사용 시 IAM 권한 확인
techdocs.builder를local로 변경하여 Backstage가 직접 빌드하도록 설정 변경 (소규모 환경)
사례 5: Backstage 업그레이드 후 플러그인 호환성 깨짐
증상: 업그레이드 후 특정 플러그인 페이지에서 화이트 스크린 또는 런타임 에러
원인 분석:
- Backstage 코어 패키지와 플러그인 간 버전 불일치
- API가 deprecated된 후 제거됨
- 새로운 Backstage 시스템(New Backend System) 마이그레이션 필요
복구 절차:
- 즉시 이전 버전으로 롤백 (Helm rollback 또는 이전 이미지 태그로 배포)
- 브라우저 콘솔에서 에러 메시지 확인
- 문제 플러그인의 GitHub 이슈 검색
- 플러그인을 최신 호환 버전으로 업데이트하거나, 임시로 비활성화
- 스테이징에서 검증 후 다시 프로덕션 배포
체크리스트
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 어노테이션 설정 (클러스터 내 워크로드 매핑)
참고자료
- Backstage 공식 문서 - What is Backstage?
- Platform Engineering - How to Set Up an Internal Developer Platform
- GitGuardian - Platform Engineering: Building Your Developer Portal with Backstage Part 1
- Growin Blog - Platform Engineering 2026
- The New Stack - In 2026, AI Is Merging with Platform Engineering: Are You Ready?
- CNCF - Backstage Project Page
- Spotify Engineering - How We Use Backstage at Spotify
Platform Engineering and Building an Internal Developer Platform with Backstage: A Practical Guide
- Introduction - Why Platform Engineering Is Replacing Traditional DevOps
- Internal Developer Platform Concepts and Components
- Backstage Architecture and Core Features
- Backstage Installation and Initial Setup
- Software Catalog Configuration
- Creating Golden Path Templates
- Plugin Development and Integration
- IDP Tool Comparison
- Operational Considerations
- Failure Cases and Recovery Procedures
- Checklists
- References
- Quiz

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
-
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.
-
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.
-
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.
-
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_TOKENrequiresrepo,read:org, andread:userpermissions. When using Fine-grained Tokens, you must explicitly grant Contents and Metadata read permissions for target repositories. Insufficient token permissions will causeNotFoundErrorduring 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 Kind | Role | Example |
|---|---|---|
| Domain | Business domain area | commerce, logistics |
| System | Logical group of related components | ecommerce-platform |
| Component | Individual software unit (service, library, website) | order-service |
| API | Interface provided by a component | order-api (gRPC/REST/GraphQL) |
| Resource | Infrastructure resource | order-database, order-redis |
| Group | Team/organizational unit | team-order |
| User | Individual user | jane.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:
| Plugin | Purpose | Configuration Point |
|---|---|---|
@backstage/plugin-kubernetes | Real-time Pod status, logs, event viewing | ServiceAccount, RBAC settings |
@backstage/plugin-techdocs | docs-as-code based doc rendering | MkDocs config, S3/GCS publisher |
@roadiehq/backstage-plugin-github-insights | PR status, contributor stats | GitHub App token |
@backstage/plugin-catalog-import | Register catalog-info.yaml from UI | Catalog rules |
@backstage-community/plugin-cost-insights | Cloud cost visualization | Cost API integration |
@pagerduty/backstage-plugin | On-call status, incident list | PagerDuty API key |
@backstage/plugin-scaffolder-backend-module-github | Automatic GitHub repository creation | GitHub 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.
| Item | Backstage | Port | Cortex | OpsLevel |
|---|---|---|---|---|
| License | Apache 2.0 (Open Source) | SaaS (free tier available) | SaaS | SaaS |
| Hosting | Self-hosted | Cloud managed | Cloud managed | Cloud managed |
| Customization | Very high (plugin dev) | Medium (widgets/blueprints) | Low | Medium |
| Initial Setup Cost | High (dedicated team needed) | Low | Low | Low |
| Operations Burden | High (upgrades, security) | None (SaaS) | None | None |
| Service Catalog | YAML-based, powerful | UI-based, intuitive | Auto-discovery | Auto-discovery |
| Scaffolding | Built-in (Scaffolder) | Self-service actions | Limited | Service templates |
| Technical Docs | TechDocs built-in | External integration | External integration | External integration |
| Scorecards | Added via plugin | Built-in | Core feature | Built-in |
| Best For | Large, highly technical teams | Small-medium, fast adoption | Service maturity focus | Mid-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:
| Metric | Measurement Method | Target |
|---|---|---|
| Catalog Coverage | Registered services / Total services | Over 95% |
| Template Usage Rate | Services created via template / Total new services | Over 80% |
| DAU/WAU | Backstage daily/weekly active users | Over 60% of developers |
| Onboarding Time | Time from new service creation to first deployment | Under 1 hour |
| MTTR Improvement | Time from incident to responsible team awareness | 50% reduction from baseline |
| TechDocs Freshness | Percentage of docs updated within 30 days | Over 70% |
Anti-Patterns to Avoid
-
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.
-
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."
-
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.
-
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:bumpregularly. -
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 topackage.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
kindvalue) - The Kind is not in the
allowlist incatalog.rules - Network issues causing GitHub API call failures
Recovery Procedure:
- Check Backstage logs:
kubectl logs -l app=backstage -c backstage --tail=100 - Validate token:
curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user - Validate YAML:
npx @backstage/cli catalog-info validate catalog-info.yaml - Manual refresh: Click the "Refresh" button on the entity page in the Backstage UI
- 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: Administrationpermission on the GitHub App - Blocked by the organization's repository creation policy
- A repository with the same name already exists
Recovery Procedure:
- Check and update GitHub App permissions
- Verify the App is allowed to create repositories in the organization settings
- Check the failed task logs in the Backstage UI (Scaffolder task log page)
- 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.ymlfile is missing from the repository- Python dependency (mkdocs-techdocs-core) installation failure
- S3 bucket permission issues (when using external publisher)
Recovery Procedure:
- Verify that
mkdocs.ymlanddocs/index.mdexist in the repository - Test the build locally with
npx @techdocs/cli generate - Check IAM permissions when using an external publisher
- Change
techdocs.buildertolocalso 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:
- Immediately roll back to the previous version (Helm rollback or deploy with the previous image tag)
- Check error messages in the browser console
- Search the plugin's GitHub issues
- Update the plugin to the latest compatible version, or temporarily disable it
- 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.yamland 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.yamland place it at the repository root - Verify
metadata.namefollows organizational naming conventions - Set
spec.ownerto the correct team group - Assign
spec.systemto 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
- Backstage Official Documentation - What is Backstage?
- Platform Engineering - How to Set Up an Internal Developer Platform
- GitGuardian - Platform Engineering: Building Your Developer Portal with Backstage Part 1
- Growin Blog - Platform Engineering 2026
- The New Stack - In 2026, AI Is Merging with Platform Engineering: Are You Ready?
- CNCF - Backstage Project Page
- Spotify Engineering - How We Use Backstage at Spotify
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.