I'm a contractor working on an existing Azure project. Currently, my client is manually configuring all their Azure resources for each environment (Dev, QA, Prod). I know this is shocking to people, but their subscriptions are a mess. I'm trying to setup IaC deployments using bicep.
For the Azure App Services, they have an IP Access Restriction setup to only allow traffic from their APIM instance. When I look at the access restrictions for the app service (under networking) for the main site, I can see it listed.
However, when I deploy using --what-if, it comes back saying it will create the access restriction rule. I'm not expecting this because it should already exist. I've searched high and low but can't find the answer.
apiAppService.bicep
#description('The name of the target app service without any prefix or suffix. i.e. Contoso')
param apiName string
#description('The abbreviation of the target environment. i.e. dev')
#allowed([
'dev'
'qa'
'prod'
])
param environment string
#description('The Azure region the resource group is to be created in.')
param region string
#description('The abbreviation of the Azure region included as part of the resource group name. i.e. NCUS')
param regionAbbreviation string
#description('The properties of the SKU for the app service plan.')
param appServicePlanSku object
#description('The runtime stack of the target app service. i.e. DOTNETCORE|6.0')
param runtimeStack string
#description('The values required to setup the IP access restriction')
param ipRestriction object
var appServicePlanName = 'ASP-${apiName}-${regionAbbreviation}-${environment}'
var appServiceName = 'productname-${apiName}-api-${environment}'
resource appServicePlan 'Microsoft.Web/serverfarms#2022-03-01' = {
name: appServicePlanName
location: region
sku: {
name: appServicePlanSku.name
tier: appServicePlanSku.tier
}
kind: 'linux'
properties: {
reserved: true
}
}
resource appService 'Microsoft.Web/sites#2022-03-01' = {
name: appServiceName
location: region
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: appServicePlan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: runtimeStack
ipSecurityRestrictions: [
{
name: ipRestriction.name
action: ipRestriction.action
priority: ipRestriction.priority
ipAddress: ipRestriction.ipAddress
}
]
}
}
}
The results of the --what-if deployment
~ Microsoft.Web/sites/productname-apiname-api-dev [2022-03-01]
+ properties.siteConfig.ipSecurityRestrictions: [
0:
action: "allow"
ipAddress: "a.b.c.d/32"
name: "Allow ONLY APIM"
priority: 300
]
+ properties.siteConfig.localMySqlEnabled: false
~ properties.httpsOnly: false => true
Am I trying to configure different things? What am I doing wrong?
Using Azure Resource Explorer proved to be very helpful. Check it out if you haven't and you're building bicep files. The IP restrictions are actually in a resource with a path 'config/web' off the app service resource. Essentially the definition looks like
resource webConfig 'Microsoft.Web/sites/config#2022-03-01' = {
name: 'web'
parent: appService
properties: {
linuxFxVersion: runtimeStack
ipSecurityRestrictions: [
{
name: ipRestriction.name
action: ipRestriction.action
priority: ipRestriction.priority
ipAddress: ipRestriction.ipAddress
}
{
name: 'Deny all'
description: 'Deny all access'
action: 'Deny'
priority: 2147483647
ipAddress: 'Any'
}
]
}
}
The name: 'web' and parent: appService is very important as is the default deny all rule at the end.
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 am using bicep to configure the site and DNS. Currently, I can configure it when using the subdomain (e.g www.foilen-lab.me) by using a CNANE, but for the main (e.g foilen-lab.me), I cannot use a CNAME and must use the IP. How can I get the IP?
Currently for the "www":
resource siteWww 'Microsoft.Web/sites#2021-03-01' = {
name: 'www-foilen-lab-me'
location: location
kind: 'app,linux,container'
properties: {
serverFarmId: serverFarmsId
reserved: true
httpsOnly: true
siteConfig: {
alwaysOn: true
numberOfWorkers: 1
linuxFxVersion: 'DOCKER|foilen/az-docker-apache_php:7.4.9-3'
}
}
}
resource dnsWww 'Microsoft.Network/dnsZones/CNAME#2018-05-01' = {
parent: dnsZone
name: 'www'
properties: {
TTL: 3600
CNAMERecord: {
cname: '${siteWww.name}.azurewebsites.net'
}
}
}
And I would like to create something like:
resource dns 'Microsoft.Network/dnsZones/A#2018-05-01' = {
parent: dnsZone
name: '#'
properties: {
TTL: 3600
ARecords: [
{
ipv4Address: '${siteWww.xxxxxxxx}'
}
]
}
}
thanks
You should be able to use siteWww.properties.inboundIpAddress to get the current ipAddress.
As a general rule of thumb you can retrieve any property on a resource in bicep by using it's symbolic name and the JSON path of the GET from the REST api.
So for example, if you go to the portal for any resource and select the JSON View from the overview page... you can see what's possible to return via that syntax. e.g. siteWww.properties.customDomainVerificationId is also handy.
I am trying to get a list of my current sites/slot app config and then combining it with a union so I don't overwrite the current deployed configuration. I am able to do this with the sites config but cannot with the deploy slot.
Here is an example of my working web sites
in main.bicep
resource sites_web_name 'Microsoft.Web/sites#2021-02-01' = {
parameters here(irrelevant for excercise)
}
module web_appsettings_config './appsettings.bicep' = {
name: 'webAppSettings'
params: {
//this gets a list of the current app settings for the web site resource
currentAppSettings: list('${sites_web_name.id}/config/appsettings', '2021-02-01').properties
appSettings: {
'Config1': 'confighere'
'Config2': 'config2here'
'Config3': 'config3here'
}
name: '${sites_web_name.name}/appsettings'
}
}
appsettings.bicep
param currentAppSettings object
param appSettings object
param name string
resource siteConfig 'Microsoft.Web/sites/config#2021-02-01' = {
name: name
properties: union(currentAppSettings, appSettings)
}
The above is taking the current app settings deployed, not deleting/overwriting them if the new ones aren't specified in the template.
Now the problem trying to get sites/slot
main.bicep
resource sites_web_slots_name 'Microsoft.Web/sites/slots#2021-02-01' = {
parameters here(irrelevant for excercise)
}
module web_staging_appsettings_config './slotappsettings.bicep' = {
name: 'webStagingAppSettings'
params: {
//this gets a list of the current app settings for the web slot resource
currentAppSettings: list('${sites_web_slots_name.id}/slots/config/appsettings', '2021-02-01').properties
appSettings: {
'Config1': 'confighere'
'Config2': 'config2here'
'Config3': 'config3here'
}
name: '${sites_web_slots_name.name}/appsettings'
}
}
even made a separate module, slotappsettings.bicep
param currentAppSettings object
param appSettings object
param name string
resource siteSlotConfig 'Microsoft.Web/sites/slots/config#2021-02-01' = {
name: name
properties: union(currentAppSettings, appSettings)
}
The error I am getting for the slots failure is not helpful at all:
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":"NotFound","message":"{\r\n "error": {\r\n "code": "InvalidResourceNamespace",\r\n "message": "The resource namespace 'subscriptions' is invalid."\r\n }\r\n}"}]}}
I also don't get any deployment failure messages in the azure portal.
Here is the documentation for the api versions I'm using:
sites/config/appsettings:
sites/config-appsettings
resource symbolicname 'Microsoft.Web/sites/config#2021-02-01' = {
name: 'appsettings'
kind: 'string'
parent: resourceSymbolicName
properties: {}
}
sites/slots/config/appsettings:
sites/slots/config-appsettings
resource symbolicname 'Microsoft.Web/sites/slots/config#2021-02-01' = {
name: 'appsettings'
kind: 'string'
parent: resourceSymbolicName
properties: {}
}
There is a typo while calling the list function for the slot settings:
You have /slots/config/appsettings but it should only be /config/appsettings because the /slots segment is already included in the resource id of the slot (sites_web_slots_name.id)
I'm trying to add a new Traffic Manager Profile via a Network/trafficManagerProfiles.bicep module which I invoke in my main.bicep file.
This works well.
The main.bicep file is creating multiple Function Apps using their own little module, which is invoked similar like this
module functionAppModule 'Web/functions.bicep' = {
dependsOn: [
appServicePlanModule
webApiStorageAccount
]
name: 'functionAppModule'
params: {
environmentName: environmentName
systemName: systemName
azureRegion: azureRegion
appServicePlanId: appServicePlanModule.outputs.id
}
}
Now I'm trying to add the necessary endpoints of my web applications (Azure Functions) to the Traffic Manager Profile, which is also possible by using the endpoints property.
However, this would mean I need to add a parameter to this file accepting an array of objects containing all information about my App Services, or I would need to resolve them over here (by retrieving the instances with the existing keyword). This doesn't sound like the way to implement this, because all those resources are already available/referenced in the main.bicep file.
The Traffic Manager Profile module now looks like this:
param systemName string
#allowed([
'dev'
'test'
'acc'
'prod'
])
param environmentName string
param relativeLiveEndpoint string = '/api/Live'
var trafficManagerProfileName = '${systemName}${environmentName}'
resource trafficManagerProfile 'Microsoft.Network/trafficmanagerprofiles#2018-08-01' = {
name: trafficManagerProfileName
location: 'global'
properties: {
allowedEndpointRecordTypes: [
'DomainName'
]
dnsConfig: {
relativeName: trafficManagerProfileName
ttl: 60
}
profileStatus: 'Enabled'
trafficRoutingMethod: 'Performance'
monitorConfig: {
profileMonitorStatus: 'Online'
protocol: 'HTTPS'
port: 443
path: relativeLiveEndpoint
timeoutInSeconds: 10
intervalInSeconds: 30
toleratedNumberOfFailures: 3
}
endpoints: [
{
id: 'the resource id'
name: 'the resource name'
type: 'the resource type'
properties: {
endpointStatus: 'Enabled'
endpointMonitorStatus: 'Online'
targetResourceId: 'the resource id'
target: 'mysite.azurewebsites.net'
endpointLocation: 'West Europe'
weight: 1
priority: 1
}
}
// All other app services
]
}
}
According to some answers here on Stack Overflow, the endpoints can also be created via a child resource in ARM templates (something like Microsoft.Network/trafficmanagerprofiles/endpoints), however, this doesn't appear to be available in Bicep (or just isn't available in the autocomplete).
What I'd like to do is something like this in my main.bicep file, because that way I can reference all App Services I want to add.
var trafficManagerProfileEndpoints 'Microsoft.Network/trafficmanagerprofiles/endpoints#2050-01-01` = {
name: '${trafficManagerProfile.Name}/endpoints'
endpoints: [
// All of my endpoints
]
}
Somewhat similar compared to what's available via Microsoft.Web/sites/config#2020-12-01 for configuration settings in an App Service.
Any ideas on this?
I just ran through your question at lightning speed, forgive me if I don't fully understand it, but it looks like you need to declare your web app as a resource again, but as existing. This allows you to access all the props you need from your app service.
This blog explains how to use existing resources in Bicep: https://hexmaster.nl/posts/bicep-existing/
This is possible through Bicep ARM but the Microsoft.Network/trafficManagerProfiles api doesn't have types available for endpoints so it will generate a warning even if the deployment works perfectly.
If you define a traffic manager profile without the endpoints property:
resource trafficManager 'Microsoft.Network/trafficmanagerprofiles#2018-08-01' = {
name: '<name>'
location: 'global'
properties: {
profileStatus: 'Enabled'
trafficRoutingMethod: '<trafficRoutingMethod>'
dnsConfig: {
...
}
monitorConfig: {
...
}
}
}
You can then add endpoints like that:
// Azure endpoints: cloud service, app service, app service slots, public ip
resource trafficManagerAzureEndpoint 'Microsoft.Network/trafficManagerProfiles/azureEndpoints#2018-08-01' = {
name: '${trafficManagerName}/${name}'
properties: {
...
}
}
// External endpoint
resource trafficManagerExternalEndpoint 'Microsoft.Network/trafficManagerProfiles/externalEndpoints#2018-08-01' = {
name: '${trafficManagerName}/${name}'
properties: {
...
}
}
// Nested endpoints
resource trafficManagerNestedEndpoint 'Microsoft.Network/trafficManagerProfiles/nestedEndpoints#2018-08-01' = {
name: '${trafficManagerName}/${name}'
properties: {
...
}
}
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