본문 바로가기
WIKI 기술 지식 베이스

모델 확장하기

원문 보기 위키 갱신

Backstage 카탈로그 엔티티 데이터 모델은 Kubernetes 객체 형식을 기반으로 하고, 그 의미론도 많이 차용해요.

출처: 문서

본문

Backstage 카탈로그 엔티티 데이터 모델은 Kubernetes 객체 형식을 기반으로 하고, 그 의미론도 꽤 많이 차용해요. 이 페이지에서는 그 의미론을 높은 수준에서 설명하고, 조직에 맞게 확장하는 방법을 다룰게요.

Backstage는 기본적으로 여러 카탈로그 개념을 제공해요.

  • Component, User 등 여러 내장 버전(versioned) kind가 있어요. 이것들은 엔티티의 상위 수준 개념을 담고, 엔티티 정의 데이터의 스키마를 정의해요.

  • 엔티티는 루트에 metadata 객체와 spec 객체를 모두 가져요.

  • 각 kind는 type을 가질 수도 있고 없을 수도 있어요. 예를 들어 service, website 같은 잘 알려진 컴포넌트 유형이 몇 가지 있어요. 이것들은 엔티티의 더 세부적인 성격을 명확히 하고, 인터페이스에서 어떤 기능이 노출되는지에 영향을 줄 수 있어요.

  • 엔티티는 여러 annotations를 가질 수 있어요. 사람이 descriptor 파일에 추가할 수도 있고, 엔티티가 카탈로그에 수집될 때 자동화 프로세스가 추가할 수도 있어요.

  • 엔티티는 여러 labels를 가질 수 있어요.

  • 엔티티는 여러 relations를 가질 수 있어, 서로 다른 방식으로 어떻게 관련되는지를 표현해요.

이 확장 가능성들을 아래에서 나열할게요.

기존 kind의 새 apiVersion 추가하기

예시 의도:

"이 핵심 kind를 진화시키고 싶어요. 의미를 조금 바꾸니 apiVersion을 한 단계 올릴게요."

"이 핵심 kind가 꽤 맞지만 마음대로 진화시키고 싶어서, 우리 회사만의 apiVersion 공간으로 옮겨 backstage.io 대신 그것을 쓸게요."

backstage.io apiVersion 공간은 Backstage 메인테이너 사용을 위해 예약되어 있어요. 그 공간 안에서 버전을 바꾸거나 추가하지 마세요.

자신만의 apiVersion 공간을 추가한다면, 사실상 기반 kind에서 분기해 나만의 kind를 만드는 것이에요. 엔티티 kind는 apiVersion + kind 쌍으로 식별되기 때문에, 결과 엔티티가 핵심 kind와 유사하더라도 플러그인이 그 데이터를 파싱하거나 이해할 수 있다는 보장은 없어요. 새 kind 추가에 대해서는 아래를 참고해요.

새 Kind 추가하기

예시 의도:

"패키지에 포함된 kind들이 부족해요. 이 다른 무언가를 모델링하고 싶은데, 내장 kind 어느 것에도 맞지 않아요."

"이 핵심 kind가 꽤 맞지만 마음대로 진화시키고 싶어서, 우리 회사만의 apiVersion 공간으로 옮겨 backstage.io 대신 그것을 쓸게요."

kind는 스키마를 공유하는 엔티티들의 포괄적인 계열(family), 혹은 하나의 아이디어라고 할 수 있어요. Backstage는 Backstage에서 모델링하고 싶을 만한 다양한 요구에 유용할 것이라 믿는 여러 내장 kind를 제공해요. 주된 목표는 그것들을 이 kind들에 매핑하는 것이지만, 때로는 그것을 넘어 확장하고 싶거나 확장해야 할 때가 있어요.

새 apiVersion을 도입하는 것은 기본적으로 새 kind를 추가하는 것과 같아요. 대부분의 플러그인이 내장 @backstage/catalog-model 패키지를 기준으로 컴파일되고 kind가 그에 부합할 것을 기대한다는 점을 명심하세요.

카탈로그 백엔드 자체는 저장과 API 관점에서 저장하는 엔티티의 kind를 신경 쓰지 않아요. 새 kind로 확장하는 것은 주로 CatalogBuilder로 백엔드 카탈로그를 만들 때 검증을 통과할 수 있게 허용하고, 그다음 플러그인이 새 kind를 이해할 수 있게 만드는 문제예요.

소비하는 쪽은 이야기가 달라요. kind 추가는 영향이 매우 커요. Backstage의 기반 자체가 우리가 어떤 의미를 부여하는 엔티티에 동작과 뷰와 기능을 붙이는 것이거든요. 어딘가에서 코드가 하드코딩된 어떤 X에 대해 if (kind === 'X')를 검사하고, @backstage/catalog-model 같은 패키지에서 임포트한 구체적인 타입으로 캐스팅하는 곳이 많을 거예요.

내장 kind 어느 것에도 맞지 않는 무언가를 모델링하고 싶다면, Backstage 메인테이너에게 연락해 가장 잘 진행하는 방법을 논의해 보세요.

결국 새 kind를 추가한다면, 그 apiVersion을 의미 있는 접두사로 네임스페이스 처리해야 해요. 보통 조직 이름을 기반으로 하죠. 예: my-company.net/v1. 또한 내장 kind와 충돌하지 않는 새 kind 식별자를 선택하세요.

기존 kind의 새 Type 추가하기

예시 의도:

"이건 분명히 컴포넌트인데, 지금껏 본 것들과는 잘 맞지 않는 유형이에요."

"우리는 팀을 'team'이라고 부르지 않는데, 'flock'을 그룹 유형으로 넣을 수 없나요?"

일부 엔티티 kind는 spec에 type 필드를 가져요. 조직이 kind 안에서 엔티티의 다양성을 표현할 자유가 있는 곳이 바로 여기예요. 이 필드는 자신에게 맞는 어떤 분류 체계를 따를 것으로 기대돼요. 선택한 값은 Backstage에서 그 엔티티에 어떤 연산과 뷰가 활성화되는지에 영향을 줄 수 있어요. Spotify 내부에서 우리 모델은 몇 년에 걸쳐 크게 성장했고, 컴포넌트 유형에는 이제 ML 모델, 앱, 데이터 파이프라인 등이 포함돼요.

기존 유형 어느 것에도 맞지 않는 소프트웨어를 Other라는 포괄(catch-all) 유형에 넣고 싶은 유혹이 있을 수 있어요. 반대하는 이유가 몇 가지 있어요. 첫째, 엔지니어들이 소프트웨어를 설명할 때 쓰는 개념적 모델과 맞추는 것이 선호된다는 것을 알게 됐어요. 둘째, Backstage는 인프라스트럭처 도구를 플러그인으로 통합해 엔지니어가 소프트웨어를 관리하도록 돕는데, 서로 다른 유형의 컴포넌트를 관리하는 데는 서로 다른 플러그인이 쓰여요.

예를 들어 Lighthouse 플러그인은 Websites에만 의미가 있어요. 소프트웨어 모델링을 더 구체적으로 할수록, 상황에 맞는 플러그인을 제공하기 쉬워져요.

새 type 추가는 상대적으로 노력이 적고 위험도 낮아요. 카탈로그 백엔드는 어떤 type 값이든 받아들이지만, 그 새 type에 특정 동작을 붙이려면 플러그인을 업데이트해야 할 수 있어요.

엔티티 Envelope 또는 Metadata 필드의 검증 규칙 바꾸기

예시 의도:

"기존 카탈로그를 가져오고 싶은데, metadata.name에 허용되는 기본 문자 집합이 너무 엄격해요."

"annotations의 규칙을 바꿔서 annotation 값에 문자열뿐 아니라 어떤 데이터든 저장할 수 있게 하고 싶어요."

location에서 읽은 원시 엔티티 데이터 조각은 필드 형식 검증 단계를 거쳐요. 이는 기본 envelope와 metadata의 유형과 구문이 타당한지 보장해 줘요. 즉 엔티티 kind에 특화되지 않은 것들을요. 이러한 검증기 중 일부 또는 전체는 카탈로그 전용 catalogModelExtensionPoint를 사용해 백엔드를 구축할 때(또는 여전히 이전 백엔드 시스템을 쓴다면 CatalogBuilder에서 직접) 교체할 수 있어요.

이런 확장 유형의 위험과 영향은 무엇을 하려는지에 따라 달라져요. 예를 들어 kind, namespace, name에 허용되는 문자 집합을 확장하는 것은 꽤 무해할 수 있지만 주목할 몇 가지 예외가 있어요. 예를 들어 콜론이나 슬래시를 절대 포함하지 않을 것을 기대하는 코드가 있고, URL에 안전하지 않은 문자를 도입하면 인자를 인코딩하는 데 주의를 기울이지 않는 플러그인이 깨질 위험이 있어요. annotations에서 비문자열(non-string) 지원은 가능할 수 있지만 현실 세계에서 아직 시도된 적은 없어요. 예측하기 어려운 수준의 플러그인 파손이 있을 가능성이 커요.

또한 카탈로그에 데이터를 채운 뒤 규칙을 이전보다 더 엄격하게 만들지 않도록 주의해야 해요. 그러면 이전에 유효했던 엔티티가 처리 오류가 나고 업데이트에 실패하기 시작할 위험이 있어요.

이런 확장을 하기 전에 Backstage 메인테이너나 지원 파트너에게 연락해 사용 사례를 논의하는 것을 권장해요.

다음은 metadata.name 필드의 형식 규칙을 완화한 예시예요.

import { createBackend } from '@backstage/backend-defaults';
import { createBackendModule } from '@backstage/backend-plugin-api';
import { catalogModelExtensionPoint } from '@backstage/plugin-catalog-node/alpha';

const myCatalogCustomizations = createBackendModule({
  pluginId: 'catalog',
  moduleId: 'catalog-customization',
  register(reg) {
    reg.registerInit({
      deps: {
        catalogModel: catalogModelExtensionPoint,
      },
      async init({ catalogModel }) {
        catalogModel.setFieldValidators({
          // This is only one of many methods that you can pass into
          // setFieldValidators; your editor of choice should help you
          // find the others. The length checks and regexp inside are
          // just examples and can be adjusted as needed, but take care
          // to test your changes thoroughly to ensure that you get
          // them right.
          isValidEntityName(value) {
            return (
              typeof value === 'string' &&
              value.length >= 1 &&
              value.length <= 63 &&
              /^[A-Za-z0-9@+_.-]+$/.test(value)
            );
          },
        });
      },
    });
  },
});

const backend = createBackend();
// ... add other backend features and the catalog backend itself here ...
backend.add(myCatalogCustomizations);
backend.start();

핵심 엔티티 필드의 검증 규칙 바꾸기

예시 의도:

"owner가 필수인 게 마음에 들지 않아요. 선택 사항으로 만들고 싶어요."

location에서 읽고 정책 검사된 엔티티 데이터는 validateEntityKind 단계를 구현하는 프로세서를 찾는 프로세서 체인을 거쳐, 데이터가 알려진 kind인지, 그 스키마를 따르는지 확인해요. 모든 알려진 핵심 kind에 대해 이를 구현하고 데이터를 고정 검증 스키마에 대조하는 내장 프로세서가 있어요. 이 프로세서는 CatalogBuilder로 백엔드 카탈로그를 구축할 때, 데이터를 다르게 검증하는 사용자 프로세서로 교체할 수 있어요.

이 교체 프로세서의 이름은 내장 프로세서인 BuiltinKindsEntityProcessor와 일치해야 해요.

이런 확장 유형은 위험이 높고, 어떤 변경을 하느냐에 따라 생태계 전체에 영향이 클 수 있어요. 그래서 일반적인 경우에는 권장하지 않아요. 데이터의 형태에 대한 가정을 하고 @backstage/catalog-model 패키지에서 타입스크립트 데이터 타입을 임포트하는 플러그인과 프로세서 - 그리고 심지어 핵심 자체까지 - 아주 많을 거예요.

Metadata 객체에 새 필드 추가하기

예시 의도:

"우리 엔티티에 여러 엔티티 kind에 대해 표현하고 싶은 부가 속성이 있는데, spec 필드로는 잘 맞지 않아요."

metadata 객체는 현재 확장을 위해 열려 있어요. metadata에서 발견된 알 수 없는 필드는 그대로 카탈로그에 저장돼요. 다만 metadata를 과도하게 확장하는 것에 대해 경고하고 싶어요. 첫째, 미래의 모델 확장과 충돌할 위험이 있어요. 둘째, 이런 확장 유형은 다른 곳에 더 잘 있는 경우가 흔해요. 주로 metadata labels나 annotations에 있죠. 가끔은 대신 새 컴포넌트 유형 등을 만드는 게 나을 수도 있어요.

metadata가 올바른 곳인 상황도 있어요. 그런 경우에 해당하고 다른 사람에게도 적용될 것 같다면 Backstage 메인테이너나 지원 파트너에게 연락해 사용 사례를 논의해 보세요. 어쩌면 핵심 모델을 확장해 여러분과 다른 사람 모두에게 도움이 될 수도 있어요.

기존 kind의 Spec 객체에 새 필드 추가하기

예시 의도:

"내장 Component kind는 괜찮은데, prod인지 staging인지 설명하는 필드를 spec에 추가하고 싶어요."

kind의 스키마 검증은 보통 엔티티 spec의 "알 수 없는" 필드를 금지하지 않고, 카탈로그는 그 안에 무엇이든 기꺼이 저장해요. 그래서 이렇게 하는 것은 보통 카탈로그 관점에서는 동작해요.

이런 필드 추가는 위에서 언급한 metadata 확장과 같은 위험에 노출돼요. 첫째, 미래의 모델 확장과 충돌할 위험이 있어요. 둘째, 이런 확장 유형은 다른 곳에 더 잘 있는 경우가 흔해요. 주로 metadata labels나 annotations에 있죠. 가끔은 대신 새 컴포넌트 유형 등을 만드는 게 나을 수도 있어요.

spec이 올바른 곳인 상황도 있어요. 그런 경우에 해당하고 다른 사람에게도 적용될 것 같다면 Backstage 메인테이너나 지원 파트너에게 연락해 사용 사례를 논의해 보세요. 어쩌면 핵심 모델을 확장해 여러분과 다른 사람 모두에게 도움이 될 수도 있어요.

새 Annotation 추가하기

예시 의도:

"우리가 만든 빌드 시스템에 이름이 붙은 파이프라인-집합 개념이 있는데, 개별 컴포넌트를 해당 파이프라인-집합과 연결해 빌드 상태를 보여주고 싶어요."

"서비스 상태를 자동으로 모니터링하는 알림 시스템이 있는데, 서비스를 알림 풀에 바인딩하는 통합 키가 있어요. Backstage에서 서비스의 진행 중인 알림을 보여주고 싶으니 그 통합 키를 어떻게든 엔티티에 붙이면 좋겠어요."

Annotations는 주로 플러그인이 기능 탐지나 외부 시스템 연결을 위해 소비하도록 의도됐어요. 때로는 사람이 추가하지만, 종종 수집 시점에 프로세서가 자동으로 생성해요. 잘 알려진 annotations 집합이 있지만, 더 추가하는 데는 자유가 있어요. 다음 명명 규칙을 지키는 한 다른 시스템에 위험이나 영향이 없어요.

  • backstage.io annotation 접두사는 Backstage 메인테이너 사용을 위해 예약되어 있어요. 그 접두사에 추가하고 싶다면 연락해 주세요.

  • 잘 알려진 제3자 시스템에 속하는 annotations는 이상적으로 도메인 접두사를 붙여, 읽는 사람에게 이해가 가고 그 시스템(또는 시스템 제조사)과 분명히 연결되게 해야 해요. 예를 들어 pagerduty 관련 annotation에는 pagerduty.com 접두사를 쓸 수 있지만, LDAP은 LDAP 재단/회사 등과 직접 관련이 없거나 소유되지 않으므로 LDAP annotation에 ldap.com을 쓰지는 않을 거예요.

  • 접두사가 전혀 없는 annotations는 여러분의 Backstage 인스턴스에 로컬한 것으로 간주되어 그렇게 자유롭게 쓸 수 있지만, 조직 밖에서는 사용하지 않아야 해요. 예를 들어 annotation을 생성하거나 소비하는 플러그인을 오픈소스로 공개한다면, 그 annotation들은 회사 도메인이나 해당 annotation에 관한 도메인으로 적절히 접두사를 붙여야 해요.

새 Label 추가하기

예시 의도:

"우리 프로세스 수거 시스템이 특정 속성을 가진 컴포넌트를 주기적으로 스크랩하고 싶어요."

"서비스 소유자가 컴포넌트에 뭔가 태그를 달아 CD 시스템에 그 서비스에 대한 SRV 레코드를 자동 생성할지 여부를 알려줄 수 있으면 좋겠어요."

Labels는 주로 특정 속성을 가진 엔티티를 찾고 싶어 하는 외부 시스템이 엔티티를 필터링하는 데 쓰이도록 의도됐어요. 기능 탐지/선택에 쓰이기도 해요. 예를 들어 deployments.my-company.net/register-srv: "true" 라벨을 추가하는 것이 그 예일 수 있어요.

이 글을 쓰는 시점에 labels의 사용은 매우 제한적이고, 커뮤니티와 함께 어떻게 가장 잘 사용할지 아직 합의 중이에요. 사용 사례가 labels에 가장 잘 맞는다고 느낀다면 Backstage 메인테이너에게 알려주면 감사하겠어요.

labels를 추가하는 데는 자유가 있어요. 다음 명명 규칙을 지키는 한 다른 시스템에 위험이나 영향이 없어요.

  • backstage.io label 접두사는 Backstage 메인테이너 사용을 위해 예약되어 있어요. 그 접두사에 추가하고 싶다면 연락해 주세요.

  • 잘 알려진 제3자 시스템에 속하는 labels는 이상적으로 도메인 접두사를 붙여, 읽는 사람에게 이해가 가고 그 시스템(또는 시스템 제조사)과 분명히 연결되게 해야 해요. 예를 들어 pagerduty 관련 label에는 pagerduty.com 접두사를 쓸 수 있지만, LDAP은 LDAP 재단/회사 등과 직접 관련이 없거나 소유되지 않으므로 LDAP label에 ldap.com을 쓰지는 않을 거예요.

  • 접두사가 전혀 없는 labels는 여러분의 Backstage 인스턴스에 로컬한 것으로 간주되어 그렇게 자유롭게 쓸 수 있지만, 조직 밖에서는 사용하지 않아야 해요. 예를 들어 label을 생성하거나 소비하는 플러그인을 오픈소스로 공개한다면, 그 label들은 회사 도메인이나 해당 label에 관한 도메인으로 적절히 접두사를 붙여야 해요.

새 Relation Type 추가하기

예시 의도:

"소유권과는 별개의 서비스 유지보수(service maintainership) 개념이 있는데, 개별 사용자에 대한 관계로 만들고 싶어요."

"팀에서 전사 부서로의 매핑을 관계로 명시적으로 모델링하고 싶어요. 우리 조직 구성의 핵심이고 자주 쿼리하기 때문이에요."

어떤 프로세서든 엔티티가 처리되는 동안 엔티티에 대한 관계를 내보낼 수 있고, CatalogBuilder로 백엔드 카탈로그를 구축할 때 새 프로세서를 추가해 관계를 내보낼 수 있어요. 엔티티 데이터 자체에 기반하거나 다른 곳에서 수집한 정보에 기반해 관계를 내보낼 수 있어요. 관계는 방향성이 있으며 소스 엔티티에서 대상 엔티티로 향해요. 또한 관계를 발생시킨 엔티티 - 관계가 내보내질 때 처리 대상이었던 엔티티 - 에 묶여 있어요. 관계는 매달려 있을(dangling) 수 있어요(카탈로그에 그 이름으로 실제로 존재하지 않는 것을 참조). 호출자는 이 점을 알아야 해요.

잘 알려진 relations 집합이 있지만, 자신만의 관계를 내보내는 것도 자유예요. 관계가 방향성을 갖고 소스와 대상이 엔티티 참조여야 하는 사실은 바꿀 수 없지만, 자신만의 유형을 만들 수는 있어요. 새 관계 유형을 받아들이기 위해 카탈로그 백엔드를 변경할 필요는 없어요.

이 글을 쓰는 시점에 우리는 관계 유형에 대한 네임스페이스/접두사 체계가 없어요. 유형이 특정 문자 집합만 포함하도록 검증되지도 않아요. 이 규칙이 정리되기 전까지는 글자, 대시, 숫자만 사용하고, 미래의 핵심 관계 유형과의 충돌을 피하려면 유형에 어떻게든 접두사를 붙이는 게 좋아요. 예: myCompany-maintainerOf + myCompany-maintainedBy.

핵심 오퍼링으로 승격할 관계 유형에 대한 제안이 있다면 Backstage 메인테이너나 지원 파트너에게 연락해 주세요.

잘 알려진 Relation Type을 새 용도로 사용하기

예시 의도:

"ownerOf/ownedBy 관계 유형이 사용자들이 우리 회사 고유 ServiceAccount kind의 기술적 소유자임을 표현하기에 좋아 보여요. 이 관계 유형을 그것에 재사용하고 싶어요."

이 글을 쓰는 시점에 이것은 미지의 영역이에요. 예를 들어 어떤 관계의 문서화된 사용법이 관계의 한쪽 끝이 보통 User나 Group이라고 말한다면, 소비자는 if (x.kind === 'User') {} else {} 형태의 조건문을 가질 가능성이 높은데, 예상치 못한 kind가 나타나면 혼란을 겪어요.

확립된 관계 유형의 사용을 조직 밖에 영향이 있는 방식으로 확장하고 싶다면 Backstage 메인테이너나 지원 파트너에게 연락해 위험/영향을 논의하세요. 어쩌면 관계의 한쪽 끝이 핵심에 추가 후보로 간주될 수도 있어요.

새 Status 필드 추가하기

예시 의도:

"엔티티 상태를 통합 계층으로서 카탈로그를 통해 일반적인 방식으로 전달하고 싶어요. 우리 모니터링·알림 시스템에 Backstage 플러그인이 있는데, 엔티티의 status 필드에 현재 알림 상태가 실제 엔티티 데이터 가까이에 있어 누구나 소비할 수 있다면 유용하겠어요. status.items 의미론은 잘 맞지 않는다고 느껴서, 이 목적을 위해 status 아래에 우리만의 커스텀 필드를 만들고 싶어요."

우리는 아직 status 객체에 대한 일반적인 의미론을 정의하는 데 착수하지 않았어요. 가능하면 status.items 메커니즘을 고수할 것을 권장해요(아래 참조). 제3자 소비자는 그렇지 않으면 여러분의 status 정보를 소비할 수 없기 때문이에요. 이 주제에 관심이 있다면 Discord에서 메인테이너에게 연락하거나 사용 사례를 설명하는 GitHub 이슈를 만들어 주세요.

새 Status Item Type 추가하기

예시 의도:

"엔티티 status.items 필드의 의미론은 우리 요구에 맞지만, 카탈로그 전용 대신 우리만의 status 유형을 그 배열에 기여하고 싶어요."

이것은 엔티티에 자신만의 status 정보를 추가하는 단순하고 위험이 낮은 방법이에요. 소비자는 다른 유형/소스와 함께 status를 쉽게 추적하고 표시할 수 있을 거예요.

조직 내부에서만 쓰이는 것이 아닌 status 유형은 충돌을 피하기 위해 네임스페이스를 붙일 것을 권장해요. 예를 들어 Backstage 핵심 프로세스가 내보내는 status는 backstage.io/ 접두사를 붙이고, 여러분 조직은 my-org.net/으로 접두사를 붙일 수 있으며, pagerduty.com/active-alerts 같은 것은 특정 외부 시스템에 적합한 온전한 status 항목 유형이 될 수 있어요.

커스텀 status를 내보내는 메커니즘은 아직 마련되지 않았어요. 관심이 있다면 Discord에서 메인테이너에게 연락하거나 사용 사례를 설명하는 GitHub 이슈를 만드는 것을 고려해 보세요. 이 이슈에 더 많은 맥락이 들어 있어요.

모델로 서로 다른 환경 참조하기

예시 의도:

"여러 환경에 서로 다른 버전의 API가 배포되어 있어서 mytool-dev와 mytool-prod를 별개의 엔티티로 두고 싶어요."

같은 것의 서로 다른 버전을 별개 엔티티로 표현하는 것이 가능하긴 하지만, 우리는 일반적으로 반대해요. 개발자가 예를 들어 서비스를 나타내는 Component 하나만 찾아서, 그 뷰 안에서 스택 전체에 배포된 서로 다른 코드 버전을 볼 수 있어야 한다고 믿어요. 이 논리는 API 같은 다른 kind에도 비슷하게 적용돼요.

그렇긴 하지만, 때로는 버전 간 차이가 너무 커서 소비자 관점에서 볼 때 완전히 새로운 엔티티나 다름없는 경우가 있어요. 예를 들어 API의 유의미한 메이저 버전이 다를 때, 특히 두 메이저 버전이 생태계에서 얼마간 공존할 때 그럴 수 있어요. 그런 경우 my-api-v2와 my-api-v3라는 별도의 엔티티를 두는 것이 타당할 수 있어요. 이는 API를 검색할 때 최종 사용자의 기대와 맞고, 둘을 위한 별도 문서를 두고 싶은 바람과도 맞아요. 다만 아껴 쓰세요. 추가적인 모델링 부담보다 사용자에게 더 나은 명확성이 더 크다고 판단될 때만 하는 거예요.

커스텀 플러그인을 작성할 때는, 카탈로그의 소프트웨어에 대한 하나의 표준 참조 아래에서 환경 등을 통한 모든 서로 다른 변형을 보여줄 수 있도록 설계하는 것을 권장해요. 예를 들어 지속적 배포 플러그인의 경우, 하나의 뷰에서 모든 다른 환경에 배포된 엔티티 버전들을 나란히 볼 수 있다면 사용자에게 큰 도움이 돼요. 거기서 한 환경에서 다른 환경으로 승격하기, 롤백, 상대적인 성능 지표 보기 등을 제공받을 수도 있어요. 이렇게 한 곳에 모인 일관성과 도구는 Backstage 같은 것이 가장 큰 가치와 사용 효과를 제공할 수 있는 지점이에요. 엔티티를 작은 섬들로 쪼개면 이것이 더 어려워져요.

커스텀 모델 확장 구현하기

이 섹션에서는 새 Entity 유형으로 카탈로그 모델을 확장하는 단계를 안내할게요.

커스텀 엔티티 정의 만들기

커스텀 엔티티를 도입하는 첫 단계는 그것이 어떤 형태와 스키마를 갖는지 정의하는 것이에요. TypeScript 타입과 JSONSchema 스키마를 이용해 이 작업을 해요.

대부분의 경우 확장의 TypeScript 타입을 최소한 프런트엔드와 백엔드 코드 양쪽에서 쓸 수 있어야 하는데, 이는 그러한 타입을 담는 동형(isomorphic) 패키지를 두고 싶어할 가능성이 높다는 뜻이에요. Backstage 메인 저장소에서는 <plugin>-common 패키지 명명 패턴이 동형 패키지에 사용되고, 이 패턴을 채택할 수도 있어요.

동형 플러그인 패키지는 yarn new를 실행한 다음 옵션 목록에서 "plugin-common"을 선택해 생성할 수 있어요.

현재 @backstage/cli로 동형 플러그인을 생성하기 위한 기존 템플릿은 없어요. 지금 시작하는 가장 간단한 방법은 메인 저장소의 기존 패키지 중 하나의 내용을 복사하는 것이에요. plugins/scaffolder-common 같은 것을요. 그리고 폴더와 파일 내용을 원하는 이름으로 바꾸면 돼요. 이 예시에서는 foobar를 플러그인 이름으로 써서 플러그인이 foobar-common으로 명명될 거예요.

공통 패키지를 마련하면 자신만의 엔티티 정의를 추가하기 시작할 수 있어요. 정확한 방법에 대해서는 기존 scaffolder-common 패키지에서 영감을 얻으세요. 간단히 말해 새 엔티티 kind에 대한 TypeScript 타입과 JSONSchema를 선언해야 해요.

엔티티용 커스텀 프로세서 만들기

다음 단계는 새 엔티티 kind용 커스텀 프로세서를 만드는 것이에요. 이것은 카탈로그 안에서 새 kind의 엔티티를 수집하고 검증할 수 있게 하는 데 쓰여요. 정의 패키지와 마찬가지로, 예를 들어 기존 ScaffolderEntitiesProcessor에서 영감을 찾을 수 있어요.

커스텀 프로세서는 카탈로그 플러그인을 위한 별도 모듈로 만들어야 해요. 설정 방법에 대한 정보는 플러그인 문서를 참고하세요. 모듈을 만들려면 yarn new를 사용하고 backend-module을 선택하세요. 이 경우 모듈 ID는 foobar, 플러그인 ID는 catalog가 될 거예요.

또한 커스텀 엔티티에 대한 카탈로그 프로세스가 어떤 모습일지에 대한 높은 수준의 예시를 제공해요.

import { CatalogProcessor, CatalogProcessorEmit, processingResult } from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common'
import { Entity, entityKindSchemaValidator } from '@backstage/catalog-model';

// For an example of the JSONSchema format and how to use $ref markers to the
// base definitions, see:
// https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema/kinds/Component.v1alpha1.schema.json
import { foobarEntityV1alpha1Schema } from '@internal/catalog-model';

export class FoobarEntitiesProcessor implements CatalogProcessor {
  // You often end up wanting to support multiple versions of your kind as you
  // iterate on the definition, so we keep each version inside this array as a
  // convenient pattern.
  private readonly validators = [
    // This is where we use the JSONSchema that we export from our isomorphic
    // package
    entityKindSchemaValidator(foobarEntityV1alpha1Schema),
  ];

  // Return processor name
  getProcessorName(): string {
    return 'FoobarEntitiesProcessor'
  }

  // validateEntityKind is responsible for signaling to the catalog processing
  // engine that this entity is valid and should therefore be submitted for
  // further processing.
  async validateEntityKind(entity: Entity): Promise<boolean> {
    for (const validator of this.validators) {
      // If the validator throws an exception, the entity will be marked as
      // invalid.
      if (validator(entity)) {
        return true;
      }
    }
    // Returning false signals that we don't know what this is, passing the
    // responsibility to other processors to try to validate it instead.
    return false;
  }

  async postProcessEntity(
    entity: Entity,
    _location: LocationSpec,
    emit: CatalogProcessorEmit,
  ): Promise<Entity> {
    if (
      entity.apiVersion === 'example.com/v1alpha1' &&
      entity.kind === 'Foobar'
    ) {
      const foobarEntity = entity as FoobarEntityV1alpha1;
      // Typically you will want to emit any relations, etc.
      ...
    }
    return entity;
  }
}

새 백엔드(New Backend)

커스텀 프로세서를 사용하려면 모듈을 백엔드에 추가하고, 모듈을 카탈로그 플러그인과 통합해야 해요.

plugins/catalog-backend-module-foobar/src/index.ts

import {
  coreServices,
  createBackendModule,
} from '@backstage/backend-plugin-api';
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
import { FoobarEntitiesProcessor } from './providers';

export const catalogModuleFoobarEntitiesProcessor = createBackendModule({
  pluginId: 'catalog',
  moduleId: 'foobar',
  register(env) {
    env.registerInit({
      deps: {
        catalog: catalogProcessingExtensionPoint,
      },
      async init({ catalog }) {
        catalog.addProcessor(new FoobarEntitiesProcessor());
      },
    });
  },
});
export default catalogModuleFoobarEntitiesProcessor;

이 모듈은 다음과 같이 백엔드에 설치할 수 있어요.

backend.add(import('@internal/plugin-catalog-backend-module-foobar'));

레거시 백엔드(Legacy Backend)

레거시 문서를 살펴보세요.

더 알아보기 (Learn more)