카탈로그 백앤드의 내부 동작 방식에 관련된 설명이다. 주로 카탈로그를 설치하거나 확장하려는 대상으로 한다.
개발자가 커스터마이징 할 수 있는 주요 확장 포인트는 3가지이다.
3단계 처리 흐름
1.단계(수집)
2단계. 처리(Processing)
3단계 스티칭(stiching)
리 단계의 모든 결과물을 하나의 최종 엔티티로 조합
처리된 엔티티 + 오류 + 관계(들어오는 것 + 나가는 것 모두) 통합
엔티티 해시값을 비교해서 변경이 있을 때만 재스티칭 (성능 최적화)
현재 이 단계는 커스터마이징 불가 → 수정이 필요하면 수집/처리 단계에서 해야 함
예시 코드는 아래에 대한 설명이다.
전체 Entity.yaml 파일
# catalog-info.yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: my-service
description: "주문 처리 서비스"
tags:
- backend
- order
annotations:
github.com/project-slug: my-org/my-service
spec:
type: service
lifecycle: production
owner: team-backend
dependsOn:
- component:payment-service
- resource:my-database
# app-config.yaml
catalog:
orphanStrategy: keep # 고아 엔티티 유지 설정
locations:
# 단일 파일 등록
- type: url
target: https://github.com/my-org/my-service/blob/main/catalog-info.yaml
# 여러 파일을 한번에 등록
- type: url
target: https://github.com/my-org/my-repo/blob/main/all-services.yaml
import {
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-node';
import { Entity } from '@backstage/catalog-model';
export class MyCustomProvider implements EntityProvider {
private connection?: EntityProviderConnection;
// Provider의 고유 식별자
getProviderName(): string {
return 'MyCustomProvider';
}
// 백엔드 시작 시 연결
async connect(connection: EntityProviderConnection): Promise<void> {
this.connection = connection;
await this.refresh();
}
// 외부 소스에서 엔티티를 가져와 카탈로그에 전달
async refresh(): Promise<void> {
const entities: Entity[] = [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'my-service',
namespace: 'default',
},
spec: {
type: 'service',
lifecycle: 'production',
owner: 'team-backend',
},
},
];
// 카탈로그에 엔티티 목록을 통째로 교체(upsert)
await this.connection?.applyMutation({
type: 'full',
entities: entities.map(entity => ({
entity,
locationKey: 'my-custom-provider',
})),
});
}
}
import {
CatalogProcessor,
CatalogProcessorEmit,
} from '@backstage/plugin-catalog-node';
import { Entity } from '@backstage/catalog-model';
import { LocationSpec } from '@backstage/plugin-catalog-common';
export class MyCustomProcessor implements CatalogProcessor {
getProcessorName(): string {
return 'MyCustomProcessor';
}
// 엔티티를 받아서 변환하거나 관계를 추가
async preProcessEntity(
entity: Entity,
_location: LocationSpec,
emit: CatalogProcessorEmit,
): Promise<Entity> {
// Component 타입일 때만 처리
if (entity.kind !== 'Component') {
return entity;
}
// 태그 자동 추가
const tags = entity.metadata.tags ?? [];
if (!tags.includes('auto-tagged')) {
tags.push('auto-tagged');
}
// 관계(Relation) 추가
emit({
type: 'relation',
relation: {
source: { kind: 'Component', namespace: 'default', name: entity.metadata.name },
type: 'ownedBy',
target: { kind: 'Group', namespace: 'default', name: 'team-backend' },
},
});
// 수정된 엔티티 반환
return {
...entity,
metadata: {
...entity.metadata,
tags,
},
};
}
}
backend에 등록
import { MyCustomProvider } from './providers/MyCustomProvider';
import { MyCustomProcessor } from './processors/MyCustomProcessor';
export default async function createPlugin(env: PluginEnvironment) {
const builder = await CatalogBuilder.create(env);
// Provider 등록
builder.addEntityProvider(new MyCustomProvider());
// Processor 등록
builder.addProcessor(new MyCustomProcessor());
const { processingEngine, router } = await builder.build();
await processingEngine.start();
return router;
}
[외부 소스 (GitHub, LDAP 등)]
↓
Entity Provider ← MyCustomProvider
↓
(수집/Ingestion)
↓
Processor ← MyCustomProcessor
↓
(처리/Processing)
↓
Stitching (자동)
↓
[카탈로그 API로 노출]
