AWS/AWS Marketplace

AWS Marketplace 연동 가이드 | 04. BatchMeterUsage

행운개발자 2024. 1. 17. 02:36
728x90

AWS Marketplace 연동 가이드 | 01. AWS Marketplace Seller 계정 생성부터 x-amzn-marketplace-token 토큰 수신까지

AWS Marketplace 연동 가이드 | 02. AWS Marketplace Client 생성하기

AWS Marketplace 연동 가이드 | 03. ResolveCustomer

AWS Marketplace 연동 가이드 | 04. BatchMeterUsage

 


BatchMeterUsage

BatchMeterUsage는 AWS로 미터링 정보를 1시간마다 1번씩 전송해야합니다.

 

AWS MP로 미터링 정보를 전송하기 위해서는 아래의 과정이 필요합니다

  1. 미터링 정보 조회
    • MeteringRawData
  2. AWS 미터링 단위로 변환하기 :
    • MetringRawData → AwsBillingData
  3. AWS 미터링 정보 BatchMeterUsage API에 맞춰 변형하기 :
    • AwsBillingData → AwsBillingRequestBuilder → List
  4. BatchMeterUsageRequest → BatchMeterUsage API 호출
    • AWS로 과금 정보 전송

AWS 미터링 단위로 변환하기

AWS MP에 제품을 등록하면 AWS을 통해서 Billing 됩니다. 저희가 보내주는 metering 정보에 전적으로 의존해서 고객에게 과금이 됩니다. 만약 metering 정보를 실수로 더 보내거나 덜 보내는 경우, 고객에에 의도치않은 금액이 청구될 수 있습니다.

AWS MP 제품을 등록할 때 dimension이라는 단위를 입력해야합니다. 각 dimension 별로 사용량에 정보를 Unit으로 환산해서 전송하면 dimension 별로 과금액이 청구됩니다.

dimension에 대한 자세한 설명은 아래 링크를 확인해주세요.

2024.01.16 - [개발/AWS Marketplace] - AWS Marketplace SaaS Subscription 과금 모델이 적합한지 미리 검토하는 방법

AWS Marketplace에서는 1시간 단위의 과금 모델을 사용하고 있기 때문에, 기존에 존재하던 과금 방식 월 단위 과금이라면 변환 공식이 필요합니다.  아래의 예시에서 MeteringRawData 클래스는 월 단위 과금량을 담고 있고, AwsMeteringData 클래스는 시간 단위 과금량을 저장합니다. 

/**
 * @param rawData 매 시 0, 20, 40분마다 갱신되는 미터링 정보
 * @param productType 매 정각마다 갱신되는 정보
 */
public AwsMeteringData(MeteringRawData rawData, String productType) {
    this.pcode = rawData.getPcode();
    this.time = DateUtil.getHourTrim(rawData.getTime());
    this.productType = productType;
    this.host = rawData.getHost();
    this.cpu = rawData.getCpu();
    this.urls = rawData.getUrls();
    this.logs = getAwsLogUnit(rawData);
    this.k8sContainers = rawData.getK8sContainers();
    this.k8sAppContainers = rawData.getK8sAppContainers();
    this.session = getAwsSessionUnit(rawData);
    this.updateTime = new Date();
}

 

변환 공식은 연동하는 서비스마다 달라집니다. 하나만 예시를 들면 아래와 같이 변환 공식을 적용할 수 있습니다. 아래의 공식을 결정하실 떄에는 이미 존재하는 과금 정보가 어떻게 집계되는지를 아주 자세히 파악해보셔야 합니다.

/**
 * 100만 로그 = 1 Log Unit = 0.04$ = 50원
 * 25000 로그 = AWS 1 Unit = 0.001$ = 50/40 원
 *
 * '100만 Log Unit 당 50원'과 AWS MP 상에서 제공하는 최소 가격 단위가 0.001인점을 고려하여 25_000으로 나누어서 Unit을 전송함
 */
private int getAwsLogUnit(MeteringRawData rawData) {
    // 매 시간 쌓여있는 총 로그 수 (보관기간이 반영되어 있음)
    long allLogsExistsInYardbase = rawData.getLogs();
    if (allLogsExistsInYardbase == 0) {
        // 사용량이 없으면 과금하지 않음
        return 0;
    }

    // AWS Marketplace는 매 시간 과금이기 때문에
    // 현재 Yard에 남아있는 로그를 24시간으로 나누어 '1시간 평균 로그 양'에 대해서 AWS Log Unit을 계산해야
    // AWS Marketplace를 사용하지 않는 과금 방식과 최대한 비슷한 과금량이 산정됨
    long oneHourAvg = allLogsExistsInYardbase / HOURS_PER_DAY;

    if (oneHourAvg < AWS_LOG_UNIT) {
        // 로그를 하나라도 사용했으면 1 AWS LogUnit으로 과금
        return 1;
    } else  {
        // 반올림
        return (int) Math.round((double) oneHourAvg / AWS_LOG_UNIT);
    }
}

 

💡과금량은 Integer로 소수점을 지원하지 않습니다.

dimension 별 quantity는 Integer이기 때문에 소수점 아래 자리는 절삭됩니다. 이 부분을 반올림/올림 처리하면 사용자의 과금액이 그만큼 늘어납니다. 정답은 없고 사내 정책에 맞추어서 작성하면 됩니다. 아래는 AwsMeteringData 데이터로 변환한 뒤, 실제로 AWS MP에 과금량을 전송하는 BatchMeterUsage API의 RequestDTO입니다. 

// BatchMeterUsage API에서 사용하는 포멧
package com.amazonaws.services.marketplacemetering.model;
public class UsageRecord implements Serializable, Cloneable, StructuredPojo {
    private java.util.Date timestamp;
    private String customerIdentifier;
    private String dimension;
    private Integer quantity;
}​

 

AWS 미터링 정보 BatchMeterUsage API에 맞춰 변형하기

BatchMeterUsage에서 API는 BatchMeterUsageRequest 객체를 요청으로 받습니다. 이 객체는 아래와 같은 구조로 되어 있습니다.

이 요청 정보의 계층을 이해해보면 아래와 같습니다.

  1. 하나의 AWS MP 제품(productCode)을 여러개의 계정(customerIdentifier)에서 구독할 수 있습니다. 구매자 별로 과금량을 전송하는 것이 아니라, AWS MP 제품 별로 과금량을 전송해야 합니다.
  2. 각 계정(customerIdentifier) 별로 dimension 별 총 사용량(quantity)을 1시간(timestamp)마다 전송해야한다
  3. dimension 별 총 사용량(quantity)은 Tag 별 사용량(allocatedUsageQuantity)의 총합이다

연동해야하는 서비스는 아래와 같은 정책으로 미터링 정보를 생성하고 있습니다.

💡 혹시 아래의 설명이 이해가 잘 되지 않는다면
글 보다는 코드를 훑어보시는 것이 더 잘 이해되실 수 있습니다.

  1. 하나의 Account(customerIdentifier)이 여러 개의 Project를 생성할 수 있다
  2. Project가 생성되면 고유한 식별자가 추가된다(Tag)
  3. 미터링 정보(allocatedUsageQuantity)는 Project 별로 생성된다
  4. Project를 생성할 때 Project의 타입에 따라서 집계되는 dimension 정보가 1개 이상 지정되어 있다.

이를 요청 정보에 맵핑하면 아래와 같습니다.

이렇게 매핑 방식을 코드로 변환하면 다음과 같습니다.

 

BatchMeterUsageRequest → BatchMeterUsage API 호출하기 

마지막 단계는 API를 호출하면 됩니다. 실패했을 경우를 대비해서 꼼꼼하게 작성하면 됩니다.

/**
 * AWS Marketplace에 AWS 미터링 정보를 전송한다
 */
public void batchMeterUsage(BatchMeterUsageRequest request) throws WhaTapError {
    int retry = 0;
    List<UsageRecord> preprocess = request.getUsageRecords();
    while (retry < conf.aws_marketplace_metring_retry_count) {
        List<UsageRecord> unprocessed = doBatchMeterUsage(request.getProductCode(), preprocess);
        if (unprocessed.isEmpty()) {
            if (conf.debug_aws_marketplace) {
                logger.info("[AWS MP] batchMeterUsage request success. productCode : {}", request.getProductCode());
            }
            return;
        }
        preprocess = unprocessed;
        retry++;
    }
    // retry도 실패한 경우
    int unprocessed = preprocess.size();
    if (unprocessed > 0) {
        String productCode = request.getProductCode();
        for (UsageRecord usageRecord : request.getUsageRecords()) {
            Date timestamp = usageRecord.getTimestamp();
            String dimension = usageRecord.getDimension();
            Integer quantity = usageRecord.getQuantity();
            logger.error("[AWS MP] batchMeterUsage request failed. productCode : {}, timestamp : {}, dimension : {}, quantity : {}",
                    productCode, timestamp, dimension, quantity);
        }
        preprocess.forEach(record -> logger.error(record.toString()));
    }
}

/**
 * @param awsProductCode AWS Marketplace 상품 코드
 * @param usageRecords 전송해야하는 미터링 정보
 * @return 처리에 실패한 미터링 정보, retry 처리되어야 한다
 */
private List<UsageRecord> doBatchMeterUsage(String awsProductCode, List<UsageRecord> usageRecords) throws WhaTapError {
    try {
        BatchMeterUsageRequest request = new BatchMeterUsageRequest()
                .withProductCode(awsProductCode)
                .withUsageRecords(usageRecords);
        BatchMeterUsageResult batchMeterUsageResult = getAwsMarketplaceClient().batchMeterUsage(request);
        for (UsageRecordResult result : batchMeterUsageResult.getResults()) {
            String meteringRecordId = result.getMeteringRecordId();
            String status = result.getStatus();
            UsageRecord usageRecord = result.getUsageRecord();
            Date timestamp = usageRecord.getTimestamp();
            String dimension = usageRecord.getDimension();
            Integer quantity = usageRecord.getQuantity();
            logger.info("[AWS MP] batchMeterUsageResult : status : {}, meteringRecordId : {}, timestamp : {}, dimension : {}, quantity : {}",
                    status, meteringRecordId, timestamp, dimension, quantity);
        }
        return batchMeterUsageResult.getUnprocessedRecords();
    } catch (Exception e) {
        logger.error("[AWS MP] " + e.getMessage());
        return usageRecords;
    }
}

 

 

728x90