This page contains information related to upcoming products, features, and functionality. It is important to note that the information presented is for informational purposes only. Please do not rely on this information for purchasing or planning purposes. As with all projects, the items mentioned on this page are subject to change or delay. The development, release, and timing of any products, features, or functionality remain at the sole discretion of GitLab Inc.
Status Authors Coach DRIs Owning Stage Created
proposed @tyleramos @fabiopitino @tgolubeva @jameslopez devops fulfillment 2023-10-12

GitLab CustomersDot Orders를 Zuora 주문과 일치시키기

요약

GitLab Customers Portal은 GitLab 고객이 계정 및 구독을 관리할 수 있는 별도의 응용 프로그램으로, 추가 좌석 구매와 같은 작업을 수행할 수 있습니다. Customer Portal에 대한 자세한 정보는 GitLab 설명서에서 찾을 수 있습니다. 내부적으로 이 응용 프로그램은 CustomersDot이라고 알려진다(CDot로도 알려짐).

GitLab은 Zuora 플랫폼을 사용하여 구독 기반 서비스를 관리합니다. CustomersDot은 Zuora Billing과 직접 통합되며 구독 데이터의 유일한 소스로 Zuora Billing을 취급합니다.

CustomersDot은 때로는 orders 데이터베이스 테이블의 형태로 일부 구독 및 주문 데이터를 로컬로 저장하며, 때로는 Zuora Billing과 동기화되지 않을 수 있습니다. 이 설계안의 주요 목표는 Zuora Billing과의 통합을 개선하여 더 신뢰할 수 있고 정확하며 성능이 우수해지도록 계획을 수립하는 것입니다.

동기

CustomersDot의 Order 모델로 작업하는 것은 이행 엔지니어들에게 어려움을 던졌습니다. Order 데이터를 신뢰하기 어렵기 때문에 종종 실제 구독 데이터의 유일한 소스인 Zuora Billing과 동기화되지 않을 수 있습니다. 이로 인해 버그, 혼란 및 기능 개발 지연으로 이어질 수 있습니다. 고객님 Orders를 Zuora 객체와 일치시키는 Epic이 있으며, 이 Epic에서는 이러한 데이터 무결성 문제와 관련된 다양한 문제가 나열되어 있습니다. 이 설계안의 동기는 CustomersDot에서 구독 및 관련 데이터 모델에 대한 더 나은 데이터 아키텍처를 개발하는 것으로 신뢰를 구축하고 버그를 줄이는 것입니다.

목표

이 재아키텍처 프로젝트에는 여러 가지 다양한 목표가 있습니다.

  • 고객님Dot 데이터의 정확성을 높입니다. 이 데이터는 고객님Dot에서 Order 레코드로 저장되며, 고객이 구매한 내용을 충분히 표현하지 못하며 다음과 같은 문제점을 보입니다:
  • Zuora Billing이 구독 및 주문 데이터의 SSoT(Single Source of Truth)로 유지되도록 계속 조정합니다.
  • Zuora Billing의 가용성에 대한 의존성 및 의존성을 줄입니다.
  • 광고님Dot 성능을 향상시켜 관련된 구독 데이터를 로컬로 저장하고 Zuora Billing과 동기화 유지합니다. 이것은 좌석 링크를 보다 효율적이고 신뢰성 있게 만드는 데 중요한 부분일 수 있습니다.
  • 구독을 더 잘 반영하는 구독과 유사한 데이터가 포함된 CustomersDot Orders와 꼭지 주문을 구분하는 것입니다. - CustomersDot orders 테이블에는 Zuora 구독 및 평가판과 GitLab 특정 메타데이터의 혼합이 포함되어 있습니다. GitLab은 현재 시간에 Zuora에 평가판 구독을 저장하지 않습니다.

제안

위의 목표 목록에서 보듯, 우리는 구현 후에 원하는 여러 가지 결과를 보고 싶어합니다. 이러한 목표에 도달하기 위해, 이 작업을 더 작은 반복 단위로 나눌 것입니다.

  1. 1단계: Zuora 구독(Cache)

    첫 번째 반복은 CustomersDot에 Zuora 구독 개체에 대한 로컬 캐시를 추가하는 데 중점을 둡니다. 이는 레코드, 레코드 요금 및 레코드 요금 티어를 포함하여 새로운 모델을 CustomersDot에 추가하는 것에 초점을 맞춥니다.

  2. 2단계: Zuora Cache 모델 활용

    두 번째 단계는 첫 번째 단계에서 소개된 Zuora 캐시 모델을 사용하는 것에 중점을 둡니다. 고객님Dot에서 Zuora 구독 데이터의 읽기 요청을하는 코드는 ActiveRecord 쿼리로 교체되어야 합니다. 이렇게 하면 큰 성능 향상이 기대됩니다.

  3. 3단계: Order에서 Subscription으로 전환

    다음 반복 단계는 CustomersDot의 Order 모델에서 구독을 위한 새로운 모델로 전환하는 데 중점을 둡니다.

디자인 및 구현 세부 정보

1단계: Zuora 구독(Cache)

이 설계안의 첫 번째 단계는 고객님Dot에서 Zuora 구독 데이터를 로컬로 캐시하기 위한 새로운 모델을 추가하는 데 중점을 둡니다. 이러한 로컬 데이터 모델을 사용하여 Zuora 구독을위한 로컬 데이터베이스 쿼리를 수행할 수 있게 합니다. 현재, 이는 Zuora에 직접 쿼리하는 것을 필요로 하며, 만일 Zuora가 다운될 경우 문제가 될 수 있습니다. 또한, 고객님Dot이 계속 성장함에 따라 API 사용에 대한 Zuora의 요율 제한이 있으므로 이를 피하려고 합니다.

이 단계는 새로운 데이터 모델을 생성하고, 로컬 데이터와 Zuora의 동기화를 유지하는 메커니즘을 구축하고, 기존 데이터를 백필링하는 것으로 구성될 것입니다. 대부분의 응용 프로그램이 데이터가 항상 동기화되도록하기 위하여 로컬 캐시 모델을 읽기 전용으로 유지해야 할 것입니다. 동기화 메커니즘에만이 모델에 쓰기 권한을 부여해야 합니다.

제안된 DB 스키마

erDiagram "Zuora::Subscription" ||--|{ "Zuora::RatePlan" : "다수 보유" "Zuora::RatePlan" ||--|{ "Zuora::RatePlanCharge" : "다수 보유" "Zuora::RatePlanCharge" ||--|{ "Zuora::RatePlanChargeTier" : "다수 보유" "Zuora::Subscription" { string(64) zuora_id PK "Zuora 구독의 `id` 필드" string(64) account_id string name string(64) previous_subscription_id string status date term_start_date date term_end_date int version boolean auto_renew "null:false default:false" date cancelled_date string(64) created_by_id integer current_term string current_term_period_type string eoa_starter_bronze_offer_accepted__c string external_subscription_id__c string external_subscription_source__c string git_lab_namespace_id__c string git_lab_namespace_name__c integer initial_term string(64) invoice_owner_id string notes string opportunity_id__c string(64) original_id string(64) ramp_id string renewal_subscription__c__c integer renewal_term date subscription_end_date date subscription_start_date string turn_on_auto_renew__c string turn_on_cloud_licensing__c string turn_on_operational_metrics__c string turn_on_seat_reconciliation__c datetime created_date datetime updated_date datetime created_at datetime updated_at } "Zuora::RatePlan" { string(64) zuora_id PK "Zuora 요금제의 `id` 필드" string(64) subscription_id FK string name string(64) product_rate_plan_id datetime created_date datetime updated_date datetime created_at datetime updated_at } "Zuora::RatePlanCharge" { string(64) zuora_id PK "Zuora 요금제의 `id` 필드" string(64) rate_plan_id FK string(64) product_rate_plan_charge_id int quantity date effective_start_date date effective_end_date string price_change_option string charge_number string charge_type boolean is_last_segment "null:false default:false" int segment int mrr int tcv int dmrc int dtcv string(64) subscription_id string(64) subscription_owner_id int version datetime created_date datetime updated_date datetime created_at datetime updated_at } "Zuora::RatePlanChargeTier" { string zuora_id PK "Zuora 요금제 티어의 `id` 필드" string rate_plan_charge_id FK string price datetime created_date datetime updated_date datetime created_at datetime updated_at }

주의사항

  • Zuora 네임스페이스는 이미 IronBank 리소스 클래스를 확장하는 데 사용되는 클래스에 의해 사용 중입니다. 이러한 클래스가 Zuora에 연결되어 있음을 나타내기 위해 이를 Zuora::Remote 네임스페이스로 이동하기로 결정했습니다. 이렇게 하면 Zuora 네임스페이스가 Zuora 캐시된 데이터와 관련된 모델을 그룹화하는 데 사용될 수 있습니다.
  • 모든 버전의 Zuora 구독은 이 테이블에 저장되어야 하며, Zuora가 다운된 경우에도 현재 및 미래 구매를 지원할 수 있어야 합니다. 2023-08-06에 열린 아키텍처 검토 미팅의 주요 원칙 중 하나는 “Zuora가 다운된 상태에서도 고객이 구매한 내용을 볼고 액세스할 수 있어야 한다”였습니다. 고객이 미래에 구매할 수 있기 때문에, CustomersDot은 구독의 현재 및 미래 버전을 저장해야 합니다.
  • zuora_id는 ActiveRecord에서 마법 같은 역할을 하는 id 필드를 피하고자 하는 우리의 의지에 따라 주 키가 될 것입니다.
  • Zuora Billing의 시간대는 태평양 시간으로 구성됩니다. Zuora에서 CDot의 캐시된 모델로 데이터를 동기화할 때 더 정확한 비교를 위해 이 시간대를 고려해야 합니다.

Zuora와 데이터 동기화 유지

현재 CDot은 Order Processed Zuora 호출을 받아들이고 Update Product와 같은 주문 작업에 대한 처리를 합니다(전체 목록). 이러한 호출은 CustomersDot을 Zuora와 동기화시키고 프로비저닝 이벤트를 트리거하는 데 도움이 됩니다. 그러나 이 기존 호출은 Zuora 구독에 대한 모든 변경 사항을 포괄할 수는 없습니다. 특히 사용자 정의 필드의 변경 사항은 이러한 기존 호출에 포착되지 않을 수 있습니다. 이 모든 리소스에 대해 CustomersDot에서 캐시된 사용자 정의 필드에 대해 CDot을 Zuora와 동기화시키기 위해 사용자 정의 이벤트와 호출을 생성해야 합니다. 이러한 변경은 현재의 Zuora::Subscription에만 영향을 미칠 것입니다. CustomersDot은 현재 시간에 다른 제안된 캐시된 리소스에서 사용자 정의 필드를 사용하지 않기 때문입니다.

읽기 전용 모델

이러한 새 모델에 저장된 데이터가 Zuora 데이터의 사본인 경우, 이러한 모델을 적절한 문맥 내에서 수정하는 것이 중요합니다. 애플리케이션 전반에 걸쳐 수정되어서는 안되며 “읽기 전용” 모드에서만 수정되어야 합니다.

이 스파이크 이슈의 일환으로 다양한 옵션을 고려했습니다.

우리는 ActiveRecord 모델에 포함된 경우 저장을 방지할 ReadOnlyRecord concern을 생성하기로 결정했습니다.

module ReadOnlyRecord
  extend ActiveSupport::Concern

  included do
    after_initialize :readonly!
  end
end
  • 이러한 모델 중 하나를 저장하려고 하면 오류가 발생합니다 (예: ActiveRecord::ReadOnlyRecord: Subscription is marked as readonly).
  • 이 코드로도 record.delete를 사용하여 레코드를 삭제할 수 있습니다. rubocop cop를 작성하여 해당 ReadOnlyModels에서 delete 사용을 피할 수 있습니다. 또한이 메서드를 덮어쓰기하여 오류가 발생하도록 할 수도 있습니다.
  • Zuora 캐시 동기화 서비스와 같은 특정 네임스페이스 내에서는 쓰기 권한이 있는 모델에 액세스하고자 합니다.

Zuora 캐시 모델의 롤아웃

캐시된 Zuora 데이터 모델을 처음 도입하는 최초의 반복적인 방식으로 롤아웃을 수행할 것입니다. 모델을 구축하고 데이터를 호출을 통해 채우고 이러한 모델을 백필하는 동안 기존 기능에는 영향이 없어야 합니다. 이 과정이 완료되면 기존 기능을 반복적으로 업데이트하여 직접 Zuora를 쿼리하는 대신 이러한 캐시된 데이터 모델을 사용하도록 업데이트할 것입니다.

기존 논리의 길이와 테스트 케이스가 유지되는 기간을 줄이기 위해 큰 기능 플래그 하나가 아닌 많은 작은 범위의 기능 플래그를 사용하여 이러한 캐시 모델을 사용하는 새로운 논리를 게이트하는 것이 좋습니다. 이를 통해 더 빨리 제공할 수 있고 테스트 케이스를 유지 관리할 필요성을 줄일 수 있습니다.

캐시된 모델이 코드베이스에서 사용되기 전에 테스트를 수행하여 캐시된 모델의 데이터 무결성을 보장할 수 있습니다.

두 번째 단계: Zuora 캐시 모델 활용

이 단계는 주문 다시 아키텍처화의 두 번째 단계를 다룹니다. 이 단계에서 새 Zuora 캐시 데이터 모델을 활용할 것입니다. 고객을 위해 Zuora의 구독 데이터를 쿼리하는 것은 기본적이므로 업데이트해야 할 위치가 많을 것입니다. CDot에서 Zuora를 읽는 곳에서 로컬 캐시 데이터 모델을 쿼리하여 대체할 수 있습니다. 이로 인해 특히 Seat Link Service와 같은 구성 요소에서 써드파티 요청을 피함으로써 성능을 크게 향상시킬 것으로 예상됩니다.

이 전환은 많은 작은 범위의 기능 플래그를 사용하여 완료될 것입니다. 이를 통해 더 빨리 제공하고 해당 캐시 모델을 사용하는 새로운 논리를 유지 관리할 때의 논리의 길이를 줄일 수 있습니다.

세 번째 단계: Order에서 Subscription으로의 전환

이 블루프린트의 두 번째 단계는 CustomersDot Order 모델에서 Subscription을 위한 새 모델로의 전환에 중점을 둡니다. 이 단계에는 Subscription을 위한 새 모델을 생성, 이전 모델을 지원하고 기존 코드를 Subscription을 사용하도록 업데이트하며 더 이상 필요하지 않을 때 Order 모델을 삭제하는 것으로 이루어질 것입니다.

Order 모델을 Subscription 모델로 대체함으로써 Order 모델 주변의 혼란을 제거하는 것이 목표입니다. CustomersDot Order 모델에 저장된 데이터는 Zuora 주문과 일치하지 않습니다. 추가 메타데이터를 포함하여 Zuora 구독과 더 유사합니다. 첫 번째 단계의 로컬 캐시 레이어와 함께 Subscription 모델로의 전환은 데이터 정확성을 향상시키고 CustomersDot 데이터에 대한 신뢰 구축의 목표를 달성해야 합니다.

제안된 DB 스키마

erDiagram Subscription ||--|{ "Zuora::Subscription" : "다수 보유" Subscription { bigint id PK bigint billing_account_id string(64) zuora_account_id string(64) zuora_subscription_id string zuora_subscription_name string gitlab_namespace_id string gitlab_namespace_name datetime last_extra_ci_minutes_sync_at datetime increased_billing_rate_notified_at boolean reconciliation_accepted "null:false default:false" datetime seat_overage_notified_at datetime auto_renew_error_notified_at date monthly_seat_digest_notified_on datetime created_at datetime updated_at } "Zuora::Subscription" { string(64) zuora_id PK "Zuora 구독의 `id` 필드" string(64) account_id string name }

참고사항

  • 이 모델의 이름은 이미 Subscription 모델이 존재하기 때문에 논의가 필요합니다. 새 모델로 교체할 수 있도록 기존 모델의 이름을 변경할 수 있습니다.
  • 이 모델은 CDot 애플리케이션에서 수정 가능한 구독을 나타내며 아래 Zuora::Subscription 테이블은 읽기 전용입니다.
  • zuora_account_id를 편의상 추가할 수도 있지만 billing_account를 통해 가져올 수도 있습니다.
  • 실제 구독 당 하나의 Subscription 레코드가 있을 것입니다.
    • 이렇게 하면 gitlab_namespace_idlast_extra_ci_minutes_sync_at과 같은 필드의 중복을 피할 수 있습니다.
    • zuora_subscription_id 열을 제거하거나 최신 Zuora 구독 버전을 참조하는 것으로 유지할 수 있습니다.

Zuora와 데이터 동기화 유지

Subscription 모델은 Zuora와 동기화되어야 합니다. 구독이 생성되거나 업데이트될 때 이 모델은 Zuora::Subscription 레코드를 동기화할 것입니다. 이는 캐시된 모델이 Zuora 콜아웃을 처리할 때 동기화되는 것과 유사합니다(1단계에서 설명한대로). Zuora::Subscription의 새 버전을 저장할 때는 zuora_subscription_name과 일치하는 Subscription 레코드가 업데이트되거나, 해당하는 레코드가 없는 경우 Subscription이 생성될 수 있습니다. zuora_subscription_id는 일반적인 업데이트에서 최신 버전으로 설정될 것입니다. Subscription의 대부분 데이터는 GitLab 메타데이터(예: last_extra_ci_minutes_sync_at)이기 때문에 업데이트할 필요가 없습니다.

이 업데이트 규칙의 예외는 zuora_account_idbilling_account_id 속성입니다. 만약 Zuora 구독의 zuora_account_id가 변경된 경우 CDot에서 Order Processed 콜아웃을 처리할 때 현재 동작을 고려해 봅시다:

  1. 청구 계정 멤버십은 판매 대상 이메일 주소와 일치하는 CDot Customer의 새로운 청구 계정에 업데이트됩니다.
  2. CDot은 새 billing_account_idsubscription_name을 가진 CDot Order를 찾으려고 시도합니다.
  3. 이 기준을 충족하는 Order가 없는 경우 새 Order가 생성됩니다. 이로 인해 동일한 Zuora 구독에 대해 두 개의 Order 레코드가 생깁니다.

이 시나리오는 새 Subscription 모델에서 피해야 합니다. 고유한 Zuora::Subscription 이름에는 하나의 Subscription만 존재해야 합니다. Zuora 구독이 계정을 이전하는 경우 Subscription도 그에 맞춰야 합니다.

알 수 없는 사항들

구현을 진행함에 따라, 다음과 같은 여러 알 수 없는 사항들이 설정될 것입니다.

구독에서의 무료 사용 데이터?

CDot Order 모델에는 유료 구독 데이터와 시험판이 포함되어 있습니다. Subscription의 경우 유료 구독과 시험판 데이터를 계속 동일한 테이블에 보유하거나 각각의 모델로 분리할 수 있습니다.

orders 테이블에는 customer_idtrial과 관련된 필드만 있습니다. 이러한 필드는 주로 시험판과 관련이 있습니다. 이러한 필드들을 Subscription 테이블에 추가해야 할까요? Zuora에는 없는데 Subscription에 시험판 정보를 포함해야 할까요?

시험 주문을 별도의 테이블로 분리한다면, (SaaS) trials 테이블에 필요한 열은 아마도 다음과 같을 것입니다:

  • customer_id
  • product_rate_plan_id (또는 plan_id로 이름 변경 또는 plan_code 사용)
  • quantity
  • start_date
  • end_date
  • gl_namespace_id
  • gl_namespace_name

자료