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. The development, release, and timing of any products, features, or functionality may be subject to change or delay and 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 Orders와 맞추기

요약

GitLab Customers Portal은 GitLab 제품과 별도로, GitLab 고객이 계정 및 구독을 관리하고 추가 좌석을 구매하는 등의 작업을 수행할 수 있는 애플리케이션입니다. Customers Portal에 대한 자세한 정보는 GitLab 문서에서 확인할 수 있습니다. 내부적으로는 CustomersDot(또는 CDot으로도 알려짐)이라고 불립니다.

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

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

동기

CustomersDot의 Order 모델을 다루는 것은 이행 엔지니어들에게 어려움을 줬습니다. Order 데이터를 신뢰하기 어렵기 때문에 Zuora Billing과 구독 데이터의 단일 소스 동기화가 발생할 수 있습니다. 이로 인해 버그, 혼란 및 기능 개발 지연으로 이어졌습니다. 이러한 데이터 무결성 문제와 관련된 다양한 이슈가 나열된 epic가 있습니다. 이 청사진의 동기는 구독 및 관련 데이터 모델에 대한 CustomersDot의 데이터 아키텍처를 개선하여 신뢰를 구축하고 버그를 줄이는 것입니다.

목표

이 재설계 프로젝트에는 여러 측면의 목표가 있습니다.

  • CustomersDot 데이터의 정확성을 높이고 구독과 할당량에 관한 데이터를 포괄하는 것이 이 목표입니다. 이 데이터는 CustomersDot에 Order 레코드로 저장되며, 고객이 구매한 내용을 충분히 나타내지 못하며 문제가 있을 수 있음을 보여주고 있습니다:
  • 구독과 주문 데이터의 단일 소스로서의 Zuora Billing과 계속 동기화
  • Zuora Billing의 가용성에 대한 의존성과 의존성 감소
  • 관련 구독 데이터를 CustomersDot에 로컬로 저장하고 Zuora Billing과 동기화하여 CustomersDot의 성능을 개선하는 것이 중요할 수 있습니다. 이는 Seat Link를 더 효율적이고 신뢰성 있게 만드는 데 중요한 부분일 수 있습니다.
  • CustomersDot의 주문에 대한 혼란을 줄이기 위해, 보다 구독과 유사한 데이터를 포함하는 CustomersDot Orders와 Zuora Orders를 구분할 필요가 있습니다.
    • CustomersDot orders 테이블에는 Zuora 구독 및 평가판 및 GitLab 특정 메타데이터(예: GitLab.com과의 동기화 타임스탬프)의 혼합이 포함되어 있습니다. GitLab은 현재 Zuora에 시험용 구독을 저장하지 않습니다.

제안

위에서 나열된 목표 디렉터리에서 보듯, 구현의 끝에 보고 싶은 바람직한 결과가 많습니다. 이를 위해 우리는 이 작업을 더 작은 단계로 나눠 수행할 것입니다.

  1. 단계 1: Zuora 구독 캐시 모델 구축

    첫 번째 반복 작업은 CustomersDot에 지역 캐시를 위한 기초를 만드는 데 초점을 맞춥니다. 이는 CustomersDot에서 Zuora 구독 개체에 대한 데이터베이스 테이블 및 모델을 생성하는 것을 포함합니다.

    단계 1: Zuora 캐시 모델 구축 (&11751)

  2. 단계 2: Zuora 캐시 동기화 및 백필 구현

    두 번째 반복 작업에는 Zuora와 새로 도입된 모델 간의 동기화를 수립하는 것이 포함됩니다. 또한, 기존 Zuora 구독 데이터를 백필하여 원활한 통합과 데이터 일관성을 보장해야 합니다.

    단계 2: Zuora 캐시 동기화 및 백필 구현 (&13630)

  3. 단계 3: Zuora 캐시 모델 활용

    세 번째 단계에서는 첫 번째 단계에서 도입된 Zuora 캐시 모델을 그리고 두 번째 단계에서 동기화된 것을 활용하는 것이 목표입니다. 주요 초점은 현재 Zuora에 대한 읽기 요청을 수행하는 CustomersDot의 코드를 ActiveRecord 쿼리로 대체하는 것에 있습니다. 이러한 전환이 중요한 성능 향상으로 이어질 것으로 예상됩니다.

    단계 3: Zuora 캐시 모델 활용 (&11752)

  4. 단계 4: OrderSubscription으로 전환

    다음 반복 작업은 CustomersDot의 Order 모델에서 새로운 구독 모델로 전환을 중점으로 합니다.

    단계 4: CDot OrderSubscription으로 대체 (&11753)

디자인 및 구현 상세

단계 1: Zuora 구독 캐시 모델 구축

이 청사진의 첫 번째 단계는 CustomersDot에 지역에서 Zuora 구독 데이터를 캐시하기 위한 새 모델을 추가하는 데 초점을 맞춥니다. 이 지역 데이터 모델을 사용하면 CustomersDot이 지역 데이터베이스에서 Zuora 구독을 쿼리할 수 있습니다. 현재는 Zuora에 직접 쿼리해야하므로 이를 회피하기 위해 적절한 시간에 발생할 수 있는 문제입니다. 또한, Zuora의 API 사용에 대한 요율 제한이 있으므로 이를 피하고자 합니다.

이 단계에는 새로운 데이터베이스 테이블을 생성하고 새로운 데이터 모델을 작성하는 것을 포함합니다. 이는 CustomersDot 애플리케이션에서 필요한 이러한 Zuora 리소스의 데이터 속성을 분석하고, 적절한 데이터 유형과 제한 사항을 가진 마이그레이션으로 구축되어야 합니다. 또한, 새로운 데이터 모델은 검증 및 연관 설정이 필요합니다.

제안된 DB 스키마

erDiagram "Zuora::Local::Subscription" ||--|{ "Zuora::Local::RatePlan" : "has many" "Zuora::Local::RatePlan" ||--|{ "Zuora::Local::RatePlanCharge" : "has many" "Zuora::Local::RatePlanCharge" ||--|{ "Zuora::Local::RatePlanChargeTier" : "has many" "Zuora::Local::Subscription" { string(64) zuora_id PK "`id` field on Zuora Subscription" integer version "null:false" integer current_term integer initial_term integer renewal_term datetime created_date "null:false" datetime updated_date "null:false" date term_start_date "null:false" date term_end_date date cancelled_date date subscription_start_date "null:false" date subscription_end_date boolean auto_renew "null:false default:false" string(3) eoa_starter_bronze_offer_accepted__c string(3) contract_auto_renew__c string(3) turn_on_auto_renew__c string(3) turn_on_operational_metrics__c string(3) turn_on_seat_reconciliation__c string(5) current_term_period_type string(7) turn_on_cloud_licensing__c string(7) marketplace_offer_type__c string(18) status string(64) account_id "null:false" string(64) previous_subscription_id string(64) invoice_owner_id "null:false" string(64) original_id "null:false" string(64) ramp_id string(64) created_by_id "null:false" string(64) updated_by_id "null:false" string(255) name "null:false" string(255) opportunity_id__c string(255) renewal_subscription__c__c string(255) external_subscription_id__c string(255) external_subscription_source__c string(255) git_lab_namespace_id__c string(255) git_lab_namespace_name__c string(255) marketplace_agreement_id__c string(255) marketplace_offer_id__c string(255) marketplace_fee_percentage__c string(500) notes datetime created_at datetime updated_at } "Zuora::Local::RatePlan" { string(64) zuora_id PK "`id` field on Zuora RatePlan" datetime created_date "null:false" datetime updated_date "null:false" datetime created_at "null:false" datetime updated_at "null:false" string(64) subscription_id FK "null:false" string(64) product_rate_plan_id "null:false" string(64) created_by_id "null:false" string(64) updated_by_id "null:false" string(255) name "null:false" } "Zuora::Local::RatePlanCharge" { string(64) zuora_id PK "`id` field on Zuora RatePlanCharge" integer version "null:false" integer segment "null:false" integer quantity integer mrr integer tcv integer dmrc integer dtcv datetime created_date "null:false" datetime updated_date "null:false" datetime created_at "null:false" datetime updated_at "null:false" date effective_start_date date effective_end_date boolean is_last_segment "null:false default:false" string(9) charge_type string(30) price_change_option string(50) charge_number "null:false" string(64) rate_plan_id FK "null:false" string(64) subscription_id "null:false" string(64) subscription_owner_id "null:false" string(64) product_rate_plan_charge_id "null:false" string(64) created_by_id "null:false" string(64) updated_by_id "null:false" string(100) name string(500) description } "Zuora::Local::RatePlanChargeTier" { string(64) zuora_id PK "`id` field on Zuora RatePlanChargeTier" integer tier decimal price "precision:18 scale:2" datetime created_date "null:false" datetime updated_date "null:false" datetime created_at "null:false" datetime updated_at "null:false" string(3) currency string(16) price_format string(64) rate_plan_charge_id FK "null:false" string(64) created_by_id "null:false" string(64) updated_by_id "null:false" }

노트

  • Zuora 네임스페이스는 이미 IronBank 리소스 클래스를 확장하는 데 사용되는 클래스에 의해 차지되어 있습니다. 이러한 클래스는 Zuora::Remote 네임스페이스로 이동될 것입니다. 이는 이러한 클래스가 Zuora에 연결하도록 의도되었음을 나타내며 후속 단계에서 Zuora 네임스페이스를 다른 목적으로 사용할 수 있게 합니다.

  • Zuora 캐시 데이터에 관련된 새로운 모델은 Zuora::Local 네임스페이스에 추가될 것입니다. 이는 Zuora::Remote와 어울리는 명명 방식을 갖추고 있으며 어떤 클래스가 원격 Zuora 데이터 원본 또는 로컬 데이터 원본을 참조하는지 명확하게 합니다.

  • Zuora 구독의 모든 버전은 이 테이블에 저장되어 원래의 Zuora에서 구입한 것 뿐만 아니라 미래의 구입을 표시하는 지원이 가능하도록 할 것입니다. 2023-08-06의 아키텍처 검토 미팅에서 안내 원칙 중 하나는 “Zuora가 다운되더라도 고객이 구매한 것을 볼고 액세스할 수 있어야 한다” 였습니다. 고객이 미래 날짜로 구매할 수 있기 때문에 CustomersDot은 현재 및 미래 버전의 구독을 저장해야 합니다.

  • zuora_id는 ActiveRecord에서 마법같은 역할을 하는 id 필드를 피하고자하기 때문에 기본 키가 될 것입니다.

  • Zuora Billing의 시간대는 태평양 시간으로 구성되어 있습니다. 이를 고려하여 CDot의 캐시된 모델로 데이터를 동기화하여 더 정확한 비교를 가능케 할 것입니다.

두 번째 단계: Zuora 캐시 동기화 및 백필 구현

이 청사진의 두 번째 단계는 로컬 데이터를 Zuora와 동기화하고 기존 데이터를 백필하는 메커니즘을 구축하는 데 초점을 맞춥니다. 이상적으로, 로컬 캐시 모델은 데이터가 동기화될 수 있도록 대부분의 애플리케이션에서 읽기 전용이어야 합니다. 동기화 메커니즘만이 이러한 모델에 쓰기 권한을 갖도록 해야 합니다.

Zuora와 데이터 동기화 유지

CDot은 현재 Order Processed Zuora 호출을 수신하고 처리하여 Update Product과 같은 주문 작업에 대한 Zuora 호출을 유지하고 있습니다(전체 디렉터리). 이러한 호출은 CustomersDot을 Zuora와 동기화시키고 프로비저닝 이벤트를 트리거하는 데 도움이 됩니다. 이러한 호출은 Zuora의 변경 사항과 관련된 캐시된 모델인 Zuora::Local::Subscription 및 관련 모델을 동기화하는 데 중요할 것입니다.

하지만 기존의 호출로는 Zuora 구독에 대한 모든 변경을 수용할 수 없을 것입니다. 특히 사용자 정의 필드에 대한 변경이 기존 호출에서는 포착되지 않을 수 있습니다. 우리는 고객이 구매한 내역을 Zuora와 동기화하기 위해 CustomersDot에서 사용되는 사용자 정의 필드의 모든 자체 이벤트 및 호출을 만들어야 합니다. 그러나 현재 다른 캐시된 리소스에는 사용자 정의 필드가 사용되지 않으므로 이 작업은 Zuora::Local::Subscription에만 영향을 미칠 것입니다.

읽기 전용 모델

이러한 새로운 모델에 저장된 데이터가 Zuora 데이터의 사본이므로 이러한 모델이 애플리케이션 전체에서 읽기 전용 모드가 되도록 보장하는 것이 중요할 것입니다. 캐시 모델이 “쓰기” 모드 대신 “읽기 전용” 모드에서 수정되는 것을 명확히 하려고 합니다. 이러한 분리는 캐시 모델이 부적절하게 또는 실수로 쓰기를 피하도록 도와줍니다. 우리는 이 Spike 이슈의 일환으로 다양한 옵션을 고려했습니다.

우리는 ReadOnlyRecord라는 컨설을 작성하기로 한 상태입니다. 이것은 ActiveRecord 모델에 포함될 때 저장을 방지할 것입니다.

module ReadOnlyRecord
  extend ActiveSupport::Concern
  
  included do
    after_initialize :readonly!
  end
end
  • 이러한 모델 중 하나를 저장하려고 시도하면 오류가 발생할 것입니다(예: ActiveRecord::ReadOnlyRecord: Subscription is marked as readonly).
  • 이 코드로도 레코드를 record.delete로 삭제할 수 있습니다. 우리는 (아마도 ReadOnlyModels에만 해당할 뿐이지만) delete 사용을 피하기 위한 RuboCop 규칙을 작성할 수 있습니다. 또한 이 방법을 덮어쓰여서 오류를 발생시킬 수도 있습니다.
  • Zuora 캐시 동기화 서비스와 같은 특정 네임스페이스 내에서는 쓰기 권한을 갖는 모델에 대한 액세스가 필요합니다.

Zuora 캐시 모델 배포

캐시된 Zuora 데이터 모델을 처음 도입할 때, 롤아웃에 대한 반복적인 접근 방식을 적용할 것입니다. 모델을 빌드하고 데이터를 시작적으로 채우고 모델을 백필하는 동안 기존 기능에는 영향이 없어야 할 것입니다. 이것이 자리 잡으면 기존 기능을 점진적으로 업데이트하여 직접 Zuora를 쿼리하는 대신 이러한 캐시된 데이터 모델을 사용할 것입니다.

우리는 이러한 새로운 로직을 사용하는 모든 기능을 게이팅하는 큰 피처 플래그가 아닌 많은 작은 범위의 피처 플래그를 사용하여 이 전환을 수행할 것입니다. 이는 우리가 더 신속하게 제공하고 피처 플래그 로직이 유지되는 기간과 테스트 케이스가 유지되는 기간을 줄일 수 있도록 하기 위한 것입니다.

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

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

이 단계는 주문 재구조화 작업의 세 번째 단계를 다룹니다. 이 단계에서는 첫 번째 단계에서 도입된 새로운 Zuora 캐시 데이터 모델을 활용하는 데 중점을 둘 것입니다. 구독 데이터를 위해 Zuora를 쿼리하는 것이 Customers에게 필수적이므로 많은 곳에서 업데이트가 필요할 것입니다. CDot이 Zuora에서 읽기를 하고 있는 곳에서는 로컬 캐시 데이터 모델을 쿼리하여 Zuora 대신 사용할 수 있어야 합니다. 특히 Seat Link Service와 같은 구성요소에서는 제3자 요청을 피함으로써 큰 성능 향상이 기대됩니다.

이전과 마찬가지로, 이 전환이 새로운 캐시 모델을 사용하는 새로운 로직을 모두 게이팅하는 큰 피처 플래그가 아니라 많은 작은 범위의 피처 플래그를 사용하여 수행될 것입니다. 이것은 더 신속하게 제공할 수 있도록 하고 피처 플래그 로직이 유지되는 기간과 테스트 케이스가 유지되는 기간을 줄일 수 있습니다.

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

이 청사진의 네 번째 단계는 CustomersDot의 Order 모델에서 Subscription을 새 모델로 전환하는 데 중점을 둘 것입니다. 이 단계는 Subscription을 위한 새로운 모델을 생성하고 전환 기간 동안 양 모델을 모두 지원하며 기존 코드를 Subscription 사용으로 업데이트하고 Order 모델을 더 이상 필요하지 않을 때 제거하는 것으로 이루어질 것입니다.

기존의 Order 모델을 Subscription 모델로 교체함으로써 Order 모델에 대한 혼란을 제거하는 것이 목표입니다. CustomersDot에 저장된 데이터는 Zuora 주문과 일치하지 않습니다. GitLab.com과의 동기화에 대한 추가 메타데이터를 포함하여 Zuora 구독과 더 유사하게 보입니다. 첫 번째 단계인 로컬 캐시 레이어와 함께 Subscription 모델로 전환함으로써 더 나은 데이터 정확성과 CustomersDot 데이터에 대한 신뢰 구축을 달성할 것입니다.

제안된 DB 스키마

erDiagram Subscription ||--|{ "Zuora::Local::Subscription" : "has many" 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::Local::Subscription" { string(64) zuora_id PK "`id` field on Zuora Subscription" string(64) account_id string name }

노트

  • 이 모델의 이름은 Subscription 모델이 이미 존재하기 때문에 논란의 여지가 있습니다. 기존 모델의 이름을 변경하고 결국 새 모델로 교체하려는 희망이 있습니다.
  • 이 모델은 CDot 애플리케이션에서 수정 가능한 Subscription의 레코드로 기능하며, 아래의 Zuora::Local::Subscription 테이블은 읽기 전용이어야 합니다.
  • zuora_account_id는 편리를 위해 추가될 수 있지만 billing_account를 통해 가져올 수도 있습니다.
  • 실제 구독 당 하나의 Subscription 레코드가 있게 되며 구독 버전 대신 사용될 것입니다.
    • 이렇게 하면 gitlab_namespace_idlast_extra_ci_minutes_sync_at과 같은 필드의 중복을 피할 수 있습니다.
    • zuora_subscription_id 열은 제거하거나 최신 Zuora 구독 버전을 참조로 남겨둘 수 있습니다.

Zuora와 데이터를 동기화 유지

Subscription 모델은 구독이 생성되거나 업데이트될 때 Zuora와 동기화 유지해아합니다. 이 모델은 Zuora::Local::Subscription 레코드를 동기화할 때 동일한 방식으로 처리할 것이며, 이는 캐시된 모델이 Zuora 호출 처리 시 동기화되는 방식과 유사합니다(첫 번째 단계에서 설명됨). Zuora::Local::Subscription의 새 버전을 저장할 때는 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에서 주문 처리 콜아웃을 처리할 때 현재 동작을 고려해봅시다.

  1. 청구 계정 멤버십은 판매 대상 이메일과 일치하는 CDot 고객을 위해 새로운 청구 계정으로 업데이트됩니다.
  2. CDot은 새 billing_account_idsubscription_name을 사용하여 새로운 CDot 주문을 찾으려고 합니다.
  3. 이 기준에 일치하는 주문이 없는 경우 새 주문이 생성됩니다. 이는 동일한 Zuora 구독에 대해 두 개의 주문 레코드로 이어집니다.

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

알 수 없는 요소들

아래에서 몇 가지 알 수 없는 것들이 설명되어 있습니다. 구현이 진행됨에 따라 이러한 불확실성은 더 명확해져야 합니다.

구독에 대한 시험 데이터?

CDot 주문 모델에는 유료 구독 데이터와 시험에 대한 데이터가 포함되어 있습니다. 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

자원