Stop using ARM templates! Use the Azure CLI instead

I was a big fan of ARM templates: for many years I’m applying ARM templates on a large number of projects for all kinds of customers. I’ve written articles and blog posts about ARM templates. Have given many workshops and started collecting ARM templates used in enterprises ready for production.  I’ve written the Best practices with ARM Templates article together with my colleague Peter Groenewegen, which is the most visited blog post of Xpirit and it’s also published by Microsoft. It’s clear I was a big fan of ARM templates. But times are changing.

Actually, I wasn’t really a big fan of ARM templates specifically, but I’m a fan of Continuous Delivery, DevOps and Infrastructure as Code on and to Azure. That’s really powerful and ARM Templates are the way to accomplish a part of these practices. It was the best way to provision resources on Azure.

Creating ARM templates is hard. They have a high learning curve and quickly become large and complex, especially with the deployment of many Azure Resources. Deployment with linked templates is not easy and the more I use them, the more I’m missing simple programming language possibilities like iterations. When using ARM templates I regularly need to iterate over lists of input data for example. Like IP-Addresses which needs to be whitelisted. Creating and reading ARM templates needs a lot of practice. Most of the time not everybody on the team is able to do this.

For the latest projects, I have not used ARM templates anymore. Instead, I started using the Azure CLI for the provisioning of resources and I love it!

AzureCLIart

Sample

Let’s take a look at an example to make clear why I like the short and powerful syntax of the Azure CLI instead of ARM templates to accomplish the same goal.

Take a look at the resources provisioned by this ARM template:

{
  "$schema""https://schema.management.azure.com/schemas/2018-05-01/deploymentTemplate.json#",
  "contentVersion""1.0.0.0",
  "parameters": {
    "resourcegroupName": {
      "type""string"
    },
    "resourcegroupLocation": {
      "type""string"
    },
    "webSiteName": {
      "type""string",
      "minLength": 1
    },
    "sqlserverName": {
      "type""string",
      "minLength": 1
    },
    "hostingPlanName": {
      "type""string",
      "minLength": 1
    },
    "skuName": {
      "type""string",
      "defaultValue""F1",
      "allowedValues": [
        "F1",
        "D1",
        "B1",
        "B2",
        "B3",
        "S1",
        "S2",
        "S3",
        "P1",
        "P2",
        "P3",
        "P4"
      ],
      "metadata": {
        "description""Describes plan's pricing tier and instance size. Check details at https://azure.microsoft.com/en-us/pricing/details/app-service/"
      }
    },
    "skuCapacity": {
      "type""int",
      "defaultValue": 1,
      "minValue": 1,
      "metadata": {
        "description""Describes plan's instance count"
      }
    },
    "administratorLogin": {
      "type""string"
    },
    "administratorLoginPassword": {
      "type""securestring"
    },
    "databaseName": {
      "type""string"
    },
    "collation": {
      "type""string",
      "defaultValue""SQL_Latin1_General_CP1_CI_AS"
    },
    "edition": {
      "type""string",
      "defaultValue""Basic",
      "allowedValues": [
        "Basic",
        "Standard",
        "Premium"
      ]
    },
    "maxSizeBytes": {
      "type""string",
      "defaultValue""1073741824"
    },
    "requestedServiceObjectiveName": {
      "type""string",
      "defaultValue""Basic",
      "allowedValues": [
        "Basic",
        "S0",
        "S1",
        "S2",
        "P1",
        "P2",
        "P3"
      ],
      "metadata": {
        "description""Describes the performance level for Edition"
      }
    }
  },
  "variables": {
  },
  "resources": [
    {
      "type""Microsoft.Resources/resourceGroups",
      "apiVersion""2018-05-01",
      "location""[parameters('resourcegroupLocation')]",
      "name""[parameters('resourcegroupName')]",
      "properties": {}
    },
    {
      "name""[parameters('sqlserverName')]",
      "type""Microsoft.Sql/servers",
      "location""[resourceGroup().location]",
      "tags": {
        "displayName""SqlServer"
      },
      "apiVersion""2014-04-01-preview",
      "dependsOn": [
        "[resourceId('Microsoft.Resources/resourceGroups/', parameters('resourcegroupName'))]"
      ],
      "properties": {
        "administratorLogin""[parameters('administratorLogin')]",
        "administratorLoginPassword""[parameters('administratorLoginPassword')]"
      },
      "resources": [
        {
          "name""[parameters('databaseName')]",
          "type""databases",
          "location""[resourceGroup().location]",
          "tags": {
            "displayName""Database"
          },
          "apiVersion""2014-04-01-preview",
          "dependsOn": [
            "[resourceId('Microsoft.Sql/servers/', parameters('sqlserverName'))]"
          ],
          "properties": {
            "edition""[parameters('edition')]",
            "collation""[parameters('collation')]",
            "maxSizeBytes""[parameters('maxSizeBytes')]",
            "requestedServiceObjectiveName""[parameters('requestedServiceObjectiveName')]"
          }
        },
        {
          "type""firewallrules",
          "apiVersion""2014-04-01-preview",
          "dependsOn": [
            "[resourceId('Microsoft.Sql/servers/', parameters('sqlserverName'))]"
          ],
          "location""[resourceGroup().location]",
          "name""AllowAllWindowsAzureIps",
          "properties": {
            "endIpAddress""0.0.0.0",
            "startIpAddress""0.0.0.0"
          }
        }
      ]
    },
    {
      "apiVersion""2015-08-01",
      "name""[parameters('hostingPlanName')]",
      "type""Microsoft.Web/serverfarms",
      "location""[resourceGroup().location]",
      "tags": {
        "displayName""HostingPlan"
      },
      "dependsOn": [
        "[resourceId('Microsoft.Resources/resourceGroups/', parameters('resourcegroupName'))]"
      ],
      "sku": {
        "name""[parameters('skuName')]",
        "capacity""[parameters('skuCapacity')]"
      },
      "properties": {
        "name""[parameters('hostingPlanName')]"
      }
    },
    {
      "apiVersion""2015-08-01",
      "name""[parameters('webSiteName')]",
      "type""Microsoft.Web/sites",
      "location""[resourceGroup().location]",
      "dependsOn": [
        "[resourceId('Microsoft.Web/serverFarms/', parameters('hostingPlanName'))]"
      ],
      "tags": {
        "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]""empty",
        "displayName""Website"
      },
      "properties": {
        "name""[parameters('webSiteName')]",
        "serverFarmId""[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]"
      },
      "resources": [
        {
          "apiVersion""2015-08-01",
          "type""config",
          "name""connectionstrings",
          "dependsOn": [
            "[resourceId('Microsoft.Web/Sites/', parameters('webSiteName'))]"
          ],
          "properties": {
            "DefaultConnection": {
              "value""[concat('Data Source=tcp:', reference(resourceId('Microsoft.Sql/servers/', parameters('sqlserverName'))).fullyQualifiedDomainName, ',1433;Initial Catalog=', parameters('databaseName'), ';User Id=', parameters('administratorLogin'), '@', parameters('sqlserverName'), ';Password=', parameters('administratorLoginPassword'), ';')]",
              "type""SQLServer"
            }
          }
        }
      ]
    }
  ]
}

And here is the Azure CLI script:

az group create --name $RESOURCEGROUP_NAME --location $LOCATION

az sql server create --name $SQL_SERVER_NAME --location $LOCATION --resource-group $RESOURCEGROUP_NAME --admin-user $SQL_ADMIN_USERNAME --admin-password $SQL_ADMIN_PASSWORD
az sql server firewall-rule create --resource-group $RESOURCEGROUP_NAME --server $SQL_SERVER_NAME -n AllowAllWindowsAzureIps --start-ip-address 0.0.0.0 --end-ip-address 0.0.0.0
az sql db create --resource-group $RESOURCEGROUP_NAME --server $SQL_SERVER_NAME --name $SQL_DATABASE_NAME --edition $SQL_EDITION --collation $SQL_COLLATION --max-size $SQL_MAXSIZE --service-objective $SQL_SERVICEOBJECTIVE

az appservice plan create --name $SERVICEPLAN_NAME --resource-group $RESOURCEGROUP_NAME --sku $SERVICEPLAN_SKU
az webapp create --name $WEBAPP_NAME --resource-group $RESOURCEGROUP_NAME --plan $SERVICEPLAN_NAME

SQL_CONNECTIONSTRING="Data Source=tcp:'$(az sql server show -g $RESOURCEGROUP_NAME -n $SQL_SERVER_NAME --query fullyQualifiedDomainName -o tsv)',1433;Initial Catalog='$SQL_DATABASE_NAME';User Id='$SQL_ADMIN_USERNAME';Password='$SQL_ADMIN_PASSWORD';"
az webapp config connection-string set --resource-group $RESOURCEGROUP_NAME --name $WEBAPP_NAME --connection-string-type SQLServer --settings DefaultConnection="$SQL_CONNECTIONSTRING"

I’ve chosen for single line commands. It’s also possible to specify each parameter on a separate line if you wish. Another optimization could be to use the abbreviation for parameter names instead of the full name (e.g. -g instead of –resourcegroup) which makes the line shorter. Because the syntax of the Azure CLI is descriptive, I can easily interpret the resources which are being provisioned.

I have a question for you: did the ARM template contains the resource to provision a WebApp slot? You probably haven’t seen that the WebApp slot is part of the ARM Template. That’s because you have to interpret the ARM template instead of having a look at it and this costs time and experience.

If you show the CLI script to your team-mate and sees it for the first time, your team-mate can tell you right away the correct answer to the question. CLI scripts are easy to read and so much clearer. All my team members are able to create these kinds of scripts.

Myths

Myth 1: It’s so easy to create ARM templates. You can just create the needed resources in the Azure Portal and click on Automation Script.

Of course, you do get an ARM template which can be executed successfully. But I was never able to use this ARM template to provision the resources on a maintainable way and were able to configure the template so I could use it in an Enterprise scenario to deploy to multiple environments. Changing the generated ARM templates is hard. In practice, I always need to connect ARM templates to use the output value as input in another template. In the case of ConnectionStrings for example.

Myth 2: I really need a declarative way to provision my resources

Let’s see what declarative actually means: solve problems without requiring the programmer to specify an exact procedure to be followed.

By using ARM templates you can configure a DependsOn so the Resource Manager knows in which sequence to execute the creation of resources. If you don’t use the DependsOn, but configure the output of one resource in another, the Resource Manager is smart enough to know in which sequence the resources should be provisioned.

You have to create a bash script or Powershell script to execute multiple Azure CLI commands. So this is not declarative. You have to think about the sequence of provisioning yourself. Is this a real disadvantage of the Azure CLI?. In my experience, it isn’t. When I create an ARM template I know in which sequence the Resource Manager will execute the provisioning of the resources. In my CLI scripts, it’s explicit now which makes the CLI script more readable and maintainable. Besides being explicit there are also other advantages: It’s much easier to use the output from one resource and use it in another, like connectionstrings. And now I’m also able to use simple programming techniques like iterations.

I’ve only used the create command in my CLI scripts. I don’t need to check if a resource already exists and if it exists apply an update command (=iterative). The create command can also update settings. It can only update settings which can be updated with the update command, which makes sense. So for example on the SQL Azure database it can update the edition and Service Objective. But it can’t update the collation. Just like with ARM templates.

So the Azure CLI is declarative for the creation and updating a resource. The sequence of provisioning isn’t declarative. But I don’t experience this as a downside.

Myth 3: The Azure CLI is not idempotent

The most important factor in the creation of resources, in my opinion, is idempotency.  Idempotent means: make that same call repeatedly while producing the same result. Both an ARM template and the Azure CLI are idempotent with the creation of resources. It’s even specified in the command design guides of the Azure CLI:

  • CREATE – standard command to create a new resource. Usually backed server-side by a PUT request. ‘create’ commands should be idempotent and should return the resource that was created.

To be concrete: You are able to execute a command as many times you want, the result is the same every time. You won’t get an error that the resource already exists.

Disadvantages

Of course, the usage of the Azure CLI to provision resources is not a silver bullet. There are also disadvantages to applying the Azure CLI compared to ARM templates.

ARM templates can be executed in Complete deployment mode, which means only the resources and configuration in the ARM template is the truth and applied on Azure. With the Azure CLI you have to accomplish this yourself. Although this is a best practice, the fact is that this mode is hardly applied when using ARM templates. One of the reasons for this is that the Complete mode cannot be applied when linked ARM templates are being used. Another reason is that you can’t use the Complete mode when you want to apply multiple deployments for the same resourcegroup.

When you deploy an ARM Template, this deployment is visible on the ResourceGroup.  The deployment shows the current status. For example, this status is visible in the Azure Portal during the run of a deployment. But also after the deployment, the result is visible in the Portal. This is not the case for executed CLI commands. Although there are exceptions. When you provision vm, vm availability-set or network application-gateway through the Azure CLI, an ARM template is executed and because of this, you do get deployment information. (If you pass parameter –validate to az create of vm, vm availability-set or network application-gateway the ARM template which would be executed can be seen.)

ARM templates are platform independent and the Azure CLI is platform independent also. To provision resources, you probably need to create a script file. When you use platform-specific syntax, like the way I read variables in the sample script above (like $RESOURCEGROUP_NAME), this script file can only be executed on the target platform. In my case on Linux. Now with the move to containers and .NET Core. This is not a problem for me, the tools (Azure DevOps) and the projects I’m working on, but it’s a result to keep in mind.

The CLI supports a lot of resources you can provision in Azure. But not every resource. So for some resources, I had to use an ARM template. A sample for this is Application Insights. To deploy the ARM template the Azure CLI can be used.

Conclusion

The title of this blogpost is a bit provocative and is chosen to challenge you on the way you provision your resources on Azure. Are you still applying the best and easiest way to to do this when you use ARM templates? This blogpost tells you about my experience and shows you there is a really good alternative.

After weighing all the advantages and disadvantages, for me the points that make the difference to use the Azure CLI in favor of ARM Templates are:

  • Better maintainability
  • Easy to read
  • Easy and much faster to create
  • More in control
  • Doable by all team members

Only when the Azure CLI does not support a resource, and I expect this is only temporary, I still use an ARM template. In case of provisioning Application Insights for example.

After my recommendation, colleagues are provisioning resources with the Azure CLI now also and are very enthusiastic about it, just like me. Try it out with your provisioning and maybe you have to reconsider your standard way to provision resources to Azure.

18 gedachtes over “Stop using ARM templates! Use the Azure CLI instead

    • If you mean the Azure RM powershell commands: Unfortunately the PowerShell commands are not idempotent. This results in imperative code which I really don’t like. You will get an exception when you execute the creation of a webapp with New-AzureRmWebApp twice.

      Another reason, for me, is that I’m working on projects where only Linux is used.

      Like

  1. I think you are right. Funny thing is though when i started off, i was creating resources using cli cause i was using Ubuntu. This evolved into using ARM Templates to achieve more granularity when creating resources, like for instance creating more managed disks etc. Of course this can also be done with cli…. I think i’m back where i started!

    Geliked door 1 persoon

  2. There’s a couple of things to consider in favor of templates:

    * Templates can be validated. You’re able to run Test-AzureRmResourceGroupDeployment to check your template for potential errors, which is a check you can also include in your workflow / pipeline. You might be able to do a similar thing for the CLI commands but it’ll require more work and it’ll be not much more than a syntax check. Or you’ll need to deploy your entire solution as a test to a temporary set-up and clean it up again before deploying to your actual environment, but the same would apply for cmdlets or templates. That said, we all know that the testing of the templates leaves a lot to desire (but it does catches errors for us every now and then).
    * When deploying based on a template, we have the option to rollback in case something within the template fails. This is functionality you do not get when firing off individual CLI commands, so you’ll need to script your way around it if you want to (which will get quite painful quite fast). Again, the rollback option might not be 100%, but at least you have the option to use it when opting for templates. Refer to https://docs.microsoft.com/bs-cyrl-ba/azure/////azure-resource-manager/resource-group-template-deploy-cli?toc=%2Fazure%2Fvirtual-network%2Ftoc.json#redeploy-when-deployment-fails

    I’m not going so far as to say I don’t agree with you, but I do feel these points need to be on the pro/con list if you want to do a complete comparison.

    Geliked door 1 persoon

    • Validation of ARM Templates: Can be done. The only thing it ensures is the validity of the json syntax. The other functionality is of no use. Because of this there is no other option to test ARM templates by executing them and delete the provisioned resources after the test. Only then you are 100% sure the ARM template works. So I do not see the added value of including ARM validation in the list.

      Rollback: To be clear, rollback functionality doesn’t exists for ARM templates. There are no transactional deployments. You are able to redeploy the last or a specific named successful deployment when the current deployment fails. This only works well if you always configure deploymentmode Complete. In my blogpost I already explain that this deployment mode is seldomly applied. Actually this rollback feature is a second deployment when the first one fails. This can be done with any technique or tool for deployment. Note that the –rollback-on-error can only be used with the Azure CLI 🙂
      So I don’t see any value of adding a redeploy to my list to compare ARM templates and the Azure CLI

      Like

      • I agree with what you’re saying, but I don’t agree to the fact that this shouldn’t way in on decision making time. Validation is not perfect, but it does validate more than just the json syntax. It depends on which resources you’ve got in your template, but it’ll check dependencies and sometimes also the values of the parameters being set. I suspect the individual product teams are responsible for implementing the validation routines and not all of them do it up to where we would want it to be. So no, you’ll won’t catch all mistakes, but you’ll catch more than when you do not validate at all. A full deployment for validation purposes can be quite costly (timewise) for larger set-ups.

        Rollback; same story, definitely not ideal but it is there. For teams that do use Complete mode, or for teams who might be able to switch, this can still be an option. An automated revert to a template that worked previously might be quite nice if you deploy your stuff at 4 AM in the morning, instead of coming to work with a bunch of errors lined up and a half-working environment. I’m not saying you cannot do the same with CLI, but you would need to arrange it in your pipeline and it’ll most definitely be more work to get working.

        Geliked door 1 persoon

    • Hi Gleb,

      Great, you have found out for yourself, you’ll stick with ARM templates because that works best for you.
      As you have read in my conclusion, the goal of my blogpost is to make you rethink the way you provision resources on Azure and you were inspired by my blogpost to do just that. You were critical of yourself and weighed up what in most cases is the best technique to provision Azure resources for your situation.

      Like

  3. Any experience with updated Azure CLI or API versions? What I like about ARM templates is that you can configure every nitty gritty setting. And because you specify ARM API versions, the resources will always be configured exactly the same way.

    I’m wondering if the same goes with the Azure CLI. I can imagine that default values for specific settings sometime change when with new releases of the Azure CLI or the ARM API’s. If that is the case, the resources are not created exactly the same way.

    Any thoughts on this?

    Geliked door 1 persoon

  4. Pingback: AzureRM, Azure CLI and the PowerShell Az Module | tenbulls.co.uk

  5. I’ve been provisioning ARM templates through Azure Pipelines. I’ve also used Azure PowerShell tasks in Pipelines to create some resources. This post made me look for CLI support and found it. Have you run into any issues with it?

    Like

  6. Thats only taking care of provisioning portion, evolution of your infrastructure will require you to write a script per change process, where in ARM template or terraform its matter of changing property deploying changes and commiting your changes to git. Also you can heavily modularize and version your subcomponents/modules.

    Like

  7. Pingback: Collection of handy Azure CLI and Bash scripts | Pascal Naber

  8. Pingback: Power Apps och Logic Apps kommer inte helt ersätta utveckling i C# och .Net Core – Robert Forsström's Blogg

Plaats een reactie

Deze site gebruikt Akismet om spam te bestrijden. Ontdek hoe de data van je reactie verwerkt wordt.