farroar

Bicep Vs. Terraform

I’m a total Terraform fan. I use it as much as I possibly can. However, I’m using Bicep more and more recently. Why?


A little context

First, let’s review what we’re talking about here:

Both Bicep and Terraform are Infrastructure as Code tooling designed to help get the deployment job done without having to fuss with the UI.

  • Bicep is dedicated to Azure while Terraform uses providers to allow integration with all sorts of third-party services, tools, platforms, and equipment.
  • Bicep is Azure’s “Terraform” in a way. It is an abstraction of the standard ARM Template framework. It is an ARM Domain Specific Language.
  • Bicep is ARM dressed up in a fancy suit with a better personality.
  • Both consider the state of the environment and are idempotent. However Terraform maintains state in its own way and allows you to make adjustments to the state file easily. Bicep is iffy in the idempotency department.

It’s interesting to note that in 2017 Microsoft and HashiCorp (Developers of Terraform) entered into a multi-year partnership to help facilitate integration of Microsoft services into the Terraform ecosystem. One might ask, why was Bicep even created and why is it so loved by some?

Why does Bicep even exist?

ARM templates are the building blocks of the entire Azure platform. A lot of users and organizations have libraries of ARM templates and pipelines built to facilitate it. Microsoft can’t just get rid of so It’s not going anywhere any time soon.

ARM is a pain in a lot of ways. It is super wordy and it can take some to get used to. It’s a great example of a very efficient way to represent infrastructure for machines but not so much for us meat sacks. Bicep was created to make it better and it really has. Authoring code with Bicep is so much better and far easier to work with. They’ve introduced some nifty features to make it a lot easier to do repetitive tasks and mess with data.

A notable limitation of ARM

The downside is that all of the limitations of Bicep are inherited from ARM since it is ARM. 

A great example of limitations inherited is one that has been haunting me since 2017 and is represented in this (still open) issue  https://github.com/Azure/azure-quickstart-templates/issues/2786.

Basically, when you build a VNET you’re going to also need some subnets. You go ahead and build all that up with an ARM template and send it off to the API machines… Boom, it’s built. Great right?!

It’s great but let’s say down the line you decide you need another subnet in this same VNet. Naturally you’d go to your template and add that new subnet right? Sure, but this is when you run into this monster of an issue (IMO). Azure will not just add this subnet, it’ll try to recreate the entire VNet, which means it’ll try to delete everything first. Azure won’t let you kick the chair (subnet) out from under your workloads, so this will fail. Unfortunately, there is no straightforward workaround for this outside of never changing subnets which isn’t very reasonable. Bicep of course has inherited this ARM issue. This is when I remind myself that ARM was designed to be a templating language… I’m probably asking too much.

Bicep has a bit of trouble with logic

Let’s say you have this block of code:

resource network_security_groups 'Microsoft.Network/networkSecurityGroups@2021-05-01' = [for sn in subnets: if (sn.nsg) {
  name: '${sn.name}-nsg'
  location: location
  properties: {
    securityRules: [for rule in sn.security_group_rules: {
      name: rule.name
      properties: {
        priority: rule.properties.priority
        access: rule.properties.access
        protocol: rule.properties.protocol
        direction: rule.properties.direction
        sourcePortRange: rule.properties.sourcePortRange
        destinationPortRange: (contains(rule.properties, 'destinationPortRange')) ? rule.properties.destinationPortRange : null
        destinationPortRanges: (contains(rule.properties, 'destinationPortRanges')) ? rule.properties.destinationPortRanges : null
        sourceAddressPrefix: rule.properties.sourceAddressPrefix
        destinationAddressPrefix: rule.properties.destinationAddressPrefix
      }
    }]
  }
}]

This is a bit busy, but focus on the “if (sn.nsg)” part at the top. Basically, if I set a variable called “nsg” to true, then I want this block of code to execute. If it isn’t true, then I’d want it to ignore this entire block. Basic logic here.

If I don’t want the NSG, then I’m not going to declare the rules listed in the code block, right?

You want some conditional deployment here. If the first block is true, you plan on deploying some VM. If it is false, you’re not going to. Simple. The issue comes to light when you go to deploy this. You might find an error like this:


{"error":{"code":"InvalidTemplate","message":"Deployment template validation failed: 'The template resource '[format('{0}-nsg', parameters('subnets')[copyIndex()].name)]' at line '1' and column '5372' is not valid: The language expression property 'security_group_rules' doesn't exist, available properties are 'name, prefix, route_table, nsg'.. Please see https://aka.ms/arm-template-expressions for usage details.'.","additionalInfo":[{"type":"TemplateViolation","info":{"lineNumber":1,"linePosition":5372,"path":"properties.template.resources[1]"}}]}}

Bicep’s interpreter isn’t very smart

The issue is that Bicep is evaluating ALL of the code BEFORE it even attempts to deploy it. In particular, it will evaluate the code without considering the conditional statements and what that means for variable usage. So, if we just omit NSG rules since we don’t plan on deploying them Bicep will error out and tell you that you need to declare all of those variables inside the block that won’t actually run.

Not really a big deal, but it ends up requiring empty variable blocks that take up space.

This is an interpreter issue, not a code issue. So you need to work around it with logic. Generally include empty variables. This issue is outlined here: https://github.com/Azure/bicep/issues/1410 and hopefully they’ll add some smarts to it.

Terraform doesn’t have these issues since it doesn’t rely on the ARM interpreter. It has its’ own way of working with the various Azure APIs which allows it to be a little smarter.

So, bottom line. Bicep is born from a templating language and Terraform was created to solve issues that ARM/Bicep (and CFT for those AWS folks) didn’t have in mind. The Terraform community is huge and growing and the learning curve is fairly low.

Why would we even think of using Bicep over Terraform?

Well:

  • Since Bicep uses ARM, it will always be first to realize any new changes or features released in Azure APIs. Terraform will lag on integrations but not by much (see previous Microsoft / Hashicorp partnership). If you need to be able to use preview features often, Bicep is the place to be.
  • If you feel the need to live entirely in PowerShell, Bicep doesn’t require any standalone binaries – it’s an easily upgradable PowerShell module and it can be deployed directly via the Azure CLI.
  • You already have a process built around ARM to the point where changing would be exceptionally painful.

In the top I said that I’m using it more now… Why? Well, I work with teams that are starting their cloud journey and introducing a new non-native process or tool isn’t always a good idea or even received well. Also, I want to be able to empower clients to be closer to taking the next step with code when they are ready. Bicep can be the next logical step for a lot of organizations.

If I were to recommend a tool, it would be Terraform hands down. But Bicep can really fit the role nicely as long as you don’t mind it’s limitations and only plan on IaC within Azure. The community backing it is very active and it’ll only get better over time.