ARM template: getting deeply nested resource properties

The other day I was deploying a private endpoint connected to a KeyVault using an ARM Template. By using a Private Endpoint one can assign a private IP address from your own Virtual Network to an Azure PaaS service like KeyVault, SQL, storage accounts, and others. To resolve the private IP using the service FQDN from within the VNET, I also needed to set an ‘A’-record in the private DNS zone.

That means that you need to grab the ip-address from the NIC that’s used by the private endpoint. Here’s the template that I started with:

{
    "type": "Microsoft.KeyVault/vaults",
    "apiVersion": "2019-09-01",
    "name": "[variables('keyVaultName')]",
    "location": "[variables('location')]",
    "properties": {
        "tenantId": "[variables('tenantid')]",
        "sku": {
            "name": "standard",
            "family": "A"
        },
        "enabledForDeployment": false,
        "enabledForDiskEncryption": false,
        "enabledForTemplateDeployment": true,
        "enableSoftDelete": true,
        "accessPolicies": "[variables('accessPolicies')]"
    }
},
{
    "type": "Microsoft.Network/privateEndpoints",
    "apiVersion": "2020-05-01",
    "name": "[variables('privateEndpointName')]",
    "location": "[variables('location')]",
    "dependsOn": [
        "[variables('keyVaultName')]"
    ],
    "properties": {
        "subnet": {
            "id": "[resourceId(parameters('virtualNetworkResourceGroupName'), 'Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]"
        },
        "privateLinkServiceConnections": [
            {
                "name": "[variables('privateEndpointName')]",
                "properties": {
                    "privateLinkServiceId": "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]",
                    "groupIds": [
                        "vault"
                    ]
                }
            }
        ]
    }
},
{
    "type": "Microsoft.Network/privateDnsZones/A",
    "apiVersion": "2018-09-01",
    "name": "[concat('privatelink.vaultcore.azure.net', '/', variables('keyVaultName'))]",
    "properties": {
        "ttl": 30,
        "aRecords": [
            {
                "ipv4Address": "[reference(variables('privateEndpointName'), '2020-05-01', 'Full').networkInterfaces[0].properties.ipConfigurations[0].properties.privateIPAddress]"
            }
        ]
    }
}

This template contains 3 resources. It first deploys the KeyVault, then deploys its private endpoint and then the A record on the Private DNS Zone. That last resource will fail. The reference() function gets runtime information on the Private Endpoint. And although I use the ‘Full’ property, the attached networkInterfaces only return their ‘id’ property. They omit their full sub-properties. My next attempt was to split the resource() method in 2 like this:

"ipv4Address": "[reference(reference(variables('privateEndpointName'), '2020-05-01', 'Full').networkInterfaces[0].id, '2020-05-01', 'Full').properties.ipConfigurations[0].properties.privateIPAddress]"

The first resource() function tries to get the resourceId of the NIC, the second should then use that to get the runtime information on the NIC. That, however, gives the following error:

‘The template function ‘reference’ is not expected at this location’

So, using the reference() function within the reference() function is not allowed.

Nested Templates

The workaround that I used to fix this was to use a nested template. I usually use them to deploy resources to multiple scopes from a single file. What helped me here is that they have this expressionEvaluationOptions setting:

"expressionEvaluationOptions": {
    "scope": "inner"
}

Using that, you get to dictate how functions like resourceId, subscriptionsId, variables, parameters, etc., are being evaluated. The default value is ‘outer’ and means you refer to the main template for the evaluation. Using ‘inner’, you dictate that you only want to be able to point to variables, parameter, resource, etc that are defined within the nested template. You kinda make them private using that option. In this case it allowed me to still use the reference function twice and reach my goal. The DNS ‘A’ record assignment was moved into a nested deployment like so:

{
    "apiVersion": "2017-05-10",
    "name": "DNSARecord",
    "type": "Microsoft.Resources/deployments",
    "resourceGroup": "<resourcegroup-where-the-dns-zone-lives>",
    "subscriptionId": "<subscription-where-the-dns-zone-lives>",
    "dependsOn": [
        "[variables('privateEndpointName')]"
    ],
    "properties": {
        "mode": "Incremental",
        "expressionEvaluationOptions": {
            "scope": "inner"
        },
        "parameters": {
            "recordName": {
                "value": "[variables('keyVaultName')]"
            },
            "NetworkInterfaceId": {
                "value": "[reference(variables('privateEndpointName')).networkInterfaces[0].id]"
            }
        },
        "template": {
            "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json",
            "contentVersion": "1.0.0.0",
            "parameters": {
                "recordName": {
                    "type": "string"
                },
                "NetworkInterfaceId": {
                    "type": "string"
                }
            },
            "resources": [
                {
                    "type": "Microsoft.Network/privateDnsZones/A",
                    "apiVersion": "2018-09-01",
                    "name": "[concat('privatelink.vaultcore.azure.net', '/', parameters('recordName'))]",
                    "properties": {
                        "ttl": 30,
                        "aRecords": [
                            {
                                "ipv4Address": "[reference(parameters('NetworkInterfaceId'), '2020-05-01', 'Full').properties.ipConfigurations[0].properties.privateIPAddress]"
                            }
                        ]
                    }
                }
            ]
        }
    }
}

In this example, you see the first reference() function being used in the parameter section. That will now pass the resouceId of the NIC into the nested template. That can then use that value in the second resource function and grab the actual IP-address!