Azure Resource Manager API calls from Python

Direct API Calls to Azure Resource Manager REST API is useful mostly in two scenarios - when integrating ARM functions in some application and when Portal, CLI, PowerShell or SDK is not enough. Of course there is also a third scenario - when you want to learn yourself how ARM really works.

Azure Resource Manager API

ARM REST API is well documented here. You will find there information about how to prepare for using REST API (i.e. create a Service Principal) and how to perform API Calls. There is also a (almost?) complete API Reference divided by service or resource type - it is where you will search for methods you can use for target resource type.

To start working with API Calls to ARM API, you need to have 5 things and know where to find 6th.

  1. Client ID
  2. Client Secret
  3. Tenant ID
  4. Resource
  5. Authority URL
  6. API version

First five of six above are related directly to oAuth2 flow, where Azure AD is an Identity Provider. The sixth one is a query-string parameter which points an API version to call.

Client ID is an Application ID you created for RBAC (as described here). This is the AAD Application with a Service Principal object related to it.

Client Secret is an AAD Application's key (password).

Tenant ID is your AD Tenant's ID you can find in AAD Properties or in output of the az ad sp create-for-rbac command.

Resource is https://management.azure.com/ - Azure Resource Manager provider APIs URI.

Authority URL is https://login.microsoftonline.com/ - the Identity Provider address.

API version is a query-string parameter with designated API version you should provide for service you are calling. You can find this parameter in API reference under provider of choice - here an example for Resource Management / Resource Groups / List:

API Reference - API version

Let say we want to list all Resource Groups in our subscription:

Find all subscription on the tenant

  1. We need to find proper page in Azure REST API reference: https://docs.microsoft.com/en-us/rest/api/resources/subscriptions/list
  2. We need to determine API Version: 2016-06-01 (from reference page).
  3. We need to determine call's method and URI:

    GET https://management.azure.com/subscriptions?api-version=2016-06-01

As an output we will get a list (JSON) of all subscriptions on our Azure tenant.

Find all resource groups in the subscription of choice

  1. We need to find proper page in Azure REST API reference: https://docs.microsoft.com/en-us/rest/api/resources/resourcegroups/list
  2. We need to determine API Version: 2017-05-10 (from reference page).
  3. We need to determine call's method and URI:

    GET https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups?api-version=2017-05-10

As an output we will get a list (JSON) of all resource groups in our Azure subscription of choice.

Python (3)

Handling GET, HEAD, PUT, POST, and PATCH methods in Python can be implemented using many libraries and in many ways of thinking. The same situation is with oAuth flows. I decided to use requests (link) library for HTTP methods and adal (Link) library for Azure AD Authentication.

I choose requests because I know it and I'm using it. It is also probably the most popular library used for handling HTTP requests.
I choose adal library because it is officially a proper way to handle authentication against AAD in Python. It is also used in Azure CLI 2.0 and Azure SDK for Python.

Beside of requests and adal I will also use json library for handling JSON requests bodies and calls responses and os for os environment variables handling (no credentials hardcoding!).

Only requests and adal libraries requires to be installed:

pip install requests adal

So, the import code block of my Python script will be as follows:

import adal
import requests
import os
import json

Next, we will declare necessary variables, storing their values as environment variables:

tenant = os.environ['TENANT']
authority_url = 'https://login.microsoftonline.com/' + tenant
client_id = os.environ['CLIENTID']
client_secret = os.environ['CLIENTSECRET']
resource = 'https://management.azure.com/'

I'm using venv for Python runtime and PyCharm IDE (link) and both venv and env variables are handled by PyCharm:

PyCharm env variables

PyCharm venv

Next, we are going to handle oAuth flow with adal, receiving an authorization token in an authentication context of https://login.microsoftonline.com/tenant - an identity provider:

context = adal.AuthenticationContext(authority_url)
token = context.acquire_token_with_client_credentials(resource, client_id, client_secret)

We will use token variable to extract a Bearer authorization token from the response which looks like this:

{
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "expiresOn": "2018-02-16 19:55:32.068528",
    "resource": "https://management.azure.com/",
    "accessToken": "eyJ0eXAiOiJKV1QiLC...",
    "isMRRT": true,
    "_clientId": "<client_id>",
    "_authority": "https://login.microsoftonline.com/<tenant>"
}

The accessToken is a JWT - Json Web Token which can be translated on http://jwt.ms/:

JWT token decoded

Try to decode it by yourself - you will notice the correlation between your Application ID and token's content.

Next we are going to start building a request to the ARM API. First, we need a headers:

headers = {'Authorization': 'Bearer ' + token['accessToken'], 'Content-Type': 'application/json'}

Then, we create query-string parameter with API version (2016-06-01 is proper version according to the documentation):

params = {'api-version': '2016-06-01'}

Next, the url for getting the subscriptions list (according to the documentation):

url = 'https://management.azure.com/' + 'subscriptions'

And at last, performing the request and printing the response:

r = requests.get(url, headers=headers, params=params)
print(json.dumps(r.json(), indent=4, separators=(',', ': ')))

The response looks like this:

{
    "value": [
        {
            "id": "/subscriptions/<subscription_id>",
            "subscriptionId": "<subscription_id>",
            "displayName": "<subscription_name",
            "state": "Enabled",
            "subscriptionPolicies": {
                "locationPlacementId": "Public_2014-09-01",
                "quotaId": "Sponsored_2016-01-01",
                "spendingLimit": "Off"
            },
            "authorizationSource": "RoleBased"
        }
    ]
}

And whole script looks like this:

import adal
import requests
import os
import json


tenant = os.environ['TENANT']
authority_url = 'https://login.microsoftonline.com/' + tenant
client_id = os.environ['CLIENTID']
client_secret = os.environ['CLIENTSECRET']
resource = 'https://management.azure.com/'
context = adal.AuthenticationContext(authority_url)
token = context.acquire_token_with_client_credentials(resource, client_id, client_secret)
headers = {'Authorization': 'Bearer ' + token['accessToken'], 'Content-Type': 'application/json'}
params = {'api-version': '2016-06-01'}
url = 'https://management.azure.com/' + 'subscriptions'

r = requests.get(url, headers=headers, params=params)

print(json.dumps(r.json(), indent=4, separators=(',', ': ')))

Of course GET operations are little bit simpler than PUT or POST. There is no request body to send. Let's create some resource group on our subscription (docs):

import adal
import requests
import os
import json


tenant = os.environ['TENANT']
authority_url = 'https://login.microsoftonline.com/' + tenant
client_id = os.environ['CLIENTID']
client_secret = os.environ['CLIENTSECRET']
resource = 'https://management.azure.com/'
context = adal.AuthenticationContext(authority_url)
token = context.acquire_token_with_client_credentials(resource, client_id, client_secret)
headers = {'Authorization': 'Bearer ' + token['accessToken'], 'Content-Type': 'application/json'}
params = {'api-version': '2017-05-10'}
url = 'https://management.azure.com/subscriptions/<subscription_id>/resourcegroups/mytestrg'

data = {'location': 'northeurope'}

r = requests.put(url, data=json.dumps(data), headers=headers, params=params)

print(json.dumps(r.json(), indent=4, separators=(',', ': ')))

Note change in params and url, adding data and method change to requests.put. The response should look like this:

{
    "id": "/subscriptions/<subscription_id>/resourceGroups/mytestrg",
    "name": "mytestrg",
    "location": "northeurope",
    "properties": {
        "provisioningState": "Succeeded"
    }
}
comments powered by Disqus