I am deploying an aks cluster with agic (application gateway ingress controller) with bicep
In the aks.bicep I declare this
resource aksCluster 'Microsoft.ContainerService/managedClusters#2021-03-01' = {
name: 'aks-core-${env}'
location: resourceGroup().location
identity: {
type: 'SystemAssigned'
}
properties: {
...
addonProfiles: {
...
ingressApplicationGateway: {
enabled: true
config: {
applicationGatewayId: applicationGatewayId
effectiveApplicationGatewayId: applicationGatewayId
}
}
}
}
}
However for some reason the identity created by aks seems to need a role in the resource group created for the node pool, because an error "needs contributor role" appears,so I added this:
resource contributorRoleDefinition 'Microsoft.Authorization/roleDefinitions#2020-08-01-preview' existing = {
scope: subscription()
name: 'xxx'
}
resource aksfix 'Microsoft.Authorization/roleAssignments#2020-04-01-preview' = {
name: guid(resourceGroup().id,'aksfix','Contributor')
scope: resourceGroup()
properties: {
description: 'fixes aks cross resource group principal permissions for agic'
principalId: aksCluster.properties.addonProfiles.ingressApplicationGateway.identity.objectId
principalType: 'ServicePrincipal'
roleDefinitionId: contributorRoleDefinition.id
}
}
Now to be clear this WORKS, but not always. Even if the principal has the appropriate permissions, the same error keeps popping up for 30 min to 1-2 hours and then by magic it works!
I had a similar error when trying to use internal load balancers which requires network contributor to the kubelet principal, and the same behavior appears, it takes an insane amount of time to reflect the role changes, now what seems interesting is a message "if your permissions changed try refreshing your credentials" on the agic pod, is there a way to force a cluster to refresh its credentials (without creating a new service principal)?
You should assign Contributor roles to both the aks magnaged cluster identity to the resource group where the application gateway resources are present and the ingress application gateway identity on the resource group.
So the solution will be better to use user-assigned identity and providing the contributor role and using it in AKS identity like below:
resource aksClusterUserDefinedManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities#2018-11-30' = {
name: 'aksClusterUserDefinedManagedIdentityName'
location: resourceGroup().location
}
resource akscontributorroleassignement 'Microsoft.Authorization/roleAssignments#2020-10-01-preview' = {
name: guid(concat(resourceGroup().id, aksClusterUserDefinedManagedIdentity.name,aksclustername))
scope: resourceGroup()
properties: {
description: 'Contributor role to the AKS identity to access the AGIC reosurce'
principalId: aksClusterUserDefinedManagedIdentity.properties.principalId
principalType: 'ServicePrincipal'
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')
}
}
resource aksCluster 'Microsoft.ContainerService/managedClusters#2021-03-01' = {
name: 'aks-core-${env}'
dependsOn:akscontributorroleassignement
location: resourceGroup().location
identity: {
type: 'UserAssigned',
userAssignedIdentities: {aksClusterUserDefinedManagedIdentity.id}
}
properties: {
...
addonProfiles: {
...
ingressApplicationGateway: {
enabled: true
config: {
applicationGatewayId: applicationGatewayId
effectiveApplicationGatewayId: applicationGatewayId
}
}
}
}
}
resource contributorRoleDefinition 'Microsoft.Authorization/roleDefinitions#2020-08-01-preview' existing = {
scope: subscription()
name: 'xxx'
}
resource aksfix 'Microsoft.Authorization/roleAssignments#2020-04-01-preview' = {
name: guid(resourceGroup().id,'aksfix','Contributor')
scope: resourceGroup()
properties: {
description: 'fixes aks cross resource group principal permissions for agic'
principalId: aksCluster.properties.addonProfiles.ingressApplicationGateway.identity.objectId
principalType: 'ServicePrincipal'
roleDefinitionId: contributorRoleDefinition.id
}
}
Related
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:
Development
Production
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:
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?
You could use a user-assigned identity:
Create a user assigned identity
Grant permission to the user-assigned identity
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: {
roleId: '7f951dda-4ed3-4680-a7ca-43fe172d538d' // AcrPull
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}'
...
}
]
}
}
}
I'm deploying Azure services by using Bicep. The property 'identity type system assigned' creates an enterprise application/service principal with a name, object id and app id. This is required to be able to process Azure Analysis Services from a Synapse pipeline.
//Create Synapse Analytics
resource synapseAnalytics 'Microsoft.Synapse/workspaces#2021-06-01' = {
name: synapse_name
location: region
identity: {
type: 'SystemAssigned'
}
properties: {
defaultDataLakeStorage: {
filesystem: storage_account_fileshare_name
resourceId: storageAccount.id
accountUrl: storage_account_url
createManagedPrivateEndpoint: true
}
managedVirtualNetwork: 'default'
publicNetworkAccess: 'Enabled'
managedResourceGroupName: synapse_workspace_name
azureADOnlyAuthentication: false
cspWorkspaceAdminProperties: {
initialWorkspaceAdminObjectId: xxxx
}
}
dependsOn: [
storageAccountFileshare
]
}
I need to retrieve the app id of the created resource to add to Azure Analysis Service as an administrator.
resource analysisServices 'Microsoft.AnalysisServices/servers#2017-08-01' = {
name: anaylsis_services_name
location: region
sku: {
name: 'B1'
tier: 'Basic'
capacity: 1
}
properties: {
asAdministrators: {
members: [
'obj:xxxxxx-xxxxxx-xxxxx-xxxxx#xxxxx-xxx-xxxxx-xxxxx'
'app:{GET APP ID OF SYNAPSE}' <------------------
]
}
managedMode: 1
}
}
How can I access the app id in my Bicep code?
I'm able to retrieve the app id by using a powershell command. Unfortunately this command needs an object id which I'm not able to retrieve by using powershell commands.
az ad sp show --id {object-id} --query appId
Using a system-assigned identity, you can't get the appId directly from bicep.
But you could output the principalId
//Create Synapse Analytics
resource synapseAnalytics 'Microsoft.Synapse/workspaces#2021-06-01' = {
name: synapse_name
...
}
// return the principalId to query the appId
output principalId string = synapseAnalytics.identity.principalId
You can then use the principalId to get the appId
az ad sp show --id <principalId from bicep> --query appId
Using a user-assigned identity, you would be able to do it all in bicep:
// Create a user identity for synapse
resource userAssignedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities#2018-11-30' = {
name: userAssignedIdentityName
location: region
}
//Create Synapse Analytics
resource synapseAnalytics 'Microsoft.Synapse/workspaces#2021-06-01' = {
name: synapse_name
identity: {
type: 'SystemAssigned,UserAssigned'
userAssignedIdentities: {
// assign the managed identity
'${userAssignedIdentity.id}': {}
}
}
...
}
// Create the analysis service
resource analysisServices 'Microsoft.AnalysisServices/servers#2017-08-01' = {
name: anaylsis_services_name
...
properties: {
asAdministrators: {
members: [
...
// Set app id and tenantid as per documentation
'app:${userAssignedIdentity.properties.clientId}#${userAssignedIdentity.properties.tenantId}'
]
}
...
}
}
I have a resource defined in my bicep file like this below, these are two of the resources in my file, i deploy an azure function with the test_resource below, this works fine.
resource test_resource 'Microsoft.Web/sites#2021-03-01' = {
name: resourceName
location: location
kind: 'functionapp'
identity: {
type: 'SystemAssigned'
}
properties: {
httpsOnly: true
serverFarmId: appServicePlan_ResourceId
}
}
and i am attempting to create an access policy as shown below, however i get an error regard the objectId, is there a way to configure the access policy for the above resource, perharps i am passing the wrong id in
"Invalid value found at accessPolicies[0].ObjectId:
but i am passing the test_resource.id as shown in the keyvault_access_policy resource definition.
resource devops_keyvault 'Microsoft.KeyVault/vaults#2021-10-01' existing = {
name: keyVaultName
}
resource keyvault_access_policy 'Microsoft.KeyVault/vaults/accessPolicies#2021-10-01' = {
name: 'add'
parent: devops_keyvault
properties: {
accessPolicies: [
{
objectId: test_resource.id
permissions: {
'keys': []
'secrets': [
'list'
'get'
]
'certificates': [
'list'
'get'
]
}
tenantId: subscription().tenantId
}
]
}
}
Looking at the documentation:
objectId: The object ID of a user, service principal or security group in the Azure Active Directory tenant for the vault. The object ID must be unique for the list of access policies.
In your case it should be the the principal ID of the managed identity:
objectId: test_resource.identity.principalId
I have a bicep template that creates 2 webApps and a KeyVault. Each WebApp is created with a managedID which I need to add to Keyvault so the webapp can pull in the secrets.
But when creating 2 webapps, I can't work out how to assign both ManagedIDs to KeyVault.
The bicep template is using modules
name: 'ciKeyVault'
params: {
keyVaultName: keyVaultName
aclBypass: keyVaultSettings.aclBypass
aclDefaultAction: keyVaultSettings.aclDefaultAction
enabledForDeployment: keyVaultSettings.enabledForDeployment
enabledForDiskEncryption: keyVaultSettings.enabledForDiskEncryption
enabledForTemplateDeployment: keyVaultSettings.enabledForTemplateDeployment
keyPermissions: keyVaultSettings.keyPermissions
keyVaultSettings: keyVaultSettings
secretsPermissions: keyVaultSettings.secretsPermissions
skuFamily: keyVaultSettings.skuFamily
skuName: keyVaultSettings.skuName
tenantId: subscription().tenantId
objectId: 'b71e61c4-7cff-41d0-8370-a7d9c01dde84'
}
}
and the objectId needs to be retrieved from the AppService Deployment. using this module:
module AppService '../../../Modules/Azure.App.Service.template.bicep' = [for i in range(0, length(webAppSettings.webApps)): {
name: webAppSettings.webApps[i].Name
dependsOn: [
frontEndAppServicePlan
]
params: {
webAppName: webAppSettings.webApps[i].appServiceType == 'functionApp' ? toLower('fnc-${webAppSettings.webApps[i].name}-${resourceGroupNameSuffix}') : toLower('web-${webAppSettings.webApps[i].name}-${resourceGroupNameSuffix}')
hostingPlan: frontEndAppServicePlan.outputs.hostingPlanId
virtualNetworkResourceGroup: virtualNetworkResourceGroup
environmentName:environmentName
webAppSettings:webAppSettings
appServiceType: webAppSettings.webApps[i].appServiceType
LinuxFX:webAppSettings.webApps[i].LinuxFX
appSettings:webAppSettings.webapps[i].appSettings
}
}]
Its fine when its a single appService cause I can reference the ID using output usid string = AppServices.identity.principalId
but when I have 2 appServices I can't work out how to pass in both IDs
Any ideas?
Cheers
Let's say you have a module Azure.App.Service.template.bicep that looks like that:
param webAppName string
...
// Create the web app
resource webApp 'Microsoft.Web/sites#2020-09-01' = {
name: webAppName
location: resourceGroup().location
identity: {
type: 'SystemAssigned'
}
...
}
output usid string = webApp.identity.principalId
In the parent template you can create an array of module to create your webapps (the same way you are doing it) and then create an access policies resource to grant access to key vault to all the web apps.
...
// Create the app services
module AppServices '../../../Modules/Azure.App.Service.template.bicep' = [for webApp in webAppSettings.webApps: {
name: webApp.Name
params: {
webAppName: webApp.Name
...
}
}]
// Granting the app services access ot key vault
resource appServicesKeyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies#2019-09-01' = {
name: '${keyVaultName}/add'
properties: {
accessPolicies: [for i in range(0, length(webAppSettings.webApps)): {
tenantId: subscription().tenantId
objectId: AppServices[i].outputs.usid
permissions: {
secrets: keyVaultSettings.secretsPermissions
keys: keyVaultSettings.keyPermissions
}
}]
}
}
I am currently trying to deploy out a resource group using azure bicep, however, I am running into an issue using key vault for my azure app service. I would like to know if I am actually doing this the correct way. I have a main bicep file that is along the lines of:
// params removed for brevity...
targetScope = 'subscription'
resource rg 'Microsoft.Resources/resourceGroups#2021-04-01' = {
name: 'rg-${appName}-${region}'
location: 'centralus'
}
module appServicePlan 'appplan.bicep' = {
params: {
sku: appServicePlanSku
appName: appName
region: region
}
scope: rg
name: 'AppServicePlanDeploy'
}
module keyVault 'keyvault.bicep' = {
params: {
keyVaultName: keyVaultName
sqlPassword: sqlServerPassword
webSiteManagedId: webSite.outputs.webAppPrincipal
}
scope: rg
name: 'KeyVaultDeploy'
dependsOn: [
webSite
]
}
module ai 'ai.bicep' = {
scope: rg
name: 'ApplicationInsightsDeploy'
params: {
name: appName
region: region
keyVaultName: keyVault.outputs.keyVaultName
}
dependsOn: [
keyVault
]
}
resource kv 'Microsoft.KeyVault/vaults#2019-09-01' existing = {
name: keyVaultName
scope: rg
}
module sql 'sqlserver.bicep' = {
scope: rg
name: 'SQLServerDeploy'
params: {
appName: appName
region: region
sqlPassword: kv.getSecret('sqlPassword')
sqlCapacitity: sqlCapacitity
sqlSku: sqlSku
sqlTier: sqlTier
}
dependsOn: [
keyVault
]
}
module webSite 'site.bicep' = {
params: {
appName: appName
region: region
keyVaultName: keyVaultName
serverFarmId: appServicePlan.outputs.appServicePlanId
}
scope: rg
name: 'AppServiceDeploy'
dependsOn: [
appServicePlan
]
}
My question comes with the implementation of the site.bicep, I started off by passing the secret uri from exported variables and creating the web app last as app insights, sql, etc... all need to be setup and in keyvault before we use their exported secret uri to construct a config. I had something along the lines of:
site.bicep (before):
properties: {
serverFarmId: serverFarmId
keyVaultReferenceIdentity: userAssignedId
siteConfig: {
appSettings: [
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: '#Microsoft.KeyVault(SecretUri=${appInsightsConnectionString})'
}
{
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
value: '#Microsoft.KeyVault(SecretUri=${appInsightsKey})'
}
]
netFrameworkVersion: 'v5.0'
}
}
The only problem with this implementation is that the key vault MUST be constructed before the website because sql, ai, and the other services will store their values inside of the key vault for the web app to consume by their respective uris. The issue with this is that the KeyVault rightfully so has no idea which azure service to let access it's keys.
My question is the solution of constructing the web app before the key vault the only way to beat this problem? I am using managed identities on the web app and would like to continue doing so if possible. My final solution ended up somewhat like this:
site.bicep (final)
// params removed for brevity...
resource webApplication 'Microsoft.Web/sites#2020-12-01' = {
name: 'app-${appName}-${region}'
location: resourceGroup().location
tags: {
'hidden-related:${resourceGroup().id}/providers/Microsoft.Web/serverfarms/appServicePlan': 'Resource'
}
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: serverFarmId
siteConfig: {
appSettings: [
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: '#Microsoft.KeyVault(SecretUri=${keyVaultName}.vault.azure.net/secrets/aiConnectionString)'
}
{
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
value: '#Microsoft.KeyVault(SecretUri=${keyVaultName}.vault.azure.net/secrets/aiInstrumentationKey)'
}
{
name: 'AngularConfig:ApplicationInsightsKey'
value: '#Microsoft.KeyVault(SecretUri=${keyVaultName}.vault.azure.net/secrets/aiInstrumentationKey)'
}
]
netFrameworkVersion: 'v5.0'
}
}
}
output webAppPrincipal string = webApplication.identity.principalId
And the KeyVault which will take a dependsOn webSite
keyVault.bicep(final):
resource keyVault 'Microsoft.KeyVault/vaults#2019-09-01' = {
name: keyVaultName
location: resourceGroup().location
properties: {
enabledForDeployment: true
enabledForTemplateDeployment: true
enabledForDiskEncryption: true
enableRbacAuthorization: true
tenantId: subscription().tenantId
sku: {
name: 'standard'
family: 'A'
}
accessPolicies: [
{
tenantId: subscription().tenantId
objectId: webSiteManagedId
permissions: {
keys: [
'get'
]
secrets: [
'list'
'get'
]
}
}
]
}
}
Just treat your accessPolicies as separate resource and add them when both Key Vault and App Service are created. Same applies for Config section and Connection Strings. Check documentation here.
In ARM templates you can achieve same effect using nested templates. In Bicep it is kind the same, but you declare them as separate resource that usually contains parent name (e.g. name: '${kv.name}/add', name: '${webSite.name}/connectionstrings')
Sample
Step 1: Create an App Service without config section
resource webSite 'Microsoft.Web/sites#2020-12-01' = {
name: webSiteName
location: location
properties: {
serverFarmId: hostingPlan.id
siteConfig:{
netFrameworkVersion: 'v5.0'
}
}
identity: {
type:'SystemAssigned'
}
}
Step 2: Create Key Vault without access policies
resource kv 'Microsoft.KeyVault/vaults#2019-09-01' = {
name: keyVaultName
location: location
properties:{
sku:{
family: 'A'
name: 'standard'
}
tenantId: tenantId
enabledForTemplateDeployment: true
accessPolicies:[
]
}
}
Step 3: Create new access policy and reference Web Apps Managed Identity
resource keyVaultAccessPolicy 'Microsoft.KeyVault/vaults/accessPolicies#2021-06-01-preview' = {
name: '${kv.name}/add'
properties: {
accessPolicies: [
{
tenantId: tenantId
objectId: webSite.identity.principalId
permissions: {
keys: [
'get'
]
secrets: [
'list'
'get'
]
}
}
]
}
}
Step 4: Update Webb app config section
resource webSiteConnectionStrings 'Microsoft.Web/sites/config#2020-06-01' = {
name: '${webSite.name}/connectionstrings'
properties: {
DefaultConnection: {
value: '#Microsoft.KeyVault(SecretUri=${keyVaultName}.vault.azure.net/secrets/aiConnectionString)'
type: 'SQLAzure'
}
}
}
One solution could be to use User Assigend Identity instead of System Assigned. Then you would deploy the following:
Deploy a user assigend identity
Key Vault and assign permissions for user assigned identity
Deploy web app with user assigned identity and read / write secrets
User assigned is independent of the resources and so you avoid your chicken and egg problem.
More:
https://learn.microsoft.com/en-us/azure/templates/microsoft.managedidentity/userassignedidentities?tabs=bicep
https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
You should split up three parts of your deployment into separate resources:
Deploy the Key Vault - without any access policies!
Deploy the App Service - with the SystemAssigned Identity, but without the app settings
Deploy the Key Vault Access Policy for the MSI
Deploy the App Settings