When deploying or managing resources within Azure, leveraging the Azure Resource Manager can be a great way to codify your service deployments, but as you’ve seen in my other post on ARM, the JSON templates themselves are not optimised for human-readability.

While there are other options out there for Declarative Languages such as Terraform, Microsoft wanted to ensure that its first-party tooling provided the best possible experience for deploying resources within Azure, which led to the creation of Bicep. Bicep is a domain-specific language for authoring Infrastructure as Code within the context of Azure; therefore, its use doesn’t extend to other Cloud service providers, network devices, or operating systems.

In this post, we’ll walk through the creation of a simple Bicep template which leverages modules to create an Azure Web App for Containers which will host a Quickstart Docker image, the App Service Plan it it runs on, and a Resource Group to group all the artefacts.

Bicep Basics

As previously noted, the Bicep language is an example of a Domain-Specific Language, but it is actually an abstraction on top of the standard JSON-based Azure Resource Manager templates. When a Bicep template is deployed to Azure (or through the CLI) it’s transpiled into a traditional ARM template, which is then executed.1 Because they exist at a similar level of abstraction, the Bicep transpiler converts the Bicep template directly into JSON, in a similar fashion to the way TypeScript is transpiled into JavaScript for use by your web browser.

While this post isn’t intended as an exhaustive introduction to Bicep, or the differences between Bicep and ARM, it’s worth covering off a quick comparison of the same resource defined in both ARM and Bicep. One of the major benefits of Bicep is that it’s much more human readable, and has a much simpler syntax. As you can see in the following example from my previous post on ARM templates in which we defined a simple public DNS zone with an ARM template, the contrast in complexity of the syntax is clear.

Example ARM and Bicep Resource Definitions

Azure DNS Resource - ARM

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "resources": [
        {
            "type": "Microsoft.Network/dnsZones",
            "apiVersion": "2018-05-01",
            "name": "example.org",
            "location": "global",
            "properties": {
                "zoneType": "Public"
            }
        },
    ],
}

The above ARM template uses the Azure Resource Manager’s JSON schema. While JSON is a well-understood data format, it’s optimised for consumption by a machine.

Azure DNS Resource - Bicep

The example below shows the same resource as above, defined with Bicep.

resource example_org 'Microsoft.Network/[email protected]' = {
  name: 'example.org'
  location: 'global'
  properties: {
    zoneType: 'Public'
  }
}

As you can see, the Bicep template is more concise, and much more human-readable.

Deploying Bicep templates

It’s worth nothing that Bicep templates can be deployed using the Azure CLI in the same way as standard ARM templates, by using the az deployment command. E.g.:

Azure CLI - ARM Template Deploy

$ az deployment group create --template-file ./template.json --resource-group <resourceGroupId>

Azure CLI - Bicep Template Deploy

$ az deployment group create --template-file ./template.bicep --resource-group <resourceGroupId>

Creating the Bicep Template

Prerequisites

Before you begin authoring Bicep templates, you’ll need a couple basic prerequisites: A text editor, and the Bicep tooling.

While there are many choices of text editors which you can use to get started composing Bicep templates, my personal preference is to use Visual Studio Code which has a great Bicep extension that provides features such as Intellisense, code snippets, code formatting, a Bicep linter.

Similar choices exist for accessing the Bicep tools; however, my preference is to either take advantage of the curated Azure Bicep remote container development environment or to install Bicep through the Azure Cli. Bicep CLI will be installed automatically by Azure CLI >v2.20.0 when a command is executed that requires it; however, it can be installed manually by issuing the command: az bicep install.

All the Azure Resource Manager definitions can be found in the documentation, which includes all the possible property values, types, and API versions.

Project Layout

Bicep projects can take many shapes, but I tend to structure my repositories in a similar way:

modules/ <- Keep reusable resource modules here
modules/{module}.bicep <- Bicep modules
parameters.{environment}.json <- JSON file which defines environment-specific resource parameters
main.bicep <- Primary Bicep file which is used for deployment
README.md <- Can’t forget your readme

There is no right way to lay out your project, so just find something that works for you.

Resource Group

When creating resources in Azure, resource groups are used to identify and categorise the objects within the Azure Resource Manager, a logical container in which to group Azure Resources; therefore, we’ll start by creating a resource group to contain our App Service Plan and Web App.

Deployments to the Azure Resource Manager are targeted at a specific scope, which defines a level within the hierarchy of Azure resources, from higher to lower: tenant -> management group -> subscription -> resource group -> resource. Typically, when deploying Azure resources with ARM templates the target scope would be a resource group; however, as we are defining the resource group within our Bicep template, we must target the template’s deployment at the subscription itself, so we’ll begin the template by defining the targetScope property.

// main.bicep

targetScope = 'subscription'

Next, we’ll create an empty resource definition for the resource group itself, based on the template reference. This template reference is a great resource to understand all the various configuration options for each resource, and it identifies which options are required for the deployment to succeed.

// main.bicep

targetScope = 'subscription'

resource symbolicname 'Microsoft.Resources/[email protected]' = {
  name: 'string'
  location: 'string'
  tags: {}
  properties: {
  }
}

We’ll change the symbolic name to rg to identify our resource group elsewhere in the template, which will become useful as we deploy our other resources. We can also change the location of the resource group to be the location of our deployment, but using the location property of the deployment(). This value will be defined based the location we specify when we execute the deployment step itself.

// main.bicep

targetScope = 'subscription'

resource rg 'Microsoft.Resources/[email protected]' = {
  name: 'string'
  location: deployment().location
  tags: {}
  properties: {
  }
}

While we can pass values directly to this resource definition, such as the resource group’s name, we can begin to parameterise the template by defining these values in an external source, such as the parameters.json.

For this purpose, we’ll create a JSON file using the parameters.dev.json convention, to identify this file as associated to our development environment.

Within the parameters file, we can group all of the values related to our resource group under a single object, which will allow us to use dot notation to reference specific values.

// parameters.dev.json

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "resGroup": {
            "value": {
                "name": "demo-app-service-rg",
                "tags": {
                    "ApplicationName": "Web App",
                    "Description": "A sample Web App deployed with Azure Bicep",
                    "Environment": "Development"
                }
            }
        }
    }
}

Here, we’ve created an object, resGroup, which contains a string value, name, which will be the name of our resource group. It also contains an object, tags, which is the key:value pairs that define a set of tags to be applied to the resource group.

The resGroup object then has to be defined as a parameter within our main.bicep. We can also reference the resource group name we’ve defined in the parameters.dev.json, along with the tags object, by using the dot notation, resGroup.name and resGroup.tags respectively.

Completed Resource Group Template

Finally, we can remove the empty properties object, which is unused in this example:

// main.bicep

targetScope = 'subscription'

param resGroup object

resource rg 'Microsoft.Resources/[email protected]' = {
  name: resGroup.name
  location: deployment().location
  tags: resGroup.tags
}

Testing a deployment

At this point, we should be ready to test and validate the current state of our Bicep template, by creating our Resource Group.

Note: The final, complete Bicep template can be deployed at once, with all the resources included. We’re deploying in stages to verify our work.

From the shell, we can use the Azure CLI to create a deployment using our template file. Unlike when resources are deployed into a resource group using the az deployment group command, we’ll instead use the az deployment sub command to specify that the deployment should be targeted at the Subscription scope.

$ az deployment sub create --template-file ./main.bicep --parameters './parameters.dev.json' --location westus2 --output jsonc

We can pass the location of our parameters file using the --parameters <param file> syntax, and the target location for our deployment, --location <azure region>. Finally, we can choose to specify an output format for the response, which in the case of our example has been set to colourised JSON.

After executing the deployment, you’ll receive a response which outlines the properties of the resource which was created, and a provisioningState: "Succeeded", like a standard ARM deployment:

{
  "id": "/subscriptions/<sub>/providers/Microsoft.Resources/deployments/main",
  "location": "westus2",
  "name": "main",
  "properties": {
    "correlationId": "5339bfb9-xxxx-xxxx-xxxx-0214c99a39a1",
    "debugSetting": null,
    "dependencies": [],
    "duration": "PT2.6741588S",
    "error": null,
    "mode": "Incremental",
    "onErrorDeployment": null,
    "outputResources": [
      {
        "id": "/subscriptions/<sub>/resourceGroups/demo-app-service-rg"
      }
    ],
    "outputs": null,
    "parameters": {
      "resGroup": {
        "type": "Object",
        "value": {
          "name": "demo-app-service-rg",
          "tags": {
            "ApplicationName": "",
            "Description": "",
            "Environment": ""
          }
        }
      }
    },
    "parametersLink": null,
    "providers": [
      {
        "id": null,
        "namespace": "Microsoft.Resources",
        "providerAuthorizationConsentState": null,
        "registrationPolicy": null,
        "registrationState": null,
        "resourceTypes": [
          {
            "aliases": null,
            "apiProfiles": null,
            "apiVersions": null,
            "capabilities": null,
            "defaultApiVersion": null,
            "locationMappings": null,
            "locations": [
              "westus2"
            ],
            "properties": null,
            "resourceType": "resourceGroups"
          }
        ]
      }
    ],
    "provisioningState": "Succeeded",
    "templateHash": "11949234530450461601",
    "templateLink": null,
    "timestamp": "2021-09-13T23:09:11.384837+00:00",
    "validatedResources": null
  },
  "tags": null,
  "type": "Microsoft.Resources/deployments"
}

App Service Plan

The next step in provisioning our Web App is to create the template to deploy the App Service Plan which is, in essence, the Virtual Machine on which our Web App will be hosted.

Because a key goal of Infrastructure as Code is reusability, we’ll abstract some of the configuration for the App Service Plan into a Bicep module, which will be referenced by our main.bicep template. These modules could be shared outside our project, or used to create a library of standard configurations for various Azure resources.

Note: Every Bicep template on its own can be used as a module, so there is no functional difference to creating a module than any other Bicep template.

To begin creating the App Service module, we start with a blank file called appServicePlan.bicep , which we’ll store under modules/ folder, and we’ll start by defining our resource which we’ll call appServicePlan.

// modules/appServicePlan.bicep

resource appServicePlan 'Microsoft.Web/[email protected]' = {
  name: 
  location: 
  kind: 
  properties: {
    
  }
  sku:
}

When you’re creating Bicep modules, you can choose how much of the configuration you present upstream to the consumer of the template by defining the parameters. This is really useful when the creator and consumer of the templates do not share the same level of domain knowledge, e.g.: a module to deploy an Application Gateway may be pre-configured with the desired settings based on your organisation’s policy, and an application developer simply needs to consume this pre-configured module in their own template.

To provide an example of this, we’ll demonstrate adding a suffix to the name of the App Service Plan, generated from the Environment tag we’re applying to the Resource Group. This way, a user can specify their resource’s name, e.g.: demo-app-service-plan, and the environment will automatically be appended based on our company policy.

Let’s receive the App Service Plan’s name parameter as a string, along with the string name of the environment. Because the sku consists of both a name and a tier, we’ll receive those as an object:

// modules/appServicePlan.bicep

param appServicePlanNameParam string
param appServicePlanSkuParam object
param environmentParam string

resource appServicePlan 'Microsoft.Web/[email protected]' = {
  name: 
  location: 
  kind: 
  properties: {
    
  }
  sku:
}

As we’ll be consuming this Bicep file as a module in our example, these parameters will be passed into the module by our main.bicep; however, if this same template was being deployed as a standalone template, these parameters could be passed by any other means, e.g.: as command line arguments, or a parameters.json file.

Next, let’s create a variable where we’ll assign the concatenated App Service Plan name and the environment. We can use simple string interpolation to reference the values of our parameters. We’ll use the resourceGroup() function to assign the resource groups location property to a variable, which we will use to deploy the App Service Plan to the same region as the resource group.

// appServicePlan.bicep

var appServicePlanName = '${appServicePlanNameParam}-${environmentParam}'
var location = resourceGroup().location

In our example, we’re not going to allow the consumers of this module to specify their own host operating system, and we’ll hardcode these values directly into the template. We’ll then reference all the previously defined parameters to assign values to resource properties:

// appServicePlan.bicep

param appServicePlanNameParam string
param appServicePlanSkuParam object
param environmentParam string

var appServicePlanName = '${appServicePlanNameParam}-${environmentParam}'
var location = resourceGroup().location

resource appServicePlan 'Microsoft.Web/[email protected]' = {
  name: appServicePlanName
  location: location
  kind: 'linux'
  properties: {
    reserved: true
  }
  sku: appServicePlanSkuParam
}

The final step in defining the App Service Plan module is to define the outputs. These are values which are passed back from the module’s execution to be used in referenced by other steps of the deployment. In our example, we’re going to be creating an Azure Web App, but our Web App template will need to reference the App Service Plan’s ID as an input to know on which App Service Plan to deploy.

// appServicePlan.bicep

output aspId string = appServicePlan.id

We define the output by providing it with a name, a type, and a value. In this case, we’ll be using the built-in id reference on our resource - the resource object being the one defined by symbolic reference, e.g.: resource { }, with our case being `appServicePlan`.

Completed App Service Plan Template

With our App Service Plan module complete, here is the completed Bicep template:

param appServicePlanNameParam string
param appServicePlanSkuParam object
param environmentParam string

var appServicePlanName = '${appServicePlanNameParam}-${environmentParam}'
var location = resourceGroup().location

resource appServicePlan 'Microsoft.Web/[email protected]' = {
  name: appServicePlanName
  location: location
  kind: 'linux'
  properties: {
    reserved: true
  }
  sku: appServicePlanSkuParam
}

output aspId string = appServicePlan.id

Referencing the App Service Plan Module

As mentioned previously, this module is a Bicep template like any other, and can stand on its own to deploy an App Service Plan.

Consuming the module within our main.bicep is straightforward, and consists of defining a module’s deployment scope, a symbolic name, the location of the module file, and any parameters or values that the template is expecting as input parameters.

As we’re using a Subscription scope on our template, but our App Service Plan will be deployed using a Resource Group level scope, we must identify in which of the subscription’s (possibly many) resource groups to deploy the resource.

Note: Using a subscription-level scope for the Bicep template, it’s possible to deploy resources across multiple resource groups within the same template by passing a different resource group object to the scope parameter.

Our App Service Plan module is expecting three input parameters, appServicePlanNameParam, appServicePlanSkuParam, and environmentParam, which we pass into the module by referencing the parameter names we’ll create in the parameters.dev.json. We also pass the resource group we’re intending to deploy the resource into as the scope value, by referencing the symbolic name of the resource group we created previously: rg.

// main.bicep

module appPlan 'modules/appService/appServicePlan.bicep' = {
  scope: rg
  name: appServicePlan.name
  params: {
    appServicePlanNameParam: appServicePlan.name
    appServicePlanSkuParam: appServicePlan.sku
    environmentParam: resGroup.tags.Environment
  }
}

Note: When using modules, the Bicep extension for VS Code is helpful in identifying which parameters are expected as inputs.

The last step in our consuming our App Service Plan module is to prep for our deployment by adding the parameters to our parameters.dev.json file.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "appServicePlan": {
            "value": {
                "name": "demo-app-service-plan",
                "sku": {
                        "name": "B1",
                        "tier": "Basic"
                    }
                }
        },
        "resGroup": {
            "value": {
                "name": "demo-app-service-rg",
                "tags": {
                    "ApplicationName": "",
                    "Description": "",
                    "Environment": "Dev"
                }
            }
        }
    }
}

Here, we’ve defined the App Service Plan’s name as a string, and the sku as on object.

At this stage, it’s possible for us to run our deployment again to create the App Service Plan resource, although we can alternatively wait and deploy the Web App and App Service Plan at the same time.

Web App

The last resource we’ll be creating in this example is an Azure Web App, and in this case more specifically: a Web App for Containers using a base static site image located in the Microsoft Container Registry.

Once again, we begin by creating a Bicep template file within our modules/ folder, this time called webApp.bicep, and defining a Bicep resource called webApp.

// modules/webApp.bicep

resource webApp 'Microsoft.Web/[email protected]' = {
  name:
  location:
  properties: {
    siteConfig: {
      appSettings: []
      linuxFxVersion:
    }
    serverFarmId:
  }
}

The structure of the resource is similar to the App Service, but in this case within our properties we’ll define an array of our App Settings, the location of our container image (linuxFxVersion), and the Id of the App Service Plan where we’ll be creating the Web App itself (serverFarmId).

Like our other module, we’ll be able to control which options are provided to the consumer of the module by defining the input parameters for the template. In this case, we’ll expect to receive an appServicePlanId as a string, a linuxFxVersion string, and a webAppNameParam as a string.

// modules/webApp.bicep

param appServicePlanIdParam string
param environmentParam string
param linuxFxVersionParam string
param webAppNameParam string

Like our App Service Plan template, we’ll assign the Web App’s name to a variable, suffixing the environment name passed into the module by our main.bicep, along with a variable for the deployment location.

// modules/webApp.bicep

var webAppName = '${webAppNameParam}-${environmentParam}'
var location = resourceGroup().location

And finally, we can assign these values to resource itself. As you can see, when specifying the serverFarmId, which is the Id of the App Service Plan on which you’re deploying the Web App, we’re referencing the value which was output from the App Service Plan module:

// modules/webApp.bicep

resource webApp 'Microsoft.Web/[email protected]' = {
  name: webAppName
  location: location
  tags: {}
  properties: {
    siteConfig: {
      appSettings: []
      linuxFxVersion: linuxFxVersionParam
    }
    serverFarmId: appServicePlanIdParam
  }
}

Completed Web App Template

With the values now assigned, our completed Web App template is ready to be referenced in the main.bicep:

// modules/webApp.bicep

param appServicePlanIdParam string
param environmentParam string
param linuxFxVersionParam string
param webAppNameParam string

var webAppName = '${webAppNameParam}-${environmentParam}'
var location = resourceGroup().location

resource webApp 'Microsoft.Web/[email protected]' = {
  name: webAppName
  location: location
  tags: {}
  properties: {
    siteConfig: {
      appSettings: []
      linuxFxVersion: linuxFxVersionParam
    }
    serverFarmId: appServicePlanIdParam
  }
}

Updating the Main Bicep Template

Now that the Web App module has been completed, we’ll reference it in the main.bicep template, which will allow us to deploy the entire solution end-to-end. This process is the same as it was for the App Service Plan module, in that we’ll use an object to contain all the parameters related to the Web App itself, and pass those expected parameters into the module.

The notable addition in this case, is that we’ll also pass the id of the App Service Plan which is created by another module in the template into the Web App module. We do this by referencing the output we defined within the webApp.bicep: aspId. The value is retrieved using dot notation, using the resource’s symbolic name which we defined as appPlan.

// main.bicep

param webApp object

module app 'modules/webApp/webApp.bicep' = {
  scope: rg
  name: webApp.name
  params: {
    appServicePlanIdParam: appPlan.outputs.aspId
    environmentParam: resGroup.tags.Environment
    linuxFxVersionParam: webApp.linuxFxVersion
    webAppNameParam: webApp.name
  }
}

The final step here is to add the webApp to the parameters.dev.json file with our desired parameters, such as the name we’ll assign to the Web App, or the location of the Docker image we want the Web App to instantiate.

// parameters.dev.json

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "appServicePlan": {
            "value": {
                "name": "demo-app-service-plan",
                "sku": {
                        "name": "B1",
                        "tier": "Basic"
                    }
                }
        },
        "resGroup": {
            "value": {
                "name": "demo-app-service-rg",
                "tags": {
                    "ApplicationName": "",
                    "Description": "",
                    "Environment": "Dev"
                }
            }
        },
        "webApp": {
            "value": {
                "name": "bicep-demo-webapp",
                "linuxFxVersion": "DOCKER|mcr.microsoft.com/appsvc/staticsite:latest"
            }
        }
    }
}

Reminder: Because a public DNS record is automatically created for each Web App, the name you assign must be globally unique. Make sure you specify your own Web App name!


Deployment

With the completed main.bicep in hand, we’re ready to begin our deployment using the same command as we did previously:

$ az deployment sub create --template-file ./main.bicep --parameters './parameters.dev.json' --location westus2 --output jsonc

After the deployment runs, we should see another provisioningState: "Succeeded" message, letting us know that our deployment was a success:

{
  "id": "/subscriptions/<sub>/providers/Microsoft.Resources/deployments/main",
  "location": "westus2",
  "name": "main",
  "properties": {
    "correlationId": "096d9408-xxxx-xxxx-xxxx-477f9d6a26a4",
    "debugSetting": null,
    "dependencies": [
      {
        "dependsOn": [
          {
            "id": "/subscriptions/<sub>/resourceGroups/demo-app-service-rg",
            "resourceName": "demo-app-service-rg",
            "resourceType": "Microsoft.Resources/resourceGroups"
          }
        ],
        "id": "/subscriptions/<sub>/resourceGroups/demo-app-service-rg/providers/Microsoft.Resources/deployments/demo-app-service-plan",
        "resourceGroup": "demo-app-service-rg",
        "resourceName": "demo-app-service-plan",
        "resourceType": "Microsoft.Resources/deployments"
      },
      {
        "dependsOn": [
          {
            "id": "/subscriptions/<sub>/resourceGroups/demo-app-service-rg/providers/Microsoft.Resources/deployments/demo-app-service-plan",
            "resourceGroup": "demo-app-service-rg",
            "resourceName": "demo-app-service-plan",
            "resourceType": "Microsoft.Resources/deployments"
          },
          {
            "id": "/subscriptions/<sub>/resourceGroups/demo-app-service-rg",
            "resourceName": "demo-app-service-rg",
            "resourceType": "Microsoft.Resources/resourceGroups"
          },
          {
            "apiVersion": "2019-10-01",
            "id": "/subscriptions/<sub>/resourceGroups/demo-app-service-rg/providers/Microsoft.Resources/deployments/demo-app-service-plan",
            "resourceGroup": "demo-app-service-rg",
            "resourceName": "demo-app-service-plan",
            "resourceType": "Microsoft.Resources/deployments"
          }
        ],
        "id": "/subscriptions/<sub>/resourceGroups/demo-app-service-rg/providers/Microsoft.Resources/deployments/bicep-demo-webapp",
        "resourceGroup": "demo-app-service-rg",
        "resourceName": "bicep-demo-webapp",
        "resourceType": "Microsoft.Resources/deployments"
      }
    ],
    "duration": "PT39.0235451S",
    "error": null,
    "mode": "Incremental",
    "onErrorDeployment": null,
    "outputResources": [
      {
        "id": "/subscriptions/<sub>/resourceGroups/demo-app-service-rg"
      },
      {
        "id": "/subscriptions/<sub>/resourceGroups/demo-app-service-rg/providers/Microsoft.Web/serverfarms/demo-app-service-plan-Dev",
        "resourceGroup": "demo-app-service-rg"
      },
      {
        "id": "/subscriptions/<sub>/resourceGroups/demo-app-service-rg/providers/Microsoft.Web/sites/bicep-demo-webapp-Dev",
        "resourceGroup": "demo-app-service-rg"
      }
    ],
    "outputs": null,
    "parameters": {
      "appServicePlan": {
        "type": "Object",
        "value": {
          "name": "demo-app-service-plan",
          "sku": {
            "name": "B1",
            "tier": "Basic"
          }
        }
      },
      "resGroup": {
        "type": "Object",
        "value": {
          "name": "demo-app-service-rg",
          "tags": {
            "ApplicationName": "",
            "Description": "",
            "Environment": "Dev"
          }
        }
      },
      "webApp": {
        "type": "Object",
        "value": {
          "linuxFxVersion": "DOCKER|mcr.microsoft.com/appsvc/staticsite:latest",
          "name": "bicep-demo-webapp"
        }
      }
    },
    "parametersLink": null,
    "providers": [
      {
        "id": null,
        "namespace": "Microsoft.Resources",
        "providerAuthorizationConsentState": null,
        "registrationPolicy": null,
        "registrationState": null,
        "resourceTypes": [
          {
            "aliases": null,
            "apiProfiles": null,
            "apiVersions": null,
            "capabilities": null,
            "defaultApiVersion": null,
            "locationMappings": null,
            "locations": [
              "westus2"
            ],
            "properties": null,
            "resourceType": "resourceGroups"
          },
          {
            "aliases": null,
            "apiProfiles": null,
            "apiVersions": null,
            "capabilities": null,
            "defaultApiVersion": null,
            "locationMappings": null,
            "locations": [
              null
            ],
            "properties": null,
            "resourceType": "deployments"
          }
        ]
      }
    ],
    "provisioningState": "Succeeded",
    "templateHash": "14679360005801183207",
    "templateLink": null,
    "timestamp": "2021-09-18T17:14:35.417201+00:00",
    "validatedResources": null
  },
  "tags": null,
  "type": "Microsoft.Resources/deployments"
}

We can use the Azure CLI to validate that our resources were created:

$ az webapp list --resource-group demo-app-service-rg -o table
Name                   Location    State    ResourceGroup        DefaultHostName                          AppServicePlan
---------------------  ----------  -------  -------------------  ---------------------------------------  -------------------------
bicep-demo-webapp-Dev  West US 2   Running  demo-app-service-rg  bicep-demo-webapp-dev.azurewebsites.net  demo-app-service-plan-dev

Now that we know our Web App is up and running, we can navigate our browse to the hostname which it was assigned to see the finished result:

Web App Running On Microsoft Azure

Summary

Working with Infrastructure as Code within Azure has been possible for a long time; however, the recent addition of Azure Bicep has made the process of using Azure’s native tooling much more simple. As you can see in this example we were able to create and consume an Infrastructure as Code template, deploying multiple Azure resources to create an end-to-end solution, and modularise it for reuse.

If you’re interested in the viewing the completed template, it’s available on GitHub: https://github.com/MrThePlague/bicep-webapp-demo.