Pascal Naber

Stop using ARM templates! Use the Azure CLI instead

Advertenties

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!

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:

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:

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.

Advertenties

Advertenties