Quickstart: Use MSBuild tasks to convert Bicep files and parameters to JSON

In this quickstart, you learn how to use the build system in Visual Studio, MSBuild, to convert Bicep files to JSON Azure Resource Manager templates (ARM templates). You can also use MSBuild with NuGet package version 0.23.x or later to convert Bicep parameters files to Azure Resource Manager parameters files. The examples in this article show how to use the command line with MSBuild and C# project files for the conversion, and these files demonstrate how to use project files in an MSBuild continuous integration pipeline.

Prerequisites

  • Install Visual Studio or Visual Studio Code. The Visual Studio community version is free and installs .NET 6.0, .NET Core 3.1, .NET SDK, MSBuild, .NET Framework 4.8, NuGet package manager, and C# compiler. From the installer, select Workloads > .NET desktop development. With Visual Studio Code, you need to install Bicep and Azure Resource Manager (ARM) Tools extensions.
  • Install Azure PowerShell or a command-line shell for your operating system.

Based on how nuget.config is configured in your environment and if nuget.org isn't configured as a package feed, you might need to run the following command:

dotnet nuget add source  https://api.nuget.org/v3/index.json -n nuget.org

In certain environments, using a single package feed helps to prevent conflicts between packages with the same ID and version but different contents in different feeds. For Azure Artifacts users, upstream sources can store packages from different sources in one feed.

MSBuild tasks and Bicep packages

You can use MSBuild tasks and CLI packages along your continuous integration pipeline to convert Bicep files and Bicep parameters files into JSON. This functionality relies on the following NuGet packages:

Package Name Description
Azure.Bicep.MSBuild Cross-platform MSBuild task that invokes the Bicep CLI and compiles Bicep files into JSON ARM templates
Azure.Bicep.CommandLine.win-x64 Bicep CLI for Windows
Azure.Bicep.CommandLine.linux-x64 Bicep CLI for Linux
Azure.Bicep.CommandLine.osx-x64 Bicep CLI for macOS

These links show the latest NuGet package version. The following screenshot shows the latest NuGet package version matching the version in the Bicep CLI:

Screenshot showing the latest NuGet package version matching the most current version in the Bicep CLI.

  • Azure.Bicep.MSBuild

    When the Azure.Bicep.MSBuild package is included in a project file's PackageReference property, it imports the Bicep task for invoking the Bicep CLI:

    <ItemGroup>
      <PackageReference Include="Azure.Bicep.MSBuild" Version="0.24.24" />
      ...
    </ItemGroup>
    
    

    The package transforms the output of the Bicep CLI into MSBuild errors and imports the BicepCompile target to streamline how the Bicep task is used. By default, the BicepCompile runs after the Build target, compiling all @(Bicep) and @(BicepParam) items. Then, it deposits the output in $(OutputPath) with the same file name and a .json extension.

    The following example shows the project file setting for compiling main.bicep and main.bicepparam files in the same directory as the project file and placing the compiled main.json and main.parameters.json in the $(OutputPath) directory.

    <ItemGroup>
      <Bicep Include="main.bicep" />
      <BicepParam Include="main.bicepparam" />
    </ItemGroup>
    

    You can override the output path per file using the OutputFile metadata on Bicep items. The following example recursively finds all main.bicep files and places the compiled .json files in $(OutputPath) under a subdirectory with the same name in $(OutputPath):

    <ItemGroup>
      <Bicep Include="**\main.bicep" OutputFile="$(OutputPath)\%(RecursiveDir)\%(FileName).json" />
      <BicepParam Include="**\main.bicepparam" OutputFile="$(OutputPath)\%(RecursiveDir)\%(FileName).parameters.json" />
    </ItemGroup>
    

    You can create more customizations by setting one of the following properties to the PropertyGroup in your project:

    Property Name Default Value Description
    BicepCompileAfterTargets Build Used as AfterTargets value for the BicepCompile target. Change the value to override the scheduling of the BicepCompile target in your project.
    BicepCompileDependsOn None Used as DependsOnTargets value for the BicepCompile target. This property can be set to targets on which you want BicepCompile target to depend.
    BicepCompileBeforeTargets None Used as BeforeTargets value for the BicepCompile target.
    BicepOutputPath $(OutputPath) Set this property to override the default output path for the compiled ARM template. OutputFile metadata on Bicep items takes precedence over this value.

    For the Azure.Bicep.MSBuild to operate, it's required to set a BicepPath environment variable. See the next bullet item for configuring BicepPath.

  • Azure.Bicep.CommandLine

    The Azure.Bicep.CommandLine.* packages are available for Windows, Linux, and macOS. The following example references the package for Windows:

    <ItemGroup>
      <PackageReference Include="Azure.Bicep.CommandLine.win-x64" Version="__LATEST_VERSION__" />
      ...
    </ItemGroup>  
    

    When referenced in a project file, the Azure.Bicep.CommandLine.* packages automatically set the BicepPath property to the full path of the Bicep executable for the platform. The reference to this package can be omitted if the Bicep CLI is installed other ways. For this case, instead of referencing an Azure.Bicep.Commandline package, you can either configure an environment variable called BicepPath or add BicepPath to the PropertyGroup. For example:

    On Windows:

    <PropertyGroup>
      <BicepPath>c:\users\john\.Azure\bin\bicep.exe</BicepPath>
      ...
    </PropertyGroup>
    

    On Linux:

    <PropertyGroup>
      <BicepPath>/usr/local/bin/bicep</BicepPath>
      ...
    </PropertyGroup>
    

Project file examples

The following examples show how to configure C# console application project files to convert Bicep files and Bicep parameters files to JSON. The following examples replace __LATEST_VERSION__ with the latest version of the Bicep NuGet package. See MSBuild tasks and Bicep packages to learn how to find the latest package.

SDK-based example

The .NET Core 3.1 and .NET 6 examples are similar. However, .NET 6 uses a different format for the Program.cs file. For more information, see .NET 6 C# console app template generates top-level statements.

  • .NET 6

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <RootNamespace>net6-sdk-project-name</RootNamespace>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Azure.Bicep.CommandLine.win-x64" Version="__LATEST_VERSION__" />
        <PackageReference Include="Azure.Bicep.MSBuild" Version="__LATEST_VERSION__" />
      </ItemGroup>
    
      <ItemGroup>
        <Bicep Include="**\main.bicep" OutputFile="$(OutputPath)\%(RecursiveDir)\%(FileName).json" />
        <BicepParam Include="**\main.bicepparam" OutputFile="$(OutputPath)\%(RecursiveDir)\%(FileName).parameters.json" />
      </ItemGroup>
    </Project>
    

    The RootNamespace property contains a placeholder value. When you create a project file, the value matches your project's name.

  • .NET Core 3.1

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.1</TargetFramework>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Azure.Bicep.CommandLine.win-x64" Version="__LATEST_VERSION__" />
        <PackageReference Include="Azure.Bicep.MSBuild" Version="__LATEST_VERSION__" />
      </ItemGroup>
    
      <ItemGroup>
        <Bicep Include="**\main.bicep" OutputFile="$(OutputPath)\%(RecursiveDir)\%(FileName).json" />
        <BicepParam Include="**\main.bicepparam" OutputFile="$(OutputPath)\%(RecursiveDir)\%(FileName).parameters.json" />
      </ItemGroup>
    </Project>
    

NoTargets SDK example

The Microsoft.Build.NoTargets MSBuild project SDK allows project tree owners to define projects that don't compile an assembly. This SDK allows the creation of standalone projects that compile only Bicep files.

<Project Sdk="Microsoft.Build.NoTargets/__LATEST_MICROSOFT.BUILD.NOTARGETS.VERSION__">
  <PropertyGroup>
    <TargetFramework>net48</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Azure.Bicep.CommandLine.win-x64" Version="__LATEST_VERSION__" />
    <PackageReference Include="Azure.Bicep.MSBuild" Version="__LATEST_VERSION__" />
  </ItemGroup>

  <ItemGroup>
    <Bicep Include="main.bicep"/>
    <BicepParam Include="main.bicepparam"/>
  </ItemGroup>
</Project>

The latest Microsoft.Build.NoTargets version can be found at Microsoft.Build.NoTargets. For Microsoft.Build.NoTargets, specify a version like Microsoft.Build.NoTargets/3.7.56.

<Project Sdk="Microsoft.Build.NoTargets/3.7.56">
  ...
</Project>

Classic framework example

Use the Classic example only if the previous examples don't work for you. In this example, the ProjectGuid, RootNamespace, and AssemblyName properties contain placeholder values. When you create a project file, a unique GUID is created, and the name values match your project's name.

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{11111111-1111-1111-1111-111111111111}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <RootNamespace>ClassicFramework</RootNamespace>
    <AssemblyName>ClassicFramework</AssemblyName>
    <TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <ItemGroup>
    <None Include="App.config" />
    <Bicep Include="main.bicep" />
    <BicepParam Include="main.bicepparam" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Azure.Bicep.CommandLine.win-x64">
      <Version>__LATEST_VERSION__</Version>
    </PackageReference>
    <PackageReference Include="Azure.Bicep.MSBuild">
      <Version>__LATEST_VERSION__</Version>
    </PackageReference>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

Convert Bicep to JSON

These examples show how to use MSBuild to convert a Bicep file and a Bicep parameters file to JSON. Start by creating a project file for .NET, .NET Core 3.1, or the Classic framework. Then, generate a Bicep file and a Bicep parameters file before running MSBuild to convert them to JSON.

Create project

To use the .NET CLI to build a project in .NET:

  1. Open Visual Studio Code, and select Terminal > New Terminal to start a PowerShell session.

  2. Create a directory named msBuildDemo, and go to the directory. This example uses C:\msBuildDemo:

    Set-Location -Path C:\
    New-Item -Name .\msBuildDemo -ItemType Directory
    Set-Location -Path .\msBuildDemo
    
  3. Run the dotnet command to create a new console with the .NET 6 framework:

    dotnet new console --framework net6.0
    

    This command creates a project file with the same name as your directory, msBuildDemo.csproj. See this Create a .NET console application using Visual Studio Code tutorial for more information about how to create a console application from Visual Studio Code.

  4. Open msBuildDemo.csproj with an editor, and replace the content with the .NET 6 or NoTargets SDK example. Also replace __LATEST_VERSION__ with the latest version of the Bicep NuGet package.

  5. Save the file.

Create Bicep file

To create a Bicep file and a BicepParam file before converting them to JSON:

  1. Create a main.bicep file in the same folder as the project file; for example: C:\msBuildDemo directory, plus the following content:

    @allowed([
      'Premium_LRS'
      'Premium_ZRS'
      'Standard_GRS'
      'Standard_GZRS'
      'Standard_LRS'
      'Standard_RAGRS'
      'Standard_RAGZRS'
      'Standard_ZRS'
    ])
    @description('Storage account type.')
    param storageAccountType string = 'Standard_LRS'
    
    @description('Location for all resources.')
    param location string = resourceGroup().location
    
    var storageAccountName = 'storage${uniqueString(resourceGroup().id)}'
    
    resource storageAccount 'Microsoft.Storage/storageAccounts@2023-04-01' = {
      name: storageAccountName
      location: location
      sku: {
        name: storageAccountType
      }
      kind: 'StorageV2'
    }
    
    output storageAccountNameOutput string = storageAccount.name
    
  2. Create a main.bicepparam file in the C:\msBuildDemo directory with the following content:

    using './main.bicep'
    
    param prefix = '{prefix}'
    

    Replace {prefix} with a string value used as a prefix for the storage account name.

Run MSBuild

Run MSBuild to convert the Bicep file and the Bicep parameters file to JSON:

  1. Open a Visual Studio Code terminal session.

  2. In the PowerShell session, go to the folder that contains the project file. For example, the C:\msBuildDemo directory.

  3. Run MSBuild.

    MSBuild.exe -restore .\msBuildDemo.csproj
    

    The restore parameter creates dependencies needed to compile the Bicep file during the initial build. The parameter is optional after the initial build.

    To use the .NET Core, run:

    dotnet build .\msBuildDemo.csproj
    

    or

    dotnet restore .\msBuildDemo.csproj
    
  4. Go to the output directory, and open the main.json file that should look like the following example.

    MSBuild creates an output directory based on SDK or framework version:

    • .NET 6: \bin\Debug\net6.0
    • .NET Core 3.1: \bin\Debug\netcoreapp3.1
    • NoTargets SDK: \bin\Debug\net48
    • Classic framework: \bin\Debug
    {
      "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
      "contentVersion": "1.0.0.0",
      "metadata": {
        "_generator": {
          "name": "bicep",
          "version": "0.8.9.13224",
          "templateHash": "12345678901234567890"
        }
      },
      "parameters": {
        "storageAccountType": {
          "type": "string",
          "defaultValue": "Standard_LRS",
          "metadata": {
            "description": "Storage account type."
          },
          "allowedValues": [
            "Premium_LRS",
            "Premium_ZRS",
            "Standard_GRS",
            "Standard_GZRS",
            "Standard_LRS",
            "Standard_RAGRS",
            "Standard_RAGZRS",
            "Standard_ZRS"
          ]
        },
        "location": {
          "type": "string",
          "defaultValue": "[resourceGroup().location]",
          "metadata": {
            "description": "Location for all resources."
          }
        }
      },
      "variables": {
        "storageAccountName": "[format('storage{0}', uniqueString(resourceGroup().id))]"
      },
      "resources": [
        {
          "type": "Microsoft.Storage/storageAccounts",
          "apiVersion": "2023-04-01",
          "name": "[variables('storageAccountName')]",
          "location": "[parameters('location')]",
          "sku": {
            "name": "[parameters('storageAccountType')]"
          },
          "kind": "StorageV2"
        }
      ],
      "outputs": {
        "storageAccountNameOutput": {
          "type": "string",
          "value": "[variables('storageAccountName')]"
        }
      }
    }
    
  5. The main.parameters.json file should look like:

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "prefix": {
      "value": "mystore"
    }
  }
}

If you make changes or want to rerun the build, delete the output directory so that new files can be created.

Clean up resources

When you're finished with the files, delete the directory. For this example, delete C:\msBuildDemo.

Remove-Item -Path "C:\msBuildDemo" -Recurse

Next steps