azureazure-managed-identityazure-container-registryazure-bicepazure-container-apps

How do I configure my Bicep scripts to allow a container app to pull an image from an ACR using managed identity across subscriptions


I am trialling the use of Bicep and Container Apps in my organisation and we have separated out concerns within the SAME tenant but in different subscriptions like so:

  1. Development
  2. Production
  3. Management

I want to be able to deploy each of these subscriptions using Bicep scripts (individual ones per subscription) and ideally only use managed identity for security.

Within the management subscription, we have an ACR which has the admin account intentionally disabled as I don't want to pull via username/password. Question one, should this be possible? As it seems that we should be able to configure an AcrPull role against the container app(s) without too much trouble.

The idea being that the moment the container app is deployed it pulls from the ACR and is actively useable. I don't want an intermediary such as Azure DevOps handling the orchestration for example.

In Bicep I've successfully configured the workspace, container environment but upon deploying my actual app I'm a bit stuck - it fails for some incomprehensible error message which I'm still digging into. I've found plenty of examples using the admin/password approach but documentation for alternatives appears lacking which makes me worry if I'm after something that isn't feasible. Perhaps user identity is my solution?

My Bicep script (whilst testing against admin/password) looks like this:

resource containerApp 'Microsoft.App/containerApps@2022-06-01-preview' = {
  name: containerAppName
  location: location

  identity: {
    type: 'SystemAssigned'
  }
  
  properties: {
    managedEnvironmentId: containerAppEnvId
    configuration: {
      secrets: [
        {
          name: 'container-registry-password'
          value: containerRegistry.listCredentials().passwords[0].value
        }
      ]
      ingress: {
        external: true
        targetPort: targetPort
        allowInsecure: false
        traffic: [
          {
            latestRevision: true
            weight: 100
          }
        ]
      }
      registries: [
        {
          server: '${registryName}.azurecr.io'
          username: containerRegistry.listCredentials().username
          passwordSecretRef: 'container-registry-password'
        }
      ]
    }
    template: {
      revisionSuffix: 'firstrevision'
      containers: [
        {
          name: containerAppName
          image: containerImage
          resources: {
            cpu: json(cpuCore)
            memory: '${memorySize}Gi'
          }
        }
      ]
      scale: {
        minReplicas: minReplicas
        maxReplicas: maxReplicas
      }
    }
  }
}

However, this is following an admin/password approach. For using managed identity, firstly do I need to put a registry entry in there?

registries: [
        {
          server: '${registryName}.azurecr.io'
          username: containerRegistry.listCredentials().username
          passwordSecretRef: 'container-registry-password'
        }
      ]

If so, the listCredentials().username obviously won't work with admin/password disabled. Secondly, what would I then need in the containers section?

containers: [
        {
          name: containerAppName
          image: containerImage ??
          resources: {
            cpu: json(cpuCore)
            memory: '${memorySize}Gi'
          }
        }
      ]

As there appears to be no mention of the need for pointing at a repository, or indeed specifying anything other than a password/admin account. Is it that my requirement is impossible as the container app needs to be provisioned before managed identity can be applied to it? Is this a chicken vs egg problem?


Solution

  • You could use a user-assigned identity:

    1. Create a user assigned identity
    2. Grant permission to the user-assigned identity
    3. Assign the identity to the container app
    # container-registry-role-assignment.bicep
    param registryName string
    param roleId string
    param principalId string
    
    // Get a reference to the existing registry
    resource registry 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' existing = {
      name: registryName
    }
    
    // Create role assignment
    resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
      name: guid(registry.id, roleId, principalId)
      scope: registry
      properties: {
        roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleId)
        principalId: principalId
        principalType: 'ServicePrincipal'
      }
    }
    

    Then from your main:

    param name string
    param identityName string
    param environmentName string
    param containerImage string
    param location string = resourceGroup().location
    
    param containerRegistrySubscriptionId string = subscription().subscriptionId
    param containerRegistryResourceGroupName string = resourceGroup().name
    param containerRegistryName string
    
    // Create identtiy
    resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2022-01-31-preview'  = {
      name: identityName
      location: location
    }
    
    // Assign AcrPull permission
    module roleAssignment 'container-registry-role-assignment.bicep' = {
      name: 'container-registry-role-assignment'
      scope: resourceGroup(containerRegistrySubscriptionId, containerRegistryResourceGroupName)
      params: {
        // https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#acrpull
        roleId: '7f951dda-4ed3-4680-a7ca-43fe172d538d'
        principalId: identity.properties.principalId
        registryName: containerRegistryName
      }
    }
    
    // Get a reference to the container app environment
    resource managedEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' existing = {
      name: environmentName
    }
    
    // create the container app
    resource containerapp 'Microsoft.App/containerApps@2022-03-01' = {
      dependsOn:[
        roleAssignment
      ]
      name: name
      ...
      identity: {
        type: 'UserAssigned'
        userAssignedIdentities: {
          '${identity.id}': {}
        }
      }
      properties: {
        managedEnvironmentId: managedEnvironment.id
        configuration: {
          ...
          registries: [
            {
              server: '${containerRegistryName}.azurecr.io'
              identity: identity.id
            }
          ]
        }
        template: {
          ...
          containers: [
            {
              name: name
              image: '${containerRegistryName}.azurecr.io/${containerImage}'
              ...
            }
          ]
        }
      }
    }