엔티티의 생애
엔티티의 생애 (The Life of an Entity)
이 문서는 카탈로그 백엔드와 그 안에서 엔티티가 흐르게 만드는 기술적 과정에 대한 높은 수준의 개요를 제공합니다.
출처: 문서
본문
이 문서는 카탈로그 백엔드와 엔티티가 그 안에서 흐르게 만드는 기술적 과정에 대한 높은 수준의 개요를 제공합니다. 주로 카탈로그를 설치하거나 확장하면서 내부를 이해하려는 개발자를 대상으로 합니다. 하지만 다른 역할을 하는 사람들에게도 도움이 될 수 있습니다.
핵심 개념
카탈로그는 일종의 허브를 형성하는데, 다양한 권위 있는 소스에서 엔티티를 수집해 데이터베이스에 보관하고, 자동화된 처리를 거친 뒤 API를 통해 Backstage와 그 밖의 곳에서 빠르고 쉽게 접근할 수 있게 제공합니다. 가장 흔한 소스는 표준 형식의 YAML 파일로, 그것이 설명하는 시스템의 소스 코드 근처의 버전 관리 시스템에 있습니다. 그 파일들은 카탈로그에 등록되어 각각의 소유자가 유지합니다. 카탈로그는 그 파일들의 변경에 맞춰 스스로 최신 상태를 유지합니다.
개발자가 카탈로그를 커스터마이즈할 수 있는 주요 확장 지점은 다음과 같습니다.
- 엔티티 제공자(Entity providers) — 초기 원시 엔티티 데이터를 카탈로그에 공급.
- 정책(Policies) — 엔티티 형태에 대한 기준 규칙을 확립.
- 프로세서(Processors) — 원시 엔티티 데이터를 검증, 분석, 변형해 최종 형태로 만듦.
관련된 높은 수준의 과정은 다음과 같습니다.
- 수집(Ingestion) — 엔티티 제공자가 외부 소스에서 원시 엔티티 데이터를 가져와 데이터베이스에 시딩.
- 처리(Processing) — 정책과 프로세서가 수집된 데이터를 계속 다루며, 다른 원시 엔티티(역시 처리 대상), 오류, 다른 엔티티와의 관계 등을 내보낼 수 있음.
- 스티칭(Stitching) — 여러 프로세서가 내보낸 모든 데이터를 최종 출력 엔티티로 조립.
엔티티는 마지막 과정을 통과해 최종 엔티티 사이에 자리 잡기 전까지는 (카탈로그 API를 통해) 외부 세계에 보이지 않습니다.
이 과정들의 세부 사항은 아래에 설명되어 있습니다.
수집
각 카탈로그 배포에는 여러 엔티티 제공자가 설치되어 있습니다. 그것들은 자신이 적합하다고 생각하는 어떤 방식으로든 외부 권위 있는 소스에서 데이터를 가져와 엔티티 객체로 변환하고, 그 엔티티가 추가되거나 제거될 때 데이터베이스에 알리는 역할을 합니다. 이것들은 이후 처리(아래 참조)의 대상이 될 미처리 엔티티이며, 엔티티 존재의 근본 기반을 이룹니다. 엔티티 제공자가 없다면 어떤 엔티티도 시스템에 들어오지 않을 것입니다.
데이터베이스는 항상 각 제공자에 속하는 엔티티 집합을 추적합니다. 두 제공자가 같은 엔티티를 출력하려 시도할 수는 없습니다. 그리고 제공자가 엔티티의 제거를 알리면, 이른 삭제(eager deletion)로 이어집니다. 즉 그 엔티티와 그것이 데이터베이스에서 유발한 모든 보조 데이터가 즉시 제거됩니다.
기본적으로 두 개의 제공자가 설치되어 있습니다. 사용자가 등록한 로케이션(예: YAML 파일 URL)을 다루는 제공자와, app-config의 정적 로케이션을 다루는 제공자입니다. 백엔드 초기화 코드에서 카탈로그 빌더에 전달해 제3자 제공자를 더 추가할 수 있으며, 자신만의 제공자를 쉽게 작성할 수 있습니다.
엔티티 제공자는 EntityProvider 인터페이스를 구현하는 클래스입니다. 세 가지 주요 부분이 있습니다.
- 신원(identity) — 각 제공자 인스턴스에는 데이터베이스가 각 미처리 엔티티의 출처를 추적하는 데 쓸 수 있는 고유하고 안정적인 식별자가 있습니다.
- 연결(connection) — 백엔드 시작 중에 각 제공자가 카탈로그 런타임에 연결됩니다.
- 이벤트 스트림 — 유지 기간 동안 제공자는 언제든지 런타임에 변경 이벤트를 발행해 자신의 미처리 엔티티 집합을 수정할 수 있습니다.
이러한 변경 이벤트를 언제 어떻게 생성할지는 전적으로 제공자가 선택합니다. 예를 들어 app-config 제공자는 시작 시 한 번만 업데이트를 발행하고 그다음에는 휴면 상태입니다. 로케이션 데이터베이스 제공자는 시작 시 초기 업데이트를 수행하고, 그다음엔 로케이션 데이터베이스 변경이 감지될 때마다 작은 델타 업데이트를 합니다. LDAP 제공자는 가끔 전체 업데이트를 트리거하는 타이머 루프에 의해 외부에서 구동됩니다. 미래의 어떤 제공자는 이벤트 버스나 웹훅을 공급받아 전적으로 이벤트로 구동될 수도 있습니다. 제공자 사이에 마법 같은 조정은 없습니다. 여러 카탈로그 서비스 머신에 걸쳐 중복 작업을 피하려고 예를 들어 동기화나 잠금을 스스로 조정해야 한다면, 그것을 대역 외(out-of-band)에서 처리해야 합니다.
내보내진 엔티티에는 일부 대략적인 검증이 적용되어, 최소한 엔티티가 어떻게 생겨야 하는지에 대한 가장 기본적인 스키마 규칙을 준수하는지 확인됩니다. 예를 들어 kind, metadata.name, 그리고 선택적으로 metadata.namespace를 가져야 합니다. 그 외에 수집 단계는 자기 일이 끝났다고 보고, 미처리 엔티티를 저장해 이후에 처리 시스템이 가져가도록 합니다. 즉 엔티티에 적용하는 더 정밀한 검증 규칙은 이 단계에서 아직 적용되지 않는다는 뜻입니다.
처리
모든 미처리 엔티티에는 타임스탬프가 함께 있어, 처리 루프가 다음에 그 엔티티를 처리해야 할 시점을 알려줍니다. 엔티티가 처음 나타나면 이 타임스탬프는 "지금"으로 설정되어 가능한 한 빨리 가져가도록 요청합니다.
각 카탈로그 배포에는 여러 프로세서가 설치되어 있습니다. 그것들은 카탈로그가 처리가 필요하다고 판단한 미처리 엔티티를 받아, 그 데이터를 여러 처리 단계로 돌려 엔티티를 변형하고 그에 대한 보조 데이터를 내보내는 역할을 합니다. 그 모든 작업이 끝나면 카탈로그는 그 정보를 모두 가져와 처리된 엔티티로 저장하고, 오류와 다른 엔티티와의 관계는 따로 저장합니다. 그런 다음 카탈로그는 그 출력이 어떤 엔티티에 영향을 주는지 확인해, 그것들의 최종 조립을 트리거합니다(아래 스티칭 참조).
엔티티는 항상 하나씩 처리되지만, 모든 카탈로그 서비스 호스트가 그렇게 하는 데 협력해 부하를 분산합니다. 각 프로세서가 처리 파이프라인의 고정된 단계 중 하나 이상에 기여할 수 있다는 점에 주목하세요. 먼저 프로세서들이 한 단계에 기여하는 모든 것을 등록된 순서대로 실행하고, 그다음 같은 순서로 다음 단계에 기여하는 모든 것을 실행하는 식입니다.
기술 메모
같은 카탈로그 모듈에서 등록된 프로세서는 항상 등록 순서대로 실행되지만, 여러 카탈로그 모듈에 걸쳐서는 그렇지 않습니다. 등록 순서가 프레임워크가 모듈을 로드하는 순서에 달려 있기 때문입니다. 프로세서 순서는
catalog.processorOptions.<processorName>.priority구성 옵션을 수정해 커스터마이즈할 수 있습니다. 기본 우선순위는20이며, 값이 낮을수록 프로세서가 더 일찍 실행됩니다.
각 단계는 선택적으로 엔티티를 수정하고, 선택적으로 다른 정보를 내보낼 기회를 가집니다. 예를 들어 프로세서는 엔티티의 spec 필드의 정보를 보고, 그 선언에 대응하는 관계를 내보낼 수 있습니다. 프로세서가 엔티티를 내보내면, 그 엔티티는 가능한 한 빨리 처리되어야 한다는 타임스탬프와 함께 그대로 저장됩니다. 오류가 내보내지면, 그것은 엔티티에 뭔가 잘못되었다는 신호이며, 최종 엔티티 사이에 이미 있던 오류 없는 버전을 대체해서는 안 됩니다. 관계가 내보내지면, 아래의 스티칭 과정이 가져갈 수 있도록 전용 관계 테이블에 놓입니다.
선택적 저수준 세부 사항 메모: 엔티티가 내보내질 때, 카탈로그는 내보내는 엔티티와 내보내진 엔티티 사이의 가장자리(edge)를 추적합니다. 이것은 외부에 숨겨진 채 뒤에서 일어나며, 그래프를 형성하는 데 사용됩니다. 이것은 관계(relations)와 같은 것이 아닙니다! 이 가장자리의 목적은 엔티티가 고아가 될 때(아래 참조)를 감지하고, 루트가 명시적으로 등록 해제되고 다른 무엇도 아래 노드를 살려두지 않을 때 그래프 전체에 걸쳐 이른 삭제를 수행하는 것입니다. 고아화와 삭제에 대해서는 이 문서의 뒷부분에서 더 이야기하겠습니다.
마지막 단계가 완료되어 오류가 없었다면, 처리된 엔티티와 모든 관계가 마침내 데이터베이스에 영구 저장됩니다. 그런 다음 카탈로그는 이 엔티티와 관계를 맺은 모든 엔티티를 스티칭 대상으로 간주합니다.
여기서 주목할 점은 처리가 엔티티의 삭제나 등록 해제로 이어지지 않는다는 것입니다. 처리는 새 엔티티를 존재하게 하거나, 이전에 존재하게 했던 엔티티를 갱신할 수만 있습니다. 이것에 대해서는 나중에 더 설명합니다.
스티칭
스티칭은 이전 단계의 출력을 모두 모아, 카탈로그 API에서 보이는 최종 객체로 병합함으로써 엔티티를 완성합니다. 최종 엔티티 자체가 갱신되면, 스티처는 검색 테이블도 그에 맞춰 갱신되도록 합니다.
참고
여기서 언급한 검색 테이블은 Backstage의 핵심 Search 기능과는 관련이 없습니다. 오히려 카탈로그 API 쿼리 결과를 필터링하는 기능을 뒷받침하는 테이블입니다.
다이어그램은 스티처가 여러 소스에서 읽는 방식을 보여줍니다.
- 처리 단계에서 반환된 처리된 엔티티
- 처리 단계가 내보낸 오류(있을 경우)
- 처리 단계가 내보낸 모든 관계뿐 아니라, 우연히 현재 엔티티를 가리키는 다른 엔티티 처리 단계가 내보낸 관계
마지막 부분이 주목할 만합니다. 이것이 스티처가 누가 생성했든 들어오고 나가는 모든 관계 가장자리를 수집할 수 있는 방식입니다.
기술 메모
엔티티가 스티칭될지는 엔티티 해시 값에 달려 있는데, 이 해시는 처리 후의 엔티티 본문, 관계, 오류, 참조된 엔티티, 엔티티 부모를 기준으로 계산됩니다. 이 구성 요소 중 하나라도 바뀌면 해시 값이 바뀝니다. 그 해시 값은 엔티티의 이전 처리 때의 해시 값과 비교되며, 다르면 엔티티가 다시 스티칭됩니다. 엔티티 본문에서 배열 순서의 변화, 예를 들어
metadata.tags는 해시 값을 바꿉니다. 카탈로그에서 스티칭된 엔티티 수를 모니터링하는 것이 좋습니다. 엔티티 수가 너무 높아지면 성능 문제를 일으킬 수 있기 때문입니다.
스티칭은 현재 고정된 과정으로, 수정하거나 확장할 수 없습니다. 즉 최종 결과에 적용하고 싶은 어떤 수정도 수집이나 처리 중에 일어나야 합니다.
오류
엔티티 수집과 처리 중 오류는 다양한 방식으로 발생할 수 있으며, 등록된 시점보다 훨씬 나중에 발생할 수도 있습니다. 예를 들어 등록된 파일이 원격 시스템에서 삭제되거나, 사용자가 실수로 파일 내용을 파싱할 수 없도록 바꾸는 경우 등이 있습니다.
이러한 오류는 두 가지 주요 방식으로 표면화됩니다.
첫째, 카탈로그 백엔드는 events 백엔드 플러그인을 사용해 이벤트를 발행합니다. 이벤트를 구독할 수 있습니다. 이벤트는 독자가 오류의 원인을 찾을 수 있을 만큼 충분한 정보를 담고 있어야 합니다. 이 오류 이벤트를 구독하고 기록하는 방법은 구성 문서를 참조하세요. 이 이벤트는 일반적으로 최종 사용자가 쉽게 찾기 어렵기 때문에, 주로 자신의 통제 아래 있는 정적으로 등록된 엔티티의 문제를 디버깅하거나 최종 사용자가 문제를 찾도록 돕고 싶은 Backstage 운영자에게 유용한 도구가 될 수 있습니다.
Backstage 버전 v1.26.0과 @backstage/plugin-catalog-backend v1.21.9 이전에는 카탈로그 오류가 기본적으로 기록되었습니다.
둘째, 대부분의 오류 종류에 대해 엔티티 자체에 문제를 설명하는 status 필드가 포함됩니다. 이 필드의 내용은 Backstage에서 엔티티 페이지 상단에 표시됩니다. 해당 위치에 대응하는 오류 콜아웃 컴포넌트(EntityProcessingErrorsPanel)를 배치했다면 말이죠.
우리는 여전히 처리 루프 오류에 대한 표면화와 관측성을 개선하기 위해 작업 중입니다.
고아화 (Orphaning)
앞서 언급했듯이 엔티티는 내부적으로 그래프를 형성합니다. 가장자리는 처리된 부모 엔티티에서, 부모를 처리하는 동안 내보내진 자식 엔티티로 이어집니다.
처리 루프는 계속 실행되므로 이 가장자리는 시간이 지나 다시 고려됩니다. 부모 엔티티를 처리할 때 더 이상 특정 자식 엔티티를 내보내지 않으면, 그 이전 가장자리는 끊어집니다. 그 자식이 또한 가리키는 다른 가장자리가 없다면 고아가 됩니다. 최종 결과는 다음과 같습니다.
- 스티칭 과정이 자식 엔티티에
backstage.io/orphan: 'true'어노테이션을 주입합니다. - 자식 엔티티는 카탈로그에서 제거되지 않고, 카탈로그 API를 통해 명시적으로 삭제되거나,
orphanStrategy: delete구성(기본값)이 설정되어 암시적으로 삭제되거나, 원래 부모나 다른 부모가 참조를 시작해 "되찾아질" 때까지 그대로 남아 있습니다. - Backstage의 해당 자식 엔티티 카탈로그 페이지는 새 어노테이션을 감지해 사용자에게 고아 상태를 알려줍니다.
고아화는 여러 다른 시나리오에서 발생할 수 있습니다.
- catalog-info YAML 파일이 카탈로그의 등록을 갱신하지 않은 채 버전 관리 시스템에서 한 곳에서 다른 곳으로 이동하면, 그 등록된 로케이션 "에 의해" 사실상 고아가 됩니다.
- 사용자가 대응하는 부모 catalog-info YAML 파일을 편집해 엔티티의 항목을 제거하면 — 예를 들어
Location부모 엔티티의 경우 — 자식 엔티티가 있는 파일을 가리키는target/targets줄을 편집하거나 제거하면 고아화가 발생할 수 있습니다. - 또 다른 흔한 원인은 원격 시스템을 훑어 엔티티를 찾는 대규모 배치 프로세서가, 이전에는 찾던 것을 더 이상 찾지 못하는 경우입니다. 어쩌면 데이터가 원격 시스템에서 이동되거나 삭제되었을 수도 있습니다. 예를 들어 어떤 사람이 회사를 떠나면 LDAP 조직 탐색 프로세서가 고아가 된
User엔티티를 남길 수 있습니다. 이것은 프로세서에만 해당되며, 엔티티 제공자를 사용해 일어나는 수집은 아래에서 설명하듯 다르게 동작합니다.
파일을 제거하거나 파일을 제대로 읽을 수 없도록 실수로 손상시키는 것은 고아화로 이어지지 않는다는 점에 주목하세요. 뚜렷한 원격을 찾거나 읽지 못하는 것을 포함한 엄격한 오류는 뭔가 잘못되었음을 소유자에게 알리기 위해 엔티티에 그렇게 표시됩니다. 하지만 처리와 다른 동작은 평소처럼 계속됩니다.
카탈로그의 기본 동작은 고아가 된 엔티티를 자동으로 제거하는 것입니다. 하지만 그것들을 대신 유지하려면 다음 app-config 옵션으로 자동 정리를 비활성화할 수 있습니다.
catalog: orphanStrategy: keep
암시적 삭제
엔티티 제공자 — 프로세서가 아니라 — 는 엔티티의 이른 삭제 대상이 되며, 이는 여러분이 삭제한다고 생각한 엔티티보다 더 많은 것의 암시적 삭제를 트리거할 수 있습니다. 이 개념은 여기서 설명합니다.
모든 엔티티 제공자는 External integrations 문서에 설명된 대로 엔티티의 비공개 "버킷"을 관리한다는 것을 기억하세요. 제공자는 그 엔티티들에 어떤 연산을 수행할 수 있는데, 추가, 갱신, 삭제가 포함됩니다. 엔티티 추가/갱신은 일반적인 처리 루프의 대상이므로, 버킷 엔티티는 프로세서가 버킷 내용물과 그 하위 항목을 재귀적으로 처리하면서 내보내는 전체 엔티티 그래프의 루트를 형성하게 될 수 있습니다.
제공자가 그 버킷의 엔티티 삭제를 발행하면, 그 엔티티뿐 아니라 그로부터 처리된 전체 엔티티 트리(있을 경우)가 즉시 삭제 후보로 간주됩니다. "간주"에 주목하세요 — 그것들은 그렇게 하지 않았다면 고아가 되었을 경우에만 삭제됩니다(다른 부모 엔티티가 그들을 내보내지 않을 때). 엔티티 그래프는 엄밀히 트리가 아니므로, 여러 루트가 실제로 그래프 아래쪽의 어떤 노드를 간접적으로 참조하게 될 수도 있습니다. 그렇다면 그 노드는 그런 루트가 모두 사라질 때까지 없어지지 않습니다.
Create 버튼으로 등록하거나 app-config에 추가하는 yaml 파일의 URL은 모두 엔티티 제공자가 처리합니다. 즉 이 암시적 삭제 메커니즘은 일상적인 상황에서 발동합니다. 예를 들어 보겠습니다.
모노레포가 있고, 루트의 catalog-info 파일에 단일 Location 엔티티가 있으며, 그 엔티티가 저장소 안의 각각 Component 엔티티를 가진 다른 catalog-info 파일 세 개를 가리킨다고 상상해 보세요.
/ feature_one/ catalog-info.yaml <- kind: Component feature_two/ catalog-info.yaml <- kind: Component feature_three/ catalog-info.yaml <- kind: Component catalog-info.yaml <- kind: Location
루트 Location 엔티티를 등록하면 실제 효과는 다섯 개의 엔티티가 카탈로그에 나타난다는 것입니다. 먼저 generated--뭔가로 이름 붙은 것이 하나 있는데, 이것이 등록된 URL 자체에 해당합니다. 제공자가 그 "버킷"에 넣는 것이 그것입니다. 그런 다음 처리 루프가 진행되면서, 여러분이 가리킨 Location 엔티티가 그것의 자식으로 나타나고, 그다음 세 Component 엔티티가 차례로 Location의 자식으로 나타납니다.
Backstage 인터페이스의 최종 사용자로서 이제 세 Component 엔티티 중 하나를 삭제하고 싶을 수 있습니다. 엔티티 보기 오른쪽 상단의 점 세 개 메뉴를 방문해 그렇게 합니다. 나타나는 팝업 대화상자는 실은 이 엔티티가 특정 루트에 속하며, 그 루트를 제거해야 할 수도 있다고 알려줄 것입니다(이는 원래 등록된 URL의 등록 해제에 해당합니다). 그렇게 하기로 선택하면 앞서 언급한 다섯 엔티티 모두가 같은 연산에서 실제로 삭제됩니다.
이 공격적인 가지치기를 수행하고 싶지 않았다면, 대신 Location catalog-info 파일의 target 줄 중 하나를 제거한 다음, 제거하고 싶은 Component를 담고 있던 catalog-info 파일을 삭제했을 수도 있습니다. 그러면 카탈로그에는 고아가 된 컴포넌트가 남게 되고, 대신 명시적 삭제(아래 참조)를 사용해 그 단일 컴포넌트를 삭제할 수 있을 것입니다.
명시적 삭제
카탈로그와 그 REST API는 개별 엔티티의 직접 삭제도 허용합니다. 고아가 된 엔티티 — 어떤 부모 엔티티도 적극적으로 최신 상태로 유지하지 않는 엔티티 — 에 대해 이렇게 하는 것이 합리적입니다. 엔티티 보기의 점 세 개 메뉴 아래 팝업 인터페이스는 이 옵션을 제공하며, 고아 상태는 엔티티 개요 페이지 상단의 정보 상자에서 볼 수 있습니다.
하지만 부모 엔티티가 적극적으로 갱신하는 엔티티에 대해 명시적 삭제를 시도한다면, 처리 루프가 여전히 남아 있는 부모 엔티티를 다시 고려할 때 곧 다시 나타날 것입니다.