Bicep Registry

Recently Microsoft released a new feature concerning Bicep called private registry for Bicep modules or Bicep private registry / Bicep registry for short. In essence this means storing Bicep modules in an Azure Container Registry (ACR). Using Bicep modules is a way to modularize Bicep code.

One of the benefits of a Bicep registry is sharing these Bicep modules inside (or even outside) your company. While you can share code in Azure DevOps or Github directly, an ACR is an Azure resource and because of this, it utilizes Azure RBAC to control who has access. With Bicep registries this basically means anyone who has push permissions to the ACR can push/write Bicep modules into the ACR and anyone with pull permissions can read the module files. When modules are in a Bicep registry ‘reading’ is called ‘restoring’.

Reading modules files is necessary when a Bicep file with a reference to a module is being transpiled into an ARM template. Transpiling can be done explicitly with the bicep build command, but this functionality is also incorporated in New-Az<scope>Deployment (pwsh) and az deployment <scope> create (az CLI) commands. These commands transpile the Bicep code to an ARM template and immediately deploys the ARM template to the target environment in one go.

Deploy cross tenant

So lets say we have a tenant A where a Bicep registry is deployed with Bicep modules. But we want to deploy a main.bicep file, with references to modules in the Bicep Registry, to tenant B. This means we need to have pull permissions to the ACR in tenant A while transpiling, and permissions to deploy to tenant B…while deploying.

There are multiple ways to solve this problem actually, but first, lets see what is happening exactly. When using Bicep registries, there is an option in the Bicep config file to decide the precedence of credentials to be used for authenticating to the Bicep registry. Here you can specify ‘AzureCLI’ or ‘AzurePowerShell’ for example. This means that when transpiling, Bicep is checking the token caches of theses tools to try to connect to the ACR.

When using New-Az<scope>Deployment (pwsh) or az deployment <scope> create (az CLI) commands to deploy a Bicep file, the step to transpile and deploy is the same. This means the used credentials need to have permissions on both the ACR and the target environment to deploy to. And here enters.. Azure Lighthouse.

If you are not familiar with this, the concept is basically: give principals (users/groups/service principals) from tenant X permissions to Azure resources in tenant Y. The Azure Lighthouse service is especially beneficial for MSPs who are managing a multitude of customer environments.

You might already see where I’m heading with this.

The idea is to give the account used for deploying in tenant B, pull permissions with Azure Lighthouse to the ACR in tenant A. And this in fact works like a charm.

DevOps

While the above method works for regular accounts, it works for Azure DevOps service connections too!! as they use SPNs to authenticate. Just hand over ACR pull permissions of the Bicep registry in tenant A to the service principal (SPN), used by the Azure DevOps service connection, in tenant B with Azure Lighthouse.

Code samples

And no better way to deploy a Lighthouse delegation than with Bicep of course.

Here are some Bicep files to deploy a Lighthouse delegation (definition and assignment) to a resource group. The assignment module file is placed into its own Bicep module and should be stored alongside this Bicep file. The idea is to target the deployment to resource group where the ACR resides.

N.B. The code does not work out of the box. Fill in where comments are placed.

targetScope = 'subscription'

@description('Specify a unique name for your offer')
param mspOfferName string = 'bicepAcrDelegation'

@description('Name of the Managed Service Provider offering')
param mspOfferDescription string = 'bicepAcrDelegation'

@description('Specify the tenant id of the Managed Service Provider')
param managedByTenantId string //fill in guid tenant Id

@description('Specify an array of objects, containing tuples of Azure Active Directory principalId, a Azure roleDefinitionId, and an optional principalIdDisplayName. The roleDefinition specified is granted to the principalId in the provider\'s Active Directory and the principalIdDisplayName is visible to customers.')
param authorizations array = [
  {
  'principalId':  //fill in objectId of principal who should get permission
  'roleDefinitionId': '7f951dda-4ed3-4680-a7ca-43fe172d538d' //current guid is acrpull
  'principalIdDisplayName':  //fill in descriptive name
  }
]
param rgName string = 'bicepregistry'

var RegistrationName = guid(mspOfferName)
var AssignmentName = guid(mspOfferName)

resource acrDelegationDefinition 'Microsoft.ManagedServices/registrationDefinitions@2020-02-01-preview' = {
  name: RegistrationName
  properties: {
    registrationDefinitionName: mspOfferName
    description: mspOfferDescription
    managedByTenantId: managedByTenantId
    authorizations: authorizations
  }
}

module acrDelegationAssignment './acrDelegationAssignment.bicep' = {
  name: AssignmentName
  scope: resourceGroup(rgName)
  params: {
    AcrDefintitionresourceId: acrDelegationDefinition.id
    AssignmentName: AssignmentName
  }
}

output mspOfferName string = 'Managed by ${mspOfferName}'
output authorizations array = authorizations
//acrDelegationAssignment.bicep (use this filename!)
targetScope = 'resourceGroup'

param AcrDefintitionresourceId string
param AssignmentName string

resource acrDelegationAssignment 'Microsoft.ManagedServices/registrationAssignments@2019-06-01' = {
  name: AssignmentName
  properties: {
    registrationDefinitionId: AcrDefintitionresourceId
  }
}

This Lighthouse delegation only needs to happen once, but needs to be deployed to tenant A. When that’s set it’s possible to deploy resources, like this storage account, to tenant B, where the bicep registry resides in tenant A:

//main.bicep
targetScope = 'resourceGroup'

param name string

module mySa 'br:jannicksbicepreg.azurecr.io/bicep/modules/storage:v1' = {
  name: name
  params: {
    kind: 'StorageV2'
    saname: '${name}${uniqueString(name)}'
    sku: {
      name: 'Standard_LRS'
    }
    tags: {
      env: 'dev'
    }
  }
}

Here is an example of the pipeline yaml:

trigger: none

pool:
  vmImage: ubuntu-latest

jobs:
- job:
  steps:
  - task: AzureCLI@2
    inputs:
      azureSubscription: 'bicepAcrLighthouseTestServiceConnection' #Tenant B
      scriptType: 'bash'
      scriptLocation: 'inlineScript'
      inlineScript: 'az bicep upgrade'
  - task: AzureCLI@2
    inputs:
      azureSubscription: 'bicepAcrLighthouseTestServiceConnection' #Tenant B
      scriptType: 'bash'
      scriptLocation: 'inlineScript'
      inlineScript: |
        az deployment group create \
          --name $(Build.BuildNumber) \
          --resource-group jannickbiceptest \
          --template-file main.bicep        

When the permissions (with Azure Lighthouse) are not in place there is this error message:

ERROR: /home/vsts/work/1/s/main.bicep(6,13) : Error BCP192: Unable to restore the module with reference “br:jannicksbicepreg.azurecr.io/bicep/modules/storage:v1”: Unhandled exception: Azure.RequestFailedException: Service request failed.

This could mean the Lighthouse delegation was either not successful or was revoked.

Keep an eye on Barbara 4bes' blog to find out another way to solve this problem.