Infrastructure as Code (IaC) is an important technique when it comes to automation. Teams are starting their journey towards cloud-native often by automating everything. Infrastructure is no exception here. The continuously growing adoption of cloud services and digitalization are just some reasons why IaC tools like Terraform are so popular.

Project Bicep is the new kid on the block. Bicep is a new language that is created to express Azure infrastructures as code.

Scope of the comparison

This article compares Project Bicep and HashiCorp Terraform. It contains

  • evaluation of differences between Bicep and Terraform
  • comparison of
    • deployment methods
    • language syntax
    • tooling

This article does not contain an introduction to Biceps syntax. The product team and the vivid open-source community provide a great tutorial and an impressive amount of samples. You should invest some time to complete the tutorial and browse through some examples to get a fundamental understanding of the language itself. My friend Tobias Zimmergren also published a great article on Bicep. It is worth reading.

What is Bicep

Bicep is a fairly new, declarative domain-specific language (DSL) invented by Microsoft. It transpiles down to Azure ARM templates. That is why you have access to all resources, their properties, in all available versions. Compared to underlying ARM templates, developers can express Azure infrastructures with way less code. The team tracks the following high-level goals publicly:

  1. Bicep will provide the best user experience when it comes to deploying, describing, and validating Azure infra
  2. Bicep should be a transparent abstraction on top of the underlying cloud platform. It should not require any kind of “opt-in” for new Azure services or new service versions
  3. Bicep language should be easy to read, learn and understand for every developer
  4. Refactoring, structuring, and modularizing should be straight forward and flexible
  5. Productivity and rich tooling is being created in parallel with the Bicep compiler to ensure a great developer experience
  6. Tooling should drive user confidence. Developers should know if their source is valid before deploying it

What is Bicep not

The Bicep team points out what they don’t want to build:

  • Bicep will not become a general-purpose language. Which means you will often need some framing. Think of scripts that run before or after a Bicep-based deployment
  • Bicep will not provide a first-class provider model for non-Azure-related tasks. Although it is technically possible. The team is focusing on Azure

Bicep compared to Terraform

In the following paragraphs, Bicep and Terraform are compared using three key metrics:

  • Deployment methods
  • Language syntax
  • Tooling

Deployment methods

Terraform uses desired state configuration. Terraform invokes the underlying cloud infrastructure APIs and mutates the target environment (e.g., Azure) to match the desired state defined by the user. Every Terraform project has a lifecycle, and Terraform CLI assists users while creating, mutating, and destroying infrastructures.

In contrast, Bicep defaults to incremental deployments like underlying ARM Templates. Incremental deployments may add or modify Azure services on a configurable scope (Resource Group, Subscription, Management Group, or Tenant). Relying on incremental deployments, Bicep will never delete a service instance from Azure when you remove it from your source code.

Additionally, you can deploy Bicep projects using the complete deployment mode. Deployments created with complete mode will result in removing all resources from the targeting scope, which are not part of the applied template. This may also affect resources that are not in the scope of the Bicep project but belong to the same Azure Resource Group.

Comparing both, Bicep (using the incremental deployment mode) is more robust in the first place. One can not accidentally delete a production service by removing some code lines from the source code. However, this results in Bicep leading to potential configuration drift. Configuration drift is the difference between the state described in the codebase and the actual infrastructure.

On the flip side, every Terraform project heavily relies on a state file. The Terraform state file is critical. Among others, the state file is responsible for:

  • holding a representation of the actual infrastructure to calculate the execution plan (preview changes before applying them)
  • controlling concurrent executions if being stored in a remote state backend such as Azure Storage Account

The state file is the basement for some of the most popular Terraform features. However, it is often the reason why teams have to troubleshoot or reconcile configuration drifts in Terraform. Terraform state may also contain sensitive data (things like passwords), so it must be protected.

Language Syntax

Bicep and Terraform have dedicated languages that developers use to express infrastructure as code. Both languages are declarative. They are fairly similar and share some grammar. Let’s highlight some of the differences, strengths, and weaknesses:

Custom validations

Although underlying ARM APIs will validate every property during deployment, providing custom validation in IaC projects is essential. Custom validation allows you to:

  • enforce lightweight governance rules before any deployment hits Azure Resource Manager API
  • validate user input against individual policies

As shown in the first snippet, custom validation in Bicep relies on a pattern called decorators. Parameters are decorated with custom validation rules. Users can apply multiple decorators to a single parameter if required:

// sample.bicep

@minLength(5)
@allowed([
  'germanywestcentral'
  'eastus2'
  'westeurope'
])
param location string

In contrast, custom validation in Terraform feels like a small macro language. Validation rules in Terraform can leverage any of the built-in functions to depict the logic:

# sample.tf

variable "location" {
  type    = string
  validation {
    condition     = contains(["eastus2", "germanywestcentral", "westeurope"], var.location)
    error_message = "Please use one of the following regions (germanywestcentral|eastus2|westeurope)."
  }
}

Looking at both - fairly simple - examples, I prefer Bicep’s custom validation syntax. It is easy to memorize and feels more focused. Concatenating different decorators feels more natural for a declarative language.

Resource Version pinning

Azure services evolve. Continuous improvements may result in new ARM API versions being published. The azurerm provider in Terraform relies on the most recent API version. That said, developers may want to use an old version of the provider when looking for a specific version for a particular Azure service. However, this may lead to using older API versions for other Azure resources too. As an effect, developers don’t have to specify the API version when describing a particular resource, as shown here:

# sample.tf

resource "azurerm_container_registry" "acr" {
  name                     = "thorstenhans"
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = var.location
  sku                      = "Standard"
}

In contrast, type identifiers in Bicep contain the version number when multiple versions are available. To deploy the same Azure Container Registry (ACR) in Bicep using the most recent API preview version, the following code is required:

// sample.bicep

resource acr 'Microsoft.ContainerRegistry/registries@2020-11-01-preview' = {
    name: 'thorstenhans'
    location: location
    sku: {
        name: 'Standard'
    }
}

I would love to get rid of the version specification @2020-11-01-preview. I would welcome some kind of pointer to the most recent version. For example something like Microsoft.ContainerRegistry/registries or Microsoft.ContainerRegistry/registry pointing to the current version.

Conditional Deployments

Conditional deployments are often required in IaC. For example, users want to deploy assistance services only for staging or production environments but ignore them for the development environment to reduce costs or reuse existing services in development. Bicep and Terraform use slightly different syntax to support conditional resource deployments.

In Bicep, a conditional expression is added to the resource declaration using the if keyword as shown here:

// sample.bicep

param is_dev bool

resource ai 'Microsoft.Insights/components@2020-02-02-preview' = if (!is_dev) {
  name: 'ai-test'
  kind: 'web'
  location: 'westeurope'
}

In contrast, Terraform uses the count property - which is available on every resource - to support conditional deployments:

# sample.tf

variable "is_dev" {
    type = bool
}

resource "azurerm_application_insights" ai {
  name                  = "thns-demo"
  resource_group_name   = "tf-sample"
  location              = "germanywestcentral"
  application_type      = "web"
  count                 = var.is_dev? 0 : 1
}

Comparing both snippets, you will quickly realize that Bicep has an advantage here. Conditionals in Bicep look and feel cleaner compared to Terraform.

Tooling

Robust and efficient tooling is very important to many developers (including me). In this section, we will look at different tool aspects like:

  • Command Line Interface (CLI) productivity
  • Coding assistance and IDE integration
  • Import existing ARM templates to head-start a project

Command Line Interfaces

Terraform CLI is pure dope. It runs almost everywhere, it is easy to install (just a binary), and it is super robust. Terraform CLI is a workhorse that has just one purpose: It assists you in getting your stuff done.

Some of the most prominent Terraform CLI commands are:

  • terraform init - Initialize a Terraform project and installs all required providers
  • terraform plan - Preview Changes before modifying underlying cloud infrastructure
  • terraform apply - Apply current configuration
  • terraform fmt - Reformat all .tf files in the current folder
  • terraform validate - Validate the Terraform project

Bicep integrates with Azure CLI, which is also amazing. I love az and use it every day. Shipping bicep as an extension for Azure CLI was obvious and better than building it from scratch with - let’s say Node.js.The Bicep CLI offers just a handful of sub-commands. Maybe because Bicep is still relatively new, maybe because it is leveraging existing mechanisms and patterns established for ARM templates.

Most important az commands in the context of Bicep are:

  • az bicep build - Transpile one or more Bicep files into an ARM template
  • az bicep decompile - Decompile ARM template to Bicep
  • az bicep upgrade - Update the Bicep CLI
  • az deployment group create - Deploy on Azure Resource Group scope
  • az deployment sub create - Deploy on Azure Subscription scope

I would love to see a dedicated format or fmt sub-command for the Bicep CLI with corresponding flags like --check. I use those commands provided by Terraform (terraform fmt --check) in CI/CD to ensure proper styling.

Bicep Execution Plan (aka preview changes)

Previewing changes before modifying underlying cloud infrastructures is perhaps the most prominent USP of Terraform. Both, Azure CLI and Azure PowerShell offer similar functionalities using the what-if commands as shown below:

# create a new Resource Group
az group create testgr -l germanywestcentral

# compile Bicep to ARM Template
az bicep build -f myinfra.bicep

# preview potential changes
az deployment group what-if -g testgr -f myinfra.json

Preview changes using the what-if command

Coding assistance and IDE integration

There are Visual Studio Code extensions for both languages. Both of them use an underlying language service to provide code completion and rich IntelliSense capabilities.

The (now) official Terraform extension for VSCode is okay-ish. Depending on the complexity of your infrastructure project, you may see IntelliSense break from time to time. Referencing resources, variables, and locals will stop working at some point. At least that’s what I experience now and then in different environments.

Terraform extension for Visual Studio Code - broken IntelliSense

Although Bicep is so new, their coding assistance already outperforms Terraform. Referencing existing elements, auto-completion, and validation works for me like a charm. At least as long as all infrastructure belongs to a single file.

Bicep extensions for Visual Studio Code

Getting proper code completion and rich IntelliSense is mission-critical. Once people grasped the language basics, they always look for great tooling to improve their productivity. Visual Studio Code tooling is an area where Terraform could and should improve in the future.

Import existing ARM templates

Many teams have existing ARM templates to migrate or import into tools like Terraform or Bicep. Both tools provide a mechanism to import existing ARM templates. However, there are fundamental differences.

Bicep CLI comes with the handy decompile command. It takes an existing ARM template (multiple JSON template files are supported) and tries to decompile it to .bicep.

I’ve tested az bicep decompile with a few (fairly simple) ARM templates, and it worked for me. On the other side, when I tried to decompile more complex infrastructures, I ran into some errors, and the resulting Bicep file was invalid. I bet that tooling will improve in this area over time. At least it is worth giving it a shot.

Terraform has a different approach regarding pre-existing ARM templates. Terraform treats an entire ARM template as an atomic resource. Although this seems handy in the first place, it lacks the possibility to do fine-granular modifications.

That said, there are several open-source tools available that try to address the translation of ARM templates into Terraform language (previously known as HashiCorp Configuration Language). However, I found those tools often rely on older versions of providers like azurerm, resulting in incompatibilities.

Conclusion

Bicep language is awesome and its tooling is robust. Compared to Terraform, developers could gain more speed when it comes to parameters and their validation logic.

Obviously, Bicep is built to provision Azure infrastructures. In multi-cloud scenarios, Terraform is still the best option.

Speaking of dedicated Azure infrastructures, Bicep is the preferred solution due to the seamless integration with the underlying Azure Resource Manager API.

In the end, I would advise you to choose the tooling based on the actual requirements. For all my Azure-only stuff, I’ll head direction Bicep. However, plenty of our customers are happy with Terraform! Their projects keep growing and need some kind of maintenance from time to time. That said, there is enough space for Terraform and Bicep - at least in my toolbelt.