Manually submitting values to the Azure Metering API

Requirements

In this script, we're using curl and jq. curl certainly is on your system, for jq, you might need to fetch it:

#!/bin/bash

curl --silent \
     --url https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 \
     --location \
     --output jq && \
chmod +x jq && \
sudo mv jq /usr/local/bin && \
sudo chown root.root /usr/local/bin/jq

Credentials

I assume you have 2 service principal credentials in the publisher/ISV AAD tenant:

  1. We need a management credential which is authorized to manage customer deployments, using with the ARM API.

    • I created an AAD group, called managed-app-admins, and made the service principal a member of that group.

    • In Partner Center -> Offer -> Plan -> Technical Configuration -> Authorizations, I granted the AAD group Owner of managed apps.

    • As a result, the isv_management_cliend_id below can interact with managed apps.

    • So 8cdea44d-38fe-4e3c-bf2a-6b96809d1d27 above is the object ID of the group, and the service principal with app id 8eff18b7-aeeb-4a88-b19c-bf60813a587c is member of that group.

  2. We need a metering credential, which is authorized to submit usage to the metering API.

    • We do not use an identity in the managed app, i.e. we do not SSH/RDP into a VM in the managed resource group to do our work there. Having a VM just for that is too complicated (and expensive).

    • This isv_metering_client_id (app id) service principal is configured under Partner Center -> Offer -> Technical Configuration:

The script

#!/bin/bash

# This is the ISV/publisher app which is also in the partner center
# All of these 3 below work (for me)
isv_tenant_id="5f9e748d-300b-48f1-85f5-3aa96d6260cb" 
isv_tenant_id="geuerpollmannde.onmicrosoft.com" 
isv_tenant_id="geuer-pollmann.de" 

# This service principal on the ISV AAD tenant has permissions to manage "managed apps" in customer environments
isv_management_client_id="8eff18b7-aeeb-4a88-b19c-bf60813a587c"
isv_management_client_secret="$( cat "/mnt/c/Users/chgeuer/.secrets/8eff18b7-aeeb-4a88-b19c-bf60813a587c-admin.txt" )"

# This service principal is the one that is configured in partner center, under the offer's technical plan
isv_metering_client_id="3eb78160-e434-4713-8374-f40174d64348"
isv_metering_client_secret="$( cat "/mnt/c/Users/chgeuer/.secrets/3eb78160-e434-4713-8374-f40174d64348-metering.txt" )"
isv_metering_client_id="${AZURE_METERING_MARKETPLACE_CLIENT_ID}"
isv_metering_client_secret="=${AZURE_METERING_MARKETPLACE_CLIENT_SECRET}"

#
# You must now what you want to submit:
#
# Something like "2023-01-27T15:00:00Z". I'm always submitting for the top of the hour
# timestamp="$( date --utc --date='-20 hour' '+%Y-%m-%dT%H:00:00Z' )" 
timestamp="$( date --utc '+%Y-%m-%dT%H:00:00Z' )" # Current hour
timestamp="2023-01-27T15:00:00Z" # Specific hour
echo "${timestamp}"

planName="plan1"
dimensionName="dimension-payg"
quantity=5

# We now need to determine the resourceUri of the of the managed app (in the customer subscription). When submitting to the metering API, your JSON body must either contain 
# a resourceUri, or a resourceId, or both. In case of a managed app, resourceUri is the way to go.
#
# Please check my blog articles, one of those
# - https://cookbook.geuer-pollmann.de/azure/marketplace-metering-ids
# - https://techcommunity.microsoft.com/t5/fasttrack-for-azure/azure-marketplace-metered-billing-picking-the-correct-id-when/ba-p/3542373
#
# The resourceUri we want to determine is the `managedBy` property of the managed resource group. In this sample, we go the long way, i.e. talk to ARM 
# on customer side. We query the **managed resource group**, using our management credential.
#
# The value we want to retrieve looks like this:
#     "/subscriptions/724467b5-bee4-484b-bf13-d6a5505d2b51/resourceGroups/managed-app-resourcegroup/providers/Microsoft.Solutions/applications/chgp20230118"
#
customer_subscription_id="724467b5-bee4-484b-bf13-d6a5505d2b51"
customer_managed_resource_group_that_contains_the_resources="mrg-chgp20230118"

# As the ISV, grab an access token for the ARM API
management_access_token="$( curl \
    --silent \
    --request POST \
    --url "https://login.microsoftonline.com/${isv_tenant_id}/oauth2/v2.0/token" \
    --data-urlencode "response_type=token" \
    --data-urlencode "grant_type=client_credentials" \
    --data-urlencode "client_id=${isv_management_client_id}" \
    --data-urlencode "client_secret=${isv_management_client_secret}" \
    --data-urlencode "scope=https://management.azure.com/.default" \
    | jq -r ".access_token" )"

# Look at the token contents, if you like
echo "${management_access_token}" | jq -R 'split(".") | .[1] | @base64d | fromjson'

# managedBy="/subscriptions/724467b5-bee4-484b-bf13-d6a5505d2b51/resourceGroups/managed-app-resourcegroup/providers/Microsoft.Solutions/applications/chgp20230118"

# Now query the properties of the managed resource group, and grab the managedBy property
managedBy="$( curl --silent --get \
    --url "https://management.azure.com/subscriptions/${customer_subscription_id}/resourcegroups/${customer_managed_resource_group_that_contains_the_resources}" \
     --data-urlencode "api-version=2019-07-01" \
     --header "Authorization: Bearer ${management_access_token}" \
     | jq -r '.managedBy' )"

echo "managedBy: ${managedBy}"

# Now we can compose the JSON body for the marketplace call.
# As you can see, the managedBy value goes as resourceUri.
#
meteringPayloadJson="$( echo "{}"                            | \
   jq --arg x "${managedBy}"     '.resourceUri=$x'           | \
   jq --arg x "${planName}"      '.planId=$x'                | \
   jq --arg x "${dimensionName}" '.dimension=$x'             | \
   jq --arg x "${quantity}"      '.quantity=($x | fromjson)' | \
   jq --arg x "${timestamp}"     '.effectiveStartTime=$x'      \
   )"

echo "${meteringPayloadJson}" > meteringPayload.json

# Now use the ISV's metering credential (the one in partner center), to fetch a token to talk to the metering API.
# The ID 20e940b3-4c77-4b0b-9a53-9e16a1b010a7 is effectively the metering API.
#
isv_metering_access_token="$( curl                                          \
   --silent                                                                \
   --request POST                                                          \
   --url "https://login.microsoftonline.com/${isv_tenant_id}/oauth2/token" \
   --data-urlencode "response_type=token"                                  \
   --data-urlencode "grant_type=client_credentials"                        \
   --data-urlencode "client_id=${isv_metering_client_id}"                  \
   --data-urlencode "client_secret=${isv_metering_client_secret}"          \
   --data-urlencode "resource=20e940b3-4c77-4b0b-9a53-9e16a1b010a7"        \
   | jq -r ".access_token" )"

# Look at the token's content.
echo "${isv_metering_access_token}" | jq -R 'split(".") | .[1] | @base64d | fromjson' > isv_metering_access_token.json

# Now finally send the data to Azure Marketplace...
marketplace_response="$( curl \
   --silent \
   --request POST \
   --url "https://marketplaceapi.microsoft.com/api/usageEvent?api-version=2018-08-31" \
   --header "Authorization: Bearer ${isv_metering_access_token}" \
   --header "Content-Type: application/json" \
   --data "${meteringPayloadJson}" )" 
   
echo "${marketplace_response}" | jq . > marketplace_response.json

So the initial metering request looks like this:

POST https://saasapi.azure.com/api/batchUsageEvent?api-version=2018-08-31 HTTP/1.1
Host: saasapi.azure.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyIsImtpZCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyJ9.eyJhdWQiOiIyMGU5NDBiMy00Yzc3LTRiMGItOWE1My05ZTE2YTFiMDEwYTciLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC81ZjllNzQ4ZC0zMDBiLTQ4ZjEtODVmNS0zYWE5NmQ2MjYwY2IvIiwiaWF0IjoxNjc0ODQ2MzAxLCJuYmYiOjE2NzQ4NDYzMDEsImV4cCI6MTY3NDg1MDIwMSwiYWlvIjoiRTJaZ1lJaXkvSmtwMWlreFFmaGhzNDdWMFU5Y0FBPT0iLCJhcHBpZCI6IjNlYjc4MTYwLWU0MzQtNDcxMy04Mzc0LWY0MDE3NGQ2NDM0OCIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzVmOWU3NDhkLTMwMGItNDhmMS04NWY1LTNhYTk2ZDYyNjBjYi8iLCJvaWQiOiIyZTgyMjA4ZS1lYWQ5LTQwMTYtOTY1OC03NTM4NTUxYTE0MWYiLCJyaCI6IjAuQVJFQWpYU2VYd3N3OFVpRjlUcXBiV0pneTdOQTZTQjNUQXRMbWxPZUZxR3dFS2NSQUFBLiIsInN1YiI6IjJlODIyMDhlLWVhZDktNDAxNi05NjU4LTc1Mzg1NTFhMTQxZiIsInRpZCI6IjVmOWU3NDhkLTMwMGItNDhmMS04NWY1LTNhYTk2ZDYyNjBjYiIsInV0aSI6Ik5XdVVOWE5uOWtPWUtRQkZUMkV1QUEiLCJ2ZXIiOiIxLjAifQ.ecjxnxfuckoffsignaturetOtG8kuXyMr0uEExxxx
Content-Type: application/json; charset=utf-8
Content-Length: 285

{
   "resourceUri":"/subscriptions/724467b5-bee4-484b-bf13-d6a5505d2b51/resourceGroups/managed-app-resourcegroup/providers/microsoft.solutions/applications/chgp20230118",
   "effectiveStartTime":"2023-01-27T18:00:00Z",
   "planId":"plan1",
   "dimension":"dimension-payg",
   "quantity":5.0
}

HTTP/1.1 200 OK
Content-Length: 464
Content-Type: application/json; charset=utf-8
Date: Fri, 27 Jan 2023 19:10:03 GMT
mise-correlation-id: 505bdb68-7806-4ab7-9b1c-12bee2a3b5b8
x-ms-requestid: fa209de8-f599-4b49-9a25-fc48c3920fd9
x-ms-correlationid: fa209de8-f599-4b49-9a25-fc48c3920fd9

{
   "usageEventId":"5e1876e2-8b31-48fe-8810-864aeb67625b",
   "status":"Accepted",
   "messageTime":"2023-01-27T19:10:02.527833Z",
   "resourceId":"ffb5220f-0876-492c-b63b-26b73a1ad74f",
   "resourceUri":"/subscriptions/724467b5-bee4-484b-bf13-d6a5505d2b51/resourceGroups/managed-app-resourcegroup/providers/microsoft.solutions/applications/chgp20230118",
   "quantity":5.0,
   "dimension":"dimension-payg",
   "effectiveStartTime":"2023-01-27T18:00:00Z",
   "planId":"plan1"
}

You can see the "status":"Accepted", and a Microsoft-issued "usageEventId".

If you try to emit the same usage a 2nd time, you'll get a different response:

{
  "message": "This usage event already exist.",
  "code": "Conflict",
  "additionalInfo": {
    "acceptedMessage": {
      "usageEventId": "5e1876e2-8b31-48fe-8810-864aeb67625b",
      "status": "Duplicate",
      "messageTime": "2023-01-27T19:10:02.527833Z",
      "resourceId": "ffb5220f-0876-492c-b63b-26b73a1ad74f",
      "resourceUri": "/subscriptions/724467b5-bee4-484b-bf13-d6a5505d2b51/resourceGroups/managed-app-resourcegroup/providers/microsoft.solutions/applications/chgp20230118",
      "quantity": 5.0,
      "dimension": "dimension-payg",
      "effectiveStartTime": "2023-01-27T18:00:00Z",
      "planId": "plan1"
    }
  }
}

Marketplace here tells us that the submission is Conflict / Duplicate of a previously submitted event.

  • "Azure Marketplace Metered Billing- Picking the correct ID when submitting usage events": To understand the difference between the resourceUri and resourceId in Azure Marketplace, please read my blog article , either in my private or the official blog.

  • https://docs.microsoft.com/en-us/azure/marketplae/marketplace-metering-service-authentication

  • https://docs.microsoft.com/en-us/azure/marketplace/partner-center-portal/pc-saas-registration#get-the-token-with-an-http-post

Last updated