Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
boto3>=1.35.0
# LicenseArn-based (Concurrent Agreements) metering requires boto3 >= 1.42.58.
boto3>=1.42.58
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ Resources:
PointInTimeRecoveryEnabled: true
SSESpecification:
SSEEnabled: true
# Expose a change stream so a SELLER can consume subscriptionStatus transitions
# (-> deprovisioning / inactive) for their OWN tenant-teardown workflow.
# NEW_AND_OLD_IMAGES lets the consumer see the transition itself.
# This is for the seller's SaaS-side teardown only; the metering flush/finalization
# is already handled by the deployed pipeline.
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
Tags:
- Key: ManagedBy
Value: marketplace-metering-skill
Expand Down Expand Up @@ -633,6 +640,13 @@ Outputs:
SubscribersTableArn:
Description: ARN of the unified subscribers table
Value: !GetAtt SubscribersTable.Arn
SubscribersTableStreamArn:
Description: >-
DynamoDB Stream ARN of the subscribers table. A SELLER MAY attach their OWN
consumer (Lambda event source mapping, Kinesis adapter, etc.) to drive their
SaaS-side tenant teardown on the subscriptionStatus -> deprovisioning/inactive
transition. Independent of the metering pipeline.
Value: !GetAtt SubscribersTable.StreamArn
SubscriptionQueueArn:
Value: !GetAtt SubscriptionQueue.Arn
DLQUrl:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1764,7 +1764,7 @@ Resources:
"title": "BatchMeterUsage exceptions by type (request-level, usage NOT billed)",
"region": "${AWS::Region}", "period": 3600, "view": "timeSeries",
"metrics": [
[ { "expression": "SELECT SUM(BatchMeterUsageException) FROM \"AwsMarketplace/Metering\" WHERE ProductCode = '${ProductCode}' GROUP BY Exception ORDER BY SUM() DESC", "label": "by exception", "id": "x1", "period": 3600 } ]
[ { "expression": "SELECT SUM(BatchMeterUsageException) FROM \"AwsMarketplace/Metering\" WHERE ProductCode = '${ProductCode}' GROUP BY \"Exception\" ORDER BY SUM() DESC", "label": "by exception", "id": "x1", "period": 3600 } ]
],
"yAxis": {"left": {"min": 0}}
}
Expand Down Expand Up @@ -1818,7 +1818,7 @@ Resources:
"title": "BatchMeterUsage exceptions by type (request-level, usage NOT billed)",
"region": "${AWS::Region}", "period": 3600, "view": "timeSeries",
"metrics": [
[ { "expression": "SELECT SUM(BatchMeterUsageException) FROM \"AwsMarketplace/Metering\" WHERE ProductCode = '${ProductCode}' GROUP BY Exception ORDER BY SUM() DESC", "label": "by exception", "id": "x1", "period": 3600 } ]
[ { "expression": "SELECT SUM(BatchMeterUsageException) FROM \"AwsMarketplace/Metering\" WHERE ProductCode = '${ProductCode}' GROUP BY \"Exception\" ORDER BY SUM() DESC", "label": "by exception", "id": "x1", "period": 3600 } ]
],
"yAxis": {"left": {"min": 0}}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,39 @@ metering Subscription Lambda. References:
- [Managing SaaS subscription events with Amazon EventBridge](https://docs.aws.amazon.com/marketplace/latest/userguide/saas-eventbridge-integration.html)
- [When a SaaS subscription ends / is cancelled](https://docs.aws.amazon.com/marketplace/latest/userguide/saas-subscriptions.html)

### Deprovisioning — the seller's own tenant-teardown workflow

The deployed pipeline handles the metering side of deprovisioning automatically — on `License
Deprovisioned` it flushes the license's remaining usage within the ~1-hour window (bypassing
`MeteringLockHours`) and the events-stack `deprovision-cleanup` Lambda finalizes
`subscriptionStatus = inactive` once `deprovisioningExpiry` passes. But **tearing down the
seller's OWN tenant** (revoking the buyer's access, releasing per-tenant resources, CRM/billing
updates) is the SELLER's application responsibility — the skill does not own the tenant model
(mapping `licenseArn`/`customerAWSAccountId` → the seller's internal tenant ID is the seller's,
see the tenant-mapping note above). There are TWO supported ways for the seller to react, and
the seller may choose either or both:

1. **Listen to the Marketplace deprovisioning EventBridge event DIRECTLY.** Add your OWN
EventBridge rule in `us-east-1` matching `source: "aws.agreement-marketplace"` /
`detail-type: "License Deprovisioned"` (and/or `"Purchase Agreement Ended"`), routed to your
own target (Lambda/SQS/Step Functions) that begins tenant teardown. This is INDEPENDENT of
the metering Subscription Lambda — add your own rule/target; **do NOT** bolt teardown logic
onto the metering path (same pattern as the `Purchase Agreement Created` use cases above).
*Trade-off:* reacts fastest, closest to the source (can act before the flush window even
matters), but you own event-shape handling and idempotency.
2. **Consume the subscribers-table DynamoDB Stream.** The events stack ENABLES a DynamoDB Stream
(`NEW_AND_OLD_IMAGES`) on the PII-free `us-east-1` subscribers table and publishes its ARN as
the `SubscribersTableStreamArn` stack output; attach your OWN consumer (a Lambda event source
mapping, Kinesis adapter, etc.) and react when a row transitions to
`subscriptionStatus = deprovisioning` (and later `inactive`). *Trade-off:* you react to the
ALREADY-RECONCILED, de-duplicated, region-collapsed state the Subscription Lambda maintains
(simpler, no event-shape parsing), but it is one hop behind the raw event. Natural when your
tenant model is already keyed off the subscriber record.

Choose based on how time-critical your teardown is relative to the ~1-hour flush window. Either
way the metering flush/finalization is already handled by the deployed pipeline — these hooks
drive only the seller's SaaS-side actions.

> **Key insight:** The subscribers table lives in `us-east-1` alongside EventBridge. When the metering region is a different region, the register Lambda and the metering pipeline (discoverer/submitter) read it cross-region. **If the seller's metering region is also `us-east-1`, the events stack and main stack are both in `us-east-1` and everything is single-region — no cross-region reads at all.** us-east-1 is a fully supported metering region for `BatchMeterUsage`.
>
> **Multi-region sellers:** Each metering region gets its own main stack (usage table + Lambdas). All regions share the single subscribers table in `us-east-1`. Deploy the events stack once (us-east-1), then deploy the main stack to each metering region — which MAY include us-east-1 itself.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Every raw usage row that the pipeline finishes with reaches exactly one terminal
| `RejectedClientSide` | aggregator | the specific reason code (see below) | Row's group failed a client-side guardrail and was never sent. |
| `AggregationExpired` | discoverer (age-out) | — | Row aged past the 24h window BEFORE it could be aggregated (lost before aggregation). |

> This is the RAW table's domain. The `awsmp-aggregated-usage` table has its OWN, disjoint status domain for the SUBMISSION stage — `Success` / `DuplicateRecord` / `CustomerNotSubscribed` (the per-record `BatchMeterUsage` `Results` statuses) / `RejectedClientSide` (a request-level `BatchMeterUsage` exception isolated to the record — reason is the exception name, e.g. `InvalidUsageDimensionException` or `InvalidLicenseException`; NOT a per-record status) / `SubmissionExpired` (aggregated but not submitted before the window/grace closed). "Expired" is never used un-qualified: raw = `AggregationExpired`, aggregated = `SubmissionExpired`.
> This is the RAW table's domain. The `awsmp-aggregated-usage` table has its OWN, disjoint status domain for the SUBMISSION stage — `Success` / `DuplicateRecord` / `CustomerNotSubscribed` (the per-record `BatchMeterUsage` `Results` statuses) / `Error` (a request-level SERVER-SIDE `BatchMeterUsage` exception isolated to the record — the exception name, e.g. `InvalidUsageDimensionException` or `InvalidLicenseException`, is kept in `meteringStatusReason`; NOT a per-record status and NOT `RejectedClientSide`) / `SubmissionExpired` (aggregated but not submitted before the window/grace closed). **`RejectedClientSide` NEVER appears on the aggregated table** — it is a RAW-table-only status for a client-side writer-contract violation; a server-side submit failure is `Error` (fix the catalog dimension / license / offer, not the writer). "Expired" is never used un-qualified: raw = `AggregationExpired`, aggregated = `SubmissionExpired`.

The SUBMISSION outcome (`meteringRecordId`, and the `Success`/`CustomerNotSubscribed`/…
`BatchMeterUsage` status) is NOT written to the raw row — it lives on the
Expand Down
Loading
Loading