Azure Bicep - Timing of Dependent Module - azure

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

Related

Can I deploy an event trigger to Function App via Bicep?

I aim to create such a bicep file that would deploy a function app with an event trigger in it, however, I'm not sure if it's possible. I've tried googling but Function App deployment via bicep tutorials tend to stop right after the deployment of an empty function app. I need to go a step further and add triggers to it.
Why do I want to deploy at least an empty event trigger via bicep?
The core of the issue is: I would like to perform two stages of CD, where the first one will be all about bicep (preparing all of the resources) and the second one about code deployment.
However, at the first CD stage, the following error occurs:
{"code":"Endpoint validation","message":"Destination endpoint not
found. Resource details: resourceId:
/subscriptions/.../functionAppNameHere/functions/eventTriggerFuncName.
Resource should pre-exist before attempting this operation.}
I assume that EventGrid EventSubscriptions requires prior presence of a resource with the trigger (seems understandable though).
Is it even possible to add an event trigger via bicep so it would move smoothly to event subscription creation? If yes: can you please give me some tips?, if no: do you have any ideas on how to circumvent this?
Here are my bicep files that may come in handy when thinking about the issue:
main.bicep
module azStorageAccount 'modules/azStorageAccount.bicep'= {
name: '${deployName}-st'
params: {
location: location
name: '${stNamePrefix}${resourceSuffix}'
}
}
module azAppInsights 'modules/azAppInsights.bicep' = {
name: '${deployName}-appi'
params: {
location: location
name: '${appiNamePrefix}${resourceSuffix}'
}
}
module azHostingPlan 'modules/azHostingPlan.bicep' = {
name: '${deployName}-asp'
params: {
location: location
name: '${aspNamePrefix}${resourceSuffix}'
}
}
module azFunctionApp 'modules/azFunctionApp.bicep' = {
name: '${deployName}-func'
params: {
storageId:azStorageAccount.outputs.storageId
storageName: azStorageAccount.outputs.storageName
azAppInsightsInstrumentationKey: azAppInsights.outputs.azAppInsightsInstrumentationKey
location: location
name: '${funcNamePrefix}${resourceSuffix}'
serverFarmId: azHostingPlan.outputs.azHostingPlaniD
}
}
module azEventGrid 'modules/azEventGrid.bicep' = {
name: '${deployName}-evg'
params: {
subscription: subscription
evgtName: '${evgtNamePrefix}${resourceSuffix}'
evgsName: '${evgsNamePrefix}${resourceSuffix}'
resourceId: '/subscriptions/${subscription}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${funcNamePrefix}${resourceSuffix}/functions/${eventFunction}'
}
}
modules/azFunctionApp
// params here
resource azFunctionApp 'Microsoft.Web/sites#2021-03-01' = {
name: name
kind: kind
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
httpsOnly: true
serverFarmId: serverFarmId
clientAffinityEnabled: true
reserved: true
siteConfig: {
appSettings: [
{
name: 'FUNCTIONS_EXTENSION_VERSION'
value: '~3'
}
// more settings here
]
alwaysOn: false
}
}
}
modules/azEventGrid
// params here
resource azEventGridSystemTopic 'Microsoft.EventGrid/systemTopics#2022-06-15' = {
name: evgtName
location: 'global'
tags: {
// tags
}
properties: {
source: '/subscriptions/${subscription}'
topicType: 'Microsoft.Resources.Subscriptions'
}
}
resource azEventGridEventSubscriptions 'Microsoft.EventGrid/systemTopics/eventSubscriptions#2022-06-15' = {
parent: azEventGridSystemTopic
name: evgsName
properties: {
destination: {
properties: {
resourceId: resourceId
maxEventsPerBatch: 1
preferredBatchSizeInKilobytes: 64
}
endpointType: 'AzureFunction'
}
filter: {
includedEventTypes: [
'Microsoft.Resources.ResourceWriteSuccess'
'Microsoft.Resources.ResourceDeleteSuccess'
]
enableAdvancedFilteringOnArrays: true
}
labels: []
eventDeliverySchema: 'EventGridSchema'
retryPolicy: {
maxDeliveryAttempts: 30
eventTimeToLiveInMinutes: 1440
}
}
}

Reference a parent resource inside a module using bicep while parent has not deployed yet?

I'm trying to deploy a child module in bicep but I get not found error as its parent has not been deployed yet.
This is the parent bicep code:
#description('The name of the app service. full site name will be DT-xxx-Open')
param site_name string
var full_site_name = 'DT-${site_name}-Open'
#description('Resource location')
param location string = resourceGroup().location
resource app 'Microsoft.Web/sites#2021-02-01' = {
kind: 'app'
name: full_site_name
location: location
...
}
module site_config 'modules/appservice-config.bicep' = {
name: 'site_config'
params: {
resource_name: full_site_name
resource_location: location
}
}
this is my module bicep file:
#description('Resource name')
param resource_name string
(error happens at this line) resource site_name_resource 'Microsoft.Web/sites#2021-02-01' existing = {
name: resource_name
}
resource site_name_logs 'Microsoft.Web/sites/config#2021-02-01' = {
parent: site_name_resource
name: 'logs'
...
}
But the problem is I'm getting a not found error as its parent service deployment has not been completed yet and if I retry then it will work fine!
I wonder, is there any way to run this module after completing its parent deployment?
You would need to add a dependsOn when invoking you module.
Bicep use implicit dependency So you can use the parent resource name as parameter for you module:
module site_config 'module.bicep' = {
name: 'site_config'
params: {
resource_name: app.name
...
}
}
This is equivalent to:
module site_config 'module.bicep' = {
name: 'site_config'
params: {
resource_name: full_site_name
}
dependsOn: [ app ]
}

Azure Bicep - Additional IP Restrictions

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

How can I combine dynamic settings and static settings in a Function App using Bicep?

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

Is there a way to deploy Azure resource modules not in the main file?

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?

Resources