Provisioning Azure Resources with the azapi Terraform Provider
Are you looking for a way to use Terraform to provision Azure resources that aren’t in the azurerm provider yet? The azapi provider talks directly to the Azure ARM REST API, which means you can manage any resource type the moment it’s available, without waiting on a provider update.
In this post, I’ll walk you through finding the resource type and API version, writing an azapi_resource block, reading outputs, and calling resource actions. I’ll use Azure Monitor Workspace and Azure Managed Grafana as the examples. Together they give you a complete managed observability stack, and both illustrate the pattern well.
How azurerm and azapi fit together
azurerm is a hand-crafted provider. Each resource is explicitly wrapped, which means it’s stable and well-documented, but it takes time after a service launches for support to appear. azapi is a thin wrapper around the ARM REST API. If Azure has a resource type and an API version, azapi can manage it, even if azurerm has no idea it exists.
Both providers work in the same root module and can reference each other’s resources. The practical pattern: use azurerm for everything it covers, and drop down to azapi when you need something it doesn’t.
Finding the resource type and API version
Every ARM resource has a type string in the form {Namespace}/{ResourceType}, and every API call uses a specific version. You need both before you can write an azapi_resource block.
The easiest way to find them is the Azure CLI. Start by listing all resource types in the namespace to find the exact name:
az provider show \
--namespace Microsoft.Monitor \
--query "resourceTypes[].resourceType" \
-o tsv
The JMESPath filter is case-sensitive, so listing first avoids guessing whether the type is accounts, Accounts, or something else. Once you have the exact name, pull the available API versions:
az provider show \
--namespace Microsoft.Monitor \
--query "resourceTypes[?resourceType=='accounts'].apiVersions" \
-o tsv
If you don’t know the namespace, az provider list -o tsv lists everything registered to your subscription.
If you’ve already deployed the resource manually, an ARM template export is even faster. Go to the resource in the portal, click Export template, and pull the type and apiVersion fields directly from the generated JSON.
For Azure Monitor Workspace, the type is Microsoft.Monitor/accounts and the current stable API version is 2023-04-03. The full type string passed to azapi_resource is:
Microsoft.Monitor/accounts@2023-04-03
Provider setup
All examples in this post only need azapi. If you’re managing other Azure resources alongside these, add hashicorp/azurerm to required_providers and configure it the same way.
terraform {
required_providers {
azapi = {
source = "azure/azapi"
}
}
}
provider "azapi" {
subscription_id = var.subscription_id
}
A locals.tf block pins the API version and pre-computes the resource group ID so both are easy to update in one place:
locals {
monitor_api_version = "2023-04-03"
grafana_api_version = "2023-09-01"
rg_id = "/subscriptions/${var.subscription_id}/resourceGroups/${var.resource_group}"
}
Provisioning an Azure Monitor Workspace
The azapi_resource block maps directly to an ARM PUT request. The type field is the resource type plus API version, parent_id is the resource group ID, and body is the request body minus the top-level name, location, and tags fields, which azapi handles separately.
resource "azapi_resource" "monitor_workspace" {
type = "Microsoft.Monitor/accounts@${local.monitor_api_version}"
name = var.workspace_name
location = var.location
parent_id = local.rg_id
tags = var.tags
body = {
properties = {
publicNetworkAccess = "Enabled"
}
}
response_export_values = ["properties.metrics.prometheusQueryEndpoint"]
}
response_export_values controls which fields Terraform retains from the response body. By default, azapi_resource discards everything after create and update. Use ["*"] to keep the full response, or specify JSON paths like the one above to keep the output lean.
schema_validation_enabled defaults to true, which tells azapi to validate your body against a cached schema before sending the request. Set it to false for preview API versions or custom resource providers where the schema may not be cached yet.
Reading output values
Once response_export_values is set, the retained fields are available on .output:
locals {
prometheus_query_endpoint = azapi_resource.monitor_workspace.output.properties.metrics.prometheusQueryEndpoint
}
output "prometheus_query_endpoint" {
value = local.prometheus_query_endpoint
}
Adding Azure Managed Grafana
With the Monitor Workspace in place, you can deploy Azure Managed Grafana alongside it and link the two at creation time. The grafanaIntegrations property takes the resource ID of your Monitor Workspace and pre-configures it as a Prometheus data source automatically, so no manual data source setup in Grafana is needed.
resource "azapi_resource" "grafana" {
type = "Microsoft.Dashboard/grafana@${local.grafana_api_version}"
name = var.grafana_name
location = var.location
parent_id = local.rg_id
tags = var.tags
identity {
type = "SystemAssigned"
}
body = {
sku = {
name = "Standard"
}
properties = {
grafanaMajorVersion = "10"
publicNetworkAccess = "Enabled"
grafanaIntegrations = {
azureMonitorWorkspaceIntegrations = [
{
azureMonitorWorkspaceResourceId = azapi_resource.monitor_workspace.id
}
]
}
}
}
response_export_values = ["properties.endpoint"]
}
Two things about this block:
- The
identityblock is a top-level attribute onazapi_resource, not insidebody. Grafana uses the system-assigned managed identity to authenticate against the linked Monitor Workspace. skusits at the top level of the ARM request body alongsideproperties, which is why it’s insidebodybut not nested underproperties.
The Grafana endpoint comes out of the response the same way as the workspace query endpoint:
locals {
prometheus_query_endpoint = azapi_resource.monitor_workspace.output.properties.metrics.prometheusQueryEndpoint
grafana_endpoint = azapi_resource.grafana.output.properties.endpoint
}
output "prometheus_query_endpoint" {
value = local.prometheus_query_endpoint
}
output "grafana_endpoint" {
value = local.grafana_endpoint
}
After terraform apply, open the Grafana URL and sign in with your Azure credentials. The Monitor Workspace will already be listed as a Prometheus data source.
The complete, runnable code for both resources is in static/code/2026/azapi-terraform/. Copy terraform.auto.tfvars.example to terraform.auto.tfvars, fill in your subscription ID and resource names, and run terraform init && terraform apply.
Calling resource actions
Some ARM resources expose custom POST actions beyond the standard CRUD operations: listing connection strings, rotating keys, or fetching connection parameters. azapi_resource_action covers those:
data "azapi_resource_action" "example" {
type = "Some.Namespace/resourceType@{api-version}"
resource_id = azapi_resource.some_resource.id
action = "listKeys"
method = "POST"
response_export_values = ["keys"]
}
The result is available under .output the same way as azapi_resource. In the follow-up post on Everpure Cloud Azure Native, I’ll use this same pattern to call listConnectionParameters on a volume group and retrieve iSCSI endpoints before mounting a volume to a VM.
Timeouts for long-running operations
Most Azure resources provision in under a minute. Some don’t. Storage pools, large managed clusters, and similar resources can take 15 to 90 minutes to create or delete. Terraform’s default operation timeout is 5 minutes, which means it will cancel a still-running operation and leave your state in a bad position.
Add a timeouts block for any resource you know will run long:
resource "azapi_resource" "some_resource" {
# ...
timeouts {
create = "30m"
update = "30m"
delete = "30m"
read = "5m"
}
}
azurerm resources have per-resource defaults baked in. azapi_resource uses a single default for everything, so set timeouts explicitly for resources you know will run long.
What’s next
In the next post, I’ll apply this same pattern to provisioning Everpure Cloud Azure Native resources on Azure. EC Azure Native uses the custom PureStorage.Block resource provider, which isn’t in azurerm, so everything from the storage pool to the VM mount extension goes through azapi.
If you run into anything or have questions, drop a comment below.
comments powered by DisqusSee Also
- Everpure Cloud Dedicated on Azure - Quick Launch
- Using the Everpure Cloud Dedicated Terraform Provider for Azure
- Everpure Cloud Dedicated Use Cases for Microsoft Azure - Terraform Edition
- Deploying a Windows Azure VM with Hashicorp Terraform to Microsoft Azure
- Unlocking Performance Insights: Using resxtop to Monitor Your Azure VMware Solution Environment