Search code examples
azure-resource-managerazure-bicep

Bicep Pass storage account connection string to key vault


I have 2 resource groups as follow:

rg-shared
rg-storage-accounts

in resource group one I am trying to create a storage account and get its connection string and pass it to resourcegroup2 on which I have a key vault.

my actual code is as follow.

Shared.bicep

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

param keyvaultmain string = 'Name-keyvault'
param keyvaultshared string = 'Name-keyvault'
param sharedManagedIdentity string = 'Name-Managed-identity'
param storageAccountString string
var storagePrefix = 'sttesteur'
var clientDataKeyPrefix = 'Key-Data-'
var learnersguidsecrets = 'Guidtest'
param tenantCodes array = [
  'tste'
]

resource keyVaultClients 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = {
  name: keyvaultmain
}

resource keyVaultShared 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = {
  name: keyvaultshared
}

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

resource kvClientsKey 'Microsoft.KeyVault/vaults/keys@2021-06-01-preview' = [for code in tenantCodes: {
  name: '${keyVaultClients.name}/${clientDataKeyPrefix}${toUpper(code)}'
  properties: {
    keySize: 2048
    kty: 'RSA'
    // Assign the least permission
    keyOps: [
      'unwrapKey'
      'wrapKey'
    ]
  }
}]

resource accessPolicy 'Microsoft.KeyVault/vaults/accessPolicies@2021-06-01-preview' = {
  name: '${keyVaultClients.name}/add'
  properties: {
    accessPolicies: [
      {
        tenantId: subscription().tenantId
        objectId: managedIdentity.properties.principalId
        permissions: {
          // minimum required permission
          keys: [
            'get'
            'unwrapKey'
            'wrapKey'
          ]
        } 
      }
    ]
  }
}

resource clientLearnersGuid 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = [for tenant in tenantCodes: {
  name: '${keyVaultClients.name}/${tenant}${learnersguidsecrets}'
  properties: {
    contentType: 'GUID Key'
    value: '${deploymentIdOne}-${deploymentIdTwo}'
  }
}]

resource storageAccountConnectionString 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = [for tenant in tenantCodes: {
  name: '${keyVaultShared.name}${storagePrefix}${tenant}'
  properties:{
    contentType: '${tenant} Storage Account Connection String'
    value: storageAccountString
  }
}]

And this is my storage-account.bicep

param tenantCodes array = [
  'tste'
]

param tenantManagedIdentity string = 'Manage-identity-Name'
param secondresource string = 'rg-sec-eur-shared'
var keyVaultKeyPrefix = 'Key-Data-'
var storagePrefix = 'sthritesteur'






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



// 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}${toUpper(tenantCode)}'
        // keyvaulturi: keyVault.properties.vaultUri
        keyvaulturi:'https://keyvaultclient.vault.azure.net'
      }
      services: {
        file: {
          keyType: 'Account'
          enabled: true
        }
        blob: {
          keyType: 'Account'
          enabled: true
        }
      }
    }
    accessTier: 'Hot'
  }

}]

resource storage_Accounts_name_default 'Microsoft.Storage/storageAccounts/blobServices@2021-04-01' = [ for (storageName, i) in tenantCodes :{
  parent: storageAccount[i]
  name: 'default'
  properties: {
    changeFeed: {
      enabled: false
    }
    restorePolicy: {
      enabled: false
    }
    containerDeleteRetentionPolicy: {
      enabled: true
      days: 7
    }
    cors: {
      corsRules: []
    }
    deleteRetentionPolicy: {
      enabled: true
      days: 30
    }
    isVersioningEnabled: true
  }
}]

module connectionString 'shared.bicep' = [for (storageName, i) in tenantCodes :{
  scope: resourceGroup(secondresource)
  name: storageName
  params: {
    storageAccountString: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount[i].name};AccountKey=${listKeys(storageAccount[i].id, storageAccount[i].apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
  }
}]

This is the details of this workflow.

in the resource group rg-sharedi have 2 key vaults, keyvault-sharedand keyvaultstorage. And their purpose is as follow:

keyvault-shared => Store StorageAccount Connection String as Secret

keyvault-storage => Generate a Key Name based on the `tenantCode` in the key section, and in secret, generate a GUID and store it

while in the other resource-group rg-storage I want to create a storage account, encrypt the storage account with the key I have generated in the keyault earlier, and pass the connection string of this storageAccount to the shared key vault.

Following your advice, I used the module from shared.bicep and called it in my storage account.bicep.

Based on my command:

az deployment group what-if -f ./storage-account.bicep -g rg-storage-accounts     

the output It shows that will create only the resource in the storage-account.bicep:

  • user identity
  • storageAccount
  • container

How to reproduce:

  • Create 2 resource groups (shared and storage accounts)
  • in Shared create 2 key vaults
  • Update the bicep files with the correct key vault names.
  • In both bicep scripts in tenantCode put a random name to create a storage account or multiple storage accounts.
  • and run the above bicep command.

I tried to explain as clear as I could this issue, as its driving me crazy and have no idea what I am doing wrong and this stage.

Please please, if you need anymore information about this issue, just ask and will be glad to clarify any doubt

UPDATE: To generate the key before hand, I moved the key creation into the storage.bicep as follow:

resource keyVaultClients 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = {
  name: keyvaultmain
  scope: resourceGroup(secondresource)
}

resource kvClientsKey 'Microsoft.KeyVault/vaults/keys@2021-06-01-preview' = [for code in tenantCodes: {
  name: '${keyVaultClients.name}-${clientDataKeyPrefix}${toUpper(code)}'
  properties: {
    keySize: 2048
    kty: 'RSA'
    // Assign the least permission
    keyOps: [
      'unwrapKey'
      'wrapKey'
    ]
  }
}]

but I get this error:

{"error":{"code":"InvalidTemplate","message":"Deployment template validation failed: 'The template resource 'keyvault-Key-Data-ORNX' for type 'Microsoft.KeyVault/vaults/keys' at line '54' and column '46' has incorrect segment lengths. A nested resource type must have identical number of segments as its resource name. A root resource type must have segment length one greater than its resource name. Please see https://aka.ms/arm-template/#resources for usage details.'.","additionalInfo":[{"type":"TemplateViolation","info":{"lineNumber":54,"linePosition":46,"path":"properties.template.resources[1].type"}}]}}

Which I don't understand exactly to what refers.

UPDATE: This is an interesting output. So according to the last update (and thank you so so much for your help) I realised that at the code is creating all the correct resource, but at the very end it throws this 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":"Conflict","message":"{\r\n  \"status\": \"Failed\",\r\n  \"error\": {\r\n    \"code\": \"ResourceDeploymentFailure\",\r\n    \"message\": \"The resource operation completed with terminal provisioning state 'Failed'.\",\r\n    \"details\": [\r\n      {\r\n        \"code\": \"DeploymentFailed\",\r\n        \"message\": \"At least one resource deployment operation failed. Please list deployment operations for details. Please see https://aka.ms/DeployOperations for usage details.\",\r\n        \"details\": [\r\n          {\r\n            \"code\": \"Conflict\",\r\n            \"message\": \"{\\r\\n  \\\"error\\\": {\\r\\n    \\\"code\\\": \\\"StorageAccountOperationInProgress\\\",\\r\\n    \\\"message\\\": \\\"An operation is currently performing on this storage account that requires exclusive access.\\\"\\r\\n  }\\r\\n}\"\r\n          },\r\n          {\r\n            \"code\": \"Conflict\",\r\n            \"message\": \"{\\r\\n  \\\"error\\\": {\\r\\n    \\\"code\\\": \\\"StorageAccountOperationInProgress\\\",\\r\\n    \\\"message\\\": \\\"An operation is currently performing on this storage account that requires exclusive access.\\\"\\r\\n  }\\r\\n}\"\r\n          }\r\n        ]\r\n      }\r\n    ]\r\n  }\r\n}"}]}}

Solution

  • For testing , I used nested template module for creating a single Storage account and then stored the connection string in the key vault present in the another resource group.

    Scenario:

    Keyvaultclient.bicep>>nested(storage.bicep)>>nested(shared.bicep)
    

    Code:

    Keyvaultclient.bicep:

    param deploymentIdOne string = newGuid()
    param deploymentIdTwo string = newGuid()
    output deploymentIdOne string = '${deploymentIdOne}-${deploymentIdTwo}'
    output deploymentIdTwo string = deploymentIdTwo
    
    param storagerg string = 'rgnamewherestorageaccountistobecreated'
    param sharedManagedIdentity string = 'identityforkeyvault'
    param keyvaultmain string = 'keyvaultclienttes1234'
    param tenantCodes array = [
      'tste'
    ]
    var clientDataKeyPrefix = 'Key-Data-'
    
    resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = {
      name: sharedManagedIdentity
      location: resourceGroup().location
    }
    
    resource keyVaultClients 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = {
      name: keyvaultmain
    }
    resource kvClientsKey 'Microsoft.KeyVault/vaults/keys@2021-06-01-preview' = [for code in tenantCodes: {
      parent:keyVaultClients
      name: '${keyVaultClients.name}-${clientDataKeyPrefix}${toUpper(code)}'
      properties: {
        keySize: 2048
        kty: 'RSA'
        // Assign the least permission
        keyOps: [
          'unwrapKey'
          'wrapKey'
        ]
      }
    }]
    resource accessPolicy 'Microsoft.KeyVault/vaults/accessPolicies@2021-06-01-preview' = {
      parent:keyVaultClients
      name: 'add'
      properties: {
        accessPolicies: [
          {
            tenantId: subscription().tenantId
            objectId: managedIdentity.properties.principalId
            permissions: {
              // minimum required permission
              keys: [
                'get'
                'unwrapKey'
                'wrapKey'
              ]
            } 
          }
        ]
      }
    }
    resource clientLearnersGuid 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = [for tenant in tenantCodes: {
      parent:keyVaultClients
      name: '${keyVaultClients.name}${tenant}'
      properties: {
        contentType: 'GUID Key'
        value: '${deploymentIdOne}-${deploymentIdTwo}'
      }
      dependsOn:kvClientsKey
    }]
    module StorageAccount './storage.bicep' = [for (storageName, i) in tenantCodes :{
      scope: resourceGroup(storagerg)
      name: storageName
      params: {
        ManagedIdentityid:managedIdentity.id
        kvname:keyVaultClients.name
        uri:keyVaultClients.properties.vaultUri
      }
      dependsOn:clientLearnersGuid
    }]
    

    Storage.bicep:

    param tenantCodes array = [
      'tste'
    ]
    param ManagedIdentityid string
    param uri string 
    param kvname string
    param keyvaultrg string = 'rgwherethekeyvaultsarepresent'
    var keyVaultKeyPrefix = 'Key-Data-'
    var storagePrefix = 'sthritesteur'
    
    
    // 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: {
            '${ManagedIdentityid}':{}
        }
      }
      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: ManagedIdentityid
          }
          keySource: 'Microsoft.Keyvault'
          keyvaultproperties: {
            keyname: '${kvname}-${keyVaultKeyPrefix}${toUpper(tenantCode)}'
            keyvaulturi:uri
          }
          services: {
            file: {
              keyType: 'Account'
              enabled: true
            }
            blob: {
              keyType: 'Account'
              enabled: true
            }
          }
        }
        accessTier: 'Hot'
      }
    
    }]
    
    
    
    resource storage_Accounts_name_default 'Microsoft.Storage/storageAccounts/blobServices@2021-04-01' = [ for (storageName, i) in tenantCodes :{
      parent: storageAccount[i]
      name: 'default'
      properties: {
        changeFeed: {
          enabled: false
        }
        restorePolicy: {
          enabled: false
        }
        containerDeleteRetentionPolicy: {
          enabled: true
          days: 7
        }
        cors: {
          corsRules: []
        }
        deleteRetentionPolicy: {
          enabled: true
          days: 30
        }
        isVersioningEnabled: true
      }
    }]
    
    module connectionString './shared.bicep' = [for (storageName, i) in tenantCodes :{
      scope: resourceGroup(keyvaultrg)
      name: storageName
      params: {
        storageAccountString: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount[i].name};AccountKey=${listKeys(storageAccount[i].id, storageAccount[i].apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
      }
    }]
    

    shared.bicep:

    param keyvaultshared string = 'keyvaultsharedtest12345'
    param storageAccountString string
    param tenantCodes array = [
      'tste'
    ]
    resource keyVaultShared 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = {
      name: keyvaultshared
    }
    resource storageAccountConnectionString 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = [for tenant in tenantCodes: {
      parent:keyVaultShared
      name: '${keyVaultShared.name}-test${tenant}'
      properties:{
        contentType: '${tenant} Storage Account Connection String'
        value: storageAccountString
      }
    }]
    

    Output:

    keyvaultclient.bicep will be deployed to the kvresourcegroup:

    az deployment group create -n TestDeployment -g keyvaultrg --template-file "path\to\keyvaultclient.bicep"
    

    enter image description here

    enter image description here

    enter image description here

    enter image description here