Backstage의 Entity 종류는 9 가지가 있고, 각자 고유한 정의를 따른다. Backstage 내의 Entity와 Relation은 Core library에서 어느정도 정의해서 제공하고 있다. 그러다 보니 단순이 app-config.yaml에서 allow되는 Entity 목록에 완전히 새로운걸 추가한다던지 그런 수준의 yaml만 수정해서는 새로운 Entity를 만들수 없다.
- Kind: Component
- Kind: Template
- Kind: API
- Kind: Group
- Kind: User
- Kind: Resource
- Kind: System
- Kind: Domain
- Kind: Location
Descriptor Format of Catalog Entities | Backstage Software Catalog and Developer Platform
Documentation on Descriptor Format of Catalog Entities which describes the default data shape and semantics of catalog entities
backstage.io
Core에 커스텀을 입힐 정도의 추가 개발이 필요하다. 많은 여러 시도를 통해 검증되었지만, 이것은 불가피하다.
과연 무엇을 옵티마이징 해야할지 여기 공유하겠다.
1. 새로운 Type의 Entity 정의하기

우선 내가 새로이 추가하고 싶은 Type의 Entity는 Feature이며, 이는 다음과 같은 속성을 가진다.
# Feature entity - Custom entity kind
apiVersion: backstage.io/v1alpha1
kind: Feature
metadata:
name: user-authentication
namespace: default
description: User authentication feature with OAuth2 and JWT support
spec:
type: capability
lifecycle: production
owner: admin
featureOf:
- component:default/micro-service-a
upstreamOf:
- api:default/example-grpc-api
`micro-service-a` 라는 Component에 귀속되며 이의 관계는 ['featureOf', 'hasFeature'] 의 쌍을 가진다.
이 새로운 Entity Type을 사용하기 위해서는 Backend에서 Core를 호출하는 부분에서 수정이 필요하다.
관련 Commit 1 : https://github.com/armyost/backstage-armyost/commit/74c5cbec521e13f5dd70ae377ea28f936b4d2dc0
debuging · armyost/backstage-armyost@f288b25
+ <EntityCatalogGraphCard variant="gridItem" renderNode={CustomRenderNode} height={400} kinds={["component", "system", "api", "resource"]} relationTypes={["featureOf","hasFeature"]}/>
github.com
관련 Commit 2: https://github.com/armyost/backstage-armyost/commit/0b29677ece3ed75c3ea13c8b866b86bb47f51882
update feature entity · armyost/backstage-armyost@0b29677
+ <EntityCatalogGraphCard variant="gridItem" renderNode={CustomRenderNode} height={400} kinds={["component", "system", "api", "resource", "feature"]} />
github.com
다음을 Backend에 추가해주어야 한다.
packages/backend/src/plugins/catalogModuleCustomRelationProcessor.ts
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { createBackendModule } from '@backstage/backend-plugin-api';
import { CustomRelationProcessor } from './CustomRelationProcessor';
/**
* Module to register custom relation processor for the catalog
* Processes: hasFeature, featureOf custom relations
*/
export const catalogModuleCustomRelationProcessor = createBackendModule({
pluginId: 'catalog',
moduleId: 'custom-relation-processor',
register(env) {
env.registerInit({
deps: {
catalog: catalogProcessingExtensionPoint,
},
async init({ catalog }) {
catalog.addProcessor(new CustomRelationProcessor());
},
});
},
});
export default catalogModuleCustomRelationProcessor;
packages/backend/src/plugins/catalogModuleFeatureEntityKind.ts
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha';
import { createBackendModule } from '@backstage/backend-plugin-api';
import { CatalogProcessor, CatalogProcessorEmit } from '@backstage/plugin-catalog-node';
import { Entity } from '@backstage/catalog-model';
import { LocationSpec } from '@backstage/plugin-catalog-common';
/**
* Custom processor that validates and processes Feature entities
*/
class FeatureEntityProcessor implements CatalogProcessor {
getProcessorName(): string {
return 'FeatureEntityProcessor';
}
async validateEntityKind(entity: Entity): Promise<boolean> {
return entity.kind === 'Feature';
}
async postProcessEntity(
entity: Entity,
_location: LocationSpec,
_emit: CatalogProcessorEmit,
): Promise<Entity> {
// Accept and pass through Feature entities
if (entity.kind === 'Feature') {
// Basic validation
if (!entity.spec) {
throw new Error('Feature entity must have a spec');
}
if (!entity.spec.type) {
throw new Error('Feature entity must have a spec.type');
}
if (!entity.spec.lifecycle) {
throw new Error('Feature entity must have a spec.lifecycle');
}
if (!entity.spec.owner) {
throw new Error('Feature entity must have a spec.owner');
}
}
return entity;
}
}
/**
* Module to register Feature entity kind processor
*/
export const catalogModuleFeatureEntityKind = createBackendModule({
pluginId: 'catalog',
moduleId: 'feature-entity-kind',
register(env) {
env.registerInit({
deps: {
catalog: catalogProcessingExtensionPoint,
},
async init({ catalog }) {
catalog.addProcessor(new FeatureEntityProcessor());
},
});
},
});
export default catalogModuleFeatureEntityKind;
packages/backend/src/plugins/CustomRelationProcessor.ts
import { CatalogProcessor, CatalogProcessorEmit, processingResult } from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
import { Entity } from '@backstage/catalog-model';
/**
* Custom catalog processor that adds featureOf/hasFeature relations
* based on custom fields in entity spec
*/
export class CustomRelationProcessor implements CatalogProcessor {
getProcessorName(): string {
return 'CustomRelationProcessor';
}
async preProcessEntity(
entity: Entity,
location: LocationSpec,
emit: CatalogProcessorEmit,
): Promise<Entity> {
// Handle hasFeature field
const hasFeature = (entity.spec?.hasFeature as string[] | string | undefined);
if (hasFeature) {
const features = Array.isArray(hasFeature) ? hasFeature : [hasFeature];
for (const targetRef of features) {
emit(processingResult.relation({
source: {
kind: entity.kind,
namespace: entity.metadata.namespace || 'default',
name: entity.metadata.name,
},
type: 'hasFeature',
target: {
kind: targetRef.split(':')[0] || 'Feature',
namespace: targetRef.split('/')[0].split(':')[1] || 'default',
name: targetRef.split('/')[1] || targetRef,
},
}));
}
// Remove from spec after processing to avoid validation errors
delete entity.spec.hasFeature;
}
// Handle featureOf field
const featureOf = (entity.spec?.featureOf as string[] | string | undefined);
if (featureOf) {
const components = Array.isArray(featureOf) ? featureOf : [featureOf];
for (const targetRef of components) {
emit(processingResult.relation({
source: {
kind: entity.kind,
namespace: entity.metadata.namespace || 'default',
name: entity.metadata.name,
},
type: 'featureOf',
target: {
kind: targetRef.split(':')[0] || 'Component',
namespace: targetRef.split('/')[0].split(':')[1] || 'default',
name: targetRef.split('/')[1] || targetRef,
},
}));
}
// Remove from spec after processing to avoid validation errors
delete entity.spec.featureOf;
}
///// Up/Down Stream feature
// Handle upstream field
const upstreamOf = (entity.spec?.upstreamOf as string[] | string | undefined);
if (upstreamOf) {
const apis = Array.isArray(upstreamOf) ? upstreamOf : [upstreamOf];
for (const targetRef of apis) {
emit(processingResult.relation({
source: {
kind: entity.kind,
namespace: entity.metadata.namespace || 'default',
name: entity.metadata.name,
},
type: 'upstreamOf',
target: {
kind: targetRef.split(':')[0] || 'Api',
namespace: targetRef.split('/')[0].split(':')[1] || 'default',
name: targetRef.split('/')[1] || targetRef,
},
}));
}
// Remove from spec after processing to avoid validation errors
delete entity.spec.upstreamOf;
}
// Handle upstream field
const upstreamBy = (entity.spec?.upstreamBy as string[] | string | undefined);
if (upstreamBy) {
const apis = Array.isArray(upstreamBy) ? upstreamBy : [upstreamBy];
for (const targetRef of apis) {
emit(processingResult.relation({
source: {
kind: entity.kind,
namespace: entity.metadata.namespace || 'default',
name: entity.metadata.name,
},
type: 'upstreamBy',
target: {
kind: targetRef.split(':')[0] || 'Feature',
namespace: targetRef.split('/')[0].split(':')[1] || 'default',
name: targetRef.split('/')[1] || targetRef,
},
}));
}
// Remove from spec after processing to avoid validation errors
delete entity.spec.upstreamBy;
}
// Handle upstream field
const downstreamOf = (entity.spec?.downstreamOf as string[] | string | undefined);
if (downstreamOf) {
const apis = Array.isArray(downstreamOf) ? downstreamOf : [downstreamOf];
for (const targetRef of apis) {
emit(processingResult.relation({
source: {
kind: entity.kind,
namespace: entity.metadata.namespace || 'default',
name: entity.metadata.name,
},
type: 'downstreamOf',
target: {
kind: targetRef.split(':')[0] || 'Api',
namespace: targetRef.split('/')[0].split(':')[1] || 'default',
name: targetRef.split('/')[1] || targetRef,
},
}));
}
// Remove from spec after processing to avoid validation errors
delete entity.spec.downstreamOf;
}
// Handle upstream field
const downstreamBy = (entity.spec?.downstreamBy as string[] | string | undefined);
if (downstreamBy) {
const apis = Array.isArray(downstreamBy) ? downstreamBy : [downstreamBy];
for (const targetRef of apis) {
emit(processingResult.relation({
source: {
kind: entity.kind,
namespace: entity.metadata.namespace || 'default',
name: entity.metadata.name,
},
type: 'downstreamBy',
target: {
kind: targetRef.split(':')[0] || 'Feature',
namespace: targetRef.split('/')[0].split(':')[1] || 'default',
name: targetRef.split('/')[1] || targetRef,
},
}));
}
// Remove from spec after processing to avoid validation errors
delete entity.spec.downstreamBy;
}
return entity;
}
}
그리고 위 정의한 Custom Processor를 Import 해야한다.
packages/backend/src/index.ts
// custom Feature entity kind
backend.add(import('./plugins/catalogModuleFeatureEntityKind'));
// custom catalog relation processor
backend.add(import('./plugins/catalogModuleCustomRelationProcessor'));
Entity의 Overview에서 Catalog Graph Card의 속성에서 기본적으로 표시할 Type을 선정할 수 있고 이중에 Feature를 추가하였다.
packages/app/src/components/catalog/EntityPage.tsx
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" renderNode={CustomRenderNode} height={400} kinds={["component", "api", "resource", "feature"]}/>
</Grid>
...
</Grid>
);
packages/backend/src/plugins/FeatureEntityProvider.ts
import { Entity } from '@backstage/catalog-model';
/**
* Feature Entity Kind definition
* Validates Feature entities in the catalog
*/
export interface FeatureEntityV1alpha1 extends Entity {
apiVersion: 'backstage.io/v1alpha1' | 'backstage.io/v1beta1';
kind: 'Feature';
spec: {
type: string;
lifecycle: string;
owner: string;
system?: string;
featureOf?: string[];
upstreamOf?: string[];
downstreamOf?: string[];
};
}
/**
* Type guard for Feature entities
*/
export function isFeatureEntity(entity: Entity): entity is FeatureEntityV1alpha1 {
return entity.kind === 'Feature';
}
Backstage에서 호환할 Entity Type 은 다음 필드에서 정의한다. catalog.rules.allow
app-config.yaml
catalog:
rules:
- allow: [Component, System, API, Resource, Location, Feature]
2. Catalog Graph에서 새로운 Relation을 추가하고 Default Choice를 수정하기

Catalog Graph에서 표시할 Default Setting은 다음과 같이 정의한다. 기본적으로 온보딩되어 있는 scmIntegrationsApi는 그대로 두며, backstage/plugin-catalog-graph 라이브러리를 사용하고 있는 catalogGraphApiRef 부분부터 추가해주면 된다.
defaultRelationTypes: {
// Don't exclude any custom relations - show them by default
exclude: ['providesApi', 'consumesApi','apiConsumedBy','apiProvidedBy'],
},
이 부분은 Default로 제외할 Relation을 정의한다.
packages/app/src/apis.ts
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
ScmAuth,
} from '@backstage/integration-react';
import {
AnyApiFactory,
configApiRef,
createApiFactory,
} from '@backstage/core-plugin-api';
import {
ALL_RELATIONS,
ALL_RELATION_PAIRS,
catalogGraphApiRef,
DefaultCatalogGraphApi,
} from '@backstage/plugin-catalog-graph';
export const apis: AnyApiFactory[] = [
createApiFactory({
api: scmIntegrationsApiRef,
deps: { configApi: configApiRef },
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
}),
ScmAuth.createDefaultApiFactory(),
// Insert Custom Relation and default relation setting
createApiFactory({
api: catalogGraphApiRef,
deps: {},
factory: () =>
new DefaultCatalogGraphApi({
// The relations to support
knownRelations: [...ALL_RELATIONS, 'featureOf', 'hasFeature','upstreamOf','downstreamOf', 'upstreamBy','downstreamBy'],
// The relation pairs to support
knownRelationPairs: [
...ALL_RELATION_PAIRS,
['featureOf', 'hasFeature'],
['upstreamOf', 'upstreamBy'],
['downstreamOf', 'downstreamBy'],
],
// Select what relations to be shown by default
defaultRelationTypes: {
// Don't exclude any custom relations - show them by default
exclude: ['providesApi', 'consumesApi','apiConsumedBy','apiProvidedBy'],
},
}),
}),
];'Programming > MSA' 카테고리의 다른 글
| Backstage) Relation - Catalog Graph 의 가시성 향상시키기 (0) | 2026.01.30 |
|---|---|
| Backstage) Install 4. TechDocs 에서 PlantUML Plugin 사용하기 (0) | 2025.12.01 |
| Backstage) Install 3. Theme 바꾸기 (1) | 2025.11.17 |
| Backstage) Install 2. Github App 설치 (0) | 2025.11.16 |
| Backstage) Install 1. Github App 설치 (0) | 2025.11.10 |