Azure Bicep - Zero to Hero

Deploy and Manage Azure Infrastructure as Code

Ravikanth Chaganti

This book is for sale at http://leanpub.com/azurebicep

This version was published on 2022-12-17

*   *   *   *   *

This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and many iterations to get reader feedback, pivot until you have the right book and build traction once you do.

*   *   *   *   *

© 2021 - 2022 Ravikanth Chaganti

To my guru, my family, and the technical community that I have been learning from.

Table of Contents

1. Part 1

2. Introduction

1. Infrastructure As Code

2. Azure Resource Manager

3. Why Bicep

4. Introducing Bicep

5. Summary

3. Getting Started

1. Installing Bicep CLI

2. Authoring Bicep templates

3. First Bicep template

4. Compiling and deploying Bicep templates

5. Summary

4. Working with resources

1. Defining resources

2. Resource dependencies

3. Child resources

4. Extension Resources

5. Reference existing resources

6. Scoped resource deployment

7. Summary

5. Authoring reusable templates

1. Parameters

2. Parameter decorators

3. Output

4. Generating Parameter files

5. Summary

6. Variables, function, and expressions

1. Summary

7. Iterations and Conditions

1. Iterations

2. Sequential deployments

3. Conditional deployments

4. Combining iterations and conditions

5. Summary

8. Creating and Consuming Modules

1. Creating modules

2. Consuming modules

3. Iterations and conditions

4. Deploying at multiple scopes

5. Sharing modules

6. Module aliases

7. Summary

9. Developing Bicep templates in VS Code devcontainers

1. Visual Studio Code Development Containers

2. GitHub Codespaces

3. Summary

10. CI and CD of Bicep templates

1. Customizing linter rules

2. Build and deploy

3. A local pipeline

4. GitHub actions for CI/CD

5. Summary

11. Built-in function primer

1. any function

2. loadJsonContent function

3. getSecret function

4. Summary

12. Part 2

13. Provisioning a Kubernetes Cluster

1. Kubernetes cluster architecture

2. Bicep template structure

3. Provisioning workflow

4. Summary

Part 1

To get started with Bicep, it is important that you understand the fundamentals of Bicep language. In this part of the book, you will learn:

- Why use Bicep when there is ARM JSON and tools like Terraform

- How to get started with Bicep

- Creating your first Bicep template by understanding the basic syntax of Bicep resources

- Authoring reusable Bicep templates

- Fundamental Bicep language constructs like variables, expressions, and functions

- Iterations and conditions to create intermediate to advanced Bicep templates

- Creating, consuming, and contributing Bicep modules

- Using VS Code development containers to author Bicep templates

- Continuous Integration and Continuous Delivery/Deployment of your Bicep templates

- Some of the built-in functions and use cases for implementing these built-in functions in your Bicep templates

By the end of this part, you should be comfortable reading and writing Bicep templates, and authoritatively answering any Bicep language questions.

Introduction

Cloud computing has changed how IT organizations see and manage IT infrastructure. The consumption-based model for any services that you need and the promise of high availability, scalability, and elasticity, along with many other benefits, made cloud computing a popular choice within IT organizations. Many cloud service providers emerged in the last decade or so, but only a handful made it to the top. Microsoft Azure is undoubtedly one of the top cloud service providers. Azure offers 100+ services in different categories such as compute, networking, storage, databases, web, etc. If you are getting started with Azure, it is always a good idea to learn about what Azure is and what services Azure offers. Teaching the basics of Azure is beyond the scope of this book. I strongly recommend that you review the Introduction to Azure fundamentals module on Microsoft Learn. This chapter, however, provides a quick tour of Azure Resource Manager (ARM) and ARM templates. You will then learn about the need for a new language called Bicep. To appreciate the need for ARM templates and Bicep, you must first understand a set of practices called Infrastructure as Code (IaC).

Note: Source code for all examples in this book is available in the Bicep book examples GitHub repository.

Infrastructure As Code

With cloud adoption and web-scale infrastructures, IT organizations have become more agile and perform several deployments a month or a week, if not every day. With such frequent deployments, businesses expect infrastructure to be always available and scale on demand. The traditional infrastructure and application deployment methods do not scale well to support the cloud and web-scale infrastructures. Automation plays a significant role in deploying and managing infrastructure at the cloud or web-scale. This automation can be just a set of scripts or tools. Building such automation may be easier but making it repeatable, reproducible, and simple for its users can be very hard, but it is the most crucial step towards agile infrastructure. Infrastructure as Code (IaC) plays a role here, and it is an enabler for building and managing agile infrastructure. IaC is not a tool but is a set of practices.

IaC brings software development practices into infrastructure deployment and configuration. With IaC, infrastructure definitions become a part of your application code. You define your infrastructure as a set of configuration files, store these configuration files in version control, and apply software development practices such as Continuous Integration (CI) and Continuous Delivery / Deployment (CD). You must implement certain IaC principles to ensure reliability in infrastructure deployments and the availability of your infrastructure.

Principles of Infrastructure as Code

The infrastructure deployments must be repeatable or idempotent. With idempotency, you can rerun the deployment any number of times without impacting what is already deployed and is in the desired state. Idempotency is the key. For example, you want to add a new SQL database to the existing application deployment, and you update the configuration files to define this requirement. When you attempt this deployment, if the underlying automation is not idempotent, it will either redeploy the existing resources or halt with errors as those resources already exist. When your code is idempotent, it will instead deploy what is new within the configuration and do not attempt changing the existing resources unless there is a necessity.

Creating idempotent automation requires significant coding to handle errors when they occur and to perform infrastructure deployment without impacting the resources already in the desired state. While idempotency is essential, writing such automation can be tedious and may not always be the user’s goal. To make it easy to define and deploy infrastructure, IaC recommends using a declarative style of configuration. With declarative style, you describe what you need within a configuration file. The underlying imperative automation takes care of the how part of the infrastructure deployment and idempotency needed within that process.

The automation that you create for performing these deployments should be reusable. Reusability helps remove duplication. For example, deploying applications to separate environments may have different requirements. Based on the type of environment – production or test or staging, you either want to reduce or increase application instances. With reusable code, you would specify the environment as an input parameter to the automation, and it takes care to configure the infrastructure to suit the environment. Similarly, if another engineer in your organization has written the automation to perform a SQL database deployment, it would be beneficial to use that in your automation rather than write another script or tool to do the same job. IaC recommends that you implement modularization in your automation to reuse the code across multiple deployments effectively.

Once you have the above principles implemented, you must make sure that your configuration files are stored in version control to ensure the reproducibility of infrastructure deployment. In the past, IT engineers and administrators carried what we described as tribal knowledge. It is information that is in their heads but not in any document that you could use to redeploy an infrastructure when needed. Tribal knowledge in IT organizations is dangerous. What if a person who deployed an application infrastructure leaves the company without documenting how to reproduce that deployment? To address this, IaC suggests that all automation and configuration files that you create should live in version control. Every change that you perform must go through a change review and always get added to the existing configuration for that infrastructure. Reproducibility helps in performing consistent deployments and in correcting any configuration drift.

In essence, the four IaC principles that you should implement are:

- Idempotency in automation

- Declarative configurations

- Reusability in automation

- Reproducible infrastructure configurations

When working with different cloud service providers or infrastructure provisioning and management tools, you must actively look at support for these principles. In this next section, you learn about Azure Resource Manager (ARM) and how ARM enables the basics tenets of IaC.

Azure Resource Manager

Azure Resource Manager is the deployment and management service for resources in Azure. It provides the capability to create, update, and delete resources as a group. The group of resources is the key here. An Azure resource group is a logical container in which you deploy different Azure resources and manage their life cycle as a single entity. Every resource you ever deploy on on Azure must be a part of the resource group and can only be in one resource group.

The management hierarchy in Azure starts at the tenant and ends the smallest manageable item, which is an Azure resource. Figure 1-1 illustrates this.

Figure 1-1 - Deployment and Management Hierarchy in Azure

What you can deploy and manage depends on the scope shown above. The default deployment scope is a resource group. As you move up in this hierarchy, the number and type of Azure resources you can deploy reduces. ARM provides a consistent management layer through a REST API interface that various management tools use. Figure 1-2 provides a high-level overview of this management layer.

Figure 1-2 - ARM management layer

Azure Resource Manager receives requests from any clients, authenticates and authorizes the request. The authenticated request then gets sent to the resource provider implementing the resource. All requests go through the same management layer. Therefore, you will always see consistent results. The Azure portal provides a friendly user interface on top of this management layer, and it is suitable for point-and-click management, which is not scalable. Azure PowerShell, Azure CLI, and REST clients offer imperative automation capabilities. ARM templates provide a declarative interface to provisioning and managing Azure resources.

Azure Resource Manager templates

ARM templates are JavaScript Object Notation (JSON) files that provide the capability to describe what you want to provision in Azure declaratively. These JSON templates are idempotent and support reusability by supporting parameters, nested, and linked templates. Like any other JSON configuration file in your application, you can store ARM JSON templates in version control and use these templates in a CI / CD pipeline that validates and deploys resources described in the template. An in-depth review of ARM JSON templates is outside the scope of this book, but here is a simple template that provisions a storage account in an existing resource group.

1 //1_1-storageAccount.json

2 {

3     "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTem\

4 plate.json#",

5     "contentVersion": "1.0.0.0",

6     "parameters": {

7         "storageAccountName": {

8             "type": "string",

9             "metadata": {

10                 "description": "Name of the storage account to provision"

11             }

12         }

13     },

14     "functions": [],

15     "variables": {

16         "location": "[resourceGroup().location]"

17     },

18     "resources": [

19         {

20             "name": "[parameters('storageAccountName')]",

21             "type": "Microsoft.Storage/storageAccounts",

22             "apiVersion": "2019-06-01",

23             "tags": {

24                 "displayName": "[parameters('storageAccountName')]"

25             },

26             "location": "[variables('location')]",

27             "kind": "StorageV2",

28             "sku": {

29                 "name": "Standard_LRS"

30             },

31             "properties" : {

32                 "accessTier": "Cool"

33             }

34         }

35     ],

36     "outputs": {

37         "sid": {

38             "type": "string",

39             "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('s\

40 torageAccountName'))]"

41         }

42     }

43 }

Template Element

Description

$schema

Identifies the version of the template language to use when deploying this template. The value of this changes based on the scope at which you are provisioning resources as well. This element is mandatory.

contentVersion

Identifies the version of the template content. This element is required.

parameters

Accepts a set of parameters that act as input to the ARM template deployment using which you can introduce reusability of JSON templates. This element is not mandatory.

functions

Used to add user-defined functions to use within JSON template expressions for simplifying templates. This element is not mandatory.

variables

Similar to functions, variables are used to simplify expressions in a JSON template. This element is not mandatory.

resources

Used to describe one or more resources to deploy or update.

outputs

Specify the values returned by the template after the deployment is complete.

ARM JSON template language offers several built-in functions that you can use within complex expressions to simplify resource definitions. You can use copy loops to perform iterative deployment of resources.

When you deploy the above template using Azure PowerShell or Azure CLI, or Azure Portal, you get prompted (assuming that you are not using a parameters file) to enter the value for the storageAccountName parameter. Once the template validation is complete with no errors, ARM creates the storage account resource and returns the storage account ID as the output. Here is the partial output from az deployment group create command.

1     "outputs": {

2       "sid": {

3         "type": "String",

4         "value": "/subscriptions/5073fd4c-3abc-4567-8765-21e034f70820/resourceGroups\

5 /bicep/providers/Microsoft.Storage/storageAccounts/bicepbookstg"

6       }

7     }

What you have seen in the above example is a trivial but good start. You can author very complex JSON templates to deploy an entire solution to provision multiple applications and services. ARM calculates dependencies either through references or explicit dependencies between resources. ARM JSON templates provide an efficient way to deploy your application services with confidence. So, why do you need Bicep language?

Why Bicep

To start with, JSON is not a programming language. It is a data representation and a data exchange format. The JSON syntax for ARM templates makes these documents quite verbose and restricts the extensibility since you have to play within the limits of JSON data representation. The ARM template language is fragments of functions and other programming constructs embedded into JSON, which only the Azure Resource Manager understands. For a beginner, these JSON templates can be pretty intimidating. As the complexity of the infrastructure grows, your ARM template becomes almost unreadable and maintain. You can, of course, decompose the template into multiple linked templates and but linked templates have their limitations. You can use linked templates only from an HTTP location or an Azure blob store. There are alternatives to ARM template deployment. These tools do not use ARM templates but provide alternate ways to define your infrastructure as code by directly interfacing with the ARM REST API.

Terraform provides a declarative way to define Azure infrastructure using the HashiCorp Configuration Language (HCL). Being a language on its own, HCL supports variables, data types, arithmetic, logical operations, functions, and expressions. With the help of HCL, Terraform delivers syntax that is concise and simple to write. Terraform deployments need a state file. Terraform keeps track of provisioned resources and their desired state in a state file. A state file is critical to Terraform-based implementations, and this brings in the need for teams to have shared access to the state file. The other downside of Terraform is the day zero support for new resources and resource providers.

Pulumi, on the other hand, provides an SDK that you can use in your favorite language and define your infrastructure as imperative scripts or programs. I have not spent much time on Pulumi because of my non-developer background, but Pulumi claims that they can provide day-zero support for any new resource or resource provider.

Both Terraform and Pulumi support deploying resources to multiple cloud providers. These tools make sense for a company that has already invested in multi-cloud. Also, these are not tools that are written only for Azure. Microsoft could have chosen to adopt one of these but instead decided to write their language that makes authoring ARM JSON templates easier. This new language is an essential step since many customers have heavily invested in JSON templates. If Microsoft moves away from JSON templates, this section of customers won’t be happy. Creating a transpiler will help maintain the backward compatibility while ensuring that those starting with Microsoft Azure do not get demotivated looking at JSON templates. Now, this is what Bicep language brings in.

Introducing Bicep

Bicep is an open-source, domain-specific language (DSL) designed for transpiling Bicep code into ARM templates similar to how TypeScript transpiles to JavaScript. Generating ARM templates is the only purpose. Bicep is not a general-purpose programming language. At the time of this writing, Bicep language is in the early stages of development, but Microsoft supports it for production use.

Figure 1-3 - Bicep

Chapter 2 will provide an overview of details shown in Figure 1-3. For now, understand that Bicep language is a transparent abstraction on Azure Resource Manager. Therefore, it supports everything that you can do with ARM templates. Bicep supports several built-in functions that you can use to simplify resource instance definitions. Whatever tooling you have been using or created to work with ARM templates will continue to be valid. Your CI and CD pipelines won’t have to change. Day 0 support isn’t an issue with the Bicep language since it uses ARM REST API specifications. Unlike Terraform, you define the desired state of your resources in a Bicep template, and the current state is in ARM, eliminating the need for creating the infrastructure for shared state files. For someone who is getting started with Azure Resource Manager and Azure deployments, Bicep language will be a great start. Undoubtedly, the Bicep language is simple and easy to learn.

Bicep is the language that provides the capability to define your Azure resources declaratively. Bicep CLI is the tool that you use to compile Bicep templates to JSON templates and decompile (on a best effort basis) JSON templates to Bicep.

With this background, you are all set to start exploring Bicep.

Summary

This chapter provided a quick overview of Infrastructure as Code, Azure Resource Manager, and Azure Resource Manager JSON templates. ARM JSON templates provide a declarative way to define and provision your Azure services. ARM templates inherit the verbosity from JSON, and the functions and expressions that you need to use within the ARM template language make a template complex and hard to read as the size of the deployment grows. Other products in the market, such as HashiCorp Terraform or Pulumi, address the challenge of simplifying ARM deployments using their tooling. Customers who have invested heavily in ARM JSON templates would prefer to continue using the same instead of adopting a new tool for Azure deployments. Bicep plays a role here. Bicep is an open-souce, domain-specific language that transpiles into ARM JSON templates and is a transparent abstraction over ARM templates. Bicep is easy to write and understand. Move on to get started with Bicep!

Getting Started

The previous chapter introduced you to Bicep and explained why developing a new language is essential to simplifying the ARM JSON template authoring experience. You also learned that Bicep is a domain-specific language (DSL) for declaratively defining your Azure resources. Bicep lets you implement the same capabilities that exist in the JSON templates in a more concise syntax. The Bicep language is in the early stages of development at this time of writing and has a few known limitations from a feature point of view. However, using the Bicep language for generating ARM JSON templates for production use is supported by Microsoft.

Reiterating what you learned in chapter 1, Bicep is the language that provides the capability to define your Azure resources declaratively. Bicep CLI is the tool you use to compile Bicep templates to JSON templates and decompile (on a best effort basis) JSON templates to Bicep templates. Bicep CLI cannot deploy your Azure resources. ARM JSON template deployment tools such as Azure CLI or Azure PowerShell are needed to deploy the JSON templates compiled using the Bicep CLI. The Visual Studio Code editor and the Bicep extension are needed for better Bicep template authoring experience and you compile those files to ARM JSON templates using the Bicep CLI.

First, start with the Bicep CLI.

Installing Bicep CLI

There are different ways to install Bicep CLI on a supported operating system.

Using Azure CLI

On any supported operating system, if you have Azure CLI version 2.20.0 or later, you can use the az bicep command group to install or upgrade Bicep CLI.

Azure CLI automatically installs Bicep CLI when you a run a command that depends on Bicep. For example, az deployment group create --template-file main.bicep --resource-group bicep.

1 az bicep list-versions

The above command lists all available versions of Bicep CLI.

1 # install the latest version

2 az bicep install

3

4 # install a specific version

5 az bicep install --version v0.8.2

You can use the install subcommand in the az bicep command group to install the latest or a particular version of Bicep CLI.

The az bicep version command outputs the version of Bicep CLI installed using Azure CLI.

The az bicep upgrade command upgrades the installed version to the latest version available.

The az bicep install command installs a separate Bicep CLI version and does not add Bicep CLI to the PATH environment variable. Therefore, you can access Bicep CLI only via the az bicep command group. If you need to use Bicep CLI outside Azure CLI context, you need to install it using other methods based on the operating system you are using.

The az bicep uninstall command uninstalls Bicep CLI.

Installing on Windows

There are multiple ways to install Bicep CLI on Windows OS. The choco install bicep command installs Bicep CLI using Chocolatey package manager. The winget install -e --id Microsoft.Bicep command installs Bicep CLI using the winget package manager. If you do not have a supported package manager, you can use the Windows installer to install Bicep CLI. The following script is what I use when building a new development machine.

1 # 2_1-bicepInstall.ps1

2 [CmdletBinding()]

3 param

4 (

5 )

6

7 $url = 'https://github.com/Azure/bicep/releases/latest/download/bicep-setup-win-x64.\

8 exe'

9 $bicepFilePath = "${ENV:TEMP}\bicep-setup-win-x64.exe"

10

11 try

12 {

13     # Download Bicep Installer

14     Write-Verbose -Message 'Downloading the Bicep CLI installer.'

15     Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $bicepFilePath

16

17     # Install Bicep CLI

18     Write-Verbose -Message 'Installing Bicep CLI'

19     $bicepInstall = Start-Process -FilePath $bicepFilePath -ArgumentList '/verysilen\

20 t /supressmsgboxes' -PassThru

21

22     # Wait for installer to complete

23     while (!$bicepInstall.HasExited)

24     {

25         Write-Verbose -Message 'Waiting for Bicep installer to complete installation\

26 .'

27         Start-Sleep -Seconds 5

28     }

29

30     # Update Path environment variable

31     if ($bicepInstall.HasExited -and $bicepInstall.ExitCode -eq 0)

32     {

33         Write-Verbose -Message 'Bicep CLI installed. Updating Path variable in the c\

34 urrent session.'

35         $env:Path = [System.Environment]::GetEnvironmentVariable('Path','User')

36         bicep --version

37     }

38     else

39     {

40         Write-Error "Bicep installer existed with an exit code: $($bicepInstall.Exit\

41 Code)."

42     }

43

44     # If exists, delete the Bicep installer

45     if (Test-Path -Path $bicepFilePath)

46     {

47         Remove-Item -Path $bicepFilePath -Force

48     }

49 }

50 catch

51 {

52     Write-Error -Message 'Error occurred during Bicep CLI install.'

53     Write-Error -Message $_

54 }

This script downloads the latest Windows installer, performs a silent install, refreshes the PATH environment variable in the current session, and verifies if the Bicep install is successful or not by printing the Bicep CLI version.

1 PS> .\2_1-bicepInstall.ps1 -Verbose

2 VERBOSE: Downloaing Bicep CLI installer

3 VERBOSE: GET https://github.com/Azure/bicep/releases/latest/download/bicep-setup-win\

4 -x64.exe with 0-byte payload

5 VERBOSE: received 15682352-byte response of content type application/octet-stream

6 VERBOSE: Installing Bicep CLI

7 VERBOSE: Waiting for Bicep installer to complete installation

8 VERBOSE: Waiting for Bicep installer to complete installation

9 VERBOSE: Bicep CLI installed. Updating Path variable in the current session

10 Bicep CLI version 0.8.2 (78a65ae4aa)

Installing on Linux

On Linux systems, the following script (2_2-bicepInstall.sh) downloads the Bicep binary and updates the PATH environment variable.

1 #!/bin/bash

2 # Download bicep binary

3 curl -Lo bicep https://github.com/Azure/bicep/releases/latest/download/bicep-linux-x\

4 64

5

6 # Change execute bit

7 chmod +x ./bicep

8

9 # Add bicep to PATH

10 sudo mv ./bicep /usr/local/bin/bicep

11

12 # print bicep version

13 bicep --version

Installing on macOS

On macOS, you can either use homebrew or a shell script to install Bicep.

The following homebrew commands install the Bicep CLI.

1 # Add tap for Bicep

2 brew tap azure/bicep

3

4 # Install Bicep

5 brew install bicep

Similar to Linux, the following shell script (2_3-bicepInstallmacOS.sh) downloads the Bicep binary and updates the PATH.

1 #!/bin/bash

2 # Download bicep binary

3 curl -Lo bicep https://github.com/Azure/bicep/releases/latest/download/bicep-osx-x64

4

5 # Change execute bit

6 chmod +x ./bicep

7

8 # Add Gatekeeper exception

9 sudo spctl --add ./bicep

10

11 # Add bicep to PATH

12 sudo mv ./bicep /usr/local/bin/bicep

13

14 # print bicep version

15 bicep --version

Bicep in a Docker container

There is no official Docker image for Bicep. At the same time, it isn’t too hard to build one. A Docker image containing necessary tools such as Azure CLI and Bicep CLI can be helpful in CI and CD pipelines. There are a couple of community-published Bicep Docker images – ravikanth/bicep and miqm/bicep-cli. Both these images are Linux-based and include all necessary tools to perform ARM template deployments. You can download any of these images using the docker image pull command.

Authoring Bicep templates

Similar to the ARM JSON templates, the Bicep templates are plain-text files. You can author these files in a notepad as well. Does it make sense? Not at all. There are better tools that provide superior Bicep authoring experience. At this time of writing, Visual Studio Code and the Bicep extension for VS Code provide this authoring experience. Bicep extension includes a Bicep language server and offers intellisense for the core language and all resource types in Azure. This extension gets activated when you open a .Bicep template or select Bicep as the language mode in the editor. This extension provides snippets to get you started quickly. Context-aware intellisense is possible since the Bicep extension uses the ARM REST API specification.

Figure 2-1 - Resource type definition completion

If the resource type is specified, you will get tips around other missing mandatory properties using the Bicep language services.

Figure 2-2 - Resource mandatory properties

With this context-aware completion, the resource property names and the possible property values are auto-completed as well.

Figure 2-3 - Resource property values

Finally, you get intellisense on the resource instance properties by placing a dot (.) after the resource instance name.

Figure 2-4 - Resource instance properties

Bicep language offers a concise syntax and the VS Code extension makes authoring even more straightforward.

This VS Code extension also comes with a helpful Bicep template visualizer. You can access the Bicep Visualizer from the VS Code command palette.

Figure 2-5 - Bicep visualization

Bicep visualizer visualizes resources and modules in a template. You can double-click on a resource or module to go to the definition in a Bicep template.

Bicep version 0.4.1 and later include support for linting. This is available both with the Bicep CLI and the Bicep VS Code extension. Linting helps ensure that your Bicep templates are following best practices as defined by a set of rules.

Figure 2-6 - Bicep linting in the VS Code editor

You can customize Bicep development experience by adding a configuration file named bicepconfig.json in the same folder as the other Bicep template files. Using this configuration file, you can customize the linter behavior. The VS Code extension supports intellisense for linter configuration. Here is an example.

1 {

2     "analyzers": {

3       "core": {

4         "verbose": true,

5         "enabled": true,

6         "rules": {

7           "no-hardcoded-env-urls": {

8             "level": "off"

9           },

10           "no-unused-params": {

11             "level": "error"

12           },

13           "no-unused-vars": {

14             "level": "error"

15           },

16           "prefer-interpolation": {

17             "level": "warning"

18           },

19           "secure-parameter-default": {

20             "level": "error"

21           },

22           "simplify-interpolation": {

23             "level": "error"

24           }

25         }

26       }

27     }

28   }

In this example, a few of the rules are set to error level. So a violation in any of those should trigger an error in the editor and in the Bicep CLI build process.

Figure 2-7 - Customized linter configuration

And, here is how the Bicep CLI reports a linter error during build.

Figure 2-8 - Linter output in Bicep build

You will learn more about linting and how you can use that in a CI/CD pipeline in a later chapter.

Bicep version 0.4.1124 and later support inserting existing resource definitions into a Bicep template. To do this, you can access the Bicep: Insert Resource from the VS Code command palette and enter the ID of the resource that you want to insert into the current document.

Figure 2-9 - Insert resources

You must make sure you login to Azure before attempting to insert a resource definition using this method.

Bicep extension attempts to retrieve the resource definition and inserts into the editor.

Figure 2-10 - Inserted resource definition

With this quick tour of Bicep install methods and VS Code extension for authoring Bicep templates, you are all set to get started with writing your first Bicep template.

First Bicep template

Like ARM JSON templates, Bicep templates have a standard set of elements that you can use.

1 targetScope = '<scope>'

2

3 @<decorator>(<argument>)

4 param <parameter-name> <parameter-data-type> = <default-value>

5

6 var <variable-name> = <variable-value>

7

8 resource <resource-symbolic-name> '<resource-type>@<api-version>' = {

9   <resource-properties>

10 }

11

12 module <module-symbolic-name> '<path-to-file>' = {

13   name: '<nested-deployment-name>'

14   params: {

15     <parameter-names-and-values>

16   }

17 }

18

19 output <output-name> <output-data-type> = <output-value>

You will learn in-depth about elements in a Bicep template as you proceed in this book. Here is a quick summary.

The targetScope element identifies the scope of this deployment. The default scope is resourceGroup.

The param element declares input parameters to a Bicep template. The parameter metadata can be added using decorators. Bicep supports different types of parameters. Parameters facilitate reusability and form the basis of modularizing Bicep templates.

Using the var element, you can declare and initialize variables in Bicep templates. Variables help create expressions that simplify resource instance and property values. You need to balance between variables and parameters to ensure that the Bicep template has the right balance between free-form and fixed configuration.

The resource element is what you use to define a resource instance. You can add any number of resource instances to a Bicep template. What resources you can add depends on the targetScope.

You can consume resource definitions from an external Bicep templates using module element. The Bicep templates added as modules get compiled as nested deployments in an ARM JSON template.

The output element defines the return values from the deployment.

Like any programming language, Bicep too supports iterations and conditions. You can use iterations with variables, resources, modules, and outputs. Conditions are supported only with resources and modules.

While Bicep templates include these different elements, to compile a .Bicep template, it can even be left empty. To test this, create an empty file, name it main.bicep, and compile it using the bicep build main.bicep command. This command generates a JSON template similar to what is shown below.

1 {

2   "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTempl\

3 ate.json#",

4   "contentVersion": "1.0.0.0",

5   "metadata": {

6     "_generator": {

7       "name": "bicep",

8       "version": "0.3.539.46024",

9       "templateHash": "5951092464443635760"

10     }

11   },

12   "functions": [],

13   "resources": []

14 }

In the generated template, $schema identifies the resource schema for the selected target deployment scope. The metadata specifies the version of the Bicep CLI used to generate the JSON template. Since main.bicep was empty, there are no resource, parameter, function, or variable definitions in the JSON template.

Compiling and deploying Bicep templates

Compiling Bicep templates to JSON templates can be done in multiple ways. As you learned earlier, you can use the bicep build command or the az bicep build command. Try compiling the following Bicep template that defines a storage account resource.

1 // 2_5-storageAccount.bicep

2 resource stg 'Microsoft.Storage/storageAccounts@2021-02-01' = {

3   name: 'bicepstg'

4   location: 'eastus'

5   kind: 'StorageV2'

6   sku: {

7     name: 'Standard_LRS'

8   }

9   properties: {

10     accessTier: 'Cool'

11   }

12 }

It is OK even if you don’t understand the syntax of the resource definition in this example. You will learn about the Bicep language syntax in-depth in Chapter 3.

The bicep build command has different command-line arguments, but at the minimum, you should provide the Bicep template name.

1 # Name of the compiled JSON will be same as the Bicep template basename

2 bicep build .\main.bicep

3

4 # Store the generated JSON in a different directory

5 bicep build --outdir C:\armjson .\main.bicep

6

7 # Store the generated JSON with a different file name

8 bicep build --outfile C:\armjson\storageAccount.json .\main.bicep

9

10 # Display generated JSON in the standard output

11 bicep build --stdout .\main.bicep

The command-line arguments when using the az bicep build command are slightly different.

1 # Default build command; use --file or -f

2 az bicep build --file main.bicep

3

4 # Store the generated JSON in a different directory

5 az bicep build --file main.bicep --outdir c:\armjson

6

7 # Display generated JSON in the standard output

8 az bicep build --file main.bicep --stdout

Deploy via Command line

You can deploy the generated JSON template using the Template Deployment option in Azure Portal. However, to deploy a Bicep template, you don’t have to generate a JSON template from it. Both Azure CLI and Azure PowerShell support creating an Azure deployment from the Bicep templates. The Bicep templates get converted to JSON format automatically.

1 # Azure CLI

2 az deployment group create --template-file main.bicep --resource-group bicep

1 # Azure PowerShell

2 New-AzResourceGroupDeployment `

3   -Name storageAccountBicep `

4   -ResourceGroupName bicep `

5   -TemplateFile .\main.bicep

Deploy via VS Code

Bicep VS Code extension version 0.5.6 added an experimental feature to deploy Bicep templates right within VS Code. You need to enable this experimental feature within the extension settings.

Figure 2-11 - Enable Bicep deploy experimental feature

Once you enable this experimental feature, you can right-click on any Bicep file within the explorer view and select “Deploy Bicep File…” context menu option. This feature requires the Azure Account extension. If not, you will be prompted to install this extension and then log in to Azure to start the deployment.

With the basics out of your way, it is time to start writing some production-ready Bicep templates. While it is important that you understand the syntax and be able to use Bicep language on your own, it is not necessary that you always start from scratch. The Bicep team at Microsoft and Bicep community have published a ton of examples to get you started. It is always a good idea to start looking at the examples to understand how a specific element or a built-in functions is used.

Summary

Phew! This chapter happened to be longer than I thought. You learned about installing the Bicep CLI using different methods for different supported platforms. You learned how using the VS Code extension can simplify the authoring experience. You looked at the skeleton structure of a Bicep template and got a quick overview of the elements within a Bicep template. Finally, you learned different methods to compile and deploy Bicep templates. With this knowledge, you can move forward in this journey to learn in-depth about Bicep template syntax.

Working with resources

Chapter 2 kick-started your Bicep journey. In this chapter, you will continue this journey by learning about how to work with resource definitions in Bicep templates. Resources are what you provision and consume. Therefore, resources occupy the most critical part of your Bicep templates. This chapter will start with a general syntax for defining resources. It will build on that by providing examples around defining resource dependencies across child resources, extension resources, and resources that are not a part of the Bicep template.

Defining resources

You have seen a glimpse of a resource definition in chapter 2, where you created a storage account. For most if not all, the elements in a Bicep template have a typical pattern in their declaration.

1 <keyword> <identifier> <type> = <value>

How you specify a value differs based on the element type. Here is the general syntax to declare a resource.

1 resource <resource-symbolic-name-identifier> '<resource-provider>/<resource-type>@<a\

2 pi-version>' = {

3   <resource-properties>

4 }

The resource element is what you use to declare a resource. The resource-symbolic-name or the identifier is the name given to a resource instance. Technically, there is no limit on the length of an identifier. A longer identifier won’t be helpful when you need to reference a resource instance elsewhere in the Bicep file. This identifier value can start with an underscore or an alphabet and can contain hyphens and integers. This identifier must be unique among different elements used in the Bicep file and not just among resource instances. This rule applies to any element that you can use within a Bicep file, and you will see why it is as you proceed in this chapter. Next comes the resource provider and type identification. You need to follow the resource-provider/resource-type@api-version. For an Azure virtual network, the resource-provider would be Microsoft.Network, resource-type would be virtualNetworks, and the value of api-version depends on what provider features you need. If you were to use the latest API version, this resource instance declaration would be Microsoft.Network/virtualNetworks@2021-02-01. This resource instance declaration in Bicep is similar to the following declaration in ARM JSON templates.

1   "resources": [

2     {

3       "type": "Microsoft.Network/virtualNetworks",

4       "apiVersion": "2021-02-01",

5       "name": "bicepVnet"

6       ...

7     }

8   ]

You can already see how concise Bicep makes it for you. If you are using VS Code with the Bicep extension (I can’t imagine why you wouldn’t) for authoring Bicep templates, you get excellent support towards the resource type declaration.

Figure 3-1 - Bicep resource declaration intellisense

Once you have the resource type declaration figured out, you can define the resource properties. This is done by enclosing all properties in curly braces.

1 resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = {

2

3 }

For any resource that you need to provision, at a minimum, you must add name and location properties. In Bicep, these resource properties are key-value pairs separated by a colon (:). Every resource instance will have dependsOn and may have tags property as well. Here is an updated definition of the Azure virtual network resource instance.

1 resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = {

2   name: 'bicepvnet'

3   location: 'eastus'

4   tags: {

5     deparment: 'Engineering'

6   }

7 }

There may be resource specific properties that you need to add as well. In general, you specify these properties using the properties property within the resource instance. For example, virtual networks require an addressSpace object that contains an array of addressPrefixes.

1 //3_1-virtualNetwork.bicep

2 resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = {

3   name: 'bicepvnet'

4   location: 'eastus'

5   tags: {

6     deparment: 'Engineering'

7   }

8   properties: {

9     addressSpace: {

10       addressPrefixes: [

11         '10.1.0.0/24'

12       ]

13     }

14   }

15 }

This is a complete example and you can use this to provision a virtual network. You can use Azure CLI for this.

1 az deployment group create --resource-group bicep --template-file .\3_1-virtualNetwo\

2 rk.bicep

You may have noticed that there are no subnets added to this virtual network. As an exercise, add the subnets property and define the necessary address prefixes for each subnet.

Hint: subnets property needs to be added under properties object. Completed example is 3_2-virtualNetwork-full.bicep in this book’s source code.

Resource dependencies

Resources within the Azure infrastructure will have dependencies. ARM uses dependencies between resources in a template to determine the order of deployment. For example, going back to the earlier example, you may want to implement a Network Security Gateway (NSG) to use within the virtual network subnets. When creating such infrastructure, ARM must create NSG before the subnets. While dependsOn property exists within every resource instance to define such dependencies across resources, you may never have to use it unless you want to control the deployment order or when Bicep cannot figure out a dependency at build time. You can, instead, use the symbolic name, which creates an implicit dependency. The following example demonstrates this.

1 //3_3-virtualNetwork-NSG.bicep

2 resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = {

3   name: 'bicepvnet'

4   location: 'eastus'

5   tags: {

6     deparment: 'Engineering'

7   }

8   properties: {

9     addressSpace: {

10       addressPrefixes: [

11         '10.0.0.0/16'

12       ]

13     }

14     subnets: [

15       {

16         name: 'bicepSubnet1'

17         properties: {

18           addressPrefix: '10.0.1.0/27'

19           networkSecurityGroup: {

20             id: nsg.id

21           }

22         }

23       }

24       {

25         name: 'bicepSubnet2'

26         properties: {

27           addressPrefix: '10.0.2.0/27'

28           networkSecurityGroup: {

29             id: nsg.id

30           }

31         }

32       }

33     ]

34   }

35 }

36

37 resource nsg 'Microsoft.Network/networkSecurityGroups@2021-02-01' = {

38   name: 'bicepnsg'

39   location: 'eastus'

40 }

In this example, the subnets within the virtual network resource instance reference the ID of the NSG resource instance. You achieve this using a property accessor with the symbolic name or identifier of the NSG resource instance – nsg.id, where nsg is the resource instance identifier, and id is the resource ID of the NSG. When you compile this example, you will see the dependsOn element in the ARM JSON set to the NSG resource instance ID, and you can observe the same in the partial output here.

1 {

2   ...

3   "resources": [

4     {

5       "type": "Microsoft.Network/virtualNetworks",

6       "apiVersion": "2021-02-01",

7       "name": "bicepvnet",

8       "location": "eastus",

9       "tags": {

10         "deparment": "Engineering"

11       },

12       ...

13       "dependsOn": [

14         "[resourceId('Microsoft.Network/networkSecurityGroups', 'bicepnsg')]"

15       ]

16     },

17     {

18       "type": "Microsoft.Network/networkSecurityGroups",

19       "apiVersion": "2021-02-01",

20       "name": "bicepnsg",

21       "location": "eastus"

22     }

23   ]

24 }

Many other resource provisioning scenarios require implicit dependency definitions. These scenarios include parent-child relationships, extension resource declarations, and referencing existing resources that do not exist in the Bicep template. You will learn about these different scenarios next.

Child resources

Some resources in Azure may not exist without a parent. For example, the subnets in a virtual network cannot exist without a virtual network. A virtual machine extension requires a virtual machine resource. Similarly, a storage container requires a storage account resource. The resources that exist only within the context of another resource, their parent, are called child resources.

Bicep provides multiple ways to define parent-child relationships.

Nested resources

As you may have used within ARM JSON templates, nested resources are typical for defining the parent-child relationship. To understand this, you can re-write the earlier example of creating a virtual network and adding two subnets.

1 //3_4-virtualNetwork-nested.bicep

2 resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = {

3   name: 'bicepvnet'

4   location: 'eastus'

5   tags: {

6     deparment: 'Engineering'

7   }

8   properties: {

9     addressSpace: {

10       addressPrefixes: [

11         '10.0.0.0/16'

12       ]

13     }

14   }

15

16   resource subnet1 'subnets' = {

17     name: 'bicepSubnet1'

18     properties: {

19       addressPrefix: '10.0.1.0/27'

20     }

21   }

22

23   resource subnet2 'subnets' = {

24     name: 'bicepSubnet2'

25     properties: {

26       addressPrefix: '10.0.2.0/27'

27     }

28   }

29 }

In this updated example, new resource declarations define the subnet resource instances, and these new definitions are within the virtual network resource. The syntax for these child resources is more or less the same as the parent except for the missing fully qualified resource type. When using nesting for parent-child relationships, you only need to specify the child resource type. The child resource inherits the parent resource type and API version. This method helps in clearly documenting the parent-child relationships and dependencies. However, remember that ARM supports only five levels of nesting, and too many levels of nesting might make it complex to read a template.

Parent element

The second method of defining parent-child relationships is to use the parent property within the resource instance.

1 //3_5-virtualNetwork-parent.bicep

2 resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = {

3   name: 'bicepvnet'

4   location: 'eastus'

5   tags: {

6     deparment: 'Engineering'

7   }

8   properties: {

9     addressSpace: {

10       addressPrefixes: [

11         '10.0.0.0/16'

12       ]

13     }

14   }

15 }

16

17 resource subnet1 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' = {

18   parent: vnet

19   name: 'bicepSubnet1'

20   properties: {

21     addressPrefix: '10.0.1.0/27'

22   }

23 }

24

25 resource subnet2 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' = {

26   parent: vnet

27   name: 'bicepSubnet2'

28   properties: {

29     addressPrefix: '10.0.2.0/27'

30   }

31 }

In this example, the subnet resources are child resources and use the parent property to declare the parent-child relationship. Unlike resource nesting, this method requires specifying the fully qualified resource type for the child resource.

Manual child resource name construction

The final approach to defining parent-child relationships is to manually construct the child resource name. In this method, you must ensure that the name of the child resource includes parent resource name in the format parentResourceName/childResourceName. Here is an example.

1 //3_6-virtualNetwork-resourceName.bicep

2 resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = {

3   name: 'bicepvnet'

4   location: 'eastus'

5   tags: {

6     deparment: 'Engineering'

7   }

8   properties: {

9     addressSpace: {

10       addressPrefixes: [

11         '10.0.0.0/16'

12       ]

13     }

14   }

15 }

16

17 resource subnet1 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' = {

18   name: 'bicepvnet/bicepSubnet1'

19   properties: {

20     addressPrefix: '10.0.1.0/27'

21   }

22   dependsOn: [

23     vnet

24   ]

25 }

26

27 resource subnet2 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' = {

28   name: 'bicepvnet/bicepSubnet2'

29   properties: {

30     addressPrefix: '10.0.2.0/27'

31   }

32   dependsOn: [

33     vnet

34   ]

35 }

This method requires a fully qualified resource type declaration for the child resource like the parent property method. Within the child resource name, the parent resource name is hardcoded. Hardcoding this way does not help Bicep determine if there are dependencies between the parent and child resources and, therefore, resource deployment order is not guaranteed. Ensuring that the parent and child resources get deployed in the correct order, an explicit dependency using dependsOn element is needed. You can simply add the symbolic name of the virtual network resource within the dependsOn element of each child resource.

If you need to eliminate explicit dependency declaration, you must reference the virtual network instance instead of hardcoding the name. Referencing a resource instance requires string interpolation syntax, which you will learn in the next chapter.

Extension Resources

Resources that modify another resource or extend other resource functionality are called extension resources. For example, you can apply lock on a resource to prevent deletion or modification. In Bicep, an extension resource gets attached to another resource using the scope property. Only the extension resources have the scope property. The following example demonstrates this.

1 //3_7-storageAccountLock.bicep

2 resource bicepstorage 'Microsoft.Storage/storageAccounts@2021-04-01' = {

3   name: 'bicepstgacct'

4   location: 'eastus'

5   kind: 'StorageV2'

6   sku: {

7     name: 'Standard_LRS'

8   }

9   properties: {

10     accessTier: 'Cool'

11   }

12 }

13

14 resource bicepstoragelock 'Microsoft.Authorization/locks@2016-09-01' = {

15   name: 'bicepstoragelock'

16   scope: bicepstorage

17   properties: {

18     level: 'ReadOnly'

19     notes: 'This resource can neither be modified or deleted.'

20   }

21 }

In this example, the scope property is set to the symbolic name of the storage account and hence creates an implicit dependency. Here is how the dependency gets defined in the compiled ARM JSON template.

1 "dependsOn": [

2 "[resourceId('Microsoft.Storage/storageAccounts', 'bicepstgacct')]"

3 ]

Reference existing resources

There may be a scenario where you need to use an existing resource within your Azure infrastructure in your Bicep template. For example, you may need to add a file share within an existing storage account. To do this, you can use the existing keyword in Bicep.

Here is the general syntax for declaring an existing resource.

1 resource <resource-symbolic-name-identifier> '<resource-provider>/<resource-type>@<a\

2 pi-version>' existing = {

3   <resource-properties>

4 }

The existing resource reference is the same as a regular resource instance declaration except for the existing keyword. Once you have this reference created, you can treat the existing resource reference as something in the same Bicep template. You can also access the resource instance properties using the symbolic name and the property accessor. You can refer to child resources as well using the existing keyword. Here is an example to better understand this.

1 //3_8-storageFileShare.bicep

2 resource bicepstgacct 'Microsoft.Storage/storageAccounts@2021-02-01' existing = {

3   name: 'bicepstgacct'

4 }

5

6 resource bicepfile 'Microsoft.Storage/storageAccounts/fileServices@2021-02-01' exist\

7 ing = {

8   name: 'default'

9   parent: bicepstgacct

10 }

11

12 resource share 'Microsoft.Storage/storageAccounts/fileServices/shares@2021-02-01' = {

13   name: 'bicepshare'

14   parent: bicepfile

15   properties: {

16     enabledProtocols: 'SMB'

17     shareQuota: 10

18   }

19 }

This example creates a new file share in an existing storage account. The second resource that you referenced exists by default within a storage account. It is just a reference to the file service. Creating a file share requires reference to both the storage account and the file service. The existing keyword on the storageAccount and fileService resource declarations will create the necessary reference for the shares resource. When you build this to an ARM JSON, you will not see the storageAccount and fileServices resources in the JSON.

1 {

2   "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTempl\

3 ate.json#",

4   "contentVersion": "1.0.0.0",

5   ...

6   "resources": [

7     {

8       "type": "Microsoft.Storage/storageAccounts/fileServices/shares",

9       "apiVersion": "2021-02-01",

10       "name": "[format('{0}/{1}/{2}', 'bicepstgacct', 'default', 'bicepshare')]",

11       "properties": {

12         "enabledProtocols": "SMB",

13         "shareQuota": 10

14       }

15     }

16   ]

17 }

This section concludes what you need to know about defining resources in Bicep templates. However, there is more. You need to know how to make these templates reusable, simplify resource definitions using built-in functions and expressions, implement resource deployment conditions, and create multiple resource instances. This is what the next few chapters will focus on.

Scoped resource deployment

When using ARM JSON templates, you can deploy to any of the four supported scopes – management groups, tenants, subscriptions, and resource groups. What you can deploy using ARM JSON templates differs between these different target scopes. For example, you can create resource groups at the subscription level only. Therefore, deployment scope for a template identifies where resources are provisioned and the context used for the template validation. In an an ARM JSON template, you specify the target scope using the $schema element in the template. Each scope has an associated schema URL.

Bicep language too supports specifying scope within the bicep templates using the targetScope keyword. This keyword takes one or more values – resourceGroup, tenant, subscription, and managementGroup. The VS Code extension provides auto-completion of available targetScope values. Default deployment scope is resource group.

Figure 3-2 - Bicep targetScope Intellisense

When the targetScope value is set, Bicep knows what resources to expect within the file.

Figure 3-3 - Bicep invalid resource in targetScope

Subscription Scope

At the subscription scope level, you can create resource groups, provision Azure blueprints, Policies, and a few other resource types. Here is an example of using the resource group resource in a subscription scoped Bicep template.

1 //3_9-subscriptionScopedDeployment.bicep

2 targetScope = 'subscription'

3

4 resource rg 'Microsoft.Resources/resourceGroups@2020-06-01' = {

5   name: 'rgTest'

6   location: 'WestUS'

7 }

This, when compiled, produces the following ARM JSON (3_10-subscriptionScopedDeployment.json) representation.

1 {

2   "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDep\

3 loymentTemplate.json#",

4   "contentVersion": "1.0.0.0",

5   "metadata": {

6     "_generator": {

7       "name": "bicep",

8       "version": "0.3.255.40792",

9       "templateHash": "12733481855883894499"

10     }

11   },

12   "functions": [],

13   "resources": [

14     {

15       "type": "Microsoft.Resources/resourceGroups",

16       "apiVersion": "2020-06-01",

17       "name": "rgTest",

18       "location": "WestUS"

19     }

20   ]

21 }

This is how you deploy a subscription level template using Azure CLI.

1 az deployment sub create --name rgDeployment --template-file .\3_8-scopedDeploymentR\

2 G.bicep --location WestUS

Notice that the command when deploying to subscription scope is different from a resource group scoped deployment.

Management group scope

At the management group scope, you can provision a few resources such as Azure Blueprints, Azure Policies, and so on. Here is an example of provisioning a policy definition at the management group level.

1 // 3_11-managementGroupScopedDeployment.bicep

2 targetScope = 'managementGroup'

3

4 param policyDefinitionName string

5

6 resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2020-03-01' = {

7   name: policyDefinitionName

8   properties: {

9     policyType: 'Custom'

10     mode: 'All'

11     parameters: {}

12     policyRule: {

13       if: {

14         allOf: [

15           {

16             field: 'type'

17             equals: 'Microsoft.Compute/virtualMachines'

18           }

19           {

20             anyOf: [

21               {

22                 field: 'Microsoft.Compute/virtualMachines/sku.name'

23                 like: 'Standard_F*'

24               }

25               {

26                 field: 'Microsoft.Compute/virtualMachines/sku.name'

27                 like: 'Standard_G*'

28               }

29             ]

30           }

31         ]

32       }

33       then: {

34         effect: 'deny'

35       }

36     }

37   }

38 }

This is how you deploy a management group level template using Azure CLI.

1 az deployment mg create --management-group-id bicepmg --name policydef01 --location \

2 WestUS --template-file .\3_9-managementGroupScopedDeployment.bicep --parameters poli\

3 cyDefinitionName=vmBicepPolicy01

Tenant scope

Similar to the management group and subscription scope, you can provision only a few types of resources at the tenant scope.

1 //3_12-tenantScopedDeployment.bicep

2 targetScope = 'tenant'

3

4 param mgName string = 'bicepmg'

5 param mgDisplayName string = 'Bicep Management Group'

6

7 resource bicepmg 'Microsoft.Management/managementGroups@2021-04-01' = {

8   name: mgName

9   properties: {

10     displayName: mgDisplayName

11   }

12 }

You can use the following command to perform the tenant scoped deployment.

1 az deployment tenant create --name demoTenantDeployment --location WestUS --template\

2 -file .\3_10-tenantScopedDeployment.bicep

In the section around extension resources, you learned about defining explicit dependencies using the scope property. Within a Bicep template, you may have a scenario where you want to deploy resources across multiple scopes. For example, you may have to deploy resources to two or more resource groups within the same template, or you may want to create a management group hierarchy that requires creating subscriptions which are tenant scoped resources. Bicep allows you create templates that create resources at different scopes. However, this requires knowledge of Bicep modules which you will learn in Chapter 7. So, you can differ this discussion to Chapter 7.

Summary

This chapter introduced you to defining resource instances in a Bicep template. Resources in Azure are the most granular manageable units. Any given resource in Azure may have zero or more dependencies. These dependencies may occur based on the required resource functionality. You define implicit dependencies when you reference the symbolic name of a resource instance in another or by nesting child resources within a parent or using the parent property within a resource instance or using the dependsOn property. You learned about referencing resources existing elsewhere within a Bicep template and finally you looked at performed deployments at different scopes. In all the examples in this chapter, you used hardcoded names and references. Hardcoding names and resource references prevents any reuse of the template you authored. The next chapter will focus on parameterizing Bicep templates.

Authoring reusable templates

As you learned in the introduction, reusability is one of the most critical principles of Infrastructure as Code (Iac). Without reusable code, you will end up in duplication of efforts and errors. All Bicep templates that you created so far used hardcoded values which prevents reuse. For example, take a look at this Bicep template.

1 //4_1-bicepStaticSite.bicep

2 resource bicepDemo 'Microsoft.Web/staticSites@2021-01-01' = {

3   name: 'bicepDemo'

4   location: 'centralus'

5   sku: {

6     tier: 'Free'

7     name: 'Free'

8   }

9   properties: {

10     repositoryUrl: 'https://github.com/rchaganti/bicepbookexamples'

11     repositoryToken: 'GITHUB_PAT'

12     branch: 'main'

13     buildProperties: {

14       appLocation: '/chapter4/staticweb'

15       apiLocation: ''

16       appArtifactLocation: 'public'

17     }

18   }

19 }

This template provisions an Azure static site and enables CI/CD of your site content via GitHub actions. Suppose you were to share this with someone else interested in provisioning a static website using the same template. In that case, they will have to update a few parts of the resource declaration, which may lead to some errors if they are not familiar with the Bicep language. To make your templates reusable, you parameterize the Bicep templates you author, and that’s what you will learn in this chapter. Towards the end, you will also learn about using the outputs element in the Bicep template to make the templates truly reusable.

Parameters

Bicep language supports parameter declarations that get compiled into ARM JSON template parameters. At a minimum, you need the param element followed by an identifier and the parameter’s data type. The general syntax for parameter declaration is similar to how you have created resource declarations. It is not mandatory to assign a value to the parameter at the time of declaration. The value that you assign to a parameter at the time of declaration becomes the parameter’s default value.

1 param <parameter-identifier> <parameter-type>  [= <parameter-value>]

You use the parameter identifier to reference the parameter value within a template. Every parameter declaration must specify a type and Bicep language, like every other programming language, supports multiple data types. Since Bicep templates transpile to ARM JSON templates, Bicep language supports all JSON-supported data types. At the same time, unlike JSON, the Bicep language is strongly typed. Strong typing enables Bicep to catch validation errors early in the template development and not wait for a failed deployment. In Bicep language, simple types include string, int, and bool, and objects and arrays indicate complex types. Bicep does not currently support floating point numbers, decimals, and binary numbers.

Similar to JSON data representation, true and false (without quotes) represent boolean values in Bicep.

1 param isPublicSite = true

Here is an example of how you declare parameters of integer type.

1 param numSites int = 10

Strings in Bicep language require a little more discussion and will probably be your most used data type.

Strings

Strings in Bicep are single-quoted and support all Unicode characters.

1 param staticSiteName string = 'bicepDemo'

A backslash () character is used for escaping reserved characters. For example, if you need a single quote in a string value, you must escape it with a backslash.

1 param staticSiteName string = 'Ravi\'s bicepDemo'

If you need a Unicode character in a string value, you need to use \u.

1 param staticSiteName string = 'We \u{2764} Bicep'

Similar to JSON, a UNC path string in Bicep needs to be escaped as well.

1 param websitePath string = '\\\\server01\\web'

Similar to these escape sequences, you can use:

- \n to represent a newline character

- \t to represent tab

- \r to represent carriage return

$ is also a reserved character when it comes to strings, but it needs to be escaped only when an opening curly bracket follows it ({). This character sequence gets used in string interpolation, which you will learn in the next chapter.

Bicep language has support for multi-line strings as well. Anything you define between a pair of ‘’’ (three single quotes) is a multi-line string in Bicep.

1 param staticSiteContent string = '''

2 <html>

3 <body>

4   <p> This is a multi-line string. Bicep's strings are enclosed in a pair of single \

5 quotes. </p>

6   <p> \, ', ${}, or any reserved character can be used without an escape character.

7 </body>

8 </html>

9 '''

Multi-line strings in Bicep are read as-is and therefore support no interpolation or escaping. Indentation in a multi-line string gets preserved.

With this knowledge, you can add a few string parameters to the Bicep template seen at the beginning of this chapter.

1 param staticSiteName string = 'bicepDemo'

2 param skuName string = 'Free'

3 param skuTier string = 'Free'

4 param location string = 'centralus'

5 param repoUrl string = 'https://github.com/rchaganti/bicepbookexamples'

6 param repoBranch string = 'main'

7 param appLocation string = '/chapter4/staticweb'

8 param appArtifactLocation string = 'public'

9 param appApiLocation string = ''

This is good, but how do you use these parameter values in a resource definition? This is where you need the parameter identifier. When you want to use the value from a parameter as a resource property value, you use the parameter identifier. This is not just for string parameters but for any type of parameter in Bicep. Here is how you can change the resource definition.

1 //4_2-bicepStaticSiteParameters.bicep

2 param staticSiteName string = 'bicepDemo'

3 param skuName string = 'Free'

4 param skuTier string = 'Free'

5 param location string = 'centralus'

6 param repoUrl string = 'https://github.com/rchaganti/bicepbookexamples'

7 param repoBranch string = 'main'

8 param appLocation string = '/chapter4/staticweb'

9 param appArtifactLocation string = 'public'

10 param appApiLocation string = ''

11

12 resource bicepDemo 'Microsoft.Web/staticSites@2021-01-01' = {

13   name: staticSiteName

14   location: location

15   sku: {

16     tier: skuTier

17     name: skuName

18   }

19   properties: {

20     repositoryUrl: repoUrl

21     repositoryToken: 'GITHUB_TOKEN'

22     branch: repoBranch

23     buildProperties: {

24       appLocation: appApiLocation

25       apiLocation: appLocation

26       appArtifactLocation: appArtifactLocation

27     }

28   }

29 }

The way you consume parameter values in Bicep is much simpler than ARM JSON. In ARM JSON, you need to use the parameters function, "[parameters('staticSiteName')]", to retrieve the value of parameter.

The parameters in a Bicep template get compiled to parameter representation (shown partially) in the ARM JSON template language.

1   "parameters": {

2     "staticSiteName": {

3       "type": "string",

4       "defaultValue": "bicepDemo"

5     },

6     ...

7     "repoUrl": {

8       "type": "string",

9       "defaultValue": "https://github.com/rchaganti/bicepbookexamples"

10     },

11     ...

12     "appApiLocation": {

13       "type": "string",

14       "defaultValue": ""

15     }

16   }

Arrays

Like any other programming language, arrays in Bicep hold a collection of values. However, Bicep allows array elements to be either of the same type or different types. This is possible since Bicep uses any type to represent values in an array. Internally, the built-in any function gets used to resolve type errors. You will learn more about the built-in functions in Chapter 10.

Here is how you declare and initialize an array type in Bicep.

1 param siteContentFiles array = [

2   'index.html'

3   'about.html'

4   'contact.html'

5 ]

6

7 param mixedArray array = [

8   10

9   '/chapter4/web'

10   false

11 ]

In the above example, you see an array named siteContentFiles that contains all values of string type. The array named mixedArray includes elements of different types. Elements in an array are separated by a newline and not comma. Irrespective of what type of data you choose to store in an array, you access the array elements using an index that starts zero. Therefore, accessing siteContentFiles[1] will give you the value at index one, which is about.html. In Bicep, you can iterate over the values of an array. You will learn about using iterations in Bicep templates in Chapter 6.

Objects

Objects are a complex type in Bicep. You can combine multiple simple and/or complex types as key-value pairs within a complex type. In the previous example, all resource properties were declared as a set of string parameters. To make it a bit more intuitive, you can combine related parameters in to an object.

1 //4_3-bicepStaticSiteComplexParameter.bicep

2 // Parameters section

3 param staticSiteName string = 'bicepDemo'

4 param skuName string = 'Free'

5 param skuTier string = 'Free'

6 param location string = 'centralus'

7

8 /*

9 This parameter is an object.

10   webapp.github.url is the GitHub repository for the static app

11   webapp.github.branch is the repository branch where you have ths static web app co\

12 ntent

13   webapp.build.appLocation is the folder where you have the static web app content

14   webapp.build.apiLocation is the API location for the static web app

15   webapp.build.artifactLocation is the artifact location for generated static site

16 */

17 param webapp object = {

18   github: {

19     url: 'https://github.com/rchaganti/bicepbookexamples'

20     branch: 'main'

21   }

22

23   build: {

24     appLocation: '/chapter4/staticweb'

25     apiLocation: ''

26     artifactLocation: 'public'

27   }

28 }

Objects are enclosed in curly brackets and key-value pairs within an object are separated using an colon (:). When you need to use the values from the complex object within the resource properties, you need to use the property accessor. For example, webapp.github.url will give you the value needed for the repositoryUrl property. Another thing you can notice in this example is how you can use comments in the Bicep language. // indicate single-line comments and /* */ indicate multiline comments. These comments live only in the Bicep template and do not get added to the compiled ARM JSON template. This is known and won’t be fixed.

1 //4_3-bicepStaticSiteComplexParameter.bicep

2 // Resource section

3 resource bicepDemo 'Microsoft.Web/staticSites@2021-01-01' = {

4   name: staticSiteName

5   location: location

6   sku: {

7     tier: skuTier

8     name: skuName

9   }

10   properties: {

11     repositoryUrl: webapp.github.url

12     repositoryToken: 'ghp_4ShBH0ABUJN'

13     branch: webapp.github.branch

14     buildProperties: {

15       appLocation: webapp.build.appLocation

16       apiLocation: webapp.build.apiLocation

17       appArtifactLocation: webapp.build.artifactLocation

18     }

19   }

20 }

When you compile this Bicep template, the generated ARM JSON template will have the parameters represented as below.

1 "properties": {

2 "repositoryUrl": "[parameters('webapp').github.url]",

3 "repositoryToken": "GITHUB_TOKEN",

4 "branch": "[parameters('webapp').github.branch]",

5 "buildProperties": {

6 "appLocation": "[parameters('webapp').build.appLocation]",

7 "apiLocation": "[parameters('webapp').build.apiLocation]",

8 "appArtifactLocation": "[parameters('webapp').build.artifactLocation]"

9 }

10 }

In ARM JSON templates, you can define parameter metadata. Like the type and defaultValue properties, depending on the type of a parameter, you can use minLength, maxLength, allowedValues, minValue, maxValue, and description. In the Bicep language, you can add parameter properties using decorators.

Parameter decorators

The general syntax for using decorated parameter declaration is as shown below.

1 @decorator()

2 param <parameter-identifier> <parameter-type> [= <parameter-value>]

The parameter decorators in Bicep are @description(), @allowed(), @secure(), @minValue(), @maxValue(), @minLength(), @maxLength(), and @metadata().

Description

Whatever you specify as the text using the @description decoration will be shown to template users are the help text. It is undoubtedly an excellent way to document your template parameters.

1 @description('Specify a name to be used for your static web app.')

2 param staticSiteName string = 'bicepDemo'

Allowed

Using the @allowed decorator, you can constrain the parameter values your template users can specify. For example, Azure static web apps are supported only in a few Azure locations. Therefore, using this decorator, you can limit the parameter values only to the supported locations. The @allowed decorator takes an array of values.

1 @allowed([

2   'westus2'

3   'centralus'

4   'eastus2'

5   'westeurope'

6   'eastasia'

7 ])

8 param location string = 'centralus'

Secure

In the Azure static web application example, you have seen earlier, the repository token is a secret. You should avoid storing secrets such as API tokens and passwords as plain text in a template. When you store secrets in a template, values of these parameters become a part of the deployment history. ARM JSON templates support secureString and secureObject types of adding these secrets to a template. Bicep, however, provides a secure decorator that you can use with string and object parameter types to create secureString and secureObject types. Here is how you use the secure decorator.

1 @secure()

2 param repoToken string

Technically, you can assign a default value to a secure parameter as well. You must avoid doing this. You can, however, assign an empty string or newGuid() function.

The @secure() decorator can be added to parameters of object type as well. For example,

1 @secure()

2 param repoSecrets object = {

3   certPath: ''

4   apiToken: ''

5 }

Similar to a secure string, adding default values to properties in a secure object is not recommended.

If you put together what you have learned so far, you can update the template that you have seen at the beginning to add necessary parameters and parameter decorators.

1 //4_4-bicepStaticSiteSecureParameter.bicep

2 @description('Specify a name for the static web app.')

3 param staticSiteName string = 'bicepDemo'

4

5 @description('Specify the tier for the static web app. Valid values are Free and Sta\

6 ndard.')

7 @allowed([

8   'Free'

9   'Standard'

10 ])

11 param skuTier string = 'Free'

12

13 @description('Specify a SKU name or code. The default value is same as skuTier.')

14 param skuName string = skuTier

15

16 @description('Specify the location where this resource needs to be created.')

17 @allowed([

18   'westus2'

19   'centralus'

20   'eastus2'

21   'westeurope'

22   'eastasia'

23 ])

24 param location string = 'centralus'

25

26 @secure()

27 param repoToken string

28

29 param webapp object = {

30   github: {

31     url: 'https://github.com/rchaganti/bicepbookexamples'

32     branch: 'main'

33   }

34

35   build: {

36     appLocation: '/chapter4/staticweb'

37     apiLocation: ''

38     artifactLocation: 'public'

39   }

40 }

41

42 resource bicepDemo 'Microsoft.Web/staticSites@2021-01-01' = {

43   name: staticSiteName

44   location: location

45   sku: {

46     tier: skuTier

47     name: skuName

48   }

49   properties: {

50     repositoryUrl: webapp.github.url

51     repositoryToken: repoToken

52     branch: webapp.github.branch

53     buildProperties: {

54       appLocation: webapp.build.appLocation

55       apiLocation: webapp.build.apiLocation

56       appArtifactLocation: webapp.build.artifactLocation

57     }

58   }

59 }

You may have noticed in the above parameter declaration that the default value of skuName is set to the value from parameter skuTier. This is valid in Bicep templates.

If you’d like to deploy this template, you first need to clone the repository containing examples from this book. You need a GitHub Personal Access Token as the repository token. You can use a parameters.json file to supply the parameter values you need to override. The bicep build command does not generate the parameters.json for you yet! The Bicep team is working towards implementing this. For now, you can manually create this parameters.json file.

1 # 4_4-bicepStaticSiteSecureParameter.parameters.json

2 {

3     "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentPar\

4 ameters.json#",

5     "contentVersion": "1.0.0.0",

6     "parameters": {

7         "repoToken": {

8             "value": "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"

9         },

10         "webapp": {

11             "value": {

12               "github": {

13                 "url": "https://github.com/rchaganti/bicepbookexamples",

14                 "branch": "main"

15               },

16               "build": {

17                 "appLocation": "/chapter4/staticweb",

18                 "apiLocation": "",

19                 "artifactLocation": "public"

20               }

21             }

22         }

23     }

24 }

Once you are ready, you can use the following Azure CLI command to provision this template.

1 # 4_5-deployTemplate.azcli

2 az deployment group create \

3   --resource-group bicep \

4   --template-file .\4_4-bicepStaticSiteSecureParameter.bicep \

5   --parameters @4_4-bicepStaticSiteSecureParameter.parameters.json

Once the deployment completes and the GitHub action runs successfully, you can access the static web app URL in a browser. You should see what is shown in Figure 4-1.

Figure 4-1. Deployed Azure static web in a browser

minValue and maxValue

You can use the @minValue and @maxValue decorators to define the upper and lower bounds for an integer parameter value. Both these decorators take an integer value as input and using any of these two decorators is supported.

1 @minValue(2)

2 @maxValue(10)

3 param shareQuota int

This example defines 2GB as the lower bound and 10GB as the upper bound for a file share quota property.

minLength and maxLength

Using the @minLength and @maxLength decorator, you can constrain the length of string and array data type parameter values. For example, a storage account length has to be minimum 3 characters and maximum 24 characters. This can be defined using the @minLength and @maxLength decorators.

1 @minLength(3)

2 @maxLength(24)

3 param bicepstgacct string

Here is an updated example of creating an Azure file share in an existing storage account. This example uses the length and value decorators.

1 //4_6-AzureFileShare.bicep

2 param shareName string = 'bicepShare'

3

4 @minLength(3)

5 @maxLength(24)

6 param storageAccountName string = 'bicepstgacct'

7

8 @minValue(2)

9 @maxValue(10)

10 param shareQuota int = 4

11

12 resource bicepstgacct 'Microsoft.Storage/storageAccounts@2021-02-01' existing = {

13   name: storageAccountName

14 }

15

16 resource bicepfile 'Microsoft.Storage/storageAccounts/fileServices@2021-02-01' exist\

17 ing = {

18   name: 'default'

19   parent: bicepstgacct

20 }

21

22 resource share 'Microsoft.Storage/storageAccounts/fileServices/shares@2021-02-01' = {

23   name: shareName

24   parent: bicepfile

25   properties: {

26     enabledProtocols: 'SMB'

27     shareQuota: shareQuota

28   }

29 }

metadata

You can use the @metadata decorator to add any other arbitrary information about the parameter.

1 @metadata({

2 addedOn: '2021-04-11'

3 })

4 @minValue(2)

5 @maxValue(10)

6 param shareQuota int = 4

This when compiled to ARM JSON results in the following.

1     "shareQuota": {

2       "type": "int",

3       "maxValue": 10,

4       "minValue": 2,

5       "metadata": {

6         "addedOn": "2021-04-11"

7       }

8     }

Output

So far, you have learned how to add parameters to a Bicep template and decorate those parameters with necessary properties or constraints. One other aspect of reusability is the output from a reusable template. In Bicep, the output element defines the output from a template. The general syntax for defining an output in Bicep template is:

1 output <output-identifier> <output-type> = <literal> or <expression>

The type of data you return from output can be an integer, a string, an array, an object, or a boolean value. For example, if you want to return the resource Id of the file share, you can add the following output declaration to the earlier example.

1 output fileShare string = share.id

In this example, share is the identifier of the file share resource instance. The id property gives you the value of the resource Id. By adding this property accessor, you can write the template to return the resource Id of the file share as fileShare. When you deploy this template using Azure CLI, you will see the following object in the output of the template deployment.

1 "outputs": {

2 "fileShare": {

3 "type": "String",

4 "value": "/subscriptions/21e034f70820/resourceGroups/bicep/providers/Microsoft.Sto\

5 rage/storageAccounts/bicepstgacct/fileServices/default/shares/bicepshare"

6 }

7 }

You will learn more about consuming output from Bicep templates in Chapter 7.

Generating Parameter files

If you have authored or deployed on ARM JSON templates, you will know that the parameter JSON files can be used to supply the input parameter values to the ARM templates. The initial release of Bicep CLI did not have the ability to generate the parameter JSON file from a Bicep template. This capability was added in a later release.

The generate-params subcommand helps you generate the parameter JSON file for any given Bicep template.

1 bicep generate-params .\4_4-bicepStaticSiteSecureParameter.bicep

This command will generate the parameter JSON for all parameters that do not have any default values.

1 {

2   "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParam\

3 eters.json#",

4   "contentVersion": "1.0.0.0",

5   "parameters": {

6     "repoToken": {

7       "value": ""

8     }

9   }

10 }

Summary

In this chapter, you learned about parameterizing your Bicep templates. Parameterizing is an essential step towards implementing Infrastructure as Code (IaC) principles. Bicep language supports different types of parameters and parameter decorators to define parameter properties or metadata. Finally, by adding output in the Bicep template, you make the template ready for modularizing your infrastructure provisioning. Adding too many parameters may not always be a good idea. There has to be a balance between free-form and fixed-configuration in Bicep templates. You can achieve this right using variables and expressions in the Bicep template, which is the topic that you will learn in the next chapter.

Variables, function, and expressions

You learned how to create reusable Bicep templates by adding parameters and outputs. Parameters help bring in reusability, but too many parameters can cause issues in deployment consistency. For example, you looked at provisioning an Azure static site in the previous chapter, and towards the end, you parameterized the template for almost every resource property. Parameterizing a template like this is called free-form configuration, and it provides higher flexibility to the users using the template to provision an Azure static site. These users may define an arbitrary static site name, choose SKU type and tier, and provision the site at any supported locations. This flexibility may, however, have governance and financial implications for the organization. For example, your organization may have static sites on Azure without its name indicating the purpose or department it belongs to or at locations where you don’t desire to provision static sites. Your organization may end up paying more when a user selects the standard tier for a static site that only requires the free tier. While parameterizing enables reusability, it may not always be beneficial to write free-form configurations. At the same time, not having any parameters is also not good. It results in fixed or rigid configurations that you cannot reuse. You need to balance between the free-form configurations and fixed or known configurations. You may use Azure Policy to control several aspects of resource provisioning, but that is outside the scope of this book. From a Bicep template point of view. to build the right balance in a template, you can use variables, functions, and expressions, and that is the subject of this chapter.

Variables are useful in a few other scenarios as well. For example, when you have to specify a resource property value multiple times in a template, you can use declare a variable and use its identifier wherever you need that value. For example, the location of multiple resource instances in a Bicep template can be a variable. Using a variable here eliminates the need and scope for human error when updating the same value at multiple places. As you will learn later in this chapter, variables, built-in functions in Bicep, and expressions provide a perfect combination to create more dynamic Bicep templates. Therefore, what you learn in this chapter is vital to developing production-grade Bicep templates.

Dive right in!

You can create variables in Bicep using the var keyword. The general syntax for declaring a variable is:

1 var <variable-name> = <variable-value>

Notice that there is no variable type that you need to specify when declaring a variable. In Bicep, you need to assign variables at the time of declaration, and the variable type gets inferred from the value you assign to a variable. Unlike other programming languages, you cannot change or reassign the value assigned to a variable in Bicep. As you learned in the previous chapters, the identifiers used as variables cannot be same as that of any other element in the Bicep template.

Here is a quick example of using variables within the earlier Azure static site example.

1 //5_1-bicepStaticSiteParamAndVar.bicep

2 // Parameters section

3 @secure()

4 param repoToken string // This is the GitHub Personal Access Token

5

6 param webapp object = {

7   github: {

8     url: 'https://github.com/rchaganti/bicepbookexamples'

9     branch: 'main'

10   }

11

12   build: {

13     appLocation: '/chapter4/staticweb'

14     apiLocation: ''

15     artifactLocation: 'public'

16   }

17 }

18

19 // Variables section 1

20 var staticSiteName = 'bicepdemo'

21 var skuTier = 'Free'

22 var skuName = skuTier

23 var location = resourceGroup().location

24

25 // Resources section

26 resource bicepDemo 'Microsoft.Web/staticSites@2021-01-01' = {

27   name: staticSiteName

28   location: location

29   sku: {

30     tier: skuTier

31     name: skuName

32   }

33   properties: {

34     repositoryUrl: webapp.github.url

35     repositoryToken: repoToken

36     branch: webapp.github.branch

37     buildProperties: {

38       appLocation: webapp.build.appLocation

39       apiLocation: webapp.build.apiLocation

40       appArtifactLocation: webapp.build.artifactLocation

41     }

42   }

43 }

44

45 // Variables section 2

46 var defaultHostname = bicepDemo.properties.defaultHostname

47

48 // Output section

49 output staticSiteHotsName string = defaultHostname

The variables in this Bicep template have the same identifiers as parameters from an earlier example. Therefore, you need not update any resource property values. You can declare string, numeric, boolean, object, and array type variables in Bicep. In Bicep, a variable can get its value from another variable or a parameter or a built-in function or an expression or a resource instance properties or a module’s output. In general, elements in a Bicep template can be in order. You don’t need to have parameters at the beginning followed by variables, resources, and outputs. As you see in the above example, there are a few variables at the beginning and one variable at the end of the template just before the outputs section.

In the above example, staticSiteName and skuTier are string variables with static values assigned to them. skuName variable gets its value from the skuTier variable. The location variable gets its value from a simple expression resourceGroup().location. This value refers to the location of the resource group to which you are provisioning the static site. Finally, the variable defaultHostName references a resource property. This variable is declared after the resource instance and just before the outputs section. How variables get translated into an ARM JSON template depends on how you assign the value to a variable. The variables with static values and simple expressions go into the variables section of the ARM JSON template.

1   "variables": {

2     "staticSiteName": "bicepdemo",

3     "skuTier": "Free",

4     "skuName": "[variables('skuTier')]",

5     "location": "[resourceGroup().location]"

6   }

Variables that reference resource properties or module outputs get embedded inline in the ARM JSON resource or output definitions. This enables using reference and list* functions within the variable declaration, which is not possible in ARM JSON templates.

1   "outputs": {

2     "staticSiteHotsName": {

3       "type": "string",

4       "value": "[reference(resourceId('Microsoft.Web/staticSites', variables('static\

5 SiteName'))).defaultHostname]"

6     }

7   }

You will explore more about variable types, built-in functions, and expressions in the following few sections of this chapter. To understand some of these concepts, you will build a slightly more complex of creating a Bicep template for an Azure virtual machine. You will develop the templates and deploy the infrastructure for a virtual machine incrementally and learn about variables, functions, and expressions as you do so. You can deploy each template separately using either Azure PowerShell or Azure CLI, as you have seen in the earlier chapters.

For creating an Azure virtual machine, you need to specify a virtual network, an NSG with security rules, a public IP address, a network interface, VM profile, an OS profile, OS and data disks, and optional diagnostic profiles and extensions.

1 //5_2-bicepVirtualNetwork.bicep

2 param vNetPrefix string = 'bicep'

3

4 param subnetPrefix string = 'bicep'

5

6 var location = resourceGroup().location

7 var vNetAddressPrefix = '10.0.0.0/16'

8 var subnetAddressPrefix = [

9   '10.0.1.0/27'

10   '10.0.2.0/27'

11 ]

12

13 resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = {

14   name: '${vNetPrefix}-Net'

15   location: location

16   properties: {

17     addressSpace: {

18       addressPrefixes: [

19         vNetAddressPrefix

20       ]

21     }

22   }

23

24   resource sb1 'subnets' = {

25     name: '${subnetPrefix}-subnet1'

26     properties: {

27       addressPrefix: subnetAddressPrefix[0]

28     }

29   }

30

31   resource sb2 'subnets' = {

32     name: '${subnetPrefix}-subnet2'

33     properties: {

34       addressPrefix: subnetAddressPrefix[1]

35     }

36   }

37 }

This is the virtual network you will be using the virtual machine network. Beyond what you have already seen in an earlier example, the variable subnetAddressPrefix is an array type. As you have seen in the previous chapter, array elements in Bicep start at index zero. So, accessing subnetAddressPrefix[0] and subnetAddressPrefix[1] will give you the two values within the array. The virtual network and subnet names are derived using string interpolation syntax. You must enclose string interpolation expressions in single quotes. Using string interpolation, you can combine values of parameters or variables or resource instance properties with any arbitrary text. This is similar to how you may have used the concat() function in ARM JSON templates. You can combine string interpolation with variables, parameters, resource properties, and built-in functions to create complex expressions.

You will now create an Azure Network Security Group (NSG) and add the necessary inbound rules for a Linux VM.

1 //5_3-bicepNetworkSecurityGroup.bicep

2 param nsgPrefix string = 'bicep'

3

4 var location = resourceGroup().location

5 var nsgProperties = [

6   {

7     name: 'SSH'

8     priority: 1001

9     protocol: 'Tcp'

10     destinationPortRange: '22'

11   }

12   {

13     name: 'HTTP'

14     priority: 1002

15     protocol: 'Tcp'

16     destinationPortRange: '80'

17   }

18 ]

19

20 resource nsg 'Microsoft.Network/networkSecurityGroups@2021-02-01' = {

21   name: '${nsgPrefix}-nsg'

22   location: location

23   properties: {

24     securityRules: [

25       {

26         name: nsgProperties[0].name

27         properties: {

28           priority: nsgProperties[0].priority

29           protocol: nsgProperties[0].protocol

30           access: 'Allow'

31           direction: 'Inbound'

32           sourceAddressPrefix: '*'

33           sourcePortRange: '*'

34           destinationAddressPrefix: '*'

35           destinationPortRange: nsgProperties[0].destinationPortRange

36         }

37       }

38       {

39         name: nsgProperties[1].name

40         properties: {

41           priority: nsgProperties[1].priority

42           protocol: nsgProperties[1].protocol

43           access: 'Allow'

44           direction: 'Inbound'

45           sourceAddressPrefix: '*'

46           sourcePortRange: '*'

47           destinationAddressPrefix: '*'

48           destinationPortRange: nsgProperties[1].destinationPortRange

49         }

50       }

51     ]

52   }

53 }

This example of creating an NSG uses a variable called nsgProperties for defining NSG security rules. This variable is an array of objects. You can access each object within this array using the array indices 0 and 1. Therefore, within the NSG resource instance properties nsgProperties[0].name will give you the first security rule name and nsgProperties[0].destinationPortRange will give the destination port for the security rule.

You can now create a Public IP address that you will be using to SSH into the Linux VM.

1 //5_4-bicepPublicIPAddress.bicep

2 param vmName string = 'ubuntu20-01'

3 param publicIPPrefix string = 'bicep'

4

5 var location = resourceGroup().location

6 var dnsLabelPrefix = toLower('${vmName}${take(uniqueString(resourceGroup().id), 6)}')

7

8 resource publicIP 'Microsoft.Network/publicIPAddresses@2021-02-01' = {

9   name: '${publicIPPrefix}${vmName}-pip'

10   location: location

11   sku: {

12     name: 'Basic'

13   }

14   properties: {

15     publicIPAllocationMethod: 'Dynamic'

16     publicIPAddressVersion: 'IPv4'

17     dnsSettings: {

18       domainNameLabel: dnsLabelPrefix

19     }

20     idleTimeoutInMinutes: 10

21   }

22 }

In this example of creating a public IP address resource, the expression assigned to the dnsLabelPrefix variable deserves an explanation. The DNS label for a public IP address has to be unique. Therefore, you need to generate a unique string. If you break the expression down into different components, the ${vmName} gives the value of the vmName parameter. You use string interpolation syntax for this parameter since you are using it within an expression. The take function takes two arguments – a string and the number of characters to take from the input string. In this example, the input string gets derived using the resource group ID, which is given by the resourceGroup().id expression. When you pass this resource group ID to the uniqueString() function, it generates a unique string. The take() function returns only the first six characters of this unique string. Once again, since take() function is within a larger expression, you need to enclose that string interpolation syntax. The expression assigned to the dnsLabelPrefix is equivalent to the following in ARM JSON.

1 "dnsLabelPrefix": "[toLower(format('{0}-{1}', replace(parameters('vmName'), '-', '')\

2 , take(uniqueString(resourceGroup().id), 6)))]"

Once you have the virtual network, NSG, and a public IP created, it is time to create the network interface.

1 //5_5-bicepNetworkInterface.bicep

2 param vmName string = 'ubuntu20-01'

3 param vNetPrefix string = 'bicep'

4 param subnetPrefix string = 'bicep'

5 param publicIPPrefix string = 'bicep'

6 param nsgPrefix string = 'bicep'

7 param netInterfacePrefix string = 'bicep'

8

9 var publicIPName = '${publicIPPrefix}${vmName}-pip'

10 var location = resourceGroup().location

11 var nicName = '${netInterfacePrefix}${vmName}-nic'

12 var nicIpConfigName = '${nicName}-ipconfig'

13

14 resource nic 'Microsoft.Network/networkInterfaces@2021-02-01' = {

15   name: nicName

16   location: location

17   properties: {

18     ipConfigurations: [

19       {

20         name: nicIpConfigName

21

22         properties: {

23           subnet: {

24             id: resourceId('Microsoft.Network/virtualNetworks/subnets', '${vNetPrefi\

25 x}-Net','${subnetPrefix}-subnet1')

26           }

27           privateIPAllocationMethod: 'Dynamic'

28           publicIPAddress: {

29             id: resourceId('Microsoft.Network/publicIPAddresses', publicIPName)

30           }

31         }

32       }

33     ]

34     networkSecurityGroup: {

35       id: resourceId('Microsoft.Network/networkSecurityGroups', '${nsgPrefix}-nsg')

36     }

37   }

38 }

The most notable thing in this example is the usage of the resourceId() function. Since you already provisioned a virtual network, NSG, and a public IP address for the VM, you can reference their unique resource instance identifiers in this template to associate the network interface. The number of arguments to this function depends on the number of resource segments in the resource type.

1 resourceId([subscriptionId], [resourceGroupName], resourceType, resourceName1, [reso\

2 urceName2], ...)

Both subscriptionId and resourceGroupName default to the subscription and resource group in the current context. For a subnet, the resource type is Microsoft.Network/virtualNetworks/subnets. This resource type contains two resource segments – virtualNetwork and subnets. Therefore, you must supply the names of virtual network and subnet resources to which you want to associate the VM network interface. The same things apply to public IP and NSG resource IDs as well.

You have the necessary infrastructure to go ahead and create a VM.

1 //5_6-bicepVirtualMachine.bicep

2 param netInterfacePrefix string = 'bicep'

3 param publicIPPrefix string = 'bicep'

4

5 param vmName string = 'ubuntu20-01'

6 param adminUsername string = 'bicepuser'

7

8 @secure()

9 param adminPassword string

10

11 var location = resourceGroup().location

12

13 var nicName = '${netInterfacePrefix}${vmName}-nic'

14 var publicIPName = '${publicIPPrefix}${vmName}-pip'

15

16 var osVersion = '18.04-LTS'

17 var vmSize = 'Standard_B2s'

18 var osDiskType = 'Standard_LRS'

19

20 resource vm 'Microsoft.Compute/virtualMachines@2021-03-01' = {

21   name: vmName

22   location: location

23   properties: {

24     hardwareProfile: {

25       vmSize: vmSize

26     }

27     storageProfile: {

28       osDisk: {

29         createOption: 'FromImage'

30         managedDisk: {

31           storageAccountType: osDiskType

32         }

33       }

34       imageReference: {

35         publisher: 'Canonical'

36         offer: 'UbuntuServer'

37         sku: osVersion

38         version: 'latest'

39       }

40     }

41     networkProfile: {

42       networkInterfaces: [

43         {

44           id: resourceId('Microsoft.Network/networkInterfaces', nicName)

45         }

46       ]

47     }

48     osProfile: {

49       computerName: vmName

50       adminUsername: adminUsername

51       adminPassword: adminPassword

52     }

53   }

54 }

55

56 var pipIP = reference(resourceId('Microsoft.Network/publicIPAddresses', publicIPName\

57 ), '2021-02-01', 'full').properties

58

59 output vmAccess object = {

60   'adminUsername': adminUsername

61   'vmDnsFqdn': pipIP.dnsSettings.fqdn

62   'connectCommand': 'ssh ${adminUsername}@${pipIP.dnsSettings.fqdn}'

63 }

This example contains a few expressions and functions that you have already learned in the previous examples. This template references the network interface created in an earlier template. This network interface is associated with the public IP address, NSG, and virtual network. Therefore, you need not explicitly reference any of those resources in this VM template. However, as you have not provisioned the public IP address resource in this VM template, you will not have access to the domain FQDN associated with the public IP address. This FQDN is essential to accessing the VM remotely. To retrieve the FQDN, you need to create a reference to the public IP address resource instance. The reference() function helps here. This function returns the current state of a resource.

1 reference(resourceName or resourceIdentifier, [apiVersion], ['Full'])

With the reference() function, you can use the resourceName argument if the resource you are referencing is provisioned in the same template. Since public IP address is not, you need to use the resourceIdentifier argument. You already have the knowledge of using the resourceId() function to retrieve a resource instance identifier. The apiVersion argument is mandatory as the public IP resource is not provisioned in this template. Finally, the full argument is needed if you want the reference() function to return the complete resource object properties.

Once you provision this VM template, you will see the vmAccess object in the deployment output.

1 "outputs": {

2 "vmAccess": {

3 "type": "Object",

4 "value": {

5 "adminUsername": "bicepuser",

6 "connectCommand": "ssh bicepuser@ubuntu20-016axmw2.westus.cloudapp.azure.com",

7 "vmDnsFqdn": "ubuntu20-016axmw2.westus.cloudapp.azure.com"

8 }

9 }

10 }

If you have any provisioned all templates so far, you must have a functioning Linux VM. You used password authentication for this. However, you can configure Linux VMs to use SSH keys as well for authentication. I find SSH key-based authentication much more secure and easier than password-based authentication. What can you do to update the above VM template to add SSH key-based authentication as an alternative?

For enabling SSH key-based authentication, you use a private-public key pair for authentication. The Azure Linux VM you create will have your public key, and the local system will have the private key. You have to specify your public key while provisioning a VM. You can generate an SSH-key using the ssh-keygen utility. The command you use to generate a private/public key pair is ssh-keygen -m PEM -t rsa -b 4096. This command will prompt you for a location to store the public key and a passphrase that you have to use while authenticating with the VM.

Now, move onto updating the template to add SSH key-based authentication support. To provide an SSH key as an alternative, you need to make sure that you accept either a password or an SSH key. You need to add a parameter to consider this user intent.

1 @description('Specify if you want to use password or sshkey for authentication')

2 @allowed([

3   'password'

4   'sshkey'

5 ])

6 param passwordOrSshKeyForAuthentication string = 'sshkey'

7

8 @description('Specify a password or ssh-key based on your authentication choice')

9 @secure()

10 param adminPasswordOrKey string

You configure the right authentication mechanism based on a user choice using the osProfile object within VM properties. You will have to add an OS profile property called linuxConfiguration object. When a user chooses SSH based authentication and supplies a public key, you need to push that into the VM using the SshPublicKey object within linuxConfiguration. To add this dynamic selection of authentication type, you will use the any() function combined with a conditional expression.

Conditional expression in Bicep is like a ternary operator in other languages and is one of the logical operators in Bicep. You will learn more about the logical operators in the next chapter.

1 condition ? true-value : false-value

This syntax is self-explanatory. The conditional expression returns a value based on result of condition evaluation. For the linuxConfiguration object, you have to do something like this.

1 passwordOrSshKeyForAuthentication == 'sshkey' ? 'ssh configuration' : null

If the value of passwordOrSshKeyForAuthentication parameter is equal to sshkey, you need to build a sshPublicKey object and otherwise return null. Finally, you wrap this as a value to the any() function. The any() function returns a value that is compatible with any data type. This is needed as you cannot conclusively decide the type of the value that gets assigned to linuxConfiguration. When a user chooses SSH based authentication, here is how the linuxConfiguration object should look like.

1 disablePasswordAuthentication: true

2 ssh: {

3 publicKeys: [

4 {

5 path: '/home/${adminUsername}/.ssh/authorized_keys'

6 keyData: adminPasswordOrKey

7 }

8 ]

9 }

Finally, you can add this within the conditional expression and wrap it as a value within the any() function.

1 linuxConfiguration: any(passwordOrSshKeyForAuthentication == 'sshkey' ? {

2 disablePasswordAuthentication: true

3     ssh: {

4     publicKeys: [

5     {

6     path: '/home/${adminUsername}/.ssh/authorized_keys'

7     keyData: adminPasswordOrKey

8     }

9     ]

10     }

11 } : null)

To understand how simple the Bicep language is, you should look at how linuxConfiguration object gets translated to in a JSON template.

1 "linuxConfiguration": "[if(equals(parameters('passwordOrSshKeyForAuthentication'), '\

2 sshkey'), createObject('disablePasswordAuthentication', true(), 'ssh', createObject(\

3 'publicKeys', createArray(createObject('path', format('/home/{0}/.ssh/authorized_key\

4 s', parameters('adminUsername')), 'keyData', parameters('adminPasswordOrKey'))))), n\

5 ull())]"

Writing and debugging an expression like this in JSON will be a nightmare.

Alright. Here is the final updated template that you can use to provision a Linux VM with SSH-based authentication.

1 //5_7-bicepVritualMachineSSH.bicep

2 param netInterfacePrefix string = 'bicep'

3 param publicIPPrefix string = 'bicep'

4

5 param vmName string = 'ubuntu20-01'

6 param adminUsername string = 'bicepuser'

7

8 @description('Specify if you want to use password or sshkey for authentication')

9 @allowed([

10   'password'

11   'sshkey'

12 ])

13 param passwordOrSshKeyForAuthentication string = 'sshkey'

14

15 @description('Specify a password or ssh-key based on your authentication choice')

16 @secure()

17 param adminPasswordOrKey string

18

19 var location = resourceGroup().location

20

21 var nicName = '${netInterfacePrefix}${vmName}-nic'

22 var publicIPName = '${publicIPPrefix}${vmName}-pip'

23

24 var osVersion = '18.04-LTS'

25 var vmSize = 'Standard_B2s'

26 var osDiskType = 'Standard_LRS'

27

28 resource vm 'Microsoft.Compute/virtualMachines@2021-03-01' = {

29   name: vmName

30   location: location

31   properties: {

32     hardwareProfile: {

33       vmSize: vmSize

34     }

35     storageProfile: {

36       osDisk: {

37         createOption: 'FromImage'

38         managedDisk: {

39           storageAccountType: osDiskType

40         }

41       }

42       imageReference: {

43         publisher: 'Canonical'

44         offer: 'UbuntuServer'

45         sku: osVersion

46         version: 'latest'

47       }

48     }

49     networkProfile: {

50       networkInterfaces: [

51         {

52           id: resourceId('Microsoft.Network/networkInterfaces', nicName)

53         }

54       ]

55     }

56     osProfile: {

57       computerName: vmName

58       adminUsername: adminUsername

59       adminPassword: adminPasswordOrKey

60       linuxConfiguration: any(passwordOrSshKeyForAuthentication == 'sshkey' ? {

61         disablePasswordAuthentication: true

62         ssh: {

63           publicKeys: [

64             {

65               path: '/home/${adminUsername}/.ssh/authorized_keys'

66               keyData: adminPasswordOrKey

67             }

68           ]

69         }

70       } : null)

71     }

72   }

73 }

74

75 var pipIP = reference(resourceId('Microsoft.Network/publicIPAddresses', publicIPName\

76 ), '2021-02-01', 'full').properties

77

78 output vmAccess object = {

79   'adminUsername': adminUsername

80   'vmDnsFqdn': pipIP.dnsSettings.fqdn

81   'connectCommand': 'ssh ${adminUsername}@${pipIP.dnsSettings.fqdn}'

82 }

To provision this VM with SSH-based authentication, you must supply the SSH public key you generated earlier as a value to the adminPasswordOrKey parameter either inline or using parameters.json. Once the deployment is complete, you can use an ssh client to connect to this VM.

1 ssh -i ~/.ssh/id_azurevm username@domainFqdn

~/.ssh/id_azurevm should be replaced with the path where you stored the private key.

Summary

Another longer chapter! In this chapter, you learned about declaring variables, using built-in functions, and creating expressions. All these three concepts are critical to creating more dynamic and flexible Bicep templates. It is nearly impossible to cover all built-in functions within a single Bicep template or even across multiple templates. Therefore, chapter 10 will act as a primer and show you several examples of using operators and built-in functions in Bicep language. You looked at building an Azure Linux VM incrementally, and in that process, you created several small resource templates. In the next chapter, you will see how you can create multiple instances of these resources using iterations. Towards the end, you learned about conditional expression in Bicep. In the next chapter, you will extend this knowledge to perform conditional deployments of resource instances.

Iterations and Conditions

In all the examples so far in this book, you have seen Bicep templates that you have used to create storage accounts, file shares, virtual networks and subnets, network security groups, and virtual machines. In these examples, there was a lot of duplication of resource instance definitions and variables. In the last chapter, you looked at creating a virtual machine that required a data disk. What if you need to add multiple data disks of the same size? What if you want to provision multiple virtual machines of the same type? And, what if you’re going to decide the number of VM instances you want to create based on some environmental configuration? Fortunately, Bicep language offers iterations and conditions as answers to these questions. And, this chapter will equip you with the knowledge you need to implement iterations and conditions to help you simplify Bicep templates.

Iterations

Within the ARM JSON template language, you may have used the copy element to add a copy loop to iterate over variables, resource instances, resource properties, and outputs. In Bicep language, you can use three different ways to implement iterations. The first method is a loop index.

Loop index

A loop index is the simplest way to implement iteration in Bicep language. Here is the general syntax for implementing a loop index.

1 [for <index> in range(<start>, <stop>): {

2   <resource-instance> or <resource-properties>

3 }]

In this syntax, the index is an enumerator that holds a value within a range of values indicated by start and stop. You assign an iteration to elements in the Bicep language such as resource instance declarations, resource properties, variables, and outputs. You enclose the iteration within square brackets, and that indicates that the iteration produces an array. For example, here is how the resource instance declaration changes if you need to add an iteration. This pattern continues for other methods of iteration as well.

1 resource <resource-symbolic-name> '<resource-type>@<api-version>' = [for <index> in \

2 range(<start>, <stop>): {

3   <resource-instance>

4 }]

Take a look at this example.

1 //6_1-storageAccountLoopIndex.bicep

2 param resourcePrefix string = 'bicep'

3

4 @minValue(2)

5 @maxValue(5)

6 param numStorageAccount int = 2

7

8 var numUniqueCharacters = 24 - (length(resourcePrefix) + 1)

9

10 var storageAccountNames = [for varIndex in range(0, numStorageAccount): '${resourceP\

11 refix}${varIndex}${take(uniqueString(resourceGroup().id), numUniqueCharacters)}']

12

13 resource sa 'Microsoft.Storage/storageAccounts@2021-04-01' = [for index in range(0, \

14 numStorageAccount): {

15   name: '${storageAccountNames[index]}'

16   location: resourceGroup().location

17   sku: {

18     name: 'Standard_LRS'

19   }

20   kind: 'StorageV2'

21   properties: {

22   accessTier: 'Hot'

23   }

24 }]

25

26 output saIds array = [for outIndex in range(0, numStorageAccount): [

27   resourceId('Microsoft.Storage/storageAccounts', '${storageAccountNames[outIndex]}')

28 ]]

In this example, you can see the loop index used with three resource elements – variables, resources, and outputs.

In the variable loop, you iterate over the range from 0 to the value indicated by the parameter numStorageAccount. The expression in this variable loop generates a unique storage account name. Based on the length of the resourcePrefix parameter value, you need to take only the necessary number of characters from the unique string. Also, notice that the expression appears on the same line as the variable declaration. You need this to ensure that you don’t run into any syntax errors. You may want to read this GitHub issue thread to understand an alternative to this if you’re going to make code formatted well.

The next place where you used the loop index is the resource instance declaration. The loop index syntax is more or less the same as what you have seen with the variable loop. And, the last place you used the loop index is with the output element. If you observe these three elements you used in the above example, you see a pattern. For top-level elements, here is how it looks.

1 <element> <identifier> [<type>] = [for <index> in range(<start>, <stop>): <element-s\

2 pecific-properties-or-expression>]

For resource instance properties, you won’t need any identifier or type.

1 <resource-property>: [for <index> in range(<start>, <stop>): <property-values-or-exp\

2 ression>]

Here is an example that shows how you can use loop index with resource instance properties.

1 //6_2-virtualNetworkSubnetLoopIndex.bicep

2 var numSubnets = 2

3 var subnetPrefix = [

4   '10.0.1.0/27'

5   '10.0.2.0/27'

6 ]

7

8 resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = {

9   name: 'vnet'

10   location: resourceGroup().location

11   properties: {

12     addressSpace: {

13       addressPrefixes: [

14         '10.0.0.0/16'

15       ]

16     }

17     subnets: [for sIndex in range(0, numSubnets): {

18       name: 'subnet${sIndex}'

19       properties: {

20         addressPrefix: subnetPrefix[sIndex]

21       }

22     }]

23   }

24 }

25

26 output subnetIds array = [for outIndex in range(0, numSubnets): [

27   resourceId('Microsoft.Storage/virtualNetworks/subnets', 'vnet','subnet${outIndex}')

28 ]]

Collection iteration

The second method you can use to create a variable, resource instance, resource instance property, and output iterations is to iterate over a collection. Here is the general syntax for the top-level elements in a Bicep template.

1 <element> <identifier> [<type>] = [for <item> in <collection>: {

2   <resource-instance> or <resource-properties>

3 }]

You can use an integer or string array or an array of objects types as the collection. The following example iterates over an array of strings to create storage accounts.

1 //6_3-storageAccountsCollection.bicep

2 param saNames array = [

3   'sqllondon'

4   'sqlchennai'

5   'archiveseattle'

6 ]

7

8 var uniqueSaNames = [for sName in saNames: '${sName}${take(uniqueString(resourceGrou\

9 p().id), 24 - (length(sName) + 1))}']

10

11 resource sa 'Microsoft.Storage/storageAccounts@2019-04-01' = [for uSName in uniqueSa\

12 Names: {

13   name: uSName

14   location: resourceGroup().location

15   sku: {

16     name: 'Standard_LRS'

17   }

18   kind: 'StorageV2'

19   properties: {

20     accessTier: 'Hot'

21   }

22 }]

23

24 output saIds array = [for sName in uniqueSaNames: [

25   resourceId('Microsoft.Storage/storageAccounts', sName)

26 ]]

In this example, you collect a set of storage account name prefixes using the saNames parameter. Like the earlier storage account creation template, you use an expression to build a unique set of storage account names. You iterate over this collection and use the value in each iteration as the name of the storage account.

When implementing resource property iterations, the syntax you use would be:

1 <resource-property>: [for <item> in <collection>: {

2   <property-values-or-expression>

3 }]

You see the usage of collection iteration in the following example.

1 //6_4-virtualNetworkCollection.bicep

2 param vNetPrefix array = [

3   '10.0.0.0/16'

4 ]

5

6 param subnets array = [

7   {

8     name: 'subnet1'

9     prefix: '10.0.1.0/27'

10   }

11   {

12     name: 'subnet2'

13     prefix: '10.0.2.0/27'

14   }

15 ]

16

17 resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = {

18   name: 'vnet'

19   location: resourceGroup().location

20   properties: {

21     addressSpace: {

22       addressPrefixes: vNetPrefix

23     }

24     subnets: [for sNet in subnets: {

25       name: sNet.name

26       properties: {

27         addressPrefix: sNet.prefix

28       }

29     }]

30   }

31 }

32

33 output subnetIds array = [for sNet in subnets: [

34   resourceId('Microsoft.Storage/virtualNetworks/subnets', 'vnet','${sNet.name}')

35 ]]

In this example, you use an object collection to represent the subnet name and prefixes. Using the resource property iteration syntax, you can iterate over this collection and create the necessary subnets. When iterating over the subnets collection, each element within the collection contains name and prefix properties. You can access these properties using the property accessor.

Array and nested iterations

Array iteration is another type of loop-index based iteration. Here is the general syntax for using this variant of index-based iteration.

1 <element> <identifier> [<type>] = [for (<item>, <itemindex>) in <collection>: {

2   <resource-instance> or <resource-properties>

3 }]

Within each iteration, item contains the value within the collection and the itemindex contains the index of the current iteration value. Unlike the first variant of loop index iteration, you get to iterate over both values and index values of items.

When using this variant of iteration with resource instance properties, you have to use the following syntax.

1 <resource-property>: [for (<item>, <itemindex>) in <collection>: {

2   <property-values-or-expression>

3 }]

The following example demonstrates using this index-based variant with both resource instance and resource instance properties.

1 //6_5-virtualNetworkArrayCollection.bicep

2 param vNetPrefix string = 'bicep'

3 var vNetworks = [

4   {

5     addressPrefix: '10.1.0.0/24'

6     subnets: [

7       {

8         addressPrefix: '10.1.0.0/26'

9       }

10       {

11         addressPrefix: '10.1.0.64/26'

12       }

13       {

14         addressPrefix: '10.1.0.128/26'

15       }

16     ]

17   }

18   {

19     addressPrefix: '10.2.0.0/24'

20     subnets: [

21       {

22         addressPrefix: '10.2.0.0/26'

23       }

24       {

25         addressPrefix: '10.2.0.64/26'

26       }

27     ]

28   }

29   {

30     addressPrefix: '10.3.0.0/24'

31     subnets: [

32       {

33         addressPrefix: '10.3.0.0/26'

34       }

35     ]

36   }

37   {

38     addressPrefix: '10.4.0.0/24'

39     subnets: [

40       {

41         addressPrefix: '10.4.0.0/26'

42       }

43       {

44         addressPrefix: '10.4.0.64/26'

45       }

46     ]

47   }

48 ]

49

50 resource vNetwork 'Microsoft.Network/virtualNetworks@2021-02-01' = [for (vNet, vNetI\

51 ndex) in vNetworks: {

52   name: '${vNetPrefix}-vnet-${vNetIndex+1}'

53   location: resourceGroup().location

54   properties: {

55     addressSpace: {

56       addressPrefixes: [

57         vNet.addressPrefix

58       ]

59     }

60     subnets: [for (sNet, sNetIndex) in vNet.subnets: {

61       name: 'subnet-${sNetIndex}'

62       properties: {

63         addressPrefix: sNet.addressPrefix

64       }

65     }]

66   }

67 }]

68

69 output vNets array = [for (vnet, vNetIndex) in vNetworks: {

70   vNetName: '${vNetPrefix}-vnet-${vNetIndex+1}'

71   subnetsPrefixes: '${vnet.subnets}'

72 }]

The above example demonstrates the usage of nested loops within Bicep templates. You can implement nested loops using any of the iteration methods you have learned so far. In this example using the array index iteration, the top-level loop is declared at the resource instance level and creates multiple virtual network instances. Within this iteration, vNet contains the first object within the vNetworks collection, and vNetIndex contains the index of the current iteration object within the collection. Each virtual network requires one or more subnets, and you use another array index loop to create necessary subnets. Bicep, at the time of this writing, does not support nested loops within the output element.

Sequential deployments

Azure Resource Manager, by default, performs parallel provision of resources in a template. The resource deployment order will be non-deterministic unless you define implicit or explicit dependencies within these resources in a template. You may, however, want to limit how many resources instances get provisioned parallelly. Limiting the number of concurrent deployments may be necessary, especially when updating an existing deployment where you may not want to update more than one or two instances at a time to prevent a production service from going down.

When using iterations in Bicep, you can use the @batchSize() decorator to limit the number of concurrent resource deployments. This decorator accepts an integer value (greater or equal to 1) as a parameter. Setting the batch size to 1 will result in resource instances in the iteration getting provisioned one after another. You must ensure that the batch size is smaller than the iteration count.

The general syntax for adding batch size is as following.

1 @batchSize(n)

2 resource <resource-symbolic-name> '<resource-type>@<api-version>' = [for <index> in \

3 range(<start>, <stop>): {

4   <resource-instance-properties>

5 }]

You may modify the earlier example to add @batchSize(2) to provision two resource instances at a time. When you provision these templates with and without batchSize() decorator, you can see the difference in the amount of time each deployment takes.

Figure 6-1. Duration of parallel vs serial deployments

The batchSize() decorator results in the following ARM JSON template.

1 "resources": [

2     {

3         "copy": {

4             "name": "vNetwork",

5             "count": "[length(variables('vNetworks'))]",

6             "mode": "serial",

7             "batchSize": 2

8         },

9         "type": "Microsoft.Network/virtualNetworks",

10         "apiVersion": "2021-02-01",

11         "name": "[format('{0}-vnet-{1}', parameters('vNetPrefix'), add(copyIndex(), \

12 1))]",

13         "location": "[resourceGroup().location]",

14         "properties": {

15             "copy": [

16                 {

17                     "name": "subnets",

18                     "count": "[length(variables('vNetworks')[copyIndex()].subnets)]",

19                     "input": {

20                         "name": "[format('subnet-{0}', copyIndex('subnets'))]",

21                         "properties": {

22                             "addressPrefix": "[variables('vNetworks')[copyIndex()].s\

23 ubnets[copyIndex('subnets')].addressPrefix]"

24                         }

25                     }

26                 }

27             ],

28             "addressSpace": {

29                 "addressPrefixes": [

30                     "[variables('vNetworks')[copyIndex()].addressPrefix]"

31                 ]

32             }

33         }

34     }

35 ]

Conditional deployments

So far, you have learned about simple resource instance declarations to iterative deployments of Azure resources. There will be situations where you need to skip a specific resource creation or reuse an existing resource. For example, when you go to the Azure portal and deploy a service, you may have seen a prompt to select an existing resource group or create a new resource group, while a few service deployment scenarios might ask you to choose between an existing or creating a new storage account. These are made possible by defining conditions in the deployment templates.

The Bicep language supports adding conditions to your deployment templates using the if keyword. The general syntax for using a conditional resource instance deployment is as follows.

1 resource <resource-symbolic-name> '<resource-type>@<api-version>' = if (<condition-e\

2 xpression>)) {

3   <resource-instance-properties>

4 }]

The condition-expression can be anything that results in a boolean value. You can use expressions that include any operators or any built-in functions that return a boolean value. For example, numVMCount >= 10 or (bool(numVMCount < 10) && bool(length(vmPrefix) >= 5)). Chapter 10 will provide good coverage of operators and built-in functions in Bicep language. For now, a simple example should be sufficient. To this extent, you can update the Azure storage account lock template that you created in Chapter 3.

1 //6_7-StorageAccountLockCondition.bicep

2 param storageAccountPrefix string = 'bicep'

3 param enableStorageLock bool

4

5 var numUniqueChars = 24 - (length(storageAccountPrefix) + 1)

6

7 resource bicepstorage 'Microsoft.Storage/storageAccounts@2021-04-01' = {

8   name: '${storageAccountPrefix}${take(uniqueString(resourceGroup().id), numUniqueCh\

9 ars)}'

10   location: 'eastus'

11   kind: 'StorageV2'

12   sku: {

13     name: 'Standard_LRS'

14   }

15   properties: {

16     accessTier: 'Cool'

17   }

18 }

19

20 resource bicepstoragelock 'Microsoft.Authorization/locks@2016-09-01' = if (enableSto\

21 rageLock) {

22   name: '${storageAccountPrefix}${take(uniqueString(resourceGroup().id), numUniqueCh\

23 ars)}'

24   scope: bicepstorage

25   properties: {

26     level: 'ReadOnly'

27     notes: 'This resource can neither be modified or deleted.'

28   }

29 }

In this example, you can use the boolean parameter enableStorageLock to indicate if a lock on the storage account resource should be provisioned or not. To deploy this template, you can use Azure CLI.

1 az deployment group create --template-file .\6_7-StorageAccountLockCondition.bicep -\

2 -resource-group bicep --parameters enableStorageLock=true

Combining iterations and conditions

Finally, you can combine iterations and conditions together to build dynamic templates. The general syntax for that will be similar to what is shown below.

1 resource <resource-symbolic-name> '<resource-type>@<api-version>' = [for <index> in \

2 range(<start>, <stop>): if (<condition>){

3   <resource-instance-properties>

4 }]

While the above syntax shows only the loop index based iterations, you can use the other iteration variants as well.

1 //6_8-StorageAccountIterationCondition.bicep

2 @allowed([

3   'new'

4   'existing'

5 ])

6 param newOrExisting string

7

8 param storageAccountPrefix string = 'bicep'

9 param numStorageAccounts int = 3

10

11 resource bicepstorage 'Microsoft.Storage/storageAccounts@2021-04-01' = [for index in\

12 range(0,numStorageAccounts): if (newOrExisting == 'new') {

13   name: '${storageAccountPrefix}stg${index}'

14   location: resourceGroup().location

15   kind: 'StorageV2'

16   sku: {

17     name: 'Standard_LRS'

18   }

19   properties: {

20     accessTier: 'Cool'

21   }

22 }]

In this example, you use the newOrExisting parameter to decide if you need to provision new storage accounts. If you want to use the existing storage accounts, you should set the parameter to a value existing.

1 az deployment group create --template-file .\6_8-StorageAccountIterationCondition.bi\

2 cep --resource-group bicep --parameters newOrExisting=new

Summary

In this chapter, you learned how to implement iterations, perform parallel and serial deployments, conditions, and combining iterations and conditions. Iterations and conditional deployments help in developing more dynamic templates. These capabilities in the Bicep language significantly reduce the number of lines of code and the number of different templates needed to provision services on Azure. In the next chapter, you will learn how to combine all concepts you have gained so far to implement reusable Bicep modules.

Creating and Consuming Modules

In the last few chapters, you learned about different concepts such as resource declarations, adding parameters and output to make the Bicep templates reusable, using expressions within templates, and using iterations and conditions to make these templates more dynamic. Reusability and repeatability are two basic requirements for implementing Infrastructure as Code (IaC) practices. You used parameters and output to create reusable Bicep templates, which is the first step towards modularizing Bicep templates. With the ARM JSON templates, you might have used linked templates to provide reusability. Bicep too has support for modularity and helps simplify complex configurations into smaller reusable modules.

In Bicep, you can consume any Bicep template as a module. Adding parameters and output to Bicep files is optional. But, you need to add these elements if you want to make your templates reusable. The parameters and outputs become a contract between the Bicep modules and the templates consuming these module files. Chapter 4 looked at creating reusable templates by adding parameters and output elements to a Bicep template.

Creating modules

In chapter 5, you developed templates for virtual networks, network security groups, public IP addresses, network interfaces, and virtual machines through several examples. Each of these templates used parameters to collect the required template input. However, these templates did not use iterations. You can do that so using the knowledge that you gained from the previous chapter.

1 // 7_1-bicepVirtualNetwork.bicep

2 // Parameters

3 @allowed([

4   'new'

5   'existing'

6 ])

7 param vNetNewOrExisting string

8

9 param vNetName string

10 param vNetAddressPrefix string

11 param subnetInfo object

12

13 // Resources

14 resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' = if (vNetNewOrExisting\

15 == 'new') {

16   name: vNetName

17   location: resourceGroup().location

18   properties: {

19     addressSpace: {

20       addressPrefixes: [

21         vNetAddressPrefix

22       ]

23     }

24   }

25 }

26

27 resource subnet 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' = {

28   parent: vnet

29   name: subnetInfo.name

30   properties: {

31     addressPrefix: subnetInfo.addressPrefix

32   }

33 }

34

35 //outputs

36 output subnetId string = subnet.id

Using the above example, you create a virtual network or a subnet in an existing virtual network or new a virtual network and subnet. You need to have name and addressPrefix keys within the subnetInfo object. For example,

1 {

2     name: 'db'

3     addressPrefix: '10.0.2.0/27'

4 }

Next, you need convert the network security group template into a true module.

1 // 7_2-bicepNetworkSecurityGroup.bicep

2 // Parameters

3 param nsgPrefix string

4 param nsgProperties array

5

6 // Resources

7 resource nsg 'Microsoft.Network/networkSecurityGroups@2021-02-01' = {

8   name: '${nsgPrefix}-nsg'

9   location: resourceGroup().location

10   properties: {

11     securityRules: [for nsg in nsgProperties: {

12         name: nsg.name

13         properties: {

14           priority: nsg.priority

15           protocol: nsg.protocol

16           access: nsg.access

17           direction: nsg.direction

18           sourceAddressPrefix: '*'

19           sourcePortRange: '*'

20           destinationAddressPrefix: '*'

21           destinationPortRange: nsg.destinationPortRange

22         }

23       }]

24   }

25 }

26

27 // Outputs

28 output id string = nsg.id

When using this module, you need to supply NSG rules that need to created an array of objects. This is assigned as value to the nsgProperties parameter. For example,

1 [

2   {

3     name: 'SSH'

4     priority: 1001

5     protocol: 'tcp'

6     access: 'allow'

7     direction: 'inbound'

8     destinationPortRange: 22

9   }

10   {

11     name: 'HTTP'

12     priority: 1002

13     protocol: 'tcp'

14     access: 'allow'

15     direction: 'inbound'

16     destinationPortRange: 80

17   }

18 ]

To complete the necessary infrastructure needed to create a virtual machine, you need a public IP address and a network interface.

1 // 7_3-bicepPublicIp.bicep

2 // Parameters

3 param vmName string

4

5 // 7_3-bicepPublicIp.bicep

6 // Variables

7 var pipName = '${vmName}pip'

8

9 // Resources

10 resource pip 'Microsoft.Network/publicIPAddresses@2021-02-01' = {

11   name: '${vmName}pip'

12   location: resourceGroup().location

13   sku: {

14     name: 'Basic'

15   }

16   properties: {

17     publicIPAllocationMethod: 'Dynamic'

18     publicIPAddressVersion: 'IPv4'

19     dnsSettings: {

20       domainNameLabel: vmName

21     }

22     idleTimeoutInMinutes: 10

23   }

24 }

25

26 // Variables

27 var pipIP = reference(resourceId('Microsoft.Network/publicIPAddresses', pipName), '2\

28 021-02-01', 'full').properties

29

30 // Outputs

31 output pipInfo object = {

32   id: pip.id

33   dnsFqdn: pipIP.dnsSettings.fqdn

34 }

This template deploys a public IP address for the virtual machine and outputs the DNS FQDN to access the virtual machine over SSH or RDP.

To connect the virtual machine to a virtual network, you need a network interface and the following template will help you create a network interface.

1 // 7_4-bicepNetworkInterfaces.bicep

2 // Parameters

3 param subnetId string

4 param publicIPId string

5 param nsgId string

6 param netInterfacePrefix string

7

8 // Resources

9 resource nic 'Microsoft.Network/networkInterfaces@2021-02-01' = {

10   name: '${netInterfacePrefix}-nic'

11   location: resourceGroup().location

12   properties: {

13     ipConfigurations: [

14       {

15         name: '${netInterfacePrefix}-nicConfig'

16

17         properties: {

18           subnet: {

19             id: subnetId

20           }

21           privateIPAllocationMethod: 'Dynamic'

22           publicIPAddress: {

23             id: publicIPId

24           }

25         }

26       }

27     ]

28     networkSecurityGroup: {

29       id: nsgId

30     }

31   }

32 }

33

34 // Outputs

35 output id string = nic.id

With this final template, you have created all the necessary modules to provision a virtual machine. You may want to organize these module files into a separate folder. For example, like what is shown here.

Figure 7-1. Bicep module structure

The next section will show how you can consume these individual Bicep templates as modules in a root Bicep template.

Consuming modules

Before delving into consuming modules, you need to understand the basics of modules in Bicep. Modules in Bicep combine one or more resources to abstract complex resource instance definitions and improve readability in a Bicep template. Bicep modules added to a template get compiled as nested templates in the resulting ARM JSON.

The general syntax for consuming a module within a Bicep template is as follows.

1 module <module-symbolic-name> '<path-to-file>' = {

2   name: '<nested-deployment-name>'

3   params: {

4     <parameter-names-and-values>

5   }

6 }

You use the module keyword to reference one Bicep template within another template. The module symbolic name is an identifier used to identify the instance of the module in the Bicep template and access any module output. The module file path must be relative and specified using forward-slash (/) as the path separator to ensure consistent compilation cross-platform. Within the module block, you can set the value of the name property used as the nested deployment’s name within the generated ARM JSON template. Finally, you can use the params property to specify any input parameters needed for the module. Visual Studio Code provides intellisense to help you discover module files and module input parameters.

Figure 7-2. Module file discovery

As you specify the module symbolic name and move to specify the file path, VS Code automatically shows all Bicep template files at the same level as the current template and within all its subfolders.

Figure 7-3. Module file discovery

Within the VS Code intellisense, you will see mandatory input parameters shown as required. You can use a module any number of times as long as each declaration provisions a unique instance of the resource.

You can now put all this together to create a Bicep template that uses consumes these modules to create a virtual machine.

1 // 7_5-bicepVirtualMachine.bicep

2 // Parameters

3 param vmName string = 'ubuntu01'

4 param vmSize string = 'Standard_B2s'

5 param osDiskType string = 'Standard_LRS'

6

7 param osImage object = {

8   sku: '18.04-LTS'

9   version: 'latest'

10   publisher: 'Canonical'

11   offer: 'UbuntuServer'

12 }

13

14 param vNetInfo object = {

15   name: 'bicepVNet'

16   vNetnewOrExisting: 'new'

17   addressPrefix: '10.0.0.0/16'

18   subnetInfo: {

19     name: 'app'

20     addressPrefix: '10.0.1.0/27'

21   }

22 }

23

24 param adminUsername string = 'bicepuser'

25

26 @secure()

27 param adminPassword string

28

29 // Modules

30 module vnet 'modules/7_1-bicepVirtualNetwork.bicep' = {

31   name: vNetInfo.name

32   params: {

33     vNetNewOrExisting: vNetInfo.vNetnewOrExisting

34     vNetName: vNetInfo.name

35     subnetInfo: vNetInfo.subnetInfo

36     vNetAddressPrefix: vNetInfo.addressPrefix

37   }

38 }

39

40 module nsg 'modules/7_2-bicepNetworkSecurityGroup.bicep' = {

41   name: '${vmName}nsg'

42   params: {

43     nsgPrefix: vmName

44     nsgProperties: [

45       {

46         name: 'SSH'

47         priority: 1001

48         protocol: 'tcp'

49         access: 'allow'

50         direction: 'inbound'

51         destinationPortRange: 22

52       }

53     ]

54   }

55 }

56

57 module pip 'modules/7_3-bicepPublicIp.bicep' = {

58   name: '${vmName}pip'

59   params: {

60     vmName: vmName

61   }

62 }

63

64 module nic 'modules/7_4-bicepNetworkInterfaces.bicep' = {

65   name: '${vmName}nic'

66   params: {

67     netInterfacePrefix: vmName

68     nsgId: nsg.outputs.id

69     publicIPId: pip.outputs.pipInfo.id

70     subnetId: vnet.outputs.subnetId

71   }

72 }

73

74 // Resources

75 resource vm 'Microsoft.Compute/virtualMachines@2021-03-01' = {

76   name: vmName

77   location: resourceGroup().location

78   properties: {

79     hardwareProfile: {

80       vmSize: vmSize

81     }

82     storageProfile: {

83       osDisk: {

84         createOption: 'FromImage'

85         managedDisk: {

86           storageAccountType: osDiskType

87         }

88       }

89       imageReference: {

90         publisher: osImage.publisher

91         offer: osImage.offer

92         sku: osImage.sku

93         version: osImage.version

94       }

95     }

96     networkProfile: {

97       networkInterfaces: [

98         {

99           id: nic.outputs.id

100         }

101       ]

102     }

103     osProfile: {

104       computerName: vmName

105       adminUsername: adminUsername

106       adminPassword: adminPassword

107     }

108   }

109 }

110

111 // Variables

112 var vmDnsFqdn = pip.outputs.pipInfo.dnsFqdn

113

114 // Outputs

115 output vmAccess object = {

116   'adminUsername': adminUsername

117   'vmDnsFqdn': vmDnsFqdn

118   'connectCommand': 'ssh ${adminUsername}@${vmDnsFqdn}'

119 }

You can use this Bicep template to deploy a Linux VM, and this can itself be another module.

Iterations and conditions

Similar to resource instances, you can use iterations and conditions with modules as well. To perform iterative module deployment, you need to use the following syntax.

1 module <module-symbolic-name> '<path-to-file>' = [for <item> in <collection>: {

2   name: '<nested-deployment-name>'

3   params: {

4     <parameter-names-and-values>

5   }

6 }]

You have multiple Bicep templates that you created so far and you can perform an iterative deployment of one of those modules.

1 // 7_6-bicepVirtualMachines.bicep

2 // Parameters

3 param virtualMachines array = [

4   {

5     vmName: 'ubuntu-01'

6     vmSize: 'Standard_B2s'

7     osDiskType: 'Standard_LRS'

8     osImage: {

9       sku: '18.04-LTS'

10       version: 'latest'

11       publisher: 'Canonical'

12       offer: 'UbuntuServer'

13     }

14     vNet: {

15       name: 'bicepVNet'

16       vNetNewOrExisting: 'new'

17       addressPrefix: '10.0.0.0/16'

18       subnetInfo: {

19         name: 'db'

20         addressPrefix: '10.0.1.0/27'

21       }

22     }

23   }

24   {

25     vmName: 'ubuntu-02'

26     vmSize: 'Standard_B2s'

27     osDiskType: 'Standard_LRS'

28     osImage: {

29       sku: '18.04-LTS'

30       version: 'latest'

31       publisher: 'Canonical'

32       offer: 'UbuntuServer'

33     }

34     vNet: {

35       name: 'bicepVNet'

36       vNetNewOrExisting: 'existing'

37       addressPrefix: '10.0.0.0/16'

38       subnetInfo: {

39         name: 'app'

40         addressPrefix: '10.0.2.0/27'

41       }

42     }

43   }

44 ]

45

46 param adminUsername string = 'bicepuser'

47

48 @secure()

49 param adminPassword string

50

51 @batchSize(1)

52 module vms '7_5-bicepVirtualMachine.bicep' = [for item in virtualMachines: {

53   name: item.vmName

54   params: {

55     vmName: item.vmName

56     vmSize: item.vmSize

57     osDiskType: item.osDiskType

58     osImage: item.osImage

59     adminUsername: adminUsername

60     adminPassword: adminPassword

61     vNetInfo: item.vNet

62   }

63 }]

In the above example, setting batch size to 1 ensures that the second virtual machine creation does not fail if the virtual network creation is still in progress. You can use any type of iteration that you have seen in the previous chapter.

Syntax for performing conditional deployment of modules should be familiar as well. As seen in Chapter 6, you can use the if keyword along with a module declaration to perform conditional deployments. Here is the general syntax.

1 module <module-symbolic-name> '<path-to-file>' = if (<condition-to-deploy>) {

2   name: '<nested-deployment-name>'

3   params: {

4     <parameter-names-and-values>

5   }

6 }

The following example demonstrates provisioning public IP address based on a condition.

1 // 7_7-bicepPublicIPCondition.bicep

2 // Parameters

3 param vmName string

4

5 @allowed([

6   'new'

7   'existing'

8 ])

9 param newOrExisting string = 'new'

10

11 // Resources

12 module pip 'modules/7_3-bicepPublicIp.bicep' = if (newOrExisting == 'new') {

13   name: 'pip'

14   params: {

15     vmName: vmName

16   }

17 }

18

19 // Outputs

20 output pipInfo object = {

21   id: pip.outputs.pipInfo.id

22   dnsFqdn: pip.outputs.pipInfo.dnsFqdn

23 }

In this example, you use a familiar pattern to deploy a module based on a condition.

Deploying at multiple scopes

You can define the scope of your Bicep template using the targetScope global property. This identifies what kind of resources you can provision within the Bicep template.

When you deploy a Bicep template, you will scope it – based on the targetScope – to a resource group or subscription or management group or tenant. However, what if you want to deploy to a different resource group than the target resource group or to a different subscription or management group or tenant than the target specified at the time of deploying the template? In other words, what if you want to deploy resources across multiple levels of resource hierarchy within the same deployment? There is a way.

In the extension resources section of chapter 3, you have seen how the scope property is used within the extension resource instance to define implicit dependencies. The scope property within the module definition can used used to perform mixed or multiple scope deployments. For module instances within the template, you can either use the global scope functions resourceGroup(), subscription(), tenant(), and managementGroup() as argument to the scope property or simply pass a reference of a resource instance as an argument.

Resource group scope

So far, you have provisioned resources at the resource group scope and to the same target resource group as specified at the command line using az deployment group command. However, you can deploy to a different resource group within the same or a resource group in another subscription as well.

To deploy resources to a resource group that is not the target resource group, you need to use the resourecGroup() function as the value passed to the scope property of a module instance.

1 //7_8-resourceGroupScope.bicep

2 targetScope = 'resourceGroup'

3 param resGroup string

4

5 module stgAcct 'modules/storageAccount.bicep' = {

6   name: 'stgAcct'

7   scope: resourceGroup(resGroup)

8   params: {

9     storageAccountName: 'bicepstgacctrc01'

10   }

11 }

The default deployment scope is resourceGroup and specifying targetScope as resourceGroup is redundant. The above example uses this to demonstrate deploying at multiple scopes.

You can deploy this using the az deployment command.

1 az deployment group create --resource-group bicep --template-file .\7_8-resourceGrou\

2 pScope.bicep --parameters resGroup=statsrg

You can even provision resources to a resource group in another subscription.

1 //7_8-resourceGroupScope.bicep

2 param resGroup string

3 param subId string

4

5 module stgAccct 'modules/storageAccount.bicep' = {

6   name: 'stgAcct'

7   scope: resourceGroup(subId, resGroup)

8   params: {

9     storageAccountName: 'bicepstgacctrc02'

10   }

11 }

Subscription scope

Using the subscription() function, you can scope a module to a different subscription than the target subscription. For example, you can create a resource group using a template defined at the subscription target scope.

1 //7_9-resourceGroupSubscriptionScope.bicep

2 targetScope = 'subscription'

3

4 param resGroupName string

5 param resGroupLocation string

6

7 resource rg 'Microsoft.Resources/resourceGroups@2020-06-01' = {

8   name: resGroupName

9   location: resGroupLocation

10 }

When you deploy this, a resource group gets created within the subscription within the current Azure authenticated context. What if you want to create a resource group in a different subscription? You can convert the above template as a module and use the scope property within the module declaration to scope resource group creation to a different subscription. The subscription() function returns the current subscription. You can specify a different subscription than the current one by supplying a target subscription ID.

1 //7_10-subscriptionScope.bicep

2 targetScope = 'subscription'

3

4 param subId string = '0d39bfe8-a1ab-4564-85c5-2a4a3a4a3aa24'

5

6 module resGroup 'modules/resourceGroup.bicep' = {

7   name: 'resGroup'

8   scope: subscription(subId)

9   params: {

10     resGroupName: 'bicep'

11     resGroupLocation: 'WestUS'

12   }

13 }

You can deploy this template using the az deployment sub command.

1 az deployment sub create --location westus --template-file .\7_10-subscriptionScope.\

2 bicep

Here is another example of mixing scopes in a templates.

1 //7_11-subscriptionScopeRBAC.bicep

2 targetScope = 'subscription'

3

4 param resGroup string

5 param principalId string

6 param roleDefinitionId string = 'b24988ac-6180-42a0-ab88-20f7382dd24c'

7 param roleAssignmentName string = guid(principalId, roleDefinitionId, resGroup)

8

9 var roleID = '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Au\

10 thorization/roleDefinitions/${roleDefinitionId}'

11

12 resource resourceGroup 'Microsoft.Resources/resourceGroups@2019-10-01' existing = {

13   name: resGroup

14 }

15

16 module applyLock 'modules/resourceLock.bicep' = {

17   name: 'applyLock'

18   scope: resourceGroup

19 }

20

21 module assignRole 'modules/resourceRole.bicep' = {

22   name: 'assignRBACRole'

23   scope: resourceGroup

24   params: {

25     principalId: principalId

26     roleNameGuid: roleAssignmentName

27     roleDefinitionId: roleID

28   }

29 }

This example demonstrates how you can use a resource instance as an argument to the scope property instead of the scope global functions. You can deploy this template using the az deployment sub command.

1 az deployment sub create --template-file .\7_11-subscriptionRBAC.bicep --parameters \

2 resGroup=bicep principalId=bb4e8204-e2d6-4240-8b25-f58ebf1410cd  --location westus

Management group scope

At the management group scope, you have seen how you can provision policy definitions. While in the management group scope, you can use the managementgroup() scope function to scope the resource deployment to a different management group.

1 //7_12-managementGroupScope.bicep

2 targetScope = 'managementGroup'

3

4 param mgmtGroupName string

5 param policyDefName string

6

7 module polDef 'modules/policyDef.bicep' = {

8   name: 'policyDef'

9   scope: managementGroup(mgmtGroupName)

10   params: {

11     policyDefinitionName: policyDefName

12   }

13 }

When deploying this template, you must specify a management group ID to scope the deployment. However, using the mgmtGroupName parameter, you can scope the policy definition to a different management group than the target scope.

1 az deployment mg create --template-file .\7_12-managementGroupScope.bicep --location\

2 westus --parameters mgmtGroupName=bicep policyDefName=bicepvm --management-group-id\

3 e8492068-d56c-42d8-8bed-f978a9a74d8e

Tenant scope

Finally, to create resources at a tenant scope, you can use the tenant() scope function.

1 //7_13-tenantScope.bicep

2 targetScope = 'managementGroup'

3

4 resource rgpResearch 'Microsoft.Management/managementGroups@2020-05-01' = {

5   scope: tenant()

6   name: 'Research'

7   properties: {

8     displayName: 'Research Group'

9   }

10 }

11

12 resource grpManagement 'Microsoft.Management/managementGroups@2020-05-01' = {

13   scope: tenant()

14   name: 'Management'

15   properties: {

16     displayName: 'Management group'

17     details: {

18       parent: {

19         id: rgpResearch.id

20       }

21     }

22   }

23 }

In this example, the targetScope is set to management group but then the management groups are provisioned at the tenant scope.

So far in this chapter, you looked at creating and consuming modules. However, in an enterprise scenario or in a team that is developing Azure Infrastructure as Code, you will come across a need to share the modules. This requires a facility to publish and consume modules from a central location.

Sharing modules

With Bicep version 0.4.1008 or later, you can now publish and share Bicep modules either using a private Azure Container Registry (ACR) or using Azure Template Specs. You can publish modules to these registries and give read access to users who need to deploy the modules. To work with module registries, you need Bicep version 0.4.1008 or above or Azure CLI version 2.3.10 or above or Azure PowerShell version 7.0.0 or later. First, you will learn about using Azure Container Registry as Bicep module private registry.

Azure Container Registry

A Bicep private registry is hosted on ACR and therefore you must first create an instance of ACR service. You can use the following Bicep template to provision an instance of ACR.

1 //7_14-provisionAcr.bicep

2 @minLength(5)

3 @maxLength(50)

4 @description('Provide a globally unique name of your Azure Container Registry')

5 param acrName string = 'acr${uniqueString(resourceGroup().id)}'

6

7 @description('Provide a location for the registry.')

8 param location string = resourceGroup().location

9

10 @description('Provide a tier of your Azure Container Registry.')

11 param acrSku string = 'Basic'

12

13 resource acrResource 'Microsoft.ContainerRegistry/registries@2021-06-01-preview' = {

14   name: acrName

15   location: location

16   sku: {

17     name: acrSku

18   }

19   properties: {

20     adminUserEnabled: false

21   }

22 }

23

24 @description('Output the login server property for later use')

25 output loginServer string = acrResource.properties.loginServer

You can deploy this template using the following Azure CLI command.

1 az deployment group create --template-file .\7_14-provisionAcr.bicep --resource-grou\

2 p bicep

This template provisions ACR and returns the login server name as output. This is needed to link Bicep files to a private registry.

1 "outputs": {

2 "loginServer": {

3 "type": "String",

4 "value": "acr6axmw2qxc2xnq.azurecr.io"

5 }

6 }

Publish a module

Once a registry is created, you can publish Bicep templates to the registry using either Azure CLI or Azure PowerShell.

You can use the az bicep publish command to publish Bicep templates to ACR.

1 bicep publish <file> --target <ref>

The argument to the --target parameter should be in the form br:<registry-name>.azurecr.io/<module-path>:<tag>. br at the beginning indicates that the reference is made to a Bicep registry. The module path denotes where the module gets saved. The tag can be used to specify a version. Similar to Docker images, you may use tags such as v1 or v2 to indicate the version of the module or SemVer notation. If you publish a bicep module at the same module path with the same tag as the earlier version, it previous version gets overwritten.

You have authored a few modules already in this chapter and you can now try publishing these modules to the private registry.

You need push image permission to publish a Bicep module to ACR.

1 az bicep publish --file .\modules\7_1-bicepVirtualNetwork.bicep --target br:acr6axmw\

2 2qxc2xnq.azurecr.io/bicep/modules/vnet:v1

3

4 az bicep publish --file .\modules\7_2-bicepNetworkSecurityGroup.bicep --target br:ac\

5 r6axmw2qxc2xnq.azurecr.io/bicep/modules/nsg:v1

6

7 az bicep publish --file .\modules\7_3-bicepPublicIp.bicep --target br:acr6axmw2qxc2x\

8 nq.azurecr.io/bicep/modules/pip:v1

9

10 az bicep publish --file .\modules\7_4-bicepNetworkInterfaces.bicep --target br:acr6a\

11 xmw2qxc2xnq.azurecr.io/bicep/modules/nic:v1

With Azure PowerShell (version 7.0.0 and above), you can use the Publish-AzBicepModule cmdlet.

1 Publish-AzBicepModule -FilePath .\7_1-bicepVirtualNetwork.bicep -Target br:acr6axmw2\

2 qxc2xnq.azurecr.io/bicep/modules/vnet:v1

Once these modules are published, you can see these appear under repositories in the Azure portal.

Figure 7-4. Bicep modules in ACR

These modules get stored as Open Container Initiative (OCI) artifacts.

Figure 7-5. Bicep module OCI artifact in ACR

The contents of manifest represent the contents of the OCI artifact.

Azure Template Specs

You can store an Azure Resource Manager (ARM) template as an Azure Template Spec for later deployment. This is yet another way you can share your Bicep templates with teams in your organization. Similar to the ACR based private Bicep registry, Template Specs too require authorization to publish and consume templates. Azure Template Specs do not yet support publishing Bicep templates. You need to publish the compiled JSON templates to template specs and link to these published specs as a part of your Bicep template.

Publish a module as Template Spec

To create a Template Spec, you need to provision an instance of the Microsoft.Resources/templateSpecs resource type. Here is a Bicep template that demonstrates how you can create Template Specs.

1 //7_15-bicepTemplateSpecCreate.bicep

2 var templateSpecName = 'nsg'

3 var templateSpecDisplayName = 'Azure Network Security Group'

4 var templateSpecVersion = '1.0.0'

5 var templateSpecDescription = 'Creates an Azure Network Security Group'

6 var templateSpecJson = loadTextContent('modules/7_2-bicepNetworkSecurityGroup.json')

7

8 var location = resourceGroup().location

9

10 resource templateSpecName_resource 'Microsoft.Resources/templateSpecs@2019-06-01-pre\

11 view' = {

12   name: templateSpecName

13   location: location

14   properties: {

15     description: templateSpecDescription

16     displayName: templateSpecDisplayName

17   }

18 }

19

20 resource templateSpecName_templateSpecVersion 'Microsoft.Resources/templateSpecs/ver\

21 sions@2019-06-01-preview' = {

22   name: '${templateSpecName_resource.name}/${templateSpecVersion}'

23   location: location

24   properties: {

25     template: json(templateSpecJson)

26   }

27 }

28

29 output templateSpecUri string = 'ts:${subscription().subscriptionId}/${resourceGroup\

30 ().name}/${templateSpecName}:${templateSpecVersion}'

This Bicep template creates a template spec for creating an Azure Virtual Network and it returns the template spec URI string as the output.

1 "outputs": {

2 "templateSpecUri": {

3 "type": "String",

4 "value": "ts:5073fd4c-3a1b-4559-8371-21e034f70820/bicep/vnet:1.1.0"

5 }

6 }

Similar to the above Bicep template, you can publish other modules that you created in this chapter as template specs.

Figure 7-6. Bicep modules as temp OCI artifact in ACR

Consume module from a registry

You can consume the Bicep templates published to a private registry or as template spec just by linking to the module.

Unlike the traditional module specification, when using a module registry you need to link to the module artifact in the registry.

The general syntax for linking to modules in a Bicep private registry is shown below.

1 module <module-symbolic-name> 'br:<registry-name>.azurecr.io/<file-path>:<tag>' = {

2   name: '<nested-deployment-name>'

3   params: {

4     <parameter-names-and-values>

5   }

6 }

And, when using modules published as template specs, you need to use the following syntax.

1 module <module-symbolic-name> 'ts:<sub-id>/<rg-name>/<template-spec-name>:<version>'\

2 = {

3   name: '<nested-deployment-name>'

4   params: {

5     <parameter-names-and-values>

6   }

7 }

Within a Bicep template, you can link to local module files and/or files published to ACR and/or as template specs. Here is an update example of the virtual machine template that you built earlier.

1 //7_16-bicepTemplateUsingRegistries.bicep

2 // Parameters

3 param vmName string = 'ubuntu01'

4 param vmSize string = 'Standard_B2s'

5 param osDiskType string = 'Standard_LRS'

6

7 param osImage object = {

8   sku: '18.04-LTS'

9   version: 'latest'

10   publisher: 'Canonical'

11   offer: 'UbuntuServer'

12 }

13

14 param vNetInfo object = {

15   name: 'bicepVNet'

16   vNetnewOrExisting: 'new'

17   addressPrefix: '10.0.0.0/16'

18   subnetInfo: {

19     name: 'app'

20     addressPrefix: '10.0.1.0/27'

21   }

22 }

23

24 param adminUsername string = 'bicepuser'

25

26 @secure()

27 param adminPassword string

28

29 // Modules

30 module vnet 'modules/7_1-bicepVirtualNetwork.bicep' = {

31   name: vNetInfo.name

32   params: {

33     vNetNewOrExisting: vNetInfo.vNetnewOrExisting

34     vNetName: vNetInfo.name

35     subnetInfo: vNetInfo.subnetInfo

36     vNetAddressPrefix: vNetInfo.addressPrefix

37   }

38 }

39

40 module nsg 'ts:5073fd4c-3a1b-4559-8371-21e034f70820/bicep/nsg:1.0.0' = {

41   name: '${vmName}nsg'

42   params: {

43     nsgPrefix: vmName

44     nsgProperties: [

45       {

46         name: 'SSH'

47         priority: 1001

48         protocol: 'tcp'

49         access: 'allow'

50         direction: 'inbound'

51         destinationPortRange: 22

52       }

53     ]

54   }

55 }

56

57 module pip 'ts:5073fd4c-3a1b-4559-8371-21e034f70820/bicep/pip:1.0.0' = {

58   name: '${vmName}pip'

59   params: {

60     vmName: vmName

61   }

62 }

63

64 module nic 'br:acr6axmw2qxc2xnq.azurecr.io/bicep/modules/nic:v1' = {

65   name: '${vmName}nic'

66   params: {

67     netInterfacePrefix: vmName

68     nsgId: nsg.outputs.id

69     publicIPId: pip.outputs.pipInfo.id

70     subnetId: vnet.outputs.subnetId

71   }

72 }

73

74 // Resources

75 resource vm 'Microsoft.Compute/virtualMachines@2021-03-01' = {

76   name: vmName

77   location: resourceGroup().location

78   properties: {

79     hardwareProfile: {

80       vmSize: vmSize

81     }

82     storageProfile: {

83       osDisk: {

84         createOption: 'FromImage'

85         managedDisk: {

86           storageAccountType: osDiskType

87         }

88       }

89       imageReference: {

90         publisher: osImage.publisher

91         offer: osImage.offer

92         sku: osImage.sku

93         version: osImage.version

94       }

95     }

96     networkProfile: {

97       networkInterfaces: [

98         {

99           id: nic.outputs.id

100         }

101       ]

102     }

103     osProfile: {

104       computerName: vmName

105       adminUsername: adminUsername

106       adminPassword: adminPassword

107     }

108   }

109 }

110

111 // Variables

112 var vmDnsFqdn = pip.outputs.pipInfo.dnsFqdn

113

114 // Outputs

115 output vmAccess object = {

116   'adminUsername': adminUsername

117   'vmDnsFqdn': vmDnsFqdn

118   'connectCommand': 'ssh ${adminUsername}@${vmDnsFqdn}'

119 }

As soon as you add references to the modules in the private registry, the VS Code Bicep extension starts downloading the modules, assuming that the user authoring the Bicep template has access to the ACR, to a local cache folder. While the download is in progress, you may see a red squigly around these module references. The VS Code extension uses bicep restore command in the background to download the modules to the local cache. This module cache, by default, is located at $HOME\.bicep on Windows systems and ~\.bicep on Linux systems.

Figure 7-7. Bicep registry local cache

You may run bicep restore command manually to update the local cache or just download the modules to the local cache.

1 bicep restore .\7_15-bicepTemplateUsingACR.bicep

Once you download the modules to the local cache, in disconnected scenarios, you can package the modules along with the main Bicep template.

Consume modules from Bicep public registry

Starting with Bicep version 0.5.6, Microsoft added support for consuming from a public module repository hosted on Microsoft Container Registry (MCR). To consume a module from Microsoft’s pubic registry, you need to use the following syntax.

1 module <module-id> 'br/public:<module-group>/<module-name>:<version>' = { ... }

br/public indicates that you want to consume the module hosted on MCR. Here is an example of creating a Azure virtual network using the virtual-network module. The version tag is mandatory.

1 //7_17-bicep-public-registry.bicep

2 module samplevNet 'br/public:network/virtual-network:1.0.1' = {

3   name: '${uniqueString(deployment().name, 'EastUS')}-vnet'

4   params: {

5     name: 'vnet-bicep-sample'

6     addressPrefixes: [

7       '10.0.0.0/24'

8     ]

9   }

10 }

An index of modules available in this public registry can be seen at Azure/bicep-registry-modules: Bicep registry modules (github.com). There is a better way to find out the list of modules from the Microsoft’s public registry. The 7_18-Get-BicepPublicModule.ps1 script in the book examples provides a way to do this.

1 PS C:\GitHub\bicepbookexamples\chapter7> .\7_18-Get-BicepPublicModule.ps1

2

3 Name                                           Tags

4 ----                                           ----

5 bicep/samples/hello-world                      {1.0.1, 1.0.2}

6 bicep/samples/array-loop                       {1.0.1}

7 bicep/compute/availability-set                 {1.0.1}

8 bicep/deployment-scripts/aks-run-command       {1.0.1}

9 bicep/deployment-scripts/create-kv-certificate {1.0.1, 1.1.1}

10 bicep/deployment-scripts/import-acr            {1.0.1, 2.0.1}

11 bicep/network/virtual-network                  {1.0.1, 1.0.2}

At this time of writing, there are only a few modules available in this public registry. You can contribute to this public registry.

Module aliases

Looking at examples that demonstrated consuming modules and template specs, you understand that the module or template spec reference requires fully-qualified path to the module or the template spec. This can be quite verbose depending the number of modules or template specs you are using in a Bicep template. This is where aliases help you. In chapter 2, you briefly looked at the Bicep configuration settings file when learning about the Bicep linter. You can define module and template spec aliases in the configuration file settings. You can use the moduleAliases property in the configuration file to define module and template spec aliases. Here is the general syntax for this property.

1 {

2   "moduleAliases": {

3     "br": {

4       "alias1": {

5           "registry": "something.azurecr.io",

6           "modulePath": "Path where modules are stored (optional)"

7       }

8     },

9     "ts": {

10       "alias2": {

11           "subscription": "Azure Subscription ID",

12           "resourceGroup": "Resource group name"

13       }

14     }

15   }

16 }

Here is a modified bicepconfig.json file that has both linter rules and the module aliases.

1 {

2     "analyzers": {

3         "core": {

4             "verbose": true,

5             "enabled": true,

6             "rules": {

7                 "no-hardcoded-env-urls": {

8                     "level": "off"

9                 },

10                 "no-unused-params": {

11                     "level": "error"

12                 },

13                 "no-unused-vars": {

14                     "level": "error"

15                 },

16                 "prefer-interpolation": {

17                     "level": "warning"

18                 },

19                 "secure-parameter-default": {

20                     "level": "error"

21                 },

22                 "simplify-interpolation": {

23                     "level": "error"

24                 }

25             }

26         }

27     },

28     "moduleAliases": {

29         "br": {

30             "breg": {

31                 "registry": "acr6axmw2qxc2xnq.azurecr.io",

32                 "modulePath": "bicep/modules"

33             }

34         },

35         "ts": {

36             "tspec": {

37                 "subscription": "5073fd4c-3a1b-4559-8371-21e034f70820",

38                 "resourceGroup": "bicep"

39             }

40         }

41     }

42 }

You can save the above JSON as bicepconfig.json and store it at the root of the project folder. Once you have the aliases defined, you can reference the same using the following format.

1 br/<alias>:<file>:<tag>

2 ts/<alias>:<file>:<tag>

You can now update the previous Bicep template example to use module aliases.

1 //7_19-bicep-modules-aliases.bicep

2 // Parameters

3 param vmName string = 'ubuntu01'

4 param vmSize string = 'Standard_B2s'

5 param osDiskType string = 'Standard_LRS'

6 param rgLocation string = resourceGroup().location

7

8 param osImage object = {

9   sku: '18.04-LTS'

10   version: 'latest'

11   publisher: 'Canonical'

12   offer: 'UbuntuServer'

13 }

14

15 param vNetInfo object = {

16   name: 'bicepVNet'

17   vNetnewOrExisting: 'new'

18   addressPrefix: '10.0.0.0/16'

19   subnetInfo: {

20     name: 'app'

21     addressPrefix: '10.0.1.0/27'

22   }

23 }

24

25 param adminUsername string = 'bicepuser'

26

27 @secure()

28 param adminPassword string

29

30 // Modules

31 module vnet 'modules/7_1-bicepVirtualNetwork.bicep' = {

32   name: vNetInfo.name

33   params: {

34     vNetNewOrExisting: vNetInfo.vNetnewOrExisting

35     vNetName: vNetInfo.name

36     subnetInfo: vNetInfo.subnetInfo

37     vNetAddressPrefix: vNetInfo.addressPrefix

38     rgLocation: rgLocation

39   }

40 }

41

42 module nsg 'ts/tspec:nsg:1.0.0' = {

43   name: '${vmName}nsg'

44   params: {

45     nsgPrefix: vmName

46     nsgProperties: [

47       {

48         name: 'SSH'

49         priority: 1001

50         protocol: 'tcp'

51         access: 'allow'

52         direction: 'inbound'

53         destinationPortRange: 22

54       }

55     ]

56   }

57 }

58

59 module pip 'ts/tspec:pip:1.0.0' = {

60   name: '${vmName}pip'

61   params: {

62     vmName: vmName

63   }

64 }

65

66 module nic 'br/breg:nic:v1' = {

67   name: '${vmName}nic'

68   params: {

69     netInterfacePrefix: vmName

70     nsgId: nsg.outputs.id

71     publicIPId: pip.outputs.pipInfo.id

72     subnetId: vnet.outputs.subnetId

73   }

74 }

75

76 // Resources

77 resource vm 'Microsoft.Compute/virtualMachines@2021-03-01' = {

78   name: vmName

79   location: rgLocation

80   properties: {

81     hardwareProfile: {

82       vmSize: vmSize

83     }

84     storageProfile: {

85       osDisk: {

86         createOption: 'FromImage'

87         managedDisk: {

88           storageAccountType: osDiskType

89         }

90       }

91       imageReference: {

92         publisher: osImage.publisher

93         offer: osImage.offer

94         sku: osImage.sku

95         version: osImage.version

96       }

97     }

98     networkProfile: {

99       networkInterfaces: [

100         {

101           id: nic.outputs.id

102         }

103       ]

104     }

105     osProfile: {

106       computerName: vmName

107       adminUsername: adminUsername

108       adminPassword: adminPassword

109     }

110   }

111 }

112

113 // Variables

114 var vmDnsFqdn = pip.outputs.pipInfo.dnsFqdn

115

116 // Outputs

117 output vmAccess object = {

118   'adminUsername': adminUsername

119   'vmDnsFqdn': vmDnsFqdn

120   'connectCommand': 'ssh ${adminUsername}@${vmDnsFqdn}'

121 }

If you have not used the modulePath property within an alias definition in the Bicep registry (br) object, you must use the fully-qualified module path within the registry. For example, br/breg:nic:v1 would then become br/breg:bicep/modules/nic:v1. You can use bicep build command convert the Bicep template to a ARM JSON template and verify if the modules get included in the compiled JSON.

Summary

This chapter extended your learning around creating reusable Bicep templates as modules. A module is a logical grouping of resources that you want to provision more than once. Modules have input parameters and outputs. You can consume any output from the modules in other Bicep templates. You can mix multiple scopes within a single deployment using the scope property of a module definition. With the support for publishing modules to Azure Container Registry and as template specs, you can now share Bicep modules within your team in an organization and consume from Microsoft’s public module registry.

Developing Bicep templates in VS Code devcontainers

Chapter 1 provided a quick overview of Infrastructure as Code (IaC) and described different principles of IaC. As you learned, IaC combines software development practices and infrastructure automation and configuration to enable agile infrastructure deployment and management. IaC is a core part of the DevOps practices, and one of the critical parts of DevOps culture is collaboration. Version control is the first step towards enabling collaboration. Adding your source code to version control allows your team members to collaborate with you in writing infrastructure as code. While this is an excellent first step, it also brings in a few challenges. The most significant of those is the usage of consistent (predictable and repeatable) development environments that resemble production environments. You know, the works on my system statement that you keep hearing!

Several tools exist to address this challenge. In the age of virtualization, IT organizations provided teams with virtual machine templates. These templates came with all necessary pre-requisites and configurations for a developer to start developing. And then came tools like Vagrant that helped create and share development environments much more effortless. The modern and dynamic application architectures based on cloud-native patterns brought micro-services to the forefront of application delivery and deployment. The evolution of containers made building micro-services easier and more efficient. Containers became a de-facto standard for developing and shipping applications for developers and companies alike, and the concept of developer containers took shape. A development container or devcontainer is a well-defined tools and runtime stack. Using a devcontainer, you and your team members share a standard definition of how your development environment is built. Thereby eliminating development time inconsistencies. These devcontainers are best suited for your IaC development.

Visual Studio Code Development Containers

Visual Studio Code development containers are magic! With the help of VS Code Remote - Containers extension and Docker Desktop, you can run a full-featured development environment within a container. Containers in general help you package application code and all the dependencies. Devcontainers take advantage of this and help you share the development environment with other members of your team and all of you have the same base setup for developing your project code. This eliminates any issues related to development environment since your team will always be using the same environment provided by a devcontainer. A devcontainer can be created from a pre-built container image, Dockerfile, or Docker compose file.

Note: Before you go forward in this chapter, you will need to install Docker Desktop and the VS Code Remote - Containers extension or the VS Code Remote Development extension pack.

Creating a devcontainer

In this chapter, you will learn how to create a devcontainer from a pre-built container image. To start with devcontainers, you need to first build devcontainer.json in your project. A devcontainer.json stored within your project defines how to start a full-featured development container. For example, the bicepbookexamples repository on GitHub includes a devcontainer.json file inside the .devcontainer.

1 {

2 "image": "mcr.microsoft.com/vscode/devcontainers/base",

3 "settings": {},

4 "extensions": [

5 "ms-vscode.azure-account",

6 "ms-azuretools.vscode-bicep"

7 ],

8 "features": {

9 "rchaganti/vsc-devcontainer-features/azurebicep" : {

10 "version": "latest"

11 },

12 "azure-cli": {

13 "version": "latest"

14 },

15 "git": {

16 "version": "latest",

17 "ppa": false

18 }

19 },

20 "remoteUser": "vscode"

21 }

When you clone this repository locally and open it in VS Code, you will be prompted with a message similar to what is shown below.

Figure 8-1. VS Code Devcontainer

VS Code containers support installing development tools as features. The above example installs Azure CLI, git, and a custom feature that installs the latest version of Bicep CLI. You can install extensions needed for your development work using the extensions object in the JSON. So, when you click on Reopen in container, VS Code uses the container image defined in the devcontainer.json and builds a container with the necessary VS Code extensions and features as described in the JSON.

Once you have created a devcontainer.json file and defined the VS Code extensions and tools you need as features in the container, you need to store that in the .devcontainer folder at the root of your project. You can commit this folder to source control so that others on your team have access to a similar development environment.

Running a devcontainer

Before you can start using a development container as defined in the devcontainer.json, you need to build the container image. There are multiple methods to do this.

When you open a project containing a devcontainer.json file within VS Code, as shown in Figure 8-1, the remote container extension prompts you if you want to reopen the project in a container.

There is an equivalent command in the command palette.

Figure 8-2. VS Code Devcontainer command

Either way, when you build a container, you can see that the image mentioned in the devcontainer.json gets pulled and the features and extensions mentioned in the JSON get installed.

Figure 8-3. VS Code Devcontainer build

This process may take a few minutes depending on the size of the image and other features and extensions that need to be installed. Once this is complete, you can see the image and the container within Docker Desktop.

Figure 8-4. VS Code Devcontainer in Docker Desktop

Note: The size of image in Figure 8-4 is around 2GB because of the way the image gets built. Each feature or extension mentioned in the devcontainer.json becomes a layer in the container image and thereby results in a larger image size. If you need a smaller container image, you must use a pre-built image with all tools and libraries.

Once a project opens in a devcontainer, the root of your project gets mounted as the workspace within the devcontainer and therefore making it similar to your local development environment.

Figure 8-5. VS Code Devcontainer workspace

GitHub Codespaces

As your project already contains a devcontainer.json, you can use the same to initialize a GitHub codespaces instance as well. You can do this by navigating to your project’s GitHub repository and then select Code -> Configure and create codespace.

Figure 8-6. Devcontainer codespace

In the codespace configuration, you can see that GitHub picks the devcontainer.json from your project repository by default and uses it for provisioning a codespace.

Figure 8-7. Devcontainer codespace creation

Summary

VS Code devcontainers help you create a consistent development environment for all developers working on a project. As the devcontainer.json becomes a part of the source control, it is available to all developers on your team, making it easier as new team members join the project. The consistent development environment helps debug issues faster. With devcontainers, a developer can choose between a local VS Code instance and a GitHub codespaces instance. Either way, they get the same consistent and common development environment. By leveraging devcontainers for developing Bicep templates, you can enforce the same version of tools and commands used by multiple infrastructure as code developers.

CI and CD of Bicep templates

One of the core enablers of Infrastructure as Code (IaC) is automation. You not only need to automate the configuration tasks but also the entire infrastructure build and deployment processes as well. You can create an end to end pipeline to automate these validation, build, and deployment processes. This end to end automation pipeline – called Continuous Integration (CI) and Continuous Deployment / Delivery (CD) pipeline – ensures that automation and configuration code you develop for your infrastructure is thoroughly tested and can be deployed automatically.

So, what does this mean for your Bicep templates? All the Bicep templates that you author must be syntactically valid and follow the best practices your organization defines, transpile to valid ARM JSON representation, and can be deployed without errors. There are different ways and tools to build this CI/CD pipeline for your Bicep templates. You can start with building a local CI/CD pipeline. But, before that, you need to first define a set of best practices for your organization and identify the steps needed within the pipeline.

Customizing linter rules

Your organization may have a different set of coding best practices that need to be enforced. Bicep comes with a set of built-in rules for checking syntax errors and best practice violations. At present, there is no way to define your own linter rules. In chapter 2, you learned about this linter experience within VS Code for authoring Bicep templates. By default, all linter rules are set to warn when there is a violation. The VS Code extension and Bicep CLI check .

The Bicep linter rules are still evolving and the existing linter rules are a subset of what ARM TTK test cases offer. There is no programmatic or CLI method to retrieve the existing set of rules but you can find a reference of all default rules within Bicep documentation. This chapter focuses on how you can customize the linter experience and not on describing each rule. You can override the default linting experience by placing a bicepconfig.json file at the root of one or more folders containing your Bicep template files. If there are different bicepconfig.json files within the folder hierarchy, the configuration file closer to Bicep template files gets used for linting. All linter rules are available under the analyzers element.

1 {

2     "analyzers": {

3       "core": {

4         "enabled": true|false,

5         "verbose": false|true,

6         "rules": {

7           "rule-name": {

8             "level": "error|warning|info|off"

9           }

10       }

11     }

12   }

13 }

The core element under the analyzers element contains the configuration settings for the linter rules.

You can set enabled to false to disable linting experience and set the verbose element to true if you want to see information around which bicepconfig.json is being used for linting. The rules object can be used to override the default behavior of each rule. The level element within each rule object can be set to a value from the set error, warning, info, and off. These values define the behavior of Bicep CLI (build time) and VS Code extension (editor) when a rule violation occurs. When you set the rule level to error, a rule violation triggers a red squiggly in the editor and a build time error and build failure. When a rule is set to warning level, code violating the rule is shown with a yellow squiggly in the editor but does not trigger any build failure but appears as warning in the build output. By setting the level to info, you see a blue squiggly under the offending code and eliminates any rule violations in the build output. You can turn off rule evaluations by setting the level element within a rule to off.

Here is the bicepconfig.json with relevant linter rule overrides for the rest of this chapter.

1 {

2     "analyzers": {

3       "core": {

4         "enabled": true,

5         "verbose": false,

6         "rules": {

7           "no-loc-expr-outside-params": {

8             "level": "error"

9           },

10           "no-hardcoded-location": {

11             "level": "error"

12           },

13           "no-unused-params": {

14             "level": "error"

15           },

16           "no-unused-vars": {

17             "level": "error"

18           },

19           "prefer-unquoted-property-names":{

20             "level": "error"

21           },

22           "explicit-values-for-loc-params": {

23             "level": "error"

24           },

25           "prefer-interpolation": {

26             "level": "error"

27           },

28           "simplify-interpolation": {

29             "level": "error"

30           },

31           "secure-parameter-default":{

32             "level": "error"

33           }

34       }

35     }

36   }

37 }

Build and deploy

Once you have the linter rule overrides relevant to your organization standards ready, it is time to build. You can do that using the bicep build command. Nothing fancy there. The build process not only validates your Bicep templates for syntax and best practices compliance but it also generates the ARM JSON representation of the template provided there are no build time errors.

Once you build the necessary ARM JSON representation of your Bicep templates, you can deploy the same using either Azure PowerShell or Azure CLI. You have seen several examples of deploying both ARM JSON and Bicep templates. So, in summary, we have two high-level steps in this process – test & build and deliver or deploy. The test & build step gets triggered when a change is committed to the source control repository. When the test & build step is successful, it generates a set of ARM JSON artifacts that you can deliver as, for example, template specs or modules to a private Bicep registry. You may optionally deploy to your Azure subscription. As a part of this pipeline execution, you should be able to run selected steps. For example, test & build only.

Figure 9-1. Bicep CI CD process

What you see in Figure 9-1 is a high-level representation of the CI/CD pipeline for your Bicep templates. There are different ways to implement this pipeline. You can start by building a local pipeline and then explore other options such as GitHub actions.

A local pipeline

You can use a local CI/CD pipeline built using Git and Git hooks for your Bicep templates. In this section, you will learn how to build this local pipeline.

Git supports different types of hooks to run tasks based on git events. One of the important hooks is the post-commit hook. This hooks gets triggered after a git commit is complete. In the local pipeline, you can use the post-commit hook to start a test & build process followed by deliver or deploy task.

You need to first have a source control repository. To create a local source control repository, you need to git and you can install it by downloading the necessary binaries from git-scm.com. Once you installed Git, run the following commands to set up a local source control repository.

1 mkdir myBicepProject

2 cd myBicepProject

3 git init .

The above commands initialize a local git repository. This initialization process creates a .git folder that includes a hooks subfolder containing sample hook definitions.

1 PS C:\temp\myBicepProject> dir .\.git\hooks\

2

3     Directory: C:\temp\myBicepProject\.git\hooks

4

5 Mode                 LastWriteTime         Length Name

6 ----                 -------------         ------ ----

7 -a---            7/8/2022 10:47 AM            478 applypatch-msg.sample

8 -a---            7/8/2022 10:47 AM            896 commit-msg.sample

9 -a---            7/8/2022 10:47 AM           4655 fsmonitor-watchman.sample

10 -a---            7/8/2022 10:47 AM            189 post-update.sample

11 -a---            7/8/2022 10:47 AM            424 pre-applypatch.sample

12 -a---            7/8/2022 10:47 AM           1643 pre-commit.sample

13 -a---            7/8/2022 10:47 AM            416 pre-merge-commit.sample

14 -a---            7/8/2022 10:47 AM           1374 pre-push.sample

15 -a---            7/8/2022 10:47 AM           4898 pre-rebase.sample

16 -a---            7/8/2022 10:47 AM            544 pre-receive.sample

17 -a---            7/8/2022 10:47 AM           1492 prepare-commit-msg.sample

18 -a---            7/8/2022 10:47 AM           2783 push-to-checkout.sample

19 -a---            7/8/2022 10:47 AM           3650 update.sample

For the local pipeline, you can create a file here and name it post-commit (with no extension) and paste the below contents.

1 #!/bin/bash

2 message=$(git log -1 --format=%s)

3

4 exec powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$PWD/.git/hooks/post-c\

5 ommit.ps1" -CommitMessage "\'$message\'"

This is a simple post commit script. It invokes a PowerShell script named post-commit.ps1 and passes the git commit message as an argument. The real magic happens in the post-commit.ps1 script.

1 [CmdletBinding()]

2 param

3 (

4     [Parameter()]

5     [String]

6     $CommitMessage

7 )

8

9 $action = [regex]::Matches($CommitMessage, '(?<=\[)[^]]+(?=\])').Value

10 $buildPath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent

11

12 function buildBicep

13 {

14     [CmdletBinding()]

15     param

16     (

17         [Parameter(Mandatory)]

18         [String]

19         $BuildPath

20     )

21

22     Write-Information "[Build] Building Bicep template"

23     bicep build "${buildPath}\main.bicep"

24     if ($LASTEXITCODE -ne 0)

25     {

26         return $false

27     }

28     return $true

29 }

30

31 if ($action.Count -ge 1)

32 {

33     switch ($action)

34     {

35         "Build" {

36             $buildStatus = buildBicep -BuildPath $buildPath

37             if (!$buildStatus)

38             {

39                 Write-Error "Failed to build the Bicep template"

40                 exit 1

41             }

42             break

43         }

44

45         "Deploy" {

46             $buildStatus = buildBicep -BuildPath $buildPath

47             if (!$buildStatus)

48             {

49                 Write-Error "Failed to build the Bicep template"

50                 exit 1

51             }

52

53             Write-Information "[Build] Deploying Bicep template"

54             $resourceGroup = $action[1]

55

56             if (!$resourceGroup)

57             {

58                 Write-Error "No resource group specified"

59                 exit 1

60             }

61

62             Write-Information "Starting Bicep template deployment"

63             az deployment group create --resource-group $resourceGroup --template-fi\

64 le "${buildPath}\main.bicep"

65             if ($LASTEXITCODE -ne 0)

66             {

67                 Write-Error "Failed to deploy the Bicep template"

68             }

69             break

70         }

71

72         default {

73             Write-Error "Invalid action specified"

74         }

75     }

76 }

77 else {

78     Write-Information "No action specified"

79 }

The above script uses certain identifiers in the git commit message to identify the action that needs to be performed. And, it expects the commit message to be in a specific format. Here is what the script is using.

1 [action][resource-group] Message

The action part can be set to Build or Deploy. And, if the action is set to Deploy, you need to specify a resource group to perform the deployment. The example Bicep template does not have any parameters but if your template has parameters, you may want to update the script to accept the parameters as another identifier within the commit message.

Finally, you need a Bicep template to test this. You can start with one that does not comply with the linter rules.

1 // 9_7-storageAccount.bicep

2 // param resLocation string = resourceGroup().location

3

4 resource stg 'Microsoft.Storage/storageAccounts@2021-02-01' = {

5   name: 'bicepstg002'

6   location: resourceGroup().location

7   kind: 'StorageV2'

8   sku: {

9     name: 'Standard_LRS'

10   }

11   properties: {

12     accessTier: 'Cool'

13   }

14 }

You can save this as main.bicep in the git repository you created earlier and run a git commit. You will see that the post commit hook gets triggered and tries to build the Bicep template.

Figure 9-2. Error in Bicep CI CD process

The error in the build output is self-explanatory. The linter rule enforced not using expressions as resource values. So, you need to fix that.

1 // 9_8-storageAccount.bicep

2 param resLocation string = resourceGroup().location

3

4 resource stg 'Microsoft.Storage/storageAccounts@2021-02-01' = {

5   name: 'bicepstg002'

6   location: resLocation

7   kind: 'StorageV2'

8   sku: {

9     name: 'Standard_LRS'

10   }

11   properties: {

12     accessTier: 'Cool'

13   }

14 }

You can now attempt deploying the modified template by triggering a git commit.

Figure 9-3. Successful Bicep CI CD process

This is a minimal local CI/CD pipeline that we implemented. You may look at modifying the above process to implement delivery of Bicep templates as template specs or publishing Bicep templates to a private Bicep module registry.

GitHub actions for CI/CD

A local pipeline is useful in development and test scenarios and when you are the only one developing these Bicep templates. However, when you work as a team, you need a better way for your entire team to collaborate and ensure the changes made by your team are thoroughly validated and can be deployed or delivered. In this section you will learn about using GitHub actions to build a CI/CD pipeline for your Bicep templates.

Note: You can clone the GitHub repository at https://github.com/rchaganti/ to follow what is explained in the subsequent sections.

Prerequisites

To create a GitHub actions CI/CD pipeline, you will need a GitHub repository first. This section assumes that you have the necessary prerequisite knowledge of creating a GitHub repo and pushing your code. Once you have a GitHub repository, you can now proceed to creating an Azure resource group and a service principal to deploy resources to the resource group in a CI/CD pipeline. You can do this using Azure CLI or Azure PowerShell.

1 az group create --name bicep --location eastus

2

3 az ad sp create-for-rbac --name  bicepcicd \

4                          --role contributor \

5                          --scopes /subscriptions/<subscriptionId>/resourceGroups/bic\

6 ep \

7                          --query "{clientId:appId, clientSecret:password, tenantId:t\

8 enant, subscriptionId:'<subscriptionId>'}"

In the above command, replace <subscriptionId> with your Azure subscription ID. Once you run the commands, you need to copy the output from the service principal creation. This is needed for adding Azure login credentials to the GitHub repository for use within the GitHub actions workflow. You can refer to the GitHub documentation to add AZURE_CREDENTIALS as a secret.

CI/CD Workflow

Once you are done with the secret creation, you need to setup a workflow that will get triggered every time you commit code to this repository. For this purpose, you must create a folder named .github and a folder named workflows underneath the .github folder. Within the workflows folder, you need to create the main.yaml file to define the steps that need to executed as a part of the workflow.

1 name: provision-stg-acct

2

3 on:

4   push:

5     branches:

6       - main

7

8 env:

9   AZURE_RESOURCEGROUP_NAME: bicep

10

11 jobs:

12   linter:

13     runs-on: ubuntu-latest

14     steps:

15     - uses: actions/checkout@v2

16     - name: Lint Bicep Template

17       run: az bicep build --file main.bicep

18

19   deploy:

20     runs-on: ubuntu-latest

21     needs: [linter]

22     steps:

23     - uses: azure/login@v1

24       name: Sign in to Azure

25       with:

26         creds: ${{ secrets.AZURE_CREDENTIALS }}

27     - uses: actions/checkout@v2

28     - uses: azure/arm-deploy@v1

29       name: Provision Storage Account

30       with:

31         failOnStdErr: false

32         deploymentName: ${{ github.run_number }}

33         resourceGroupName: ${{ env.AZURE_RESOURCEGROUP_NAME }}

34         template: ./main.bicep

In the above workflow, similar to the local pipeline, you can see two steps – linter and deploy. The linting step uses bicep build command to lint and build the ARM JSON. The deploy step depends on the linter steps and runs only if linter step is successful. Within the deploy step, azure/login action is used to login to Azure and the azure/arm-deploy action is used to deploy the Bicep template.

Once you copied the above contents to main.yaml file, you need to copy the Bicep template from the previous example as main.bicep and the bicepconfig.json that you prepared at the beginning of this chapter to the new GitHub repository you created. Here is how the folder structure should be.

Figure 9-4. GitHub Bicep CI CD process

Once the above steps are complete, you can commit the code to the GitHub repository and push the contents. Once the content is pushed, you should see workflow getting triggered under GitHub actions.

Figure 9-5. GitHub Bicep CI CD success

Summary

Continuous integration and continuous delivery/deployment shifts testing to the left to catch any issues within the Bicep template code before it reaches production. In this chapter, you have seen how to customize linter rules based on your organization coding standards, set up a local CI/CD pipeline using git hooks, and finally a bit more sophisticated CI/CD pipeline using GitHub actions. In both methods, you have seen how you can implement linting/testing and deployment of Bicep templates. You can extend this knowledge to add publishing Bicep templates as modules to template specs or Bicep module registry.

Built-in function primer

Bicep is not a general purpose language. It is purpose built to transpile from domain-specific language (DSL) representation to Azure Resource Manager (ARM) JSON template representation. That said, to provide the capabilities of a complete programming language, Bicep offers a set of built-in functions for building expressions and simplifying how you write Bicep templates. Bicep offers built-in functions across multiple categories across two different namespaces – az and sys. The az namespace contains functions that are related to Azure deployment and the sys namespace contains functions that are helpful in working with values. By default, you don’t need to reference the namespace when calling a function unless there is another identifier in the Bicep template that creates a name conflict. For example, if you have a parameter or variable named dataUri and you want to access the string function named dataUri in the sys namespace, you need to call the function with a reference to the namespace – sys.dataUri().

In this book so far, you have seen a few built-in functions already. However, there is more and you may not be able to learn or have a need to use every built-in function in your Bicep templates. In this chapter, you will learn about some important built-in functions and how to use them in your Bicep templates.

any function

You have seen an example of using any() function in chapter 5. In this chapter, you will learn this function in-depth. You use this special function to resolve type errors in a Bicep template especially when the type of value you supply to a resource property does not match what the resource expects. When such as type mismatch occurs, using the any() function will suppress any errors and simply convert the value you provided to the type that is expected.

To understand this, take a look at the Azure Network Interface resource API. This resource requires ipConfigurations object in the properties. Using this ipConfigurations object, you can provide both private and public IP configurations. The publicIPAddress configuration is interesting. The value that you need to pass is an object containing at least the id property of the public IP address resource that you want to attach to the network interface. Now, let us say you want to create Bicep template or a module for creating a network interface but want to keep the public IP address configuration optional. So, if a user wants to attach a public IP address to a network interface they would specify an id of the resource otherwise nothing. As you already understand, the publicIPAddress is not just a string property but rather it is a complex object. How do you make this an optional configuration in the template but provide the flexibility to the user? Similar to the example in Chapter 5, you can combine the ternary operator with the any function to build the flexibility in the template. Here is how.

1 @description('Specify a name for the NIC')

2 param nicName string

3

4 @description('Specify a location where this NIC resource needs to be created; defaul\

5 t is resource group location.')

6 param location string = resourceGroup().location

7

8 @description('Specify a subnet ID')

9 param subnetId string

10

11 @description('specify a Public IP address ID')

12 param pipId string = ''

13

14 @description('Specify the IP allocation method; default is Dynamic')

15 @allowed([

16   'Dynamic'

17   'Static'

18 ])

19 param ipAllocationMethod string = 'Dynamic'

20

21 @description('Specify the static IP address; this becomes optional when ipAllocation\

22 Method is set to Dynamic')

23 param staticIpAddress string = ''

24

25 @description('Specify if IP forwarding should be enabled; default is false')

26 param enableIPForwarding bool = false

27

28 resource nicResource 'Microsoft.Network/networkInterfaces@2020-06-01' = {

29   name: nicName

30   location: location

31   properties: {

32     ipConfigurations: [

33       {

34         name: 'ipconfig'

35         properties: {

36           primary: true

37           privateIPAllocationMethod: ipAllocationMethod

38           privateIPAddress: staticIpAddress

39           subnet: {

40             id: subnetId

41           }

42           publicIPAddress: any((pipId == '') ? null : {

43             id: pipId

44           })

45         }

46       }

47     ]

48     enableIPForwarding: enableIPForwarding

49   }

50 }

51

52 output nicId string = nicResource.id

53 output privateIP string = nicResource.properties.ipConfigurations[0].properties.priv\

54 ateIPAddress

In the parameter definitions, you can see that the pipId parameter is set to an empty string by default. If a user needs to attach a public IP address, they simply specify the resource ID of the public IP address resource as input.

1 (pipId == '') ? null : { id: pipId }

The above expression returns null if the pipId is left to default and if not it returns an object with id property set to the value assigned to the pipId by the user. This is still not enough since null is not what the publicIPAddress property expects. This is where the any functions helps resolve the type error. So, you need to wrap the above expression in the any() function and then assign that as the value to publicIPAddress property.

publicIPAddress: any((pipId == ‘’) ? null : {

id: pipId

})

loadJsonContent function

The next interesting function is the loadJsonContent function. This is one of the file functions. This function reads and loads the content of a JSON file as an object of any type. To understand this, consider an example scenario where you have multiple Bicep templates that provision storage accounts as a part of larger service deployments. As a part of this, you want to ensure certain property values for each of these storage accounts are set to what is most relevant for your organization. You do not want to hard code these values in these templates but rather want to have the flexibility to store and update all of that in a central location. This is where loadJsonContent can be helpful. Here is an example of a JSON document that contains a bunch of storage account configuration defaults.

1 {

2     "sku": "Standard_LRS",

3     "kind": "StorageV2",

4     "properties": {

5         "minimumTlsVersion": "TLS1_2"

6     }

7 }

This JSON defines the values of specific storage account properties. Within the Bicep template, you can use the loadJsonContent function to load the JSON content and assign the resulting object to a variable.

You cannot dynamically determine the JSON file name or use parameters to load the JSON content. Parameters are runtime artifacts but Bicep needs the JSON content at build time to generate the ARM JSON.

1 param stgAccountName string

2 param resLocation string = resourceGroup().location

3 var stgAccountConfig = loadJsonContent('10_2-storageDefaults.json')

4

5 resource stgAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = {

6   name: stgAccountName

7   location: resLocation

8   kind: stgAccountConfig.kind

9   sku: {

10     name: stgAccountConfig.sku

11   }

12   properties: {

13     minimumTlsVersion: stgAccountConfig.properties.minimumTlsVersion

14   }

15 }

In the above example, the loadJsonContent function is used to load the contents of 10_2-storageDefaults.json file. Once this object is available, you can access the values using the dot reference. For example, stgAccountConfig.kind.

The loadJsonContent function has two additional parameters.

1 loadJsonContent(filePath, [jsonPath], [encoding])

The jsonPath parameter is optional and can be used to filter down to a specific path within the loaded JSON object.

The encoding parameter is used to specify the JSON file encoding.

getSecret function

A few services that you provision may require credentials, certificates, and other type of secrets as a part of provisioning process. For example, a virtual machine requires either login password or SSH keys to be configured to access the virtual machine. Similarly, a SQL instance requires a login password. In a typical deployment template, you gather these secrets as parameters during the deployment and assign. However, this may not always be a good practice, in an enterprise scenario, to let users decide an assign a password. With the getSecret function, you can retrieve the secrets stored in a keyvault and use the same within a Bicep template. To demonstrate this, you can try the below template that provisions a SQL instance. First, a Bicep module that provisions SQL instance and database.

getSecret function can be used an argument to module parameters only.

1 // 10_4-sql.bicep

2 param serverName string = uniqueString('sql', resourceGroup().id)

3 param sqlDBName string = 'mySQLDB'

4 param resLocation string = resourceGroup().location

5 param adminUserName string = 'sqladmin'

6

7 @secure()

8 param adminPassword string

9

10 resource sqlServer 'Microsoft.Sql/servers@2021-08-01-preview' = {

11   name: serverName

12   location: resLocation

13   properties: {

14     administratorLogin: adminUserName

15     administratorLoginPassword: adminPassword

16   }

17 }

18

19 resource sqlDB 'Microsoft.Sql/servers/databases@2021-08-01-preview' = {

20   parent: sqlServer

21   name: sqlDBName

22   location: resLocation

23   sku: {

24     name: 'Standard'

25     tier: 'Standard'

26   }

27 }

The above template can be used as a module. This template has adminPassword as the secure string parameter.

1 // 10_5-sqlDeploy.bicep

2 param subscriptionId string = '5073fa4c-1234-2314-8371-21e034f70820'

3 param kvResourceGroup string = 'bicep'

4 param kvName string = 'bicepkvault'

5 param resLocation string = resourceGroup().location

6

7 resource keyVault 'Microsoft.KeyVault/vaults@2019-09-01' existing = {

8   name: kvName

9   scope: resourceGroup(subscriptionId, kvResourceGroup )

10 }

11

12 module deploySQL '10_4-sql.bicep' = {

13   name: 'deploySQL'

14   params: {

15     resLocation: resLocation

16     adminPassword: keyVault.getSecret('sqlPassword')

17   }

18 }

This template consumes the SQL module. The keyvault resource references to an existing keyvault instance. Within the module parameters, the reference using keyvault resource name and the getSecret function retrieves the secret stored as sqlPassword.

Summary

This chapter demonstrated a few examples of using some built-in functions in Bicep. There are many built-in functions across different categories and namespaces in Bicep. You may never need to use a good subset of these built-in functions. These functions exist to help you create better expressions and simplify dynamic template authoring. This chapter will get updated often as more interesting examples of using some of the built-in functions emerge.

Part 2

With all basics out of your way, it is time to apply the knowledge you have gained and create some real-world Bicep templates. In the chapters that follow, you will learn advanced techniques in the Bicep language while authoring templates that are purpose-built. You have seen, in the earlier chapters, how to create virtual machines, storage accounts, and a few other Azure resources. This chapter takes this to the next level. This chapter is a living part of the book and will get continuous updates to add useful Bicep templates with explanations around techniques used in each template.

This part of the book includes:

- Deploying your own Kubernetes cluster using Azure virtual machines

Provisioning a Kubernetes Cluster

The best way to learn new techniques in a language is to implement something big and complex. This not only touches different parts of the language but also tests your knowledge of the language. Kubernetes is one such complex infrastructure. When using Bicep to provision a Kubernetes cluster containing multiple worker nodes, you need to understand the exact steps that you need to orchestrate and how.

This chapter walks through one such example of provisioning a Kubernetes cluster on Azure Virtual Machines (VM). For configuring the Kubernetes control plane and worker nodes, this template uses the kubeadm tool.

Kubernetes cluster architecture

In this chapter, you will learn to build a Bicep template for deploying a four-node Kubernetes cluster with one control-plane node and three worker nodes. All these nodes will be Azure Linux VMs running Ubuntu 22.04 LTS. The template that you build must include provisioning any additional resources needed for the Kubernetes cluster to function properly.

Figure 11-1. Kubernetes Cluster in Azure

In this Kubernetes cluster, each virtual machine will have a public IP address for the user to access the instance over SSH from an external system. The storage account is needed for the control plane node to store the kubeadm join command along with the token for the worker nodes to join the Kubernetes cluster. This storage account can be removed once the deployment is complete.

Bicep template structure

For implementing this Bicep module, you need a set of modules that help you in creating a storage account, network interfaces for the virtual machines, virtual network, public IP addresses, managed run commands, and a network security group to enable inbound SSH traffic and Kubernetes cluster access.

You will also need a few shell scripts to install and configure the necessary prerequisites, perform Kubernetes cluster initialization, install a CNI plugin, and finally join the worker nodes to the Kubernetes cluster.

Note: You can download these modules and scripts from the Bicep bookexamples GitHub repository.

1 vscode ➜ /workspaces/bicepbookexamples/chapter11 $ tree .

2 .

3 ├── main.bicep

4 ├── modules

5 │   ├── linuxvm.bicep

6 │   ├── managedRunCmd.bicep

7 │   ├── nic.bicep

8 │   ├── nsg.bicep

9 │   ├── pip.bicep

10 │   ├── storage.bicep

11 │   └── vnet.bicep

12 └── scripts

13     ├── cniPlugin.sh

14     ├── common-prerequisites.sh

15     ├── finalizeDeploy.sh

16     └── kubeadmInit.sh

17

18 2 directories, 12 files

The above listing shows the contents of the chapter11 folder in the examples repository. You have already seen all the modules used in this template except the managed run command template. You msut review the code before trying to provision or using those modules in your own templates.

The managed run command module takes the shell script text and executes it in a Liunx VM.

1 // Name        : managedRunCmd.bicep

2 // Description : Runs a Linux shell script as managed run command in a Linux VM

3 // Version     : 1.0.0

4 // Author      : github.com/rchaganti

5

6 // parameters

7 @description('VM name where this script needs to execute.')

8 param vmName string

9

10 @description('Type of the action performed by this script.')

11 param configType string

12

13 @description('Location of the VM.')

14 param location string

15

16 @description('Should this script be executed asynchronously. Default is false.')

17 param asyncExec bool = false

18

19 @description('Script content to be executed.')

20 param scriptContent string

21

22 @description('Parameters to pass to the script.')

23 param scriptParams array = []

24

25 @description('Protected parameters to pass to the script.')

26 param protectedScriptParams array = []

27

28 @description('Script timeout in seconds. 300 is default.')

29 param timeout int = 300

30

31 // variables

32 var scriptSource = {

33   script: scriptContent

34 }

35

36 // resources

37 resource mrc 'Microsoft.Compute/virtualMachines/runCommands@2022-08-01' = {

38   name: '${vmName}/${configType}mrc'

39   location: location

40   properties: {

41     asyncExecution: asyncExec

42     parameters: scriptParams

43     protectedParameters: protectedScriptParams

44     source: scriptSource

45     timeoutInSeconds: timeout

46   }

47 }

48

49 // output

50 output status object = mrc

This Bicep template implemented as a module takes the shell script content as string and optional parameters using either scriptParams or protectedScriptParams parameters. This module implements the Microsoft.Compute/virtualMachines/runCommands resource and therefore the name of the resource must contain the VM name – the vmName parameter – as a part of the segment.

Provisioning workflow

The main.bicep template consumes the modules and perform the overall Kubernetes cluster deployment. The overall workflow includes provisiong of resources such as:

- A network security group for allowing inbound SSH (22/TCP) and Kubernetes control plane access (6443/TCP)

- A virtual network and a subnet for Linux VMs

- Multiple public IP addresses (one for each Linux VM)

- Network interfaces to connect each VM to the virtual network and associate a public IP address

- Multiple Linux VM that will be a part of the Kubernetes cluster

Note: This chapter does not include the complete code snippets. Refer to the bicepbookexamples repository for the complete code listing.

Once the VMs are provisioned, the custom script extension resource is used to install and configure the prerequisites required for Kubernetes cluster creation. The script content for the custom script resource is available in common-prerequisites.sh.

Using the loadTextContent() function, you can read that into the variable.

1 var commonPrerequisiteConfig = loadTextContent('scripts/common-prerequisites.sh', 'u\

2 tf-8')

1 // Provision common config using custom script extension

2 resource cse 'Microsoft.Compute/virtualMachines/extensions@2022-03-01' = [for (vm, i\

3 ) in vmObject: {

4   name: '${vm.name}/commonfcse'

5   dependsOn: vms

6   location: location

7   properties: {

8     publisher: 'Microsoft.Azure.Extensions'

9     type: 'CustomScript'

10     typeHandlerVersion: '2.1'

11     autoUpgradeMinorVersion: true

12     settings: {

13       script: base64(commonPrerequisiteConfig)

14     }

15   }

16 }]

Custom script resource requires this script content in the base64 encoded format and this is done using the base64() function. This custom script extension gets provisioned on all VMs in parallel to diasble swap usage, configure br_netfiter, install and configure containerd, kubectl, kubeadm, and helm client.

Once the prereqsuites are in place, you can proceed towards initializing the Kubernetes cluster on the control plane node. This is done using the kubeadmInit.sh script. This is done using the managed run command resource as there can only be one instance of custom script extension.

1 // Perform kubeadm init on cplane1

2 module kubeadmInitMrc 'modules/managedRunCmd.bicep' = {

3   name: 'kubeadmInitMrc'

4   dependsOn: cse

5   params: {

6     configType: 'kubeadminit'

7     location: location

8     vmName: 'cplane1'

9     scriptContent: kubeadmInit

10     scriptParams: [

11       {

12         value: cniCidr

13       }

14     ]

15   }

16 }

The kubeadm init command requires a bunch of parameters such as control plane node IP address and CNI CIDR value. CNI CIDR is a user supplied parameter and others can be derived at runtime. The cniCidr parameter value is provided as a part of the scriptParams array. The script content to this resource is supplied using the kubeadmInit variable.

1 var kubeadmInit = loadTextContent('scripts/kubeadmInit.sh','utf-8')

If you have manually deployed a Kubernetes cluster using kubeadm, you will know that kubeadm init generates a kubeadm join command for worker to join the Kubernetes cluster. In an automated workflow, the kubeadm join command along with the auth token needs to be made available to the worker nodes somehow. To implement this handover, this Bicep template implements a storage file share.

Figure 11-2. Kubernetes cluster finalization

You need to provision the storage account and the file share needed for this part of the workflow. This is done by consuming the storage.bicep module.

Once the kubeadm init is complete, you need to install the CNI plugin. This is done by creating another instance of the managed run command to execute only on the control plane node. The cniPlugin.sh script handles this and the content of this script is supplied to the managed run command instance using the cniInstall variable.

1 // Install CNI plugin

2 module cniInstallMrc 'modules/managedRunCmd.bicep' = {

3   name: 'CniInstallMrc'

4   dependsOn: [

5     storageAccount

6   ]

7   params: {

8     configType: 'cniInstall'

9     location: location

10     vmName: 'cplane1'

11     scriptContent: cniInstall

12     scriptParams: [

13       {

14         value: username

15       }

16       {

17         value: cniPlugin

18       }

19       {

20         value: cniCidr

21       }

22     ]

23   }

24 }

To finalize the deployment, you need to execute the workflow shown in Figure 11-2. This is implemented as two separate steps. The first step runs on the control plane node and stores the kubeadm join command in the Azure file share you created earlier. The second step runs on all worker nodes. In this step, each worker node reads and executes the kubeadm join command. Both the steps are implemented as managed run command instances and have the explicit dependencies defined across to ensure that the steps run in a sequence.

1 // Generate kubedam join command on control plane

2 module finalizeDeployCPMrc 'modules/managedRunCmd.bicep' = {

3   name: 'finalizeDeployCP'

4   dependsOn: [

5     cniInstallMrc

6   ]

7   params: {

8     configType: 'finalizeDeployCP'

9     location: location

10     vmName: 'cplane1'

11     scriptContent: finalizeDeploy

12     scriptParams: [

13       {

14         value: storageAccountName

15       }

16       {

17         value: storageAccount.outputs.storage.storageKey

18       }

19       {

20         value: storageAccount.outputs.storage.shareUri

21       }

22       {

23         value: 'cp'

24       }

25     ]

26   }

27 }

28

29 // Join nodes to the Kubernetes cluster

30 module finalizeDeployWorkerMrc 'modules/managedRunCmd.bicep' = [for vm in vmObject: \

31 if (vm.role == 'worker') {

32   name: '${vm.name}-finalizeDeployWorker'

33   dependsOn: [

34     finalizeDeployCPMrc

35   ]

36   params: {

37     configType: 'finalizeDeployWorker'

38     location: location

39     vmName: vm.name

40     scriptContent: finalizeDeploy

41     scriptParams: [

42       {

43         value: storageAccountName

44       }

45       {

46         value: storageAccount.outputs.storage.storageKey

47       }

48       {

49         value: storageAccount.outputs.storage.shareUri

50       }

51       {

52         value: 'worker'

53       }

54     ]

55   }

56 }]

Step 1 ensures that the CNI install on control plane node is complete and step 2 waits for control plane node to complete storing the kubeadm join command on the file share.

Finally, once the template deployment is complete, you will receive the FQDN of each Linux VM for external SSH access.

Summary

This chapter introduced you to using custom script extension and managed run command resources. You have seen how to use the loadTextContent and base64 functions to read the script content and use it with custom script extension and managed run command resources. You have also learned how you can translate the manual steps into an automated process and how you can provision resources such as storage accounts as ephemeral resources within the deployment flow to automate complex tasks.