How to build an Azure Function for checking available names across Azure resources

holger

holger

Posted on January 4, 2023

How to build an Azure Function for checking available names across Azure resources

Introduction

In a previous post we've looked at a Python script to query available names across Azure resources [5]. The script uses the Azure Resource Graph to find resources with a given name across all subscriptions in scope of the principal that is running the script [6]. Within this post we are going to use an Azure Function to run this script using a Managed Identity that holds the corresponding Reader role against a certain scope [7].

Important

This post contains some code for testing and playing around with Python in Azure Functions.

The code presented here must not be used in production environments since it is lacking important functionality such as security settings, logging, error handling and more. It is only intended for testing and learning.

Resources should be removed right away after testing in order to avoid costs.

Also, the code is leveraging the Python v2 programming model in Azure Functions, which is currently in preview [1].

Building the Azure Infrastructure

We are using terraform and the corresponding azurerm provider to build the Azure infrastructure for the Azure Function App [2]. The terraform code is also located in a GitHub Repository.

See the Terraform documentation if you need details on how to install terraform itself on the system of your choice [9].

Resources required:

  • Storage Account
  • App Service Plan
  • Function App
  • App Insights

Terraform Code

Terraform Provider

File: provider.tf

As for the provider block, only the azurerm provider is required.

terraform {
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
    }
  }
}

provider "azurerm" {
  features {}
}
Enter fullscreen mode Exit fullscreen mode

Resource Group

File resource_group.tf

We will use the West Europe region in this example - however, this can be amended to whatever region suits best. Subsequently created resources will simply refer to the resource group region in order to make sure that the same region is used for all resources.

resource "azurerm_resource_group" "rg" {
  location = "westeurope"
  name     = "{resource-group-name}"
  tags = {
    owner       = "me"
    environment = "test"
  }
}
Enter fullscreen mode Exit fullscreen mode

Storage Account

Files:

Azure Functions require a storage account since they rely on Azure Storage for operations such as managing triggers and logging function executions. See Storage considerations for Azure Functions if more details are required around this topic [3].

resource "azurerm_storage_account" "storage_acct" {
  name                = "{storage-account-name}"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location

  account_kind             = "StorageV2"
  account_tier             = "Standard"
  account_replication_type = "LRS"
  access_tier              = "Hot"

  min_tls_version           = "TLS1_2"
  enable_https_traffic_only = true

  tags = {
    owner       = "me"
    environment = "test"
  }
}
Enter fullscreen mode Exit fullscreen mode

Since I wanted to check the primary access key when required for troubleshooting, I've added it as an output, however, this is not required. If added, it should be marked as sensitive = true for obvious reasons.

output "sas_connection_string" {
  sensitive = true
  value     = azurerm_storage_account.storage_acct.primary_connection_string
}
Enter fullscreen mode Exit fullscreen mode

If we want to put it out to the console, we can use this this terraform command to do so (after it has been deployed successfully):

terraform output sas_connection_string
Enter fullscreen mode Exit fullscreen mode

Application Insights

File: app_insights.tf

For logging purposes we are also deploying App Insights, which will allow us to monitor executions in Azure Functions [4]. This will be helpful for troubleshooting.

resource "azurerm_application_insights" "func_app_insights" {
  name                = "{app-insights-name}"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  application_type    = "other"
}
Enter fullscreen mode Exit fullscreen mode

Function App (and App Service Plan)

Files:

Since we are running Python Code, the App Service Plan must be of os_type = "Linux". Furthermore, in this example we're deploying it in a Consumption Plan [8]. This means it is in the Dynamic tier and we therefore need to use the sku_name = "Y1" configuration in terraform.

As we are using the aforementioned Python v2 programming model in Azure Functions, it is important that we add AzureWebJobsFeatureFlags = "EnableWorkerIndexing" to the app_settings section. Additionally, we need ENABLE_ORYX_BUILD = true and SCM_DO_BUILD_DURING_DEPLOYMENT = true - otherwise we won't be able to use the Remote build feature [10].

resource "azurerm_service_plan" "consumption_plan" {
  name                = "{service-plan-name}"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  os_type             = "Linux"
  sku_name            = "Y1"
}

resource "azurerm_linux_function_app" "naming_func" {
  name                       = "{function-app-name}"
  location                   = azurerm_resource_group.rg.location
  resource_group_name        = azurerm_resource_group.rg.name
  service_plan_id            = azurerm_service_plan.consumption_plan.id
  storage_account_name       = azurerm_storage_account.storage_acct.name
  storage_account_access_key = azurerm_storage_account.storage_acct.primary_access_key

  identity {
    type = "SystemAssigned"
  }

  app_settings = {
    ENABLE_ORYX_BUILD              = true
    SCM_DO_BUILD_DURING_DEPLOYMENT = true
    AzureWebJobsFeatureFlags       = "EnableWorkerIndexing"
    APPINSIGHTS_INSTRUMENTATIONKEY = azurerm_application_insights.func_app_insights.instrumentation_key
  }

  site_config {
    application_stack {
      python_version = "3.9"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Lastly, it is required to assign the Function Apps Managed Identity to a scope. For testing, I'm adding it as a reader role to the resource group scope, however, if we wanted to check for available names across multiple subscriptions, it would need to be added to the corresponding subscription or management group scopes.

resource "azurerm_role_assignment" "func_mi_assignment" {
  scope                = azurerm_resource_group.rg.id
  role_definition_name = "Reader"
  principal_id         = azurerm_linux_function_app.naming_func.identity[0].principal_id
}
Enter fullscreen mode Exit fullscreen mode

Deployment

We can now run terraform init, terraform plan and terraform deploy. If all went well, we would have terraform confirm the successful deployment.

Terraform Output

Using the Azure CLI, we can also confirm that the resources exist as expected.

az resource list --resource-group {resource-group-name} --output table
Enter fullscreen mode Exit fullscreen mode

Azure Resources List

Creating the Function App

Modifying the Code

File: function_app.py

First of all, we need to make sure that the original Python script would be able to run within the Azure Function as there are a few things that are different now.

Managed Identity

When running the script locally, we were simply using the AzureCliCredential class from the azure.identity library, which would use the context of the user that is locally logged on through the Azure CLI. Obviously, this won't work with a managed identity. Fortunately, this part can be easily replaced through a different class from the same library: the DefaultAzureCredential Class [11].

We would therefore import the following libraries:

from azure.identity import DefaultAzureCredential
from azure.mgmt.resource import SubscriptionClient
import azure.mgmt.resourcegraph as arg
import json

import azure.functions as func
Enter fullscreen mode Exit fullscreen mode

import azure.functions as func is required for classes specific to Function Apps and we are also adding import json as we want to return json values to the caller of the Function App.

We can then just add the authentication part like this and as we were using the credential variable already in the original code, nothign else needs to change from an authentication perspective.

# Authenticate
credential = DefaultAzureCredential()
Enter fullscreen mode Exit fullscreen mode

Python Functions Declaration

In the original script, the code was wrapped around these two functions, which can remain unchanged:

def resource_graph_query( query ):
    # Get your credentials from Azure CLI (development only!) and get your subscription list
    subs_client = SubscriptionClient(credential)
    subscriptions_dict = []

    for subscription in subs_client.subscriptions.list():
        subscriptions_dict.append(subscription.as_dict())

    subscription_ids_dict = []

    for subscription in subscriptions_dict:
        subscription_ids_dict.append(subscription.get('subscription_id'))

    # Create Azure Resource Graph client and set options
    resource_graph_client = arg.ResourceGraphClient(credential)
    resource_graph_query_options = arg.models.QueryRequestOptions(result_format="objectArray")

    # Create query
    resource_graph_query = arg.models.QueryRequest(subscriptions=subscription_ids_dict, query=query, options=resource_graph_query_options)

    # Run query
    resource_graph_query_results = resource_graph_client.resources(resource_graph_query)

    # Show Python object
    return resource_graph_query_results

def check_name_availability(resource_name, resource_type=None):

    if(resource_type):
        rg_query = f"Resources | where name =~ '{resource_name}' | where type =~ '{resource_type}'"
    else:
        rg_query = f"Resources | where name =~ '{resource_name}'"


    rg_results = resource_graph_query(rg_query)

    results_dict = []

    if(rg_results.data):
        availability = False
    else:
        availability = True

    results_dict = dict({
        'resource_name': resource_name,
        'available': availability
    })

    return results_dict
Enter fullscreen mode Exit fullscreen mode

Code specific to the Function App

The following code had to be modified to suit the Function App.
We are basically defining the name and the route:

app = func.FunctionApp()

@app.function_name(name="CheckNameAvailability")
@app.route(route="checkNameAvailability")
Enter fullscreen mode Exit fullscreen mode

This will create the Function App with a name of CheckNameAvailability and the route, which defines the URL - in the case of above example this is https://{function-app-name}.azurewebsites.net/api/checknameavailability.

Lastly, we need the main function containing the code. The function accepts two parameters, resourceName and resourceType, which are added to the corresponding variables r_name and r_type. These can be passed as parameters or json payload. Then we do check whether only resourceName or bothm resourceName and resourceType, or none of these are populated and return the corresponding result to the caller.

def main(req: func.HttpRequest) -> func.HttpResponse:
    r_name = req.params.get('resourceName')
    r_type = req.params.get('resourceType')
    if not r_name:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            r_name = req_body.get('resourceName')

    if not r_type:
        try:
            req_body = req.get_json()
        except ValueError:
            pass
        else:
            r_type = req_body.get('resourceType')

    if r_name and r_type:
        result = check_name_availability(resource_name=r_name, resource_type=r_type)
        result_as_json = json.dumps(result)
        return func.HttpResponse(result_as_json)
    elif r_name:
        result = check_name_availability(resource_name=r_name)
        result_as_json = json.dumps(result)
        return func.HttpResponse(result_as_json)
    else:
        return func.HttpResponse(
            "This HTTP triggered function executed successfully. Pass a resourceName in the query string or in the request body for a proper response.",
            status_code=200
        ) 
Enter fullscreen mode Exit fullscreen mode

Creating the Function Locally

For creating the function locally, we need to make sure the appropriate prerequisites are met [12].
We can then following along with the Azure Function documentation on creating a local function project [13].

func init CheckNameAvailability --python -m V2
Enter fullscreen mode Exit fullscreen mode

This will create a new folder along with files required for the Function App.

Create Local Function

Add the Code

Files:

We can then add the previously created code into the function_app.py file. Furthermore, the requirements.txt file needs to be populated since we are using the remote build feature. A sample can be found in here, that includes the following:

# Do not include azure-functions-worker in this file
# The Python Worker is managed by the Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues

azure-functions
azure-common
azure-core
azure-identity
azure-mgmt-core
azure-mgmt-resource
azure-mgmt-resourcegraph
certifi
cffi
charset-normalizer
cryptography
idna
isodate
msal
msal-extensions
msrest
oauthlib
portalocker
pycparser
PyJWT
requests
requests-oauthlib
six
typing-extensions
urllib3
Enter fullscreen mode Exit fullscreen mode

Publishing the Function

For publishing the code to our Azure Function App, we can run the following command in order to trigger the remote build:

func azure functionapp publish {function-app-name}
Enter fullscreen mode Exit fullscreen mode

After some time, we should see the notification Remote build succeeded! and the Syncing triggers... is successfully displaying the new function URL. If this is running into a timeout, then there might be missing dependencies in the requirements.txt or another issue with the code.

Successful Function Deployment

Testing the Functionality

In order to test the Function App, we would need to know the Function Key. We could either look it up through the portal or use the command line:

func azure functionapp list-functions {function-app-name} --show-keys
Enter fullscreen mode Exit fullscreen mode

Also from the command line, we can use curl to test the newly created function:

curl -X POST https://{function-app-name}.azurewebsites.net/api/checknameavailability \
     -H 'Content-Type: application/json' \
     -H 'x-functions-key: {FUNCTION-KEY}' \
     -d '{"resourceName": "{storage-account-name}", "resourceType": "Microsoft.Storage/storageAccounts"}' | jq
Enter fullscreen mode Exit fullscreen mode

In the case of above example, the result should be false, since I used the same name for the Function Apps Storage Account.

{
  "resource_name": "{storage-account-name}",
  "available": false
}
Enter fullscreen mode Exit fullscreen mode

If we tried with a different name, it should return true:

curl -X POST https://{function-app-name}.azurewebsites.net/api/checknameavailability \
     -H 'Content-Type: application/json' \
     -H 'x-functions-key: {FUNCTION-KEY}' \
     -d '{"resourceName": "{unused-storage-account-name}", "resourceType": "Microsoft.Storage/storageAccounts"}' | jq
Enter fullscreen mode Exit fullscreen mode
{
  "resource_name": "{unused-storage-account-name}",
  "available": true
}
Enter fullscreen mode Exit fullscreen mode

Cleaning Up

After successful learning and testing, we can clean everything up by running terraform destroy and remove the previously created resources.

References

# Title URL Accessed-On
1 Azure Functions Python developer guide https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-python 2023-01-04
2 Terraform: azurerm https://registry.terraform.io/providers/hashicorp/azurerm/3.37.0 2023-01-04
3 Storage considerations for Azure Functions https://learn.microsoft.com/en-us/azure/azure-functions/storage-considerations 2023-01-04
4 Monitor executions in Azure Functions https://learn.microsoft.com/en-us/azure/azure-functions/functions-monitoring 2023-01-04
5 Azure SDK for Python - How to check for Available Resource Names https://dev.to/holger/azure-sdk-for-python-how-to-check-for-available-resource-names-1lg5 2023-01-04
6 What is Azure Resource Graph? https://learn.microsoft.com/en-us/azure/governance/resource-graph/overview 2023-01-04
7 What are managed identities for Azure resources? https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview 2023-01-04
8 Azure Functions Consumption plan hosting https://learn.microsoft.com/en-us/azure/azure-functions/consumption-plan 2023-01-04
9 Install Terraform https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli 2023-01-04
10 Azure Functions - Remote build https://learn.microsoft.com/en-us/azure/azure-functions/functions-deployment-technologies#remote-build 2023-01-04
11 DefaultAzureCredential Class https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.defaultazurecredential 2023-01-04
12 Quickstart: Create a Python function in Azure from the command line - Prerequisite check https://learn.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-python?pivots=python-mode-decorators&tabs=azure-cli%2Cbash#prerequisite-check 2023-01-04
13 Quickstart: Create a Python function in Azure from the command line - Create a local function project https://learn.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-python?pivots=python-mode-decorators&tabs=azure-cli%2Cbash#create-a-local-function-project 2023-01-04
💖 💪 🙅 🚩
holger
holger

Posted on January 4, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related