Recommendations for using Azure CLI in your workflow
Azure CLI is widely used in GitHub Actions and Azure Pipelines, as well as many other CI/CD tools. Over the last few weeks, I've been looking into its performance and security and based on that here are a number of recommendations.
Reduce azure-cli chattiness
In its default configuration Azure CLI can be quite chatty, even accidentally echoing secrets to the console if you're not using it wisely. There are a number of settings you can apply to reduce the chattiness and by doing so automatically improve your security posture:
az config set core.only_show_errors=true
az config set core.error_recommendation=off
az config set core.collect_telemetry=false
az config set logging.enable_log_file=false
az config set core.survey_message=false
az config set auto-upgrade.enable=false
az config set core.no_color=true
az config set extension.use_dynamic_install=false
Most switches are explained in the azure cli configuration docs. There is a module called init
that you can use to configure a number of these settings with ease (not all unfortunately):
> az extension add --name init
> az init
Select an option by typing its number
[1] Optimize for interaction
These settings improve the output legibility and optimize for human interaction
[2] Optimize for automation
These settings optimize for machine efficiency
Unfortunately, the AzureCLI@2
task in Azure Pipelines, by default, ignores the global configuration (see below), an even better way to set these options is through environment variables:
AZURE_CORE_ONLY_SHOW_ERRORS=TRUE
AZURE_CORE_ERROR_RECOMMENDATION=FALSE
AZURE_CORE_COLLECT_TELEMETRY=FALSE
AZURE_LOGGING_ENABLE_LOG_FILE=FALSE
AZURE_CORE_SURVEY_MESSAGE=FALSE
AZURE_AUTO-UPGRADE_ENABLE=FALSE
AZURE_CORE_NO_COLOR=TRUE
AZURE_EXTENSION_USE_DYNAMIC_INSTALL=FALSE
For self-hosted runners/agents you can either set these variables in the VMs global environment settings, or in the runner/agent's .env
file:
Be sure to restart the agent afterwards.
Since workflow/pipeline variables are automatically lifted to environment variables, you can also define these in your workflow or in the repository settings:
# Azure Pipelines
variables:
AZURE_CORE_ONLY_SHOW_ERRORS: TRUE
AZURE_CORE_ERROR_RECOMMENDATION: FALSE
AZURE_CORE_COLLECT_TELEMETRY: FALSE
AZURE_LOGGING_ENABLE_LOG_FILE: FALSE
AZURE_CORE_SURVEY_MESSAGE: FALSE
AZURE_AUTO-UPGRADE_ENABLE: FALSE
AZURE_CORE_NO_COLOR: TRUE
AZURE_EXTENSION_USE_DYNAMIC_INSTALL: FALSE
# GitHub Actions
env:
AZURE_CORE_ONLY_SHOW_ERRORS: TRUE
AZURE_CORE_ERROR_RECOMMENDATION: FALSE
AZURE_CORE_COLLECT_TELEMETRY: FALSE
AZURE_LOGGING_ENABLE_LOG_FILE: FALSE
AZURE_CORE_SURVEY_MESSAGE: FALSE
AZURE_AUTO-UPGRADE_ENABLE: FALSE
AZURE_CORE_NO_COLOR: TRUE
AZURE_EXTENSION_USE_DYNAMIC_INSTALL: FALSE
For GitHub Actions, you can import the env file to an organization level variable library in a single command with the github cli:
> gh variable set --env-file .env --organization myorg
For Azure Pipelines, you can create a variable group and import that into every pipeline:
pwsh> az pipelines variable-group create --name azure-cli-default-settings --authorize --variables `
AZURE_CORE_ONLY_SHOW_ERRORS=TRUE `
AZURE_CORE_ERROR_RECOMMENDATION=FALSE `
AZURE_CORE_COLLECT_TELEMETRY=FALSE `
AZURE_LOGGING_ENABLE_LOG_FILE=FALSE `
AZURE_CORE_SURVEY_MESSAGE=FALSE `
AZURE_AUTO-UPGRADE_ENABLE=FALSE `
AZURE_CORE_NO_COLOR=TRUE `
AZURE_EXTENSION_USE_DYNAMIC_INSTALL=FALSE
For security capture the output
Some commands (such as reading appsettings) may return connection strings or passwords. You don't want these to end up in the logs. In addition to setting core.only_show_errors=true
, you can protect yourself further by redirecting the output from az
to a variable, null
or a file.
# capture the result in a variable
$output = & az ...
# redirect output
& az ... > $null
& az ... 2>&1 > $null # incl the error stream for extra protection
# write output to a file
& az ... -o ./output.json
& az ... --output ./output.json
Mind the case
While the Azure CLI accepts its commands and parameters in any case you use:
⚠️ Works, but don't do it
az CoNfIG SeT a=b
It greatly reduces the performance, especially on windows, due to the way the internal caching mechanism looks up the command implementations.
Instead, pass all the commands and switches in lowercase:
✅ use lowercase for all commands and switches
az config set a=b
This will save you precious time (about 10 seconds on Linux and up to a minute on Windows) every time you run az
.
Use the global configuration on Hosted Runners/Agents
The Hosted Runners/Agents take great care to set-up and warm-up the Azure CLI to improve its performance, especially the first-run performance. It does so by setting the AZURE_GLOBAL_CONFIG
environment variable to a folder that has been prepped during the virtual machine image creation.
Do not overwrite the AZURE_GLOBAL_CONFIG
variable in your own scripts.
However, on Azure Pipelines, the AzureCLI@2
task overwrites the AZURE_GLOBAL_CONFIG
variable and redirects it to the agent's temp directory.
You can add a switch to the task to turn off this behavior:
- task: AzureCLI@2
inputs:
useGlobalConfig: true
This was the default behavior in AzureCLI@1
.
Using the global configuration will reduce the time needed to set-up the task by more than 1 minute on the Windows Hosted Runner/Agent and by about 10 seconds on Linux.
You might wonder why it doesn't use global config by default. The reason for this is to support multiple agents on the same VM callingaz
at the same time. By redirecting the global config for each agent to its own temp directory they can't accidentally overwrite each other's settings or fight over file locks.
For self-hosted non-ephemeral runners/agents it also ensures that each job starts with a fresh set of settings as the temp folder is cleared before each run.
Since the hosted runners/agents don't run more than one job in parallel and always start with a fresh VM, there is no need to perform this redirection.
az devops
: Don't rely on auto-discover for speed
The Azure DevOps extension for Azure CLI offers an auto-discover option which uses the git repository's remote to automatically identify the Azure DevOps organization and project. While super convenient, it takes time to look up this information each time you run az devops
or a related command.
Instead, pass the --organization
and --project
settings explicitly:
- pwsh: |
az pipelines show --organization=$env:SYSTEM_COLLECTIONURI --project=$env:SYSTEM_TEAMPROJECT
Or set them once as defaults at the start of your workflow:
- pwsh: |
az devops configure --defaults organization=$env:SYSTEM_COLLECTIONURI project=$env:SYSTEM_TEAMPROJECT
az devops
: Don't use AzureCLI@2
, use standard shell instead
The AzureCLI@2
task does a number of setup steps, authenticates to Azure, redirects the global configuration folder, does an update-check... But if you only need to run az devops
commands, then you don't need any of that.
In that case you can use the standard scripting features of Azure Pipelines such as script:
and pwsh:
or - task: Bash@2
or - task: PowerShell@2
to gain a massive performance boost.
az devops
: Use environment variable for authentication in Azure Pipelines
When you're using the az devops
extension, you can authenticate in 3 ways:
az login
az devops login
- environment variable
Since Azure Pipelines already holds an authentication token in its environment, the fastest way to authenticate is to leverage that token:
- pwsh: |
az pipelines list --organization=$env:SYSTEM_COLLECTIONURI --project=$env:SYSTEM_TEAMPROJECT
env:
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
You do have to pass the token explicitly, because Azure Pipelines won't pass secrets to tasks by default.