Backstage 소프트웨어 카탈로그의 엔티티 모델, catalog-info.yaml 스키마, 메타데이터 표준화, 그리고 Scaffolder를 활용한 프로젝트 자동 생성을 다룹니다.
catalog-info.yaml의 상세 스키마를 작성할 수 있습니다.3장에서 Backstage 카탈로그의 엔티티 종류를 간략히 소개했습니다. 이번 장에서는 각 엔티티의 관계와 설계 패턴을 깊이 있게 다룹니다.
Backstage의 엔티티들은 계층적 관계를 형성합니다.
**Domain(도메인)**은 비즈니스 영역을 나타냅니다. DDD(Domain-Driven Design)의 Bounded Context와 유사한 개념입니다.
apiVersion: backstage.io/v1alpha1
kind: Domain
metadata:
name: payment
description: "결제 및 정산 도메인"
annotations:
backstage.io/techdocs-ref: dir:.
tags:
- payment
- fintech
spec:
owner: group:default/payment-division**System(시스템)**은 하나의 기능을 제공하기 위해 협력하는 컴포넌트들의 집합입니다.
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
name: checkout-system
description: "결제 처리 시스템 - 주문, 결제, 영수증 발행"
tags:
- payment
- checkout
spec:
owner: group:default/checkout-team
domain: payment**Component(컴포넌트)**는 카탈로그에서 가장 많이 사용되는 엔티티입니다. 서비스, 라이브러리, 웹사이트 등 다양한 소프트웨어 자산을 나타냅니다.
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: checkout-service
description: "결제 처리 핵심 서비스"
annotations:
github.com/project-slug: mycompany/checkout-service
backstage.io/techdocs-ref: dir:.
pagerduty.com/service-id: P1234567
grafana/dashboard-selector: "checkout"
sonarqube.org/project-key: mycompany_checkout-service
tags:
- java
- spring-boot
- grpc
links:
- url: https://grafana.mycompany.com/d/checkout
title: "Grafana Dashboard"
icon: dashboard
- url: https://mycompany.atlassian.net/wiki/spaces/CHECKOUT
title: "Confluence 문서"
icon: docs
spec:
type: service
lifecycle: production
owner: group:default/checkout-team
system: checkout-system
dependsOn:
- resource:default/checkout-database
- resource:default/payment-queue
- component:default/user-service
providesApis:
- checkout-api
consumesApis:
- payment-gateway-api
- notification-apiAPI 엔티티는 서비스 간 인터페이스를 정의합니다. OpenAPI, AsyncAPI, gRPC 등 다양한 형식을 지원합니다.
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: checkout-api
description: "결제 처리 REST API"
tags:
- rest
- payment
spec:
type: openapi
lifecycle: production
owner: group:default/checkout-team
system: checkout-system
definition:
$text: ./openapi.yamlResource(리소스) 엔티티는 서비스가 의존하는 인프라 리소스를 나타냅니다.
apiVersion: backstage.io/v1alpha1
kind: Resource
metadata:
name: checkout-database
description: "결제 데이터 저장용 PostgreSQL"
annotations:
mycompany.com/cloud-resource-id: "rds:ap-northeast-2:checkout-db"
tags:
- postgresql
- rds
spec:
type: database
lifecycle: production
owner: group:default/checkout-team
system: checkout-system
dependencyOf:
- component:default/checkout-service엔티티 간 관계(dependsOn, providesApis, consumesApis)를 정확하게 기술하면, Backstage가 자동으로 의존성 그래프를 생성합니다. 이는 장애 분석, 영향도 평가, 아키텍처 시각화에 매우 유용합니다.
카탈로그의 가치는 데이터의 일관성에서 나옵니다. 팀마다 다른 방식으로 메타데이터를 작성하면 카탈로그의 유용성이 급격히 떨어집니다.
조직 전체에 적용할 네이밍 규칙을 정의합니다.
conventions:
component_name:
pattern: "[a-z][a-z0-9-]*"
examples:
good: ["user-service", "payment-gateway", "auth-library"]
bad: ["UserService", "payment_gateway", "auth lib"]
system_name:
pattern: "[a-z][a-z0-9-]*-system"
examples:
good: ["checkout-system", "user-management-system"]
bad: ["checkout", "UserManagement"]
api_name:
pattern: "[a-z][a-z0-9-]*-api"
examples:
good: ["checkout-api", "user-api"]
bad: ["checkoutAPI", "user_api"]
tags:
categories:
language: ["java", "kotlin", "go", "python", "typescript"]
framework: ["spring-boot", "gin", "fastapi", "nestjs"]
protocol: ["rest", "grpc", "graphql", "async-api"]
criticality: ["tier-1", "tier-2", "tier-3"]모든 Component 엔티티에 필수적으로 포함해야 할 어노테이션을 정의합니다.
required_annotations:
all_components:
- key: "github.com/project-slug"
description: "GitHub 저장소 경로"
example: "mycompany/checkout-service"
- key: "backstage.io/techdocs-ref"
description: "TechDocs 문서 위치"
example: "dir:."
production_services:
- key: "pagerduty.com/service-id"
description: "PagerDuty 서비스 ID"
example: "P1234567"
- key: "mycompany.com/tier"
description: "서비스 중요도 등급"
example: "tier-1"
- key: "mycompany.com/slo-target"
description: "SLO 목표 가용성"
example: "99.9%"메타데이터 표준을 자동으로 검증하는 CI 파이프라인을 구성합니다.
name: Catalog Validation
on:
pull_request:
paths:
- "catalog-info.yaml"
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate catalog-info.yaml
run: |
npx @backstage/cli catalog-info validate catalog-info.yaml
- name: Check required annotations
run: |
python scripts/check-annotations.py catalog-info.yaml
- name: Check naming conventions
run: |
python scripts/check-naming.py catalog-info.yaml메타데이터 표준화는 처음부터 엄격하게 적용하되, 기존 서비스의 마이그레이션에는 유예 기간을 두세요. 모든 서비스가 한 번에 표준을 충족하기 어려우므로, 분기별로 컴플라이언스 비율을 높여가는 전략이 효과적입니다.
수백 개의 서비스를 수동으로 카탈로그에 등록하는 것은 현실적이지 않습니다. 자동 등록 메커니즘을 활용해야 합니다.
GitHub Organization의 모든 저장소를 스캔하여 catalog-info.yaml이 있는 저장소를 자동으로 등록합니다.
catalog:
providers:
github:
myCompanyOrg:
organization: mycompany
catalogPath: /catalog-info.yaml
filters:
branch: main
repository: ".*"
schedule:
frequency:
minutes: 30
timeout:
minutes: 3Kubernetes 클러스터에서 실행 중인 워크로드를 자동으로 카탈로그에 등록하는 것도 가능합니다.
# Kubernetes Deployment에 Backstage 어노테이션 추가
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout-service
annotations:
backstage.io/kubernetes-id: checkout-service
backstage.io/kubernetes-namespace: checkout
backstage.io/kubernetes-label-selector: "app=checkout-service"**Scaffolder(스캐폴더)**는 Backstage의 소프트웨어 템플릿 엔진입니다. 개발자가 몇 번의 클릭만으로 표준화된 프로젝트를 생성할 수 있도록 합니다.
templates/
spring-boot-service/
template.yaml # 템플릿 정의
skeleton/ # 프로젝트 스켈레톤
catalog-info.yaml
mkdocs.yml
docs/
index.md
src/
main/
java/...
test/
java/...
build.gradle
Dockerfile
.github/
workflows/
ci.yamlapiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: spring-boot-service
title: "Spring Boot 마이크로서비스"
description: "표준 Spring Boot 마이크로서비스를 생성합니다. CI/CD, 모니터링, TechDocs가 사전 구성됩니다."
tags:
- java
- spring-boot
- recommended
spec:
owner: group:default/platform-team
type: service
parameters:
- title: "서비스 기본 정보"
required:
- serviceName
- description
- owner
- system
properties:
serviceName:
title: "서비스 이름"
type: string
description: "소문자, 하이픈 구분 (예: checkout-service)"
pattern: "^[a-z][a-z0-9-]*$"
ui:autofocus: true
description:
title: "서비스 설명"
type: string
description: "이 서비스가 하는 일을 간략히 설명하세요"
owner:
title: "소유 팀"
type: string
description: "서비스를 관리하는 팀"
ui:field: OwnerPicker
ui:options:
catalogFilter:
kind: Group
system:
title: "시스템"
type: string
description: "이 서비스가 속하는 시스템"
ui:field: EntityPicker
ui:options:
catalogFilter:
kind: System
- title: "기술 옵션"
properties:
javaVersion:
title: "Java 버전"
type: string
enum: ["21", "17"]
default: "21"
springBootVersion:
title: "Spring Boot 버전"
type: string
enum: ["3.4.x", "3.3.x"]
default: "3.4.x"
database:
title: "데이터베이스"
type: string
enum: ["postgresql", "mysql", "none"]
default: "postgresql"
messaging:
title: "메시징"
type: string
enum: ["kafka", "rabbitmq", "none"]
default: "none"
- title: "인프라 옵션"
properties:
environment:
title: "배포 환경"
type: array
items:
type: string
enum: ["dev", "staging", "production"]
default: ["dev", "staging", "production"]
uniqueItems: true
tier:
title: "서비스 등급"
type: string
enum: ["tier-1", "tier-2", "tier-3"]
default: "tier-3"
description: "tier-1: 핵심 서비스, tier-2: 중요 서비스, tier-3: 일반 서비스"
steps:
- id: fetch-skeleton
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 }}
- id: create-repo
name: "GitHub 저장소 생성"
action: publish:github
input:
repoUrl: "github.com?owner=mycompany&repo=${{ parameters.serviceName }}"
description: ${{ parameters.description }}
defaultBranch: main
protectDefaultBranch: true
repoVisibility: internal
- id: register-catalog
name: "카탈로그에 등록"
action: catalog:register
input:
repoContentsUrl: ${{ steps['create-repo'].output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
- id: create-argocd-app
name: "ArgoCD 앱 생성"
action: argocd:create-resources
input:
appName: ${{ parameters.serviceName }}
argoInstance: main
namespace: ${{ parameters.serviceName }}
repoUrl: ${{ steps['create-repo'].output.remoteUrl }}
path: k8s/overlays/dev
output:
links:
- title: "GitHub 저장소"
url: ${{ steps['create-repo'].output.remoteUrl }}
- title: "카탈로그 페이지"
icon: catalog
entityRef: ${{ steps['register-catalog'].output.entityRef }}스켈레톤 파일 내에서 Nunjucks 템플릿 문법을 사용하여 변수를 치환합니다.
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: ${{ values.serviceName }}
description: "${{ values.description }}"
annotations:
github.com/project-slug: "mycompany/${{ values.serviceName }}"
backstage.io/techdocs-ref: dir:.
mycompany.com/tier: "${{ values.tier }}"
tags:
- java
- spring-boot
spec:
type: service
lifecycle: production
owner: ${{ values.owner }}
system: ${{ values.system }}Scaffolder의 강점은 프로젝트 생성 시 코드만이 아니라 CI/CD 파이프라인, 모니터링 설정, 문서 템플릿, 카탈로그 등록까지 한 번에 수행한다는 점입니다. 이것이 바로 5장에서 다룰 Golden Path의 핵심입니다.
기본 제공 액션 외에 조직 특화 액션을 직접 개발할 수 있습니다.
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
export function createSlackChannelAction() {
return createTemplateAction<{
channelName: string;
description: string;
teamMembers: string[];
}>({
id: 'mycompany:slack:create-channel',
schema: {
input: {
required: ['channelName'],
type: 'object',
properties: {
channelName: {
type: 'string',
title: 'Slack 채널 이름',
},
description: {
type: 'string',
title: '채널 설명',
},
teamMembers: {
type: 'array',
items: { type: 'string' },
title: '초대할 팀원 이메일',
},
},
},
},
async handler(ctx) {
const { channelName, description, teamMembers } = ctx.input;
ctx.logger.info(
`Creating Slack channel: #${channelName}`,
);
// Slack API를 호출하여 채널 생성
const response = await fetch('https://slack.com/api/conversations.create', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SLACK_BOT_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: channelName,
is_private: false,
}),
});
const data = await response.json();
if (!data.ok) {
throw new Error(
`Slack 채널 생성 실패: ${data.error}`,
);
}
ctx.logger.info(
`Slack channel created: ${data.channel.id}`,
);
ctx.output('channelId', data.channel.id);
ctx.output('channelUrl', `https://mycompany.slack.com/archives/${data.channel.id}`);
},
});
}이 커스텀 액션을 템플릿에서 사용할 수 있습니다.
steps:
# ... 기존 스텝 ...
- id: create-slack-channel
name: "Slack 채널 생성"
action: mycompany:slack:create-channel
input:
channelName: "svc-${{ parameters.serviceName }}"
description: "${{ parameters.description }}"
teamMembers: ${{ parameters.teamMembers }}이번 장에서는 Backstage 소프트웨어 카탈로그의 엔티티 모델과 소프트웨어 템플릿을 상세히 다루었습니다.
catalog-info.yaml에는 일관된 네이밍 규칙과 필수 어노테이션을 적용해야 합니다.다음 장에서는 **Golden Path(골든 패스)**를 다룹니다. Scaffolder로 생성한 프로젝트가 어떻게 조직의 모범 사례를 자연스럽게 포함하게 되는지, 그리고 강제가 아닌 매력으로 80% 이상의 자발적 채택률을 달성하는 전략을 살펴보겠습니다.
이 글이 도움이 되셨나요?
Golden Path의 정의와 철학, 옵셔널하되 매력적인 경로 설계, Terraform/Pulumi 기반 자동 인프라 프로비저닝, 그리고 80% 이상 자발적 채택률의 비결을 다룹니다.
Spotify 오픈소스이자 CNCF 졸업 프로젝트인 Backstage의 아키텍처, 설치, 소프트웨어 카탈로그, TechDocs, 플러그인 시스템을 실습합니다.
셀프서비스의 핵심 원칙, GitOps 기반 인프라 요청, Crossplane을 활용한 Kubernetes 네이티브 인프라 추상화, 그리고 승인 워크플로우를 다룹니다.