Comparing IaC Tools for Azure: Terraform and Bicep

Infrastructure as Code on Azure - ARM

These days, most developers are familiar with the concept of having our applications’ and systems’ infrastructure defined in descriptive and version-controlled code which generates the same environment, every time it’s applied. IaC solves the problem of having different environments like development, staging, and production drift out-of-sync – as, without having our infrastructure defined as code, each environment has to be maintained manually which usually leads to introducing accidental inconsistencies between them, due to human error and general forgetfulness to always apply the exact same change on all environments.

For the longest time, the native option for IaC on Azure was to use Azure Resource Manager (ARM) templates: the Resource Manager itself is a deployment and management service on Azure. Every single option to manage resources on Azure interacts with the ARM either directly (Azure Portal, SDKs, REST clients), or indirectly (Azure CLI, PowerShell – both using the SDK, which then calls over to the ARM).

ARM templates are essentially just JSON files, describing the expected infrastructure to end up with, after applying the template. These JSON files can be extended with template expressions, which leverage the functions provided by the ARM itself. Each template can contain the following sections:

  1. Parameters (e.g., different values for different environments)
  2. Variables (e.g., construct values from a parameter for a resource name, to reuse in the template)
  3. User-defined functions
  4. Resources
  5. Outputs

When an ARM template gets deployed, the JSON file is converted into a REST API call, addressed to the actual provider of the resource in the template – e.g., if the ARM template aims to create a Container Registry, the Microsoft.ContainerRegistry namespace will appear in the URL, called by the Resource Manager.

Being a mature, native option for Azure, zero-day support for new features are guaranteed: all features and capabilities of the ARM will be immediately available for the SDK and REST clients, however, it might appear only months later in the Portal, and in third-party IaC tools whenever its developers or open-source contributors catch up and implement it.

Terraform for Azure

In my day-to-day project, we decided not to use ARM templates but go with Terraform instead. Other than the de facto benefits of having all the positives when the infrastructure is defined as code, Terraform offered some pros over ARM templates which convinced us to give it a go, in an enterprise environment.

Terraform’s syntax is much more developer-friendly and readable. While the ARM template is just a lengthy JSON file, its readability is quite difficult, especially after introducing parameters, variables, functions, referencing outputs between the resources, and so on. While Terraform uses HashiCorp’s own configuration language (HCL), its simplicity makes it easy to get up to speed with it quickly, and then writing quite sophisticated infrastructures still can end up in a fairly readable and easy-to-follow format.

We were also curious about Terraform’s state management, a concept not quite present in ARM templates. Other teams in the company were already using Terraform in production successfully, so there were no questions about the maturity of the tool.

Although for our use-case, the point of Terraform being host agnostic (single configuration to be deployed cross-cloud, not only to Azure but to other providers like AWS or Google Cloud) was not of consideration, however, it might be one of the most powerful arguments for other teams to vouch for it.

Since Terraform is not a native tool by Microsoft, we had to accept the fact that support for new features in the ARM might take a while to appear in Terraform (although there are always workarounds, with deploying actual ARM templates through Terraform or just running Azure CLI snippets – both of which should support all the latest features of the ARM). We did not think this would ever cause headaches for us, since our project was not using the latest, cutting-edge features of Azure, anyway. However, we did end up coming face to face with a problem, related to a missing ARM feature in Terraform.

Our backend, running on an App Service had to accept incoming certificates, optionally (an external service calling one of our APIs exposed by the App Service, using SSL certificate-based authentication, while the rest of our endpoints using OAuth 2, not expecting or validating any incoming certificates in the HTTP calls, just JSON Web Tokens).

On the Azure Portal, under the App Service’s Configuration, General settings, this can be set through a control:

Incoming Client Certificates


Client certificate mode: Require | Allow | Ignore

However, in Terraform, the client_cert_enabled property was just a simple boolean, indicating to either require (true) or ignore (false, the default) the incoming certificate – we needed the non-existing third option.

Another headache we’ve encountered, which is not inherently Terraform’s fault, is how we ended up with an inconsistent state in Terraform’s .tfstate file, and in the actually provisioned Azure resources. This was caused by our team not being rigorous enough to have every member stick to using Terraform to implement changes to our resources (rather than doing ‘quick fixes’ manually).

Bicep

Personally, for me, Terraform’s biggest strength over ARM templates is its more concise and readable syntax. Due to that, I was immediately curious about Bicep upon hearing its release: Bicep is an open-source, domain-specific, declarative language, acting as a layer on top of the ARM template (initially I compared this relationship to TypeScript and JavaScript). It was created and is maintained mostly by Microsoft – due to its native nature to Azure, all current and incoming features should be supported immediately by Bicep.

Following Microsoft’s recommendation, we can create .bicep files with VS Code, in order to leverage the IntelliSense, syntax highlighting, and validation of the official Bicep extension (however it is still in Preview).

The .bicep files are required to be transpiled into JSON files, for which the bicep Azure CLI command group has to be installed: az bicep install. Interestingly enough, the CLI contains a decompile command, which attempts to convert an ARM template JSON into a Bicep file.

Bicep’s state management is conceptually different from Terraform’s: while Terraform uses .tfstate files to keep track of the state, Bicep queries directly from Azure for what is currently provisioned and gets the differences between the already existing resources and the desired state from the Bicep files locally. It exposes a what-if command, which is similar to Terraform’s terraform plan command, both predicting the changes to be applied, without actually executing any modifications on the live resources.

Of course, there are a few cons to note. After playing around with Bicep for a while, the syntax still didn’t feel quite as natural and easy to write as Terraform’s. Personally, for me, Bicep’s syntax fits somewhere in between ARM and Terraform, weighing moreover to the ARM side of the scale. Given, it’s also the early days of Bicep, it implies that parts of the tool are changing rapidly and it doesn’t feel as mature as Terraform, just yet.

While support for multiple cloud providers isn’t something I consider during my day-to-day activities in my current project, it’s important to note that, unlike Terraform, Bicep is limited to Azure only.

Provision an App Service: Bicep

In order to compare the experience of using Bicep versus Terraform, in the following examples, an App Service Plan and App Service will be provisioned, via both tools.

The simple .bicep file looks as follow:

 
param skuName string = 'F1'
param skuCapacity int = 1
param location string = resourceGroup().location

var appServicePlanName = 'asp-demo1'
var webSiteName = 'app-demo1'

resource appServicePlan 'Microsoft.Web/serverfarms@2020-06-01' = {
  name: appServicePlanName
  location: location
  sku: {
    name: skuName
    capacity: skuCapacity
  }
}


 
resource appService 'Microsoft.Web/sites@2020-06-01' = {
  name: webSiteName
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
    siteConfig: {
      minTlsVersion: '1.2'
    }
  }
}


Just glancing at the content of the template, one slight annoyance immediately jumped out to me – the namespace of the Azure resource. When typing out ‘resource appService’, I imagine most developers would expect the App Service Plan provider to live under a namespace containing the words ‘app’, ‘service’ and ‘plan’ – unfortunately, the namespaces often don’t quite match up to the resource’s name. In this case, it’s under the Web namespace and is called serverfarms. 

A similar scenario can be observed on the appService resource as well, in that case, the namespace is Microsoft.Web/sites. At least for consistency’s sake, the reference inside the App Service to the App Service Plan is done through a property called serverFarmId. Unfortunately, the IntelliSense of the Bicep extension isn’t so helpful for cases like this:

IntelliSense of Bicep Extension


Applying .bicep files can be done in a similar fashion as ARM templates – my preferred way of doing so is through the Azure CLI’s az deployment command. Using Bicep’s what-if option, we can run the following, to get a sense of what changes will be applied in our Azure resource group, without actually executing the deployment:

 
az deployment group what-if \
  --name DemoDeployment1 \
  --resource-group myResourceGroupName \
  --template-file test.bicep 


The output of the command looks similar enough to Terraform’s terraform plan command’s output:

 
Resource and property changes are indicated with these symbols:
  + Create
  * Ignore

The deployment will update the following scope:

Scope: /subscriptions/:subscriptionId/resourceGroups/:resourceGroupId

  + Microsoft.Web/serverfarms/bicep-demo1-asp [2020-06-01]

      apiVersion:               "2020-06-01"
      id:                       "/subscriptions/:subscriptionId/resourceGroups/:resourceGroupId/providers/Microsoft.Web/serverfarms/bicep-demo1-asp"
      location:                 "westeurope"
      name:                     "bicep-demo1-asp"
      sku.capacity:             1
      sku.name:                 "F1"
      type:                     "Microsoft.Web/serverfarms"

  + Microsoft.Web/sites/bicep-demo1-app [2020-06-01]

      apiVersion:               "2020-06-01"
      id:                       "/subscriptions/:subscriptionId/resourceGroups/:resourceGroupId/providers/Microsoft.Web/sites/bicep-demo1-app"
      identity:                 "*******"
      location:                 "westeurope"
      name:                     "bicep-demo1-app"
      properties.httpsOnly:     true
      properties.serverFarmId:  "/subscriptions/:subscriptionId/resourceGroups/:resourceGroupId/providers/Microsoft.Web/serverfarms/bicep-demo1-asp"
      properties.siteConfig:    "*******"
      type:                     "Microsoft.Web/sites"


By removing the what-if part of the command, the deployment goes through. Afterward, if we were to change a configuration (eg. the Free tier to a Standard tier of the App Service Plan), the what-if command generates the following:

 
Resource and property changes are indicated with these symbols:
  + Create
  ~ Modify
  * Ignore

The deployment will update the following scope:

Scope: /subscriptions/:subscriptionId/resourceGroups/:resourceGroupId

  ~ Microsoft.Web/serverfarms/bicep-demo1-asp [2020-06-01]
    ~ sku.capacity: 0 => 1
    ~ sku.name:     "F1" => "S1"

  ~ Microsoft.Web/sites/bicep-demo1-app [2020-06-01]
    + properties.siteConfig.localMySqlEnabled:   false
    + properties.siteConfig.minTlsVersion:       "1.2"
    + properties.siteConfig.netFrameworkVersion: "v4.6"


Since Bicep does not track the state locally, if we were to change to name of the App Service after the deployment, the original App Service would still stay online, and a second one provisioned with the new name – later on, we’ll see how in Terraform, this would generate a “1 delete, 1 create” plan, instead of a “1 create” only.

Provision an App Service: Terraform

An App Service Plan and App Service, with the same variables and parameter extractions, would look like so, as a Terraform file:

 
variable "skuTier" {
  type    = string
  default = "Free"
}

variable "skuSize" {
  type    = string
  default = "F1"
}

variable "location" {
  type    = string
  default = "westeurope"
}

variable "resource_group_name" {
  type    = string
}

locals {
  appServicePlanName = "bicep-demo1-asp"
  webSiteName        = "bicep-demo1-app"
}

resource "azurerm_app_service_plan" "AppServicePlan" {
    name = local.appServicePlanName
    location = var.location
    resource_group_name = var.resource_group_name
    kind = "app"
  
    sku {
      size = var.skuSize
      tier = var.skuTier
    }
}

resource "azurerm_app_service" "AppService" {
  depends_on = [
    azurerm_app_service_plan.AppServicePlan]
  name                = local.webSiteName
  location            = var.location
  resource_group_name = var.resource_group_name
  app_service_plan_id = azurerm_app_service_plan.AppServicePlan.id
  https_only          = true

  site_config {
    min_tls_version      = "1.2"
  }

  identity {
    type = "SystemAssigned"
  }
}


Terraform’s way of defining variables is a bit more verbose than Bicep’s, however, in a real-world project, probably the input parameters would be extracted into variables.tf file, while the output variables into output.tf, such as:

 
app_service_plan
→ main.tf
→ variables.tf
→ output.tf


We can see the providers’ names are a bit easier to guess. To mimic Bicep’s what-if command, we can use terraform plan:

 

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_app_service.AppService will be created
  + resource "azurerm_app_service" "AppService" {
      + app_service_plan_id               = (known after apply)
      + app_settings                      = (known after apply)
      + location                          = "westeurope"
      + name                              = "bicep-demo1-app"

      + auth_settings {
          + additional_login_params        = (known after apply)
          + allowed_external_redirect_urls = (known after apply)

          + active_directory { ... }
          + facebook { ... }
          + google { ... }
          + microsoft { ... }
          + twitter { ... }

          ...
        }

      + identity {
          + principal_id = (known after apply)
          + tenant_id    = (known after apply)
          + type         = "SystemAssigned"
        }

      + site_config {
          + dotnet_framework_version             = "v4.0"
          + min_tls_version                      = "1.2"
          ...
        }
    
       ...
    }

  # azurerm_app_service_plan.AppServicePlan will be created
  + resource "azurerm_app_service_plan" "AppServicePlan" {
      + id                           = (known after apply)
      + kind                         = "app"
      + location                     = "westeurope"
      + maximum_elastic_worker_count = (known after apply)
      + maximum_number_of_workers    = (known after apply)
      + name                         = "bicep-demo1-asp"

      + sku {
          + capacity = (known after apply)
          + size     = "F1"
          + tier     = "Free"
        }
    }

Plan: 2 to add, 0 to change, 0 to destroy.


With terraform apply, we can execute the changes and see the two resources get provisioned in our resource group.

Since Terraform does manage the state locally, if we were to change the name of the App Service, instead of leaving the current one as-is and trying to only create a new one, Terraform shows the following, during terraform plan:

 
Plan: 1 to add, 0 to change, 1 to destroy.


Conclusion

After spending a few days researching Bicep, reading the documentation, and watching demonstrations, I feel like as of today, it’d be a strong contender for people who are familiar with ARM already, but maybe not with other IaC tools, such as Terraform. It definitely feels easier to use than writing pure ARM templates, however, I probably won’t pitch the idea to my team to rewrite our Terraform code to Bicep.

Given that it’s still early days for the tool, the usual challenges that come with new tech are also present: watching a six-month-old presentation today, the commands might have changed a bit in just that short amount of time.

The documentation for Bicep is easy to follow and fairly detailed, however, it doesn’t have as complete of a Reference section as Terraform does – the best place I could find was the Bicep Playground, where sample templates can be found.

 

 

 

 

Top