The Senator
The Senator

Reputation: 5391

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?

Upvotes: 4

Views: 4859

Answers (2)

Thomas
Thomas

Reputation: 29522

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}'
          ...
        }
      ]
    }
  }
}

Upvotes: 6

tuomastik
tuomastik

Reputation: 4906

Here is how @Thomas's answer can be extended for App Service:

resource webApp 'Microsoft.Web/sites@2022-09-01' = {
  // ...
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${identity.id}': {}
    }
  }
  properties: {
    // ...
    siteConfig: {
      linuxFxVersion: 'DOCKER|${acrName}.azurecr.io/${imageName}:${imageTag}'
      acrUseManagedIdentityCreds: true
      acrUserManagedIdentityID: identity.properties.clientId
    }
  }
}

For more information, see the documentation of Microsoft.Web/sites@2022-09-01.

Upvotes: 0

Related Questions