Strengthen Your Infrastructure-As-Code with Azure Bicep

Photo of Mikołaj Maćkowiak

Mikołaj Maćkowiak

Updated Oct 17, 2023 • 30 min read
Back view of modern programmer sitting and writing code in dark room-4

Azure Bicep language has emerged as a popular successor to the ARM-JSON format for declaring resources in Azure. It overcomes some of the key issues that held developers back, and helps DevOps engineers and developers create infrastructure-as-code that is far more flexible, adaptable, and fast to author and deploy.

While Microsoft Azure Resource Manager templates served their purpose well, the JSON format came with its own range of difficulties. Bicep was developed as a direct response to those difficulties, and uses a different language.

The popularity of Azure Bicep extension for infrastructure-as-code (IaC) projects comes from its simplified syntax and superior integration abilities with Azure CLI and Visual Studio Code. What’s more, existing ARM templates can still be used, as anything created in Azure Bicep will be transpiled into an existing ARM template when deployed.

So, with those general benefits in mind, let’s look at Azure Bicep’s features in more detail, and find out how it can help us strengthen our infrastructure-as-code.

What is Azure Bicep?

Bicep is a a domain-specific language (DSL) used in deploying Azure resources declaratively (in this case, those resources are infrastructure) to Azure. Azure Bicep syntax is simplified and concise in comparison with Microsoft Azure Resource Manager (ARM) templates, which makes it easier to learn and use. As a result, it is a great basis for an introduction to the possibilities of infrastructure-as-code.

As Azure Bicep infrastructure can be used continuously throughout the development lifecycle of your project, you can ensure that your infrastructure remains consistent. It supports code re-use without copy-and-paste, has an improved type safety, and better support for modularity, making for a superior authoring experience. Bicep is OpenSource, so the source code can be accessed freely on GitHub - with such easy access, its development community consistently grows.

Bicep vs ARM templates

Azure Bicep is a viable, effective alternative to authoring traditional Microsoft ARM templates for a few key reasons:

  • Bicep is a transparent abstraction over ARM templates, which means that anything, outside of the temporary limitations we know about, that can be done in an ARM template can be done in Bicep.
  • Resource types and built-in functions that are ARM template valid can also be used with Bicep from the very start.
  • Bicep code is compiled (or transpiled) to standard ARM template JSON files, which effectively treats the ARM template as an Intermediate Language.
  • Bicep provides a much more concise, clean syntax compared to the JSON format of ARM templates. In some instances, the Bicep will be around half the size of the equivalent ARM template. This is because expressions, parameters, strings, logical operators, deployment scope, variables, and more, in Bicep require shorter code.

Benefits of Azure Bicep

Let’s take a look at the more general benefits of Azure Bicep.

  • Support for all resource types and API versions. Preview and GA versions for Azure services are supported by Bicep as soon as they are provided, without the need for any updates.
  • Simplified syntax. As explained above, Bicep uses a much simpler, cleaner syntax style compared to its counterparts.
  • Modularity. Bicep code can be divided into easy-to-manage modules, each of which deploys a set of related resources. This simplifies the development process and speeds up the process when reusing code.
  • 1st class authoring experience with Visual Studio Code (VS Code). VS Code, when used as your editor of choice, provides rich type-safety, intellisense, and syntax validation. Support for Visual Studio 2022 is underway.
  • Idempotent files. Bicep files can be deployed over and over again to achieve the same resource type and state - there is no need to develop many files to represent updates for later stages in the development lifecycle.
  • No state or state files to manage. As all state data is stored in Azure, multiple users can collaborate in development and feel confident that every individual update will be handled as expected.
  • Resource Manager. Azure Bicep’s Resource Manager simplifies the process of ordering operations, and allows full deployment with just one command. Interdependent resources are deployed in the correct order; and, if possible, in parallel for a faster finish.
  • Open-source, with Microsoft support. Bicep is free to install and use, with no pay grades for premium functionality. As it is also a Microsoft-owned product, users have the benefit of a reliable and professional support team.

How do you deploy resources with Azure Bicep?

The benefits of Azure Bicep are clear. So, the next step is to begin deploying resources - but how is this done? What steps are involved in deploying Azure Bicep code? And how do you know your code is correct?

Resources vs modules

Deployment Scope. It is important to understand the difference between working with resources and working with modules, and establish the focus of your deployment - this is your deployment scope. Deployment scopes for resources are resource groups, subscriptions, management groups, or a tenant.

Resources. A resource is an entity managed by Azure. Virtual machines, virtual networks, storage accounts or even resource groups or subscriptions are all examples of Azure resources. In Bicep, resources are represented by a resource declaration that starts with a resource keyword followed by a symbolic name (which is used for referencing within other resources or modules in bicep - not to be confused with the resource name, which is defined in the resource body), resource type and API version, plus a set of properties that shape it to your needs.

Child resources. This type of resource exists only within the context of another - one resource is literally the child of another. There are limitations to which type of resource can become a child resource to different types of parent resources.

Extension resources. This type of resource modifies another resource. An example of this would be assigning an access role or diagnostic settings to a resource.

Modules. Deployments can be organized into modules with Bicep. Each module is a Bicep file that is deployed from another Bicep file. Modules keep your Bicep file clear and easy to understand, but grouping together complex details and content. Modules can be re-used and shared with other developers in your organization. Technically, a module (or in more general - any deployment) is a resource of Microsoft.Resource/Deployments type. Bicep modules are converted into a single Azure Resource Manager template with nested templates. A nested template is effectively a resource of type Microsoft.Resource/Deployments written in another ARM template that in one of its parameters takes the ARM template that should be deployed. A module can be a local file, template spec or link to Bicep registry. Modules also allow you to deploy to a different scope than that of the calling module.

Parameters vs variables

Establishing the parameters of your Bicep file is the next important step. Different parameter values will enable you to reuse a Bicep file for different environments. You define them using keyword ‘param’ followed by the parameter name, type and optionally a default value. Additionally, you can annotate the parameter to set one or more of the following:

  • Whether a string or object is secure
  • What values are allowed through deployment
  • Constraints on string length
  • Minimum and maximum integer values

Developers should also include a description for the param, so that it’s purpose can be easily seen. You can also add a metadata param to define custom properties of any value or type. To reduce the number of individual params used in your template, pass related values in as an object.

Variables, on the other hand, are used to simplify your Bicep file development. Variables can be used to contain complicated expressions, which can then be dropped in as a whole as it is needed. Resource Manager resolves these variables before deployment begins, replacing them with the complex expressions they represent. In contrast with params, no data type needs to be specified when setting up a variable, because it is implied by the expression and value it contains.

Referencing resources

You can reference resources within your template file in order to manage the order that resources are deployed - it may, for example, be essential for you to deploy a logical SQL server before your database. To do this, you must make one resource dependent on the one it needs to follow.

You can use the dot-syntax to access Resource Properties and module outputs. The VS Code extension is also very helpful for seeing all available properties of a resource (though it’s important to remember that source data is not always 100% accurate).

Resource dependencies come in two forms: implicit dependency and explicit dependency. Implicit dependencies mean that one resource declaration references another resource in the same deployment - when this is the case, explicit dependencies are not necessary.

Explicit dependencies use the dependsOn property to describe the relationship between resources. They should, however, be avoided where possible - instead, imply dependencies through their symbolic names. You can visualize your network of dependencies in Visual Studio Code.

You can reference resources that are not deployed in your current Bicep file by using the ‘existing’ keyword and the resource’s symbolic name. Likewise, you must also establish whether the resource is in the same scope or a different scope.

Conditionals and loops

You need to be able to individually deploy resources or modules in Bicep - conditionals allow you to do this. Use the ‘if’ keyword to determine whether the resource or module is deployed; then, during deployment, the conditional value will resolve to either true or false.

Note that conditionals can only be added to whole resources or modules. Conditions can be established using parameters, and can also take into account dependencies, references, and list functions. To conditionally set a property value, you can use the ternary operator: ‘condition ? value-if-true : value-if-false’.

Iterative loops can define multiple copies of a resource, variable, module, property, or output, to help you avoid syntax repetitions and set the number of copies that need to be created during deployment.

They use the ‘for’ keyword. Loops can be declared by an integer index (for creating a specific number of instances), items in an array (for number of instances per element), items in a dictionary object (for an instance for each item in an object), integer index and items in an array, and conditional deployment.

Compile and deploy

In order to deploy, your Bicep files need to be converted to ARM templates. This is done using the ‘build’ command - but, generally, this is done automatically, and you don’t need to run the command manually unless you want to preview the ARM template JSON. The command ‘decompile’ converts ARM template JSON back to Bicep.

To deploy your resources to Azure, your Bicep file must be stored locally, and you must be using an up-to-date Azure CLI. The correct required permissions must be set up, with write access on the resources that will be deployed and access to all operations on the Microsoft.Resources/deployments resource type.

Establish your deployment scope, as discussed above, and the parameters. You can preview the effect of your deployment, and validate the Bicep file for errors by running the ‘what-if’ operation. Make sure you give your deployment a name that reflects its purpose.

LoadTextContent and LoadJsonContent functions

These functions are specific to Bicep, and therefore constitute a game-changer against ARM template alternatives. The ‘loadTextContent’ function, along with the ‘loadFileAsBase64’ function, were introduced in Bicep v0.4 while ‘loadJsonContent’ is available from 0.7 version onwards.

They are compile-time functions, which means that they are run during the compilation phase, which happens on the developer's local machine (see more details below). The content of files that are specified as an argument will become part of the template. They require compile-time arguments, so you cannot use parameters to specify which file should be loaded into an ARM template.

The most common use-cases for those functions is when a resource requires either a JSON, or an XML or a script file as its property, i.e. Azure Policies (can take ARM templates to be deployed), Deployment Scripts (takes a script) or API-Management APIs (can take openAPI specifications). Although you could use raw strings (strings that start and end with a triple apostrophe- ’’’), it’s often better to author necessary files outside to get editor syntax highlighting or force language-specific linter rules.

LoadTextContent and loadFileAsBase64 functions have a serious limitation - the length of a string in an ARM template cannot be more than 131,072 characters (9 KB binary file size). Up until v0.7, a common pattern was to use loadTextContent wrapped in a ’json’ function in runtime converts a string to a JSON object.

If the loaded JSON file was not minified (stripped of unnecessary spaces), all the formatting characters, plus characters necessary to escape quotation marks, were quickly reaching this limit. The Bicep community was regularly hitting this limit, so the team decided to implement a function called ‘loadJsonContent’ - since an ARM template is a JSON itself, JSON files could be “natively” compiled into the template and the character limit will not be taken into account.

Additionally, loadJsonContent allows users to select particular parts of a JSON file and dynamically build a template based on external, non-Bicep files.

However, there’s another limit - the 4 MB maximum template size. Although the ARM template is minified before deployment, we still need to be cautious when including files in templates.

Azure Bicep deployment flow

Sometimes it’s not entirely obvious what happens under the hood once your code is written - your Bicep files should result in real resources available for you to use in Azure Cloud, but errors and setbacks can happen.

The process may seem complicated, but there are three significant areas in which errors could occur during the deployment process - which developers should be aware of for faster, more effective troubleshooting.

When you author Bicep code, it is not uncommon to encounter errors. Some of the most common errors include:

  • “BCP032: The value must be a compile-time constant”
  • “BCP120: This expression is being used in an assignment to the property [propertyName] of the [objectName] type, which requires a value that can be calculated at the start of the deployment”
  • “BCP177: This expression is being used in the if-condition expression, which requires a value that can be calculated at the start of the deployment.”
  • “BCP178: This expression is being used in the for-expression, which requires a value that can be calculated at the start of the deployment”
  • “BCP181: This expression is being used in an argument of the function [functionName], which requires a value that can be calculated at the start of the deployment.”
  • “BCP182: This expression is being used in the for-body of the variable [variableName], which requires values that can be calculated at the start of the deployment.”

These errors are related to how the flow between you typing the code and resources being provisioned on Azure is organized.

Deployment flow stages

The three main stages of the Azure Bicep deployment flow are: code compilation on a local machine, deployment to ARM API, and calling resource provider APIs by ARM runtime.

bicep blogpost-Page-1.drawio

Local machine stage

The first step is to compile the Bicep code into JSON-ARM template code. You can do this manually by using the Bicep build command, or, if you specify the Bicep file as a source for deployment, this will be done for you. In both cases, compilation occurs on your machine.

When you use Bicep modules, during compilation time, they are included in the output ARM template; therefore, values specifying paths of modules need to be compile-time constants, which means that they must be known during the compilation phase. Although, as mentioned above, the Azure CLI tool does the compilation for you before the deployment phase - they are separate processes and cannot be treated as one.

ARM API stage

The next phase is the deployment. Azure CLI takes the generated ARM template and, together with parameters file, uploads it to Azure Resource Manager API to be processed. Once there, ARM API first runs a template validation - this involves checking that the template is syntactically correct, all parameters have correct types and the required ones are supplied.

If the template cannot be processed correctly, it executes additional internal procedures to fail. Values that can be determined before the start of this phase are the deployment-time constants.

Usually this refers to parameters passed to the deployment. They are used to determine what the template will look like - which resources (when using if statement) and how many (when using loops) are going to be deployed. Since compilation occurs before deployment, the compile-time constants are also deployment-time constants.

Resource provider API stage

The last phase - runtime - can be described as the process of going through the template and executing API calls to Resource Provider APIs. In this phase, when preparing the body for API calls, ARM runtime can get a value from another resource property or module output and transform it using various functions.

Before the runtime phase starts, we need to know the resource names. An important thing to notice is that each module we call is a deployment - even though it’s triggered by ARM API and not by a user, it's still a deployment, and ‘params’ passed to the module definition will become deployment-time constants in the called module.

Knowing that, we can leverage modules to convert runtime values (i.e. resource properties) to deployment-time constants required for resource names or locations, loop inputs, if-conditions, etc.

9 steps to create multiple Azure Web Apps with Azure Policy for collecting logs using Azure Bicep

Now let’s try to use the knowledge we gained and deploy 3 resource groups each with an Azure App Service Web Apps. Additionally. We will write an Azure Policy that will make sure that all Web Apps have Diagnostic Settings enabled and write logs to a Log Analytics Workspace.

Each WebApp requires an App Service Plan. WebApp will be Linux-container based, so it will require an Azure Container Registry with rights to pull images. We will place it in a core resource group for Log Analytics Workspace and Container Registry. The architecture will look like this:

bicep blogpost-Page-2.drawio

We will also need to do a subscription-level deployment. Therefore, we need to have a module for deploying the core resource group and a module for project resource groups. The code for the Azure Policy deployment will be in yet another Bicep file, which we will manually compile and include it as a JSON to be passed to the policy resource.

Step 1

The first step will be to create a top-level module that targets the subscription. In this module, we will create resource groups necessary to hold resources:


targetScope = 'subscription'
 
param location string
 
resource coreResourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = {
 name: 'core-rg'
 location: location
}
 
var projects = {
 project1: {
   webAppCount: 1
 }
 project2: {
   webAppCount: 3
 }
 project3: {
   webAppCount: 1
 }
}
 
resource projectResourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = [for item in items(projects): {
 name: '${item.key}-rg'
 location: location
}]

In the first line, we specify that this Bicep file will be targeting subscription as deployment target.

Next, we declare a parameter, in which Azure Region resources will be created.

Lines 5-8 are declaring a resource group to hold core resources.

In line 10, we declare an object (which is in fact a kind of dictionary) that will hold configuration of our projects’ resource groups. We follow it with the resource group declaration, which will loop over items in the object. The resource group name will be taken from the key and followed by a ‘-rg’ suffix.

Let’s save it as main.bicep. Now we move to create a module to deploy the Log Analytics Workspace and Container Registry.

Step 2


param location string
 
param logAnalyticsName string
param logRetentionInDays int = 30
resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-06-01' = {
 name: logAnalyticsName
 location: location
 properties: {
   sku: {
     name: 'PerGB2018'
   }
   retentionInDays: logRetentionInDays
 }
}
output logAnalyticsId string = logAnalyticsWorkspace.id
 
 
param containerRegistryName string
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-09-01' = {
 name: containerRegistryName
 location: location
 sku: {
   name: 'Basic'
 }
}
output containerRegistryId string = containerRegistry.id

Again, we first need to define parameters and a location for all of our resources. Then, we specify a parameter for the name of the Log Analytics Workspace and an integer value indicating how many days’ logs will be kept.

We also specify a default value of 30 days that will be applied if it’s not set. In lines 6-15, we declare the Log Analytics Workspace resource itself and in line 16 we declare the output of the module with its resource ID.

Then, we move to part with the specification for the Container Registry. As you can see, we don’t need to specify parameters on top of the file, but we can do this in any place, so we can keep resource-specific parameters close to the resource declaration.

Let’s save the file as core.bicep.

Step 3

Now we need to go back to the main.bicep file, where we will declare deploying the module in the core resource group:


var _dep = deployment().name
 
module coreModule 'core.bicep' = {
 name: '${_dep}-core'
 scope: coreResourceGroup
 params: {
   location: location
   logAnalyticsName: 'system-log'
   containerRegistryName: 'mycompanyacr'
 }
}

First, we start with something you may find unusual - we declare a variable ‘_dep’ that will hold the current deployment name. As I mentioned earlier, modules are in fact resources of a ‘Microsoft.Resource/Deployment’ type, and they need to have a name.

To avoid any potential clash with other deployments, when we build multi-level deployments at Netguru, we prefix each sub-deployment with a parent deployment name. However, you need to be careful, as the deployment/module name has a limit of 64 characters.

Once we have the variable that will help us use the current deployment name, we define a module and use the created file as the source. We specify the name using the string interpolation technique.

We need to define the value of parameters that the module takes, and provide the scope of the module - and then we set it to the resource group we defined earlier by providing its symbolic name. We could use a resourceGroup function here and provide the resource group name, but if we use the resource symbolic name, Bicep compiler will know that we need the resource first, so it will auto generate the ‘dependsOn’ clause rather than needing to do this manually.

Step 4

Let’s prepare a module file with WebApps. Besides the WebApp itself, we will need an App Service Plan to host it.


param location string
param projectName string
param webAppCount int
 
resource appServicePlan 'Microsoft.Web/serverfarms@2020-12-01' = {
 name: 'plan-${projectName}'
 location: location
 properties: {
   reserved: true //true indicates that app service plan is linux based
 }
 sku: {
   name: 'B1'
 }
}
 
resource webApplication 'Microsoft.Web/sites@2018-11-01' = [for i in range(0, webAppCount): {
 name: 'webapp-${projectName}-${uniqueString(resourceGroup().id)}-${i}'
 location: location
 tags: {
   'hidden-related:${appServicePlan.id}': 'Resource'
 }
 properties: {
   serverFarmId: appServicePlan.id
 }
 identity: {
   type: 'SystemAssigned'
 }
}]

Again, we need to specify the parameters that we will use to create resources. The WebApp name must be globally unique, so we use a uniqueString function to generate a pseudo-random value with seed set to resource group ID. As in the parameter, we declare how many WebApps we need, we create a list of integers using ‘range’ function that we will iterate over to create a series of WebApps.

Step 5

Now we need to set permission to the Container Registry with the identities of the created WebApps. A permission role assignment is an extension resource. Since Container Registry is in a separate resource group, we need to use a module to deploy to a different scope. So let’s save this file as webapp.bicep and open a new file to prepare an extension resource to an existing resource:


param containerRegistryName string
param identities array
 
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2021-09-01' existing = {
 name: containerRegistryName
}
 
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = [for id in identities: {
 name: guid('acrPull', id)
 scope: containerRegistry
 properties: {
   roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
   principalId: id
   principalType: 'ServicePrincipal'
 }
}]

The module takes the container registry name and list of identities to grant the AcrPull permission. Since the container registry is already deployed, we just need a reference to it - so we declare the resource using the ‘existing’ keyword.

Then, once again, we iterate through the list of identities and build the role assignment resource. The name must be a GUID, so we must generate one based on the name of the permission and identity. We set the scope to the Container Registry (if we don’t do this - the role would be assigned to the resource group).

Built-in role definitions have a constant GUID that we can safely put in the template body. To get the GUID, we can run an Azure CLI command ‘az role definition list --name AcrPull’.

Let’s save this as acrpull.bicep and declare using it in the webapp.bicep file:


param containerRegistryId string
var _containerRegistryId = split(containerRegistryId, '/')
module acrpull 'acrpull.bicep' = {
 name: '${deployment().name}-acrpull'
 scope: resourceGroup(_containerRegistryId[2], _containerRegistryId[4])
 params: {
   containerRegistryName: _containerRegistryId[8]
   identities: [for i in range(0, webAppCount): webApplication[i].identity.principalId]
 }
}

First, we need to get the containerRegistry resource ID to know which ACR needs WebApps permission. Azure Resource IDs are always constructed in the same way, so we use a ‘split’ function to get particular parts required to set the scope properly and get the resource name:

Under index 2 is always the subscription GUID, the 4 indicates the resource group name, and 8 is the name of the resource.

As we need to provision an array of identities, we again use a loop over this same range as we used for deploying WebApp resources, and we reference each to get the principalId of their managed identity.

Step 6

Once we have written this module, we need to declare that it will be deployed in the resource groups we created. We go back to the main.bicep and define the module with a loop:


module webAppModules 'webapp.bicep' = [for (item, index) in items(projects): {
 name: '${_dep}-webapp-${index}'
 scope: projectResourceGroup[index]
 params: {
   location: location
   containerRegistryId: coreModule.outputs.containerRegistryId
   projectName: item.key
   webAppCount: item.value.webAppCount
 }
}]

What’s new here is that for the syntax, we use an indexed look in order to reference the resource group. The Items function will always return items in this same order, so we can be sure that we will deploy to the correct resource group.

Step 7

Now we can move to the last part, in which we will write a template to deploy by Azure Policy to configure Diagnostic Settings for all WebApps:


param diagnosticSettingsName string
param logAnalyticsId string
param webAppName string
 
resource webApp 'Microsoft.Web/sites@2021-03-01' existing = {
 name: webAppName
}
resource webAppLogs 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
 name: diagnosticSettingsName
 scope: webApp
 properties: {
   workspaceId: logAnalyticsId
   logAnalyticsDestinationType: 'Dedicated'
   logs: [
     {
       categoryGroup: 'allLogs'
       enabled: true
     }
     {
       categoryGroup: 'audit'
       enabled: true
     }
   ]
 }
}

DiagnosticSettings resource is also an extension resource type, therefore we need to have a reference to the resource. As we need to deploy to the resourceGroup with WebApp, we just need to specify the name of the WebApp.

Now, we need to make this an ARM template - let’s save it as diagnostics.bicep, and we can either use az bicep build -f diagnostics.bicep; or, if we are using Visual Studio Code, we can right-click on the file and select the “Build” option. As a result, we will have a diagnostics.json file in our folder.

Step 8

Now, let’s go back to the main.bicep file and declare the Azure Policy Definition, as this type of resource needs to be created on subscription level. We will also assign the definition to the subscription, and permission the managed service identity with the roles specified in the Policy Definition. It is common practice to have Azure Policies in a JSON file, so let’s create one and name it ‘policy.json’:


{
 "mode": "All",
 "policyRule": {
   "if": {
     "field": "type",
     "equals": "Microsoft.Web/sites"
   },
   "then": {
     "effect": "DeployIfNotExists",
     "details": {
       "roleDefinitionIds": [
         "/providers/microsoft.authorization/roleDefinitions/749f88d5-cbae-40b8-bcfc-e573ddc772fa",
         "/providers/microsoft.authorization/roleDefinitions/92aaf0da-9dab-42b6-94a3-d43ce8d16293"
       ],
       "type": "Microsoft.Insights/diagnosticSettings",
       "name": "[parameters('profileName')]",
       "existenceCondition": {
         "allOf": [
           {
             "count": {
               "field": "Microsoft.Insights/diagnosticSettings/logs[*]",
               "where": {
                 "allOf": [
                   {
                     "field": "Microsoft.Insights/diagnosticSettings/logs[*].enabled",
                     "equals": false
                   }
                 ]
               }
             },
             "equals": 0
           }
         ]
       },
       "deployment": {
         "properties": {
           "mode": "incremental",
           "parameters": {
             "diagnosticSettingsName": {
               "value": "[parameters('profileName')]"
             },
             "logAnalyticsId": {
               "value": "[parameters('logAnalyticsId')]"
             },
             "webAppName": {
               "value": "[field('name')]"
             }
           }
         }
       }
     }
   }
 },
 "parameters": {
   "logAnalyticsId": {
     "type": "String"
   },
   "profileName": {
     "type": "String",
     "defaultValue": "logs"
   }
 }
}

Next, we need to create a Policy Definition resource using both policy.json and diagnostics.json files. To do that, we leverage the ‘union’ function that allows you to combine multiple objects:


resource policyDefinitionLogs 'Microsoft.Authorization/policyDefinitions@2020-09-01' = {
 name: 'policy-logs-webapp'
 properties: union(
   {
     displayName: 'App Service Web App Diagnostic Settings'
     policyType: 'Custom'
     description: 'This policy definition will enable logging of App Service Web App into Azure Log Analytics'
     metadata: {
       category: 'Logging'
     }
   },
   loadJsonContent('policy.json'),
   { policyRule: { then: { details: { deployment: { properties: { template: loadJsonContent('diagnostics.json') } } } } } }
 )
}
 
resource policyAssignment 'Microsoft.Authorization/policyAssignments@2020-09-01' = {
 name: 'policy-logs-webapp'
 location: location
 identity: {
   type: 'SystemAssigned'
 }
 properties: {
   displayName: 'WebApp Diagnostic Settings'
   enforcementMode: 'Default'
   policyDefinitionId: policyDefinitionLogs.id
   parameters: {
     logAnalyticsId: {
       value: coreModule.outputs.logAnalyticsId
     }
   }
 }
}
 
resource policyRoleAssignments 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = [for role in loadJsonContent('policy.json', '$.policyRule.then.details.roleDefinitionIds'): {
 name: guid('policyAssignment', toLower(role))
 properties: {
   roleDefinitionId: '${subscription().id}${role}'
   principalId: policyAssignment.identity.principalId
   principalType: 'ServicePrincipal'
 }
}]

Notice that we used loadJsonContent to take both the Policy file and compiled ARM Template with the diagnosticSettings deployment. This means we don’t need to author it in this file, and we could benefit from Visual Studio Code plugin when creating the deployment template.

Additionally, we extracted necessary roles from the Policy Definition file and used them to assign permissions to the Managed Service Identity.

Step 9

Once we're done, we can run our deployment using Azure CLI:

az deployment sub create -n webapps -l westeurope -f main.bicep -p location=westeurope

After the deployment is complete, we can go to our subscription Policy blade in Azure Portal and see that we have a Policy assignment because our WebApps are not compliant.

To fix this, we need to create a remediation task, also using CLI:

az policy remediation create --name webapp-logs --resource-discovery-mode ReEvaluateCompliance --policy-assignment policy-logs-webapp

After the task runs, we can see that the deployment we provided from the JSON file has been run on the resource groups with WebApps.

As our policy is now in place, any additional WebApps we create in the subscription with this policy will automatically receive the same diagnostics settings after several minutes, which is when the policy evaluation kicks in.

Start using Azure Bicep for your infrastructure-as-code

Once implemented, Azure Bicep provides a much more effective and intuitive infrastructure-as-code method than ARM templates. Developers are increasingly turning to this OpenSource option to streamline their infrastructure deployment at every stage of their project development lifecycle.

With a fast-building online community of developers, the support of Microsoft, and a simpler, easier coding experience, more and more developers are turning to Bicep on Azure. To keep up with popular development progress, and ensure you’re not using out-dated concepts, take a look at our overview of development on Azure. You’ll be sure to find a solution to propel your development team forwards!

Photo of Mikołaj Maćkowiak

More posts by this author

Mikołaj Maćkowiak

Mikołaj is a Cloud Architect and a leader of Azure technology in Netguru. Previously, he worked as...
Fuel your digital growth with cloud solutions  Discover powerful tools to drive revenue in the cloud  Learn more!

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business