I'd like to use a shared bicep module to add several ip security restriction records to existing app services.
This is the module I've come up with:
param appSvcName string
resource appSvc 'Microsoft.Web/sites#2021-02-01' existing = {
name: appSvcName
}
var proxyIpAddresses = ['xxx.xxx.xxx.250/32','xxx.xxx.xxx.245/32']
resource sitesConfig 'Microsoft.Web/sites/config#2021-02-01' = {
name: 'web'
parent: appSvc
properties: {
ipSecurityRestrictions: [for (ip,i) in proxyIpAddresses: {
ipAddress: ip
action: 'Allow'
tag: 'Default'
priority: 900 + i
name: 'ProxyIp_${i}'
description: 'Allow request from proxy ${i}'
}]
}
}
I call this from the main bicep as follows:
module ipRestrictions 'common.appSvc.ipSecurityRestrictions.bicep' = {
scope: resourceGroup(utrnRg)
name: 'ipRestrictionsDeploy'
params: {
appSvcName: functionAppName
}
dependsOn: [
functionAppDeploy
]
}
Prior to this, there's a call to a specific Azure Function module the provisions the function app and ip restrictions that are specific to that function app (typically subnets that it should allow traffic from)
The behaviour I see is that the function app gets created with its specific ip restrictions, but these get deleted and replaced with the two rules from the shared module.
Is there a way I can make the module add to existing ip restrictions rather than replacing them?
In you module, you would need to have a new parameter for the existings ip restrictions then you can add the new restirctions to it:
// common.appSvc.ipSecurityRestrictions.bicep
param appSvcName string
param existingIpSecurityRestrictions array = []
resource appSvc 'Microsoft.Web/sites#2021-02-01' existing = {
name: appSvcName
}
var proxyIpAddresses = ['xxx.xxx.xxx.250/32','xxx.xxx.xxx.245/32']
var additionalIpSecurityRestrictions = [for (ip,i) in proxyIpAddresses: {
ipAddress: ip
action: 'Allow'
tag: 'Default'
priority: 900 + i
name: 'ProxyIp_${i}'
description: 'Allow request from proxy ${i}'
}]
resource sitesConfig 'Microsoft.Web/sites/config#2021-02-01' = {
name: 'web'
parent: appSvc
properties: {
ipSecurityRestrictions: concat(existingIpSecurityRestrictions, additionalIpSecurityRestrictions)
}
}
Then invoke the module with the existing restrictions:
// main.bicep
param functionAppName string
module ipRestrictions 'common.appSvc.ipSecurityRestrictions.bicep' = {
scope: resourceGroup(utrnRg)
name: 'ipRestrictionsDeploy'
params: {
appSvcName: functionAppName
existingIpSecurityRestrictions: reference(resourceId('Microsoft.Web/sites/config', functionAppName, 'web'), '2021-02-01').ipSecurityRestrictions
}
dependsOn: [
functionAppDeploy
]
}
Related
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.
I'd like a module that could add a fixed set of ip restriction rules to an existing app service (function app in this case).
I have added a call to a "ipSecurityRestrictions" module from my main.bicep as follows:
module ipRestrictions 'common.appSvc.ipSecurityRestrictions.bicep' = {
scope: resourceGroup(utrnRg)
name: 'ipRestrictionsDeploy'
params: {
appSvcName: functionAppName
existingIpSecurityRestrictions: reference(resourceId('Microsoft.Web/sites/config', functionAppName, 'web'), '2021-02-01').ipSecurityRestrictions
}
dependsOn: [
functionAppDeploy
]
}
The code for the "ipSecurityRetrictions" module is:
param appSvcName string
param existingIpSecurityRestrictions array = []
resource appSvc 'Microsoft.Web/sites#2021-02-01' existing = {
name: appSvcName
}
var proxyIpAddresses = ['xxx.xxx.xxx.250/32','xxx.xxx.xxx.245/32','xxx.xxx.xxx.251/32']
var proxyIpRestrictions = [for (ip,i) in proxyIpAddresses: {
ipAddress: ip
action: 'Allow'
tag: 'Default'
priority: 900 + i
name: 'ProxyIp_${i}'
description: 'Allow request from proxy ${i}'
}]
resource sitesConfig 'Microsoft.Web/sites/config#2021-02-01' = {
name: 'web'
parent: appSvc
properties: {
ipSecurityRestrictions: concat(existingIpSecurityRestrictions, proxyIpRestrictions)
}
}
The call to the function app module is as follows:
module functionAppDeploy 'utrngen.functionApp.bicep' = {
name: 'functionAppDeploy'
scope: resourceGroup(utrnRg)
params: {
pcPublicIp: pcPublicIp
functionAppName: functionAppName
}
dependsOn: [
appPlanDeploy
storageAccountDeploy
]
}
The function app moddule has a sites/config resource to create ipSecurityRestrictions as follows:
resource sitesConfig 'Microsoft.Web/sites/config#2021-02-01' = {
name: 'web'
parent: functionApp
properties: {
ipSecurityRestrictions: [
{
ipAddress: '${pcPublicIp}/32'
action: 'Allow'
tag: 'Default'
priority: 101
name: 'laptop ip'
description: 'Allow requests from test laptop'
}
]
}
The problem is, when main.bicep is run, it only adds the new set of rules to those that already existed; any rules specified in the function app module (in this case the one for pcPublicIp) are not added.
I guess this is because at the time the call to the ipRestrictions module is made from main.bicep, the ipSecurityRestrictions from the function app module have not yet been created; and so the following function call only returns what's present before the main.bicep is run:
existingIpSecurityRestrictions: reference(resourceId('Microsoft.Web/sites/config', functionAppName, 'web'), '2021-02-01').ipSecurityRestrictions
So I think bicep is working as expected but curious if there's a solution this this problem? I could simply pass in the dedicated ip restriction rules for the function app as a parameter to the shared ipSecurityRestrictions module but that has a bad smell, since it breaks single responsibility for the ipSecurityRestrictions module. It would no longer be responsible for just adding the ip restrictions that are common to all our app services.
You could move the call to the common module inside the functionapp module:
// utrngen.functionApp.bicep
...
module ipRestrictions 'common.appSvc.ipSecurityRestrictions.bicep' = {
name: 'ipRestrictionsDeploy-${functionAppName}'
params: {
appSvcName: functionApp.name
existingIpSecurityRestrictions: reference(resourceId('Microsoft.Web/sites/config', functionApp.name, 'web'), '2021-02-01').ipSecurityRestrictions
}
}
Or you could return the ipSecurityRestrictions from your function app module and use it in your main:
// utrngen.functionApp.bicep
...
output ipSecurityRestrictions array = sitesConfig.properties.ipSecurityRestrictions
// main.bicep
...
module ipRestrictions 'common.appSvc.ipSecurityRestrictions.bicep' = {
name: 'ipRestrictionsDeploy-${functionAppName}'
params: {
appSvcName: functionAppName
existingIpSecurityRestrictions: functionAppDeploy.outputs.ipSecurityRestrictions
}
}
Or you could wrap the call to the ip restriction module inside another module
// common.appSvc.ipSecurityRestrictions-wrapper.bicep
param appSvcName string
module ipRestrictions 'common.appSvc.ipSecurityRestrictions.bicep' = {
name: 'ipRestrictionsDeploy-${appSvcName}'
params: {
appSvcName: appSvcName
existingIpSecurityRestrictions: reference(resourceId('Microsoft.Web/sites/config', appSvcName, 'web'), '2021-02-01').ipSecurityRestrictions
}
}
// main.bicep
...
module ipRestrictions 'common.appSvc.ipSecurityRestrictions-wrapper.bicep' = {
name: 'ipRestrictionsDeploy-${functionAppName}'
params: {
appSvcName: functionAppName
}
dependsOn: [ functionAppDeploy ]
}
I am trying to defines settings (static and dynamic) for a Function App using Bicep.
The following is the module that generates the Function App itself. This module contains a loop that creates a collection of dynamic settings (which does work):
param serverFarmId string
param availableHubs array
resource functionAppProdSlotResource 'Microsoft.Web/sites#2021-03-01' = {
name:'function-app-name'
location: 'Canada Central'
kind: 'functionapp,linux'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: serverFarmId
httpsOnly: true
siteConfig: {
linuxFxVersion: 'dotnet|3.1'
appSettings: [for (availableHubId, hubIndex) in availableHubs: {
'name': 'AvailableHubsConfiguration__Hub__${hubIndex}'
'value': 'Endpoint=https://hub-${availableHubId}.service.signalr.net;AccessKey=${listKeys(resourceId('Microsoft.SignalRService/SignalR', 'hub-${availableHubId}'), providers('Microsoft.SignalRService', 'SignalR').apiVersions[0]).primaryKey};Version=1.0;'
}]
}
}
}
However I also have this other module that defines static settings for that same function:
param functionIdentity object = {
prodSlotName: ''
stagingSlotName: ''
}
#secure()
param systemStorageAccountConnectionString string
param applicationInsightsInstrumentationKey string
param stsDiscoveryEndpoint string
param accountsManagerEndpoint string
param azureActiveDirectory object
param updateManagerClientIdKeyVaultUrl string
param updateManagerClientSecretKeyVaultUrl string
var functionExtensionVersion = '~4'
var functionWorkerRuntime = 'dotnet-isolated'
resource prodSlotAppSettingsResource 'Microsoft.Web/sites/config#2021-03-01' = {
name: '${functionIdentity.prodSlotName}/appsettings'
properties: {
AzureWebJobsStorage: systemStorageAccountConnectionString
FUNCTIONS_EXTENSION_VERSION: functionExtensionVersion
FUNCTIONS_WORKER_RUNTIME: functionWorkerRuntime
APPINSIGHTS_INSTRUMENTATIONKEY: applicationInsightsInstrumentationKey
StsConfiguration__DiscoveryEndpoints__0: stsDiscoveryEndpoint
AccountsManagerConfiguration__Endpoint: accountsManagerEndpoint
AzureActiveDirectoryConfiguration__ClientId: '#Microsoft.KeyVault(SecretUri=${updateManagerClientIdKeyVaultUrl})'
AzureActiveDirectoryConfiguration__ClientSecret: '#Microsoft.KeyVault(SecretUri=${updateManagerClientSecretKeyVaultUrl})'
AzureActiveDirectoryConfiguration__Scope: azureActiveDirectory.scope
AzureActiveDirectoryConfiguration__TokenEndpoint: azureActiveDirectory.tokenEndpoint
AzureActiveDirectoryConfiguration__TenantId: subscription().tenantId
AzureActiveDirectoryConfiguration__SubscriptionId: subscription().subscriptionId
}
}
Problem
The problem is that the 2nd module overrides the dynamic settings set by the 1st module. Because of the loop in the 1st module, I can't find a way to either prevent the override by the 2nd module or somehow combine the two.
Question
How can I combine dynamic settings and static settings in a Function App using Bicep?
I struggled for a few weeks to find an actual solution. What I have found rely on the concat function.
I dropped the 2nd module (which used to define a resource for the Function's settings) and only keep the 1st one (which had the Function resource itself along with the dynamic settings).
I now call the 1st module with an array that I built:
module functionAppModule 'modules/function-app.bicep' = {
name: 'Function_App'
dependsOn: [
serverFarmModule
]
params: {
[...]
availableHubs: [for availableHub in availableHubs: 'Endpoint=https://hub-${environment}-${availableHub}.service.signalr.net;AccessKey=${listKeys(resourceId('Microsoft.SignalRService/SignalR', 'hub-${environment}-${availableHub}'), providers('Microsoft.SignalRService', 'SignalR').apiVersions[0]).primaryKey};Version=1.0;']
}
}
In the module, I build 2 distinct arrays: one with the static settings and another one with the dynamic settings:
var staticSettings = [
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~4'
}
{
name: 'FUNCTIONS_WORKER_RUNTIME'
value: 'dotnet-isolated'
}
{
name: 'AzureWebJobsStorage'
value: systemStorageAccountConnectionString
}
{
name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
value: applicationInsightsInstrumentationKey
}
[...]
]
var dynamicSettings = [for (availableHub, hubIndex) in availableHubs: {
name: 'AvailableHubsConfiguration__Hub__${hubIndex}'
value: availableHub
}]
Then, using the concat function, I merge the arrays together:
var functionSettings = concat(staticSettings, dynamicSettings)
Finally, I can assign the merged functionSettings array to the Function App by looping over it and specifying the name & value:
resource functionAppProdSlotResource 'Microsoft.Web/sites#2021-03-01' = {
name: 'function-app-name'
location: 'Canada Central'
kind: 'functionapp,linux'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: serverFarmId
httpsOnly: true
siteConfig: {
linuxFxVersion: 'dotnet|3.1'
appSettings: [for setting in functionSettings: {
name: setting.name
value: setting.value
}]
}
}
}
How to select and add multiple subnets of a VNet in the networking section of an azure eventHub resource using azure bicep.
// Create an event hub namespace
var eventHubNamespaceName = 'evhns-demo1436'
resource eventHubNamespace 'Microsoft.EventHub/namespaces#2021-01-01-preview' = {
name: eventHubNamespaceName
location: resourceGroup().location
sku: {
name: 'Standard'
tier: 'Standard'
capacity: 1
}
properties: {
zoneRedundant: true
}
}
// Create an event hub inside the namespace
var eventHubName = 'evh-demo1436'
resource eventHubNamespaceName_eventHubName 'Microsoft.EventHub/namespaces/eventhubs#2021-01-01-preview' = {
parent: eventHubNamespace
name: eventHubName
properties: {
messageRetentionInDays: 7
partitionCount: 1
}
}
// Grant Listen and Send on our event hub
resource eventHubNamespaceName_eventHubName_ListenSend 'Microsoft.EventHub/namespaces/eventhubs/authorizationRules#2021-01-01-preview' = {
parent: eventHubNamespaceName_eventHubName
name: 'ListenSend'
properties: {
rights: [
'Listen'
'Send'
]
}
dependsOn: [
eventHubNamespace
]
}
resource testVnet 'Microsoft.Network/virtualNetworks#2021-03-01' existing = {
name: 'testvnet'
}
resource testsubnet 'Microsoft.Network/virtualNetworks/subnets#2021-03-01' existing = {
parent: testVnet
name: 'testsubnet'
}
resource enHubVnetRule 'Microsoft.EventHub/namespaces/virtualnetworkrules#2018-01-01-preview' = {
name: 'vnetName'
parent: eventHubNamespace
properties: {
virtualNetworkSubnetId: testsubnet.id
}
}
With above code I can only add one particular subnet of a VNet to the VNet entry of azure EventHub resource in networking section using azure bicep.
How do I fetch all subnets of a VNet and add/select them all to the VNet entry of azure EventHub resource in networking section using azure bicep.
You have to use for loop for the subnet block and virtual-network rule block like below:
// Create an event hub namespace
param eventHubNamespaceName string = 'evhns-demo1436'
resource eventHubNamespace 'Microsoft.EventHub/namespaces#2021-01-01-preview' = {
name: eventHubNamespaceName
location: resourceGroup().location
sku: {
name: 'Standard'
tier: 'Standard'
capacity: 1
}
properties: {
zoneRedundant: true
}
}
// Create an event hub inside the namespace
param eventHubName string = 'evh-demo1436'
resource eventHubNamespaceName_eventHubName 'Microsoft.EventHub/namespaces/eventhubs#2021-01-01-preview' = {
parent: eventHubNamespace
name: eventHubName
properties: {
messageRetentionInDays: 7
partitionCount: 1
}
}
// Grant Listen and Send on our event hub
resource eventHubNamespaceName_eventHubName_ListenSend 'Microsoft.EventHub/namespaces/eventhubs/authorizationRules#2021-01-01-preview' = {
parent: eventHubNamespaceName_eventHubName
name: 'ListenSend'
properties: {
rights: [
'Listen'
'Send'
]
}
dependsOn: [
eventHubNamespace
]
}
param subnets array =[
'test'
'mysubnet'
]
param vnetname string = 'test-ansuman'
resource testVnet 'Microsoft.Network/virtualNetworks#2021-03-01' existing = {
name: vnetname
}
resource testsubnet 'Microsoft.Network/virtualNetworks/subnets#2021-03-01' existing =[for subnet in subnets : {
parent: testVnet
name: subnet
}]
resource enHubVnetRule 'Microsoft.EventHub/namespaces/virtualnetworkrules#2018-01-01-preview' = [for (subnet,i) in subnets :{
name: '${vnetname}-${subnet}'
parent: eventHubNamespace
properties: {
virtualNetworkSubnetId:testsubnet[i].id
}
}]
Output:
Normally when creating Azure resources through Bicep modules I would have 2 files. One file designated to hold the parameterized resource and another file, the main file, which will consume that module.
As an example for creating an action group my resource file looks like:
action-group.bicep
param actionGroupName string
param groupShortName string
param emailReceivers array = []
// Alerting Action Group
resource action_group 'microsoft.insights/actionGroups#2019-06-01' = {
name: actionGroupName
location: 'global'
tags: {}
properties: {
groupShortName: groupShortName
enabled: true
emailReceivers: emailReceivers
}
}
This resource is then consumed as a module in the main file, main.bicep:
// Alerting Action Group
module actionGroup '../modules/alerts/alert-group.bicep' = {
name: 'action-group-dply'
params: {
actionGroupName: actionGroupName
groupShortName: actionGroupShortName
emailReceivers: [
{
name: '<Name of email receivers>'
emailAddress: alertEmailList
}
]
}
}
My pipeline references main.bicep and will deploy the listed resources in the file. My question is, is there a way to add a third file in the mix? One file to still hold the parameterized resource, one file to hold the associated resource modules, and the main.bicep file. The idea is to create various alerts throughout my existing resources but I don't want to add a ton of modules to main.bicep as it will quickly increase the complexity and amount of code in this file.
Is there a way that I can have this file of modules, and reference the entire file in main.bicep to still deployment from the original pipeline?
As an example:
alerts.bicep
param metricAlertsName string
param description string
param severity int
param enabled bool = true
param scopes array = []
param evaluationFrequency string
param windowSize string
param targetResourceRegion string = resourceGroup().location
param allOf array = []
param actionGroupName string
var actionGroupId = resourceId(resourceGroup().name, 'microsoft.insights/actionGroups', actionGroupName)
resource dealsMetricAlerts 'Microsoft.Insights/metricAlerts#2018-03-01' = {
name: metricAlertsName
location: 'global'
tags: {}
properties: {
description: description
severity: severity
enabled: enabled
scopes: scopes
evaluationFrequency: evaluationFrequency
windowSize: windowSize
targetResourceRegion: targetResourceRegion
criteria: {
'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
allOf: allOf
}
actions: [
{
actionGroupId: actionGroupId
}
]
}
alert-modules.bicep
// Function/Web Apps 403 Error
module appServicePlan403Errors '../modules/alerts/alerts.bicep' = {
// Alert Logic
}
// Function/Web Apps 500 Error
module appServicePlan500Errors '../modules/alerts/alerts.bicep' = {
// Alert Logic
}
main.bicep
// Some reference to alert-modules.bicep so when the pipeline runs and looks for main.bicep, it will still deploy all the resources
You can call the module multiple times (in main.bicep or a module) using looping:
https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/loops
e.g.
param alertsCollection array = [
{
actionGroupName: 'group1'
groupShortName: 'g1'
emailReceivers: []
}
{
actionGroupName: 'group2'
groupShortName: 'g2'
emailReceivers: [
'foo#bar.com'
'bar#baz.com'
]
}
]
module alerts '../modules/alerts/alert-group.bicep' = [for alert in alertsCollection): {
name: '${alert.actionGroupName}'
params: {
actionGroupName: alert.actionGroupName
groupShortName: alert.groupShortName
emailReceivers: alert.emailReceivers
}
}]
You could simplify the params passing via:
module alerts '../modules/alerts/alert-group.bicep' = [for alert in alertsCollection): {
name: '${alert.actionGroupName}'
params: alert
}]
But you need to be really strict on the schema of the alertsCollection parameter...
That help?