Nayden Van
Nayden Van

Reputation: 1569

Azure Bicep multiple scopes in template

After using terraform for a very long time, I decided to start learning azure bicep. So far I am trying to have a grip on the logic. So far I have playing around on deployment of a storage account and keyvault. What I am doing here is the following.

And this works as I am expected.

So I wanted to take one step forward. and here is where I am a bit confused.

What I wanted to do, is to use the same bicep template, to create a new secret but in a different resource group into a different key vault.

Now according to my understand of azure documentation, the template comes with a default scope which in my specific case target my default subscription and to run my bicep template from the terminal I use the command

az deployment group create -f ./template.bicep -g <resource-group-name>

and this is my template:

// Default values I'm using to test 
param keyVaultName string = '<keyvault-name>'
param managedIdentityName string = 'test-managed-identity'

param tenantCodes array = [
  'elhm'
  'feor'
]

// I'm using prefix so I dont need to create additional arrays
var keyVaultKeyPrefix = 'Client-Key-'
var storagePrefix = 'sthrideveur'

// Get a reference to key vault
resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = {
  name: keyVaultName
}

// Create a managed identity
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
  name: managedIdentityName
  location: resourceGroup().location
}

// Grant permissions to key vault
resource accessPolicy 'Microsoft.KeyVault/vaults/accessPolicies@2019-09-01' = {
  name: '${keyVault.name}/add'
  properties: {
    accessPolicies: [
      {
        tenantId: subscription().tenantId
        objectId: managedIdentity.properties.principalId
        permissions: {
          // minimum required permissions
          keys: [
            'get'
            'unwrapKey'
            'wrapKey'
          ]
        }
      }
    ]
  }
}

// Create key vault keys
resource keyVaultKeys 'Microsoft.KeyVault/vaults/keys@2021-06-01-preview' = [for tenantCode in tenantCodes: {
  name: '${keyVault.name}/${keyVaultKeyPrefix}${tenantCode}'
  properties: {
    keySize: 2048
    kty: 'RSA'
    // storage key should only needs these operations
    keyOps: [
      'unwrapKey'
      'wrapKey'
    ]
  }
}]

// Create storage accounts
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = [for tenantCode in tenantCodes: {
  name: '${storagePrefix}${tenantCode}'
  location: resourceGroup().location
  kind: 'StorageV2'
  sku: {
    name: 'Standard_RAGRS'
  }
  // Assign the identity
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${managedIdentity.id}': {}
    }
  }
  properties: {
    allowCrossTenantReplication: true
    minimumTlsVersion: 'TLS1_2'
    allowBlobPublicAccess: false
    allowSharedKeyAccess: true
    networkAcls: {
      bypass: 'AzureServices'
      virtualNetworkRules: []
      ipRules: []
      defaultAction: 'Allow'
    }
    supportsHttpsTrafficOnly: true
    encryption: {
      identity: {
        // specify which identity to use
        userAssignedIdentity: managedIdentity.id
      }
      keySource: 'Microsoft.Keyvault'
      keyvaultproperties: {
        keyname: '${keyVaultKeyPrefix}${tenantCode}'
        keyvaulturi: keyVault.properties.vaultUri
      }
      services: {
        file: {
          keyType: 'Account'
          enabled: true
        }
        blob: {
          keyType: 'Account'
          enabled: true
        }
      }
    }
    accessTier: 'Cool'
  }
}]

// Store the connectionstrings in KV if specified
resource storageAccountConnectionStrings 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = [ for (name, i) in tenantCodes :{
  name: '${keyVault.name}/${storagePrefix}${name}'
  properties: {
    value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount[i].name};AccountKey=${listKeys(storageAccount[i].id, storageAccount[i].apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
  }
}]

according to the documentation here https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/deploy-to-resource-group?tabs=azure-cli

When I need to target a specific resource group, I can use the scope in the resource, so I create this:

resource keyvaultApi 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = {
  name: keyVaultApiName
  scope: resourceGroup('secondresourcegroup')
}

So far no errors, but the problem happens when I had to create a managed identity resource.

resource keyvaultApi 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = {
  name: keyVaultApiName
  scope: resourceGroup('secondresourcegroup')
}

resource managedIdentityTwo 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
  name: managedIdentityNameTwo
  location: resourceGroup().location
}

resource accessPolicyApi 'Microsoft.Media/videoAnalyzers/accessPolicies@2021-11-01-preview' = {
  name: '${keyvaultApi.name}/add'
  properties: {
    accessPolicies: [
      {
        tenantId: subscription().tenantId
        objectId: managedIdentityTwo.properties.principalId
        permissions: {
          // minimum required permissions
          keys: [
            'get'
            'unwrapKey'
            'wrapKey'
          ]
        }
      }
    ]
  }
}

In the key vault I could declare the scope, but to the underlying resources, such as access policy etc, I cannot declare the scope. So how can bicep understand that those resources needs to target a specific resource group and specific key vault?

Because when I run the terminal command, I am targeting a specific resource group, so I don't really understand how I can use one template to target different resource groups and resources accordingly.

I hope I made my point clear, and please if I didn't, just feel free to ask me more informations.

Thank you so much for your time and help

UPDATE: When I try to run the code as it is, I get the following error:

{"status":"Failed","error":{"code":"DeploymentFailed","message":"At least one resource deployment operation failed. Please list deployment operations for details. Please see https://aka.ms/DeployOperations for usage details.","details":[{"code":"NotFound","message":"{\r\n  \"error\": {\r\n    \"code\": \"ParentResourceNotFound\",\r\n    \"message\": \"Can not perform requested operation on nested resource. Parent resource 'secondkeyvault' not found.\"\r\n  }\r\n}"}]}}

UPDATE: So I followed the Daniel lead and in a second template I deployed the code I needed for the second template as follow:

template2.bicep

param deploymentIdOne string = newGuid()
param deploymentIdTwo string = newGuid()
output deploymentIdOne string = '${deploymentIdOne}-${deploymentIdTwo}'
output deploymentIdTwo string = deploymentIdTwo

// Default values I'm using to test 
param keyVaultApiName string = 'secondkeyvaultapi'
param managedIdentityNameTwo string = 'second-second-identity'
var keyVaultKeyPrefixTw = 'Client-Key-'
param tenantCodes array = [
  'tgrf'
]
resource keyvaultApi 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = {
  name: keyVaultApiName
}

resource managedIdentityTwo 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
  name: managedIdentityNameTwo
  location: resourceGroup().location
}

resource accessPolicyApi 'Microsoft.KeyVault/vaults/accessPolicies@2019-09-01' = {
  name: '${keyvaultApi.name}/add'
  properties: {
    accessPolicies: [
      {
        tenantId: subscription().tenantId
        objectId: managedIdentityTwo.properties.principalId
        permissions: {
          // minimum required permissions
          keys: [
            'get'
            'unwrapKey'
            'wrapKey'
          ]
        }
      }
    ]
  }
}

// Store the connectionstrings in KV if specified
resource clientApiKeys 'Microsoft.KeyVault/vaults/secrets@2019-09-01' = [ for name in tenantCodes :{
  name: '${keyvaultApi.name}/${keyVaultKeyPrefixTw}${name}'
  properties: {
    value: '${deploymentIdOne}-${deploymentIdTwo}'
  }
}]

and in my main template I added the module:

module clientKeyApi 'template2.bicep' = {
  name: 'testfrgs'
  scope: 'secondresourcegroup'
}

But there is something that is not clear 100% to me. How does it work to override all the for loop and parameters name I have declared in my template2.bicep , and yet the module require a scope subscription, if I declare the scope, wouldn't this override the default value?

Sorry guys for the newbie questions, I am trying to break my mindset from terraform and understand better how bicep work.

Any explanation would be amazing and helpful

Upvotes: 7

Views: 10801

Answers (1)

Daniel Mann
Daniel Mann

Reputation: 59055

You can't specify scope on the resource, but you can specify it on a module. You'll need to turn the resource that adds the access policy to the keyvault into a separate module, then specify scope on the module. You can also make the scope for your deployment subscription, but then you'll need to break everything that targets a specific resource group into modules, as well.

This is due to how ARM deployments work. The default scope for an ARM deployment is at the resource group level. You can't point a resource at a different resource group because it's outside the scope of the deployment.

Modules, however, run as sub-deployments and therefore can have a different scope set.

This is a case where Terraform is more straightforward, since it calls the Azure APIs directly instead of using the ARM deployment model. Terraform doesn't care about deployment scopes because it doesn't use them.

Upvotes: 17

Related Questions