A controversial and long-running debate in the cloud ecosystem centers around three heavyweights: ARM templates, Bicep, and Terraform. Which one is better? More importantly, which one should you choose?
From drift detection to the way they make their calls to Azure, all three tools have their quirks. Their comparison has become a focal point within architectural discussions, especially as Terraform’s popularity within the Azure landscape has surged. This article provides a technical comparison and a clear guidance to help you decide which tool best fits your needs, whether for personal projects or enterprise-scale organization-wide deployments.
Azure REST API call path
The most fundamental difference between Terraform vs. Bicep/ARM is the way they create resources. Below is a simplified depiction of how a resource creation request is handled by these three tools.

When you execute a deployment through Bicep or ARM, your deployment request first arrives at the ARM Engine. At this stage, your ARM functions (or Bicep functions) and other deployment-related pre-processes such as deployment order resolution (based on the resource dependencies) are handled and your deployment request is put into its ultimate form. Then ARM Engine sends this ultimate deployment request to corresponding Resource Providers. On the other hand, Terraform handles these pre-processes at the client side (on your laptop or on a CI/CD runner) and then directly sends your request to Resource Providers. This has an implication on the deployments’ Resiliency to Disconnection. If your internet connection drops mid-deployment while using Bicep or ARM, the deployment will still complete successfully because the ARM Engine is managing it on Azure's infrastructure. With Terraform, because your local machine is driving the step-by-step execution, losing your connection or interrupting the process will halt the deployment entirely.
Note: The ARM Engine is also exposed by the Azure REST API (via the Microsoft.Resources/deployments Resource Provider). ARM and Bicep therefore also send their initial request to the Azure REST API, but for the sake of brevity the above depiction doesn’t mention that.
Deployment logs
A more significant impact of these differing deployment patterns is how logs are presented within the Azure Portal.
A very handy feature of the ARM Engine is that it automatically structures deployment logs from the underlying Activity Logs. It then presents them cleanly under the Deployments tab of your Resource Group or Subscription, alongside the exact template used for the deployment (as shown below).


Because Terraform bypasses the ARM Engine and pushes resource creations directly to individual Resource Providers, Azure's Deployments tab remains entirely empty for Terraform managed infrastructure. This introduces a notable troubleshooting hurdle for Terraform. If a resource creation fails, nothing appears in the Deployments tab. While the error will be caught in your local Terraform CLI output or CI/CD runner logs, looking at the Azure Portal will only show individual, disconnected events scattered across the Activity Log. If you want to see why some of your previous deployments failed, you have to sift through these raw logs and map which API failure corresponds to which part of your deployment, as Azure has no awareness of Terraform deployments.
Folder Structure
Declarative Infrastructure as Code in Azure began with ARM templates. Years later, Bicep was introduced to solve many of its predecessor's design flaws.
A major complaint regarding ARM templates has been the way it handled modularization. To break a large ARM template down into smaller, reusable components, you had to use linked templates. Crucially, the ARM Engine required these linked templates to be hosted in an internet-reachable location such as an Azure Storage Account blob or a public GitHub repository. Operationally, this meant you couldn't just deploy from your local machine. You first had to upload your nested templates to storage, generate secure URLs (often requiring SAS tokens), pass those URLs into your parent template, and then trigger the deployment of your parent template. This unnecessary complexity is a primary reason why ARM templates have been deemed frustrating and overly complicated.
Both Bicep and Terraform completely eliminated this friction. They allow you to structure your code locally using native folders and modules. All you need to do is reference a local folder path to call a module. The tooling handles the rest behind the scenes during compilation or planning.
Linked Template in ARM

Vs.
Bicep Modules

Terraform Modules

Note on Template Specs: Azure later introduced Template Specs to alleviate the ARM template storage issue by letting users store templates natively as Azure resources. However, this still failed to fix the developer experience regarding modularization. With Bicep and Terraform, any change to a module can be made directly in the local file and tested instantly. With Template Specs, every single tweak requires you to republish the spec to Azure first, reintroducing the exact same slow, multi-step feedback loop found in the old ARM method where you upload your linked templates to storage account first.
Resource Structure
The structure of resource configurations in Azure is dictated by the Resource Providers and can be quite complex for several resources such as Application Gateway.
Because ARM templates, Bicep, and the Terraform AzAPI provider are all maintained directly by Microsoft, they strictly follow the official Resource Provider schemas. The Terraform AzureRM provider, however, takes a completely different architectural approach. The maintainers of AzureRm thought, it would be easier to read, if they had simplified the structure of the JSON published by the Azure Resource Providers. Below is a comparison of how ARM, Bicep, AzAPI, and AzureRM specify the backendAddressPools of an Application Gateway:

Bicep / ARM / Terraform AzAPI

Terraform AzureRM
To achieve brevity, AzureRM often strips out nested layers from the resource configuration. For example, on the Application Gateway configuration shown above, AzureRM got rid of the properties object and the backendAddresseses array for the sake of brevity. .
While engineers who are new to the Azure ecosystem often appreciate this clean, simplified syntax, it comes with a major caveat: users must learn a secondary, provider-specific schema and understand how AzureRM fields map back to the actual Azure Resource Provider specifications. Such structural deviation between your IaC code and platform's native API structure introduces an extra layer of cognitive load which will have direct impact on the troubleshooting complexity. For this reason, many experienced cloud architects view AzureRM's structural deviation not as a simplification, but as a potential source of confusion and an unnecessary layer of translation.
API version coverage
As ARM, Bicep and AzAPI are all maintained by Azure, any change in a Resource Configuration, such as addition of a new property, is directly implemented by these three through a new API version publication. Moreover, if there is a new resource addition to Azure (which nowadays happens rather rarely compared to previous years), this is also directly available through new APIs.
AzureRM lacks such support and you will also find that only one API version is associated with AzureRM. For example, the latest AzureRM version (v4.75.0) uses the API version of 2025-01-01 for Application Gateway. You don't have the option to choose a different Application Gateway API version than the one that is associated with that AzureRM release you use. Conversely, ARM templates, Bicep, and AzAPI give you ultimate flexibility. They allow you to mix different API versions side-by-side within the exact same deployment file, granting immediate access to the entire depth of the Azure platform.
Resource coverage
Just like full API versions coverage, ARM, Bicep and AzAPI cover all the resources that are available in Azure. AzureRM lacks this support as well. For example, the Data Masking Policy which is a child resource of Azure SQL Database is not available in AzureRM at all. If your responsibilities require you to operate beyond standard, basic use cases and delve into advanced enterprise architectures, it is highly likely you will eventually hit this exact resource coverage blocker when using AzureRM.
