Announcing AzAPI Dynamic Properties

This post has been republished via RSS; it originally appeared at: Microsoft Tech Community - Latest Blogs - .

 

It’s been almost two years since the announcement of AzAPI, and the provider has eclipsed 20M+ downloads thanks to all of you. We’re excited to share today that we’ve released v1.13 of AzAPI, which comes with support for dynamic pr While this is not a major release, the features here are significant in impact and thus we want to explain what v1.13 enables, as we believe you’ll find it useful.

 

Our Motivation

 

Previously, AzAPI resource definitions required JSON encoding and decoding. We know this experience was not perfect; plan outputs were not clear, HCL concepts were not supported, and JSON just didn’t feel like Terraform. So we sought out to improve the experience through conversations with you, the community.

In our research, we wanted to ensure a quality authoring experience that didn’t compromise on our promises of AzAPI: consistent resource definitions available from day 0. The top points of concern that we wanted to address were the JSON, clear terraform plan outputs, and exploring simplification of resource definitions.

These goals were part of our larger overarching goal, to make AzAPI a reliable first-class experience like AzureRM, and coexist alongside it as the recommended Terraform provider for latest and greatest functionality.

 

What are dynamic properties?

 

Simply put, dynamic properties enable the AzAPI provider to take a block of HCL instead of requiring JSON. For the below example of AzAPI code, we can see the difference between the first code sample of dynamic properties and second code sample of the old JSONEncode blocks.

resource "azapi_resource" "automationAccount" {
  type      = "Microsoft.Automation/automationAccounts@2023-11-01"
  parent_id = azapi_resource.resourceGroup.id
  name      = "example-automation-account"
  location  = "westeurope"
  body = {
    properties = {
      encryption = {
        keySource = "Microsoft.Automation"
      }
      publicNetworkAccess = true
      sku = {
        name = "Basic"
      }
    }
  }
  response_export_values = ["properties"]
}


resource "azapi_resource" "automationAccount" {
  type      = "Microsoft.Automation/automationAccounts@2023-11-01"
  parent_id = azapi_resource.resourceGroup.id
  name      = "example-automation-account"
  location  = "westeurope"
  body = jsonencode({
    properties = {
      encryption = {
        keySource = "Microsoft.Automation"
      }
      publicNetworkAccess = true
      sku = {
        name = "Basic"
      }
    }
  })
}

As you can see, very little has changed for the actual resource definition; the only change with this automation account is the lack of the jsonencode() function and the response_export_values property. Similarly, for the resulting outputs:

output "o1" {
  value = azapi_resource.automationAccount.output.properties.automationHybridServiceUrl
}

output "o1" {
  value = jsondecode(azapi_resource.automationAccount.output).properties.automationHybridServiceUrl
}

Despite the minute change in resource definition, the impact is significant, as we will see through a variety of scenarios.

 

Scenarios

azapi_update_resource with Dynamic Properties

 

You can validate resource updates by comparing outputs from dynamic properties:

resource "azapi_update_resource" "automationAccount" {
  type        = "Microsoft.Automation/automationAccounts@2023-11-01"
  resource_id = azapi_resource.automationAccount.id
  body = {
    properties = {
      publicNetworkAccess = true
    }
  }
  response_export_values = ["properties.publicNetworkAccess"]
}

output "o1" {
  value = azapi_update_resource.automationAccount.output
}

Note that this will give a block output still as shown below; you can specify output.properties.publicNetworkAccess to just see the Boolean value here.

o1 = {
  "properties" = {
    "publicNetworkAccess" = true
  }
}

azapi_resource_action and Dynamic Properties

 

Resource actions can also display outputs using dynamic properties.

data "azapi_resource_action" "listKeys" {
  type                   = "Microsoft.Automation/automationAccounts@2021-06-22"
  resource_id            = azapi_resource.automationAccount.id
  action                 = "listKeys"
  response_export_values = ["*"]
}

resource "azapi_resource_action" "regenerateKey" {
  type        = "Microsoft.Automation/automationAccounts@2021-06-22"
  resource_id = azapi_resource.automationAccount.id
  action      = "agentRegistrationInformation/regenerateKey"
  body = {
    keyName = "primary"
  }
  depends_on = [
    data.azapi_resource_action.listKeys
  ]
  response_export_values = ["*"]
}

output "o1" {
  value = data.azapi_resource_action.listKeys.output.keys[0].Value
}

output "o2" {
  value = azapi_resource_action.regenerateKey.output.endpoint
}

Managing Secrets with Dynamic Properties

 

You can now see accurate terraform plan outputs when you have sensitive property fields. Before, if a single property within your resource definition was marked sensitive, the entirety of the body would be marked sensitive. The below example (with some code removed for clarity) demonstrates this issue:

 

resource "azapi_resource" "test" {
  type      = "Microsoft.AppPlatform/Spring/storages@2024-01-01-preview"
  name      = "example-storage"
  parent_id = azurerm_spring_cloud_service.test.id

  body = jsonencode({
    properties = {
      accountKey  = azurerm_storage_account.test.primary_access_key
      accountName = azurerm_storage_account.test.name
      storageType = "StorageAccount"
    }
  })
}

The storage account key with JSON encode would produce the following output: Terraform will perform the following actions:

  # azapi_resource.test will be created
  + resource "azapi_resource" "test" {
      + body                      = (sensitive value)
      + id                        = (known after apply)
      + ignore_casing             = false
      + ignore_missing_property   = true
      + name                      = "example-storage"
      + output                    = (known after apply)
      + parent_id                 = "/subscriptions/0000/resourceGroups/example-rg/providers/Microsoft.AppPlatform/spring/example-service"
      + removing_special_chars    = false
      + schema_validation_enabled = true
      + type                      = "Microsoft.AppPlatform/Spring/storages@2024-01-01-preview"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Whereas, replacing the storage here with the dynamic properties configuration:

resource "azapi_resource" "test" {
  type      = "Microsoft.AppPlatform/Spring/storages@2024-01-01-preview"
  name      = "example-storage"
  parent_id = azurerm_spring_cloud_service.test.id

  body = {
    properties = {
      accountKey  = azurerm_storage_account.test.primary_access_key
      accountName = azurerm_storage_account.test.name
      storageType = "StorageAccount"
    }
  }
}

Would yield the following output:

Terraform will perform the following actions:

  # azapi_resource.test will be created
  + resource "azapi_resource" "test" {
      + body                      = {
          + properties = {
              + accountKey  = (sensitive value)
              + accountName = "examplestorageaccount"
              + storageType = "StorageAccount"
            }
        }
      + id                        = (known after apply)
      + ignore_casing             = false
      + ignore_missing_property   = true
      + name                      = "example-storage"
      + output                    = (known after apply)
      + parent_id                 = "/subscriptions/0000/resourceGroups/example-rg/providers/Microsoft.AppPlatform/spring/example-service"
      + removing_special_chars    = false
      + schema_validation_enabled = true
      + type                      = "Microsoft.AppPlatform/Spring/storages@2024-01-01-preview"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Dynamic Blocks and HCL Functions

 

Before dynamic properties, none of the native functionality that was available with HCL could be utilized with the JSON blocks of the AzAPI provider. Dynamic properties can define and return HCL, enabling Terraform HCL functions, like for each loops and lifecycle.ignore_changes. For example, the following is a for_each loop for creating subnets in a VNet:

locals {
  subnets = [
    {
      name          = "subnet1"
      addressPrefix = "10.0.1.0/24"
    },
    {
      name          = "subnet2"
      addressPrefix = "10.0.2.0/24"
    }
  ]
}

resource "azapi_resource" "virtualNetwork" {
  type      = "Microsoft.Network/virtualNetworks@2021-02-01"
  parent_id = azapi_resource.resourceGroup.id
  name      = "example-vnet"
  location  = "westeurope"
  body = {
    properties = {
      addressSpace = {
        addressPrefixes = [
          "10.0.0.0/16",
        ]
      }
      dhcpOptions = {
        dnsServers = [
        ]
      }
      subnets = [
        for subnet in local.subnets : {
          name = subnet.name
          properties = {
            addressPrefix = subnet.addressPrefix
          }
        }
      ]
    }
  }

This is an example for lifecycle_ignore_changes:

resource "azapi_resource" "automationAccount" {
  type      = "Microsoft.Automation/automationAccounts@2023-11-01"
  parent_id = azapi_resource.resourceGroup.id
  name      = "example-account"
  location  = azapi_resource.resourceGroup.location
  body = {
    properties = {
      encryption = {
        keySource = "Microsoft.Automation"
      }
      publicNetworkAccess = true
      sku = {
        name = "Free" // Config: "Free"; Remote: "Basic"
      }
    }
  }
  lifecycle { // Terraform feature
    ignore_changes = [
      body.properties.sku.name
    ]
  }
}

Looking Ahead

 

We’re continuing to actively invest in AzAPI as part of trying to deliver on creating a first-class Terraform experience. Look forward to more exciting features and releases as we continue to improve AzAPI with your feedback and help! Maybe 2.0 is on the horizon...

 

How to Upgrade

 

Without any breaking changes

 

To avoid any breaking changes with this release as it is technically a minor one, your code should all work simply through terraform init-upgrade. If this is not the case, please open an issue against the provider on its GitHub page.

We would not recommend using v1.13.0, as the release came with inadvertent breaking changes. Please move to v1.13.1 of the provider.

 

With the latest functionality

 

The upgrade to the latest functionality is mostly straightforward. First, run terraform init -upgrade and you’ll automatically get the latest version of AzAPI. Remove the jsonencode() and/or jsondecode() from your code. If you have any data sources in your configuration, you’ll need to enable the flag enable_hcl_output_for_data_source to true for the provider:

provider "azapi" {
enable_hcl_output_for_data_source = true
}

This flag is set to false by default to ensure no breaking changes for customers’ existing environments.

 

Note: In previous versions of AzAPI, your terraform state stored the body of the resource as a JSON string. Now, the body will properly store each property as its corresponding HCL type (i.e. object or string). However, this will only convert properly if you run terraform apply after upgrading to v1.13.1 of the provider.

 

Conclusion

 

Make sure to download the latest version of the provider and give it a try! We are excited for you to try this new functionality as we believe that it addresses some of the main concerns that customers shared with us about their challenges with adopting the AzAPI provider. Thank you to all who have given us feedback on the provider, and we look forward to continuing to improve the experience of AzAPI for you.

 

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.