Search code examples
azure-devopsazure-resource-managerazure-keyvaultazure-managed-identityinfrastructure-as-code

How to create idempotent, re-deployable ARM templates that utilize Key Vault? Circular dependencies present issues


I'm trying to incorporate some additional security features on my resources such as encryption using customer managed keys. For the Service Bus, this requires the Service Bus to have a managed identity created and granted access to the Key Vault. However, the managed identity is not known until after the Service Bus exists.

In my ARM template, I need to initialize the Service Bus without encryption in order to get a managed identity, grant that identity access to key vault, then update the Service Bus with encryption. However, this deployment process is not repeatable. On subsequent re-deployments, it will fail because encryption cannot be removed from a Service Bus once it is granted. Even if it did work, I would be performing unnecessary steps to remove encryption and add it back on every deployment. It seems that an IAC template that describes the expected final state of the resource cannot be used to both maintain and bootstrap a new environment.

The same problem exists for API Management, I want to add custom domains but they require Key Vault access. It means I can't re-deploy my ARM template without removing the custom domains when the init step gets repeated (or keep a separate set of templates for 'initialization' vs the 'real deployment'.

Is there a better solution for this? I looked into user assigned identities which seems like it could solve the problem, but they aren't supported in ARM templates. I checked if there is a way to make the 'init' step conditional by checking if the resource already exists, and this is also not supported via ARM template.


Solution

  • If you want to maintain a single ARM template, you can use nested deployments to define a resource then reference it again to update it.

    In the following example, I create the Service Bus with a system-assigned managed identity, a Key Vault, and an RSA key. In a nested deployment within the same ARM template that depends on that key being generated, I then update the Service Bus to enable encryption. An advantage of being all in the same template is that resourceId and reference can be used with abbreviated syntax (i.e. just the resource name).

    {
        "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
        "contentVersion": "1.0.0.0",
        "parameters": {
            "name": {
                "type": "string",
                "defaultValue": "[resourceGroup().name]"
            },
            "location": {
                "type": "string",
                "defaultValue": "[resourceGroup().location]"
            },
            "tenantId": {
                "type": "string",
                "defaultValue": "[subscription().tenantId]"
            }
        },
        "variables": {
            "kv_name": "[concat(parameters('name'), 'kv')]",
            "kv_version": "2019-09-01",
            "sb_name": "[concat(parameters('name'), 'sb')]",
            "sb_version": "2018-01-01-preview",
            "sb_keyname": "sbkey"
        },
        "resources": [
            {
                "type": "Microsoft.ServiceBus/namespaces",
                "apiVersion": "[variables('sb_version')]",
                "name": "[variables('sb_name')]",
                "location": "[parameters('location')]",
                "sku": {
                    "name": "Premium",
                    "tier": "Premium",
                    "capacity": 1
                },
                "identity": {
                    "type": "SystemAssigned"
                },
                "properties": {
                    "zoneRedundant": false
                }
            },
            {
                "type": "Microsoft.KeyVault/vaults",
                "apiVersion": "[variables('kv_version')]",
                "name": "[variables('kv_name')]",
                "location": "[parameters('location')]",
                "dependsOn": [
                    "[variables('sb_name')]"
                ],
                "properties": {
                    "sku": {
                        "family": "A",
                        "name": "Standard"
                    },
                    "tenantId": "[parameters('tenantId')]",
                    "accessPolicies": [
                        {
                            "tenantId": "[reference(variables('sb_name'), variables('sb_version'), 'Full').identity.tenantId]",
                            "objectId": "[reference(variables('sb_name'), variables('sb_version'), 'Full').identity.principalId]",
                            "permissions": {
                                "keys": [
                                    "get",
                                    "wrapKey",
                                    "unwrapKey"
                                ]
                            }
                        }
                    ],
                    // Both must be enabled to encrypt Service Bus at rest.
                    "enableSoftDelete": true,
                    "enablePurgeProtection": true
                }
            },
            {
                "type": "Microsoft.KeyVault/vaults/keys",
                "apiVersion": "[variables('kv_version')]",
                "name": "[concat(variables('kv_name'), '/', variables('sb_keyname'))]",
                "location": "[parameters('location')]",
                "dependsOn": [
                    "[variables('kv_name')]"
                ],
                "properties": {
                    "kty": "RSA",
                    "keySize": 2048,
                    "keyOps": [
                        "wrapKey",
                        "unwrapKey"
                    ],
                    "attributes": {
                        "enabled": true
                    }
                }
            },
            {
                "type": "Microsoft.Resources/deployments",
                "apiVersion": "2020-10-01",
                "name": "sb_deployment",
                "dependsOn": [
                    "[resourceId('Microsoft.KeyVault/vaults/keys', variables('kv_name'), variables('sb_keyname'))]"
                ],
                "properties": {
                    "mode": "Incremental",
                    "template": {
                        "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
                        "contentVersion": "1.0.0.0",
                        "resources": [
                            {
                                "type": "Microsoft.ServiceBus/namespaces",
                                "apiVersion": "[variables('sb_version')]",
                                "name": "[variables('sb_name')]",
                                "location": "[parameters('location')]",
                                "sku": {
                                    "name": "Premium",
                                    "tier": "Premium",
                                    "capacity": 1
                                },
                                "identity": {
                                    "type": "SystemAssigned"
                                },
                                "properties": {
                                    "zoneRedundant": false,
                                    "encryption": {
                                        "keySource": "Microsoft.KeyVault",
                                        "keyVaultProperties": [
                                            {
                                                // Ideally should specify a specific version, but no ARM template function to get this currently.
                                                "keyVaultUri": "[reference(variables('kv_name')).vaultUri]",
                                                "keyName": "[variables('sb_keyname')]"
                                            }
                                        ]
                                    }
                                }
                            }
                        ]
                    }
                }
            }
        ]
    }
    

    To deploy this using the Azure CLI:

    az group create -g rg-example -l westus2
    az deployment group create -g rg-example -f template.json --parameters name=example