Run Commands in Azure Virtual Machine (Linux/Windows)

Mars Wang
9 min readMay 4, 2023

--

Photo by Tobias Keller on Unsplash

What is happened to run scripts in Azure VM?

You must encounter the situation that you might want to configure your virtual machines through accessing VM directly, such as SSH or RDP. However, it is really annoying that you need to temporarily configure your IP address every time on Azure Portal, waiting about 5 minutes for configuration in your network security group only because you just want to try to use PowerShell on Windows and echo “Hello World!” What a time-wasted action, right?

On Azure Portal, the common method of connecting to Windows VM is Native RDP.

So here we go, you can run scripts just as same as you do in your host now. You can use AzCLI to run your scripts in your Azure VM, which is a good chance to accelerate the action or configuration we try to do in the VM. Also, it helps to remediate VM access and network issues, like I told in the beginning. You don’t need to configure and check your IP address is valid to log into your VM through RDP or SSH.

How does Azure VM run the scripts without accessing into desktop or terminal? Actually, like Azure DevOps self-hosted machine. Azure VM has been already installed an agent to listen to the action from outside when you are provisioning. Therefore, when you run “az vm run-command”, the resource manager in Azure will analyze it beforehand, and send the script to the target VM.

Action Oriented vs Management Oriented

Two types of command are available to run scripts in Azure VM. Let me list some important notes below to show the difference or you may find the details in official doc.

Action Oriented. Basically, you send a POST action to the VM. There is no record can be stored in storage or log.

  • Fixed Timeout (90 mins): The only way to cancel the running script is to shutdown/restart the VM, or to wait for 90 minutes till timing out.
  • Executed Role (Root/System account): It could have some security issues because the role to run the script almost get all privileges to do anything.
  • Only One Active Command: This could be the most important one to notice. ONLY ONE RUNNING COMMAND at the same time like it said. If you start the script using action oriented run-commands, it can’t be stopped till it fails or completes. In the meanwhile, it is impossible to send another running script request to Azure VM.
  • Non-Async Execution: In action-oriented command, the goal state/provisioning need to wait for script to complete.

If you try to run action-oriented command, you may find it in Azure Portal as the picture below showing or use it through AzCLI.

Use action-oriented commands in Azure Portal

Management Oriented. It means you expect you can manage all commands you ran, maybe updating the command or reusing it. The commands will be created as a resource type under the type of Microsoft.Compute/virtualMachines/runCommands. Here are some important features:

  • Customized Timeout
  • Customized Executed Role
  • Multiple Active Command in Parallel/Sequence
  • Async Execution: you may set the async flag in AzCLI to be asynchronous command.
Find out all of your managed-oriented actions through Resource Explorer in Azure Portal

I will use MacBook to be the host and Windows Server 2019 (hostname: runcommand-vm) to be my VM on Azure and run scripts through managed action in this demo. You can find the supported machine and version if you use other Windows versions or Linux OS.

Permission you need

  • Read: Microsoft.Compute/locations/runCommands/read or at least Reader roles setting IAM
  • Write: Microsoft.Compute/virtualMachines/runCommand/write or at least VM contributors role

Create & Execute Scripts with the VM

There are 3 ways (Inline, Script Path, Script URI) to run commands through AzCLI. And, in the next article, I will dive into these commands and see what is actually running in the backend.

  1. Inline Script:
    You can write inline script in “--script” argument if your script is short or simple.
az vm run-command \
--name "myFirstRunCommand" \ # Give this command a name for manaegment in the future
--vm-name "runcommand-vm" \
--resource-group "test-env-rg" \
--script "Write-Host Hello World"

Then, the below is the output after it completes the script:

{
"asyncExecution": false,
"errorBlobUri": null,
"id": "/subscriptions/<subscription-id>/resourceGroups/test-env-rg/providers/Microsoft.Compute/virtualMachines/runcommand-vm/runCommands/myFirstRunCommand",
"instanceView": null,
"location": "japaneast",
"name": "myFirstRunCommand",
"outputBlobUri": null,
"parameters": null,
"protectedParameters": null,
"provisioningState": "Succeeded",
"resourceGroup": "test-env-rg",
"runAsPassword": null,
"runAsUser": null,
"source": {
"commandId": null,
"script": "Write-Host Hello World",
"scriptUri": null
},
"tags": null,
"timeoutInSeconds": 0,
"type": "Microsoft.Compute/virtualMachines/runCommands"
}

You must wonder where is the result show in the VM, right?

It is very tricky that when creating and executing the script, the output doesn’t return the result how the script was doing, such as Error or Succeeded. We have to use “az vm run-command show” and use “--instance-view” in AzCLI.

az vm run-command show \
--name "myFirstRunCommand" \ # put on the name of your command
--vm-name "runcommand-vm" \
--resource-group "test-env-rg" \
--instance-view # you must add this option to see the result that VM ran

You will also find out if you don’t add “--instance-view ”, the output will exactly as same as the output of “az vm run-command create”. Super Wired…

View Running Result:

{
"asyncExecution": false,
"errorBlobUri": null,
"id": "/subscriptions/<subscription-id>/resourceGroups/test-env-rg/providers/Microsoft.Compute/virtualMachines/runcommand-vm/runCommands/myFirstRunCommand",
"instanceView": {
"endTime": "2023-05-02T14:09:40.609229+00:00",
"error": null,
"executionMessage": null,
"executionState": "Succeeded",
"exitCode": 0,
"output": "Hello World", --> Here is the output or the result that script ran
"startTime": "2023-05-02T14:09:33.229226+00:00",
"statuses": null
},
"location": "japaneast",
"name": "myFirstRunCommand",
"outputBlobUri": null,
"parameters": null,
"protectedParameters": null,
"provisioningState": "Succeeded",
"resourceGroup": "test-env-rg",
"runAsPassword": null,
"runAsUser": null,
"source": {
"commandId": null,
"script": "Write-Host Hello World",
"scriptUri": null
},
"tags": null,
"timeoutInSeconds": 0,
"type": "Microsoft.Compute/virtualMachines/runCommands"
}

2. Script Path:
Besides inline script, you can do it with script files such as .ps1 or .sh etc. This can eliminate the issue that you get a complicated script and you don’t want to copy all commands from scripts to your terminal (which is not a ideal way to run commands)

~/Blog/Azure/VM/Commands> vim script.ps1 # or you can name it script.sh

#--------In script.ps1--------
Write-Host Hello World!!!
#------------End--------------

# run AzCLI command to send the script to Azure VM
~/Blog/Azure/VM/Commands> \
az vm run-command \
create --name "myScriptFileRunCommand" \
--vm-name "runcommand-vm" \
--resource-group "test-env-rg" \
--script @script.ps1 # use "@<script-file-name>" to assign the script file

Output:

{
"asyncExecution": false,
"errorBlobUri": null,
"id": "/subscriptions/<subscription-id>/resourceGroups/test-env-rg/providers/Microsoft.Compute/virtualMachines/runcommand-vm/runCommands/myScriptFileRunCommand",
"instanceView": null,
"location": "japaneast",
"name": "myScriptFileRunCommand",
"outputBlobUri": null,
"parameters": null,
"protectedParameters": null,
"provisioningState": "Succeeded",
"resourceGroup": "test-env-rg",
"runAsPassword": null,
"runAsUser": null,
"source": {
"commandId": null,
"script": "Write-Host Hello World!!!",
"scriptUri": null
},
"tags": null,
"timeoutInSeconds": 0,
"type": "Microsoft.Compute/virtualMachines/runCommands"
}

View Running Result:

az vm run-command show \
--name "myScriptFileRunCommand" \
--vm-name "runcommand-vm" \
--resource-group "test-env-rg" \
--instance-view
{
"asyncExecution": false,
"errorBlobUri": null,
"id": "/subscriptions/<subscription-id>/resourceGroups/test-env-rg/providers/Microsoft.Compute/virtualMachines/runcommand-vm/runCommands/myScriptFileRunCommand",
"instanceView": {
"endTime": "2023-05-02T14:28:20.588320+00:00",
"error": null,
"executionMessage": null,
"executionState": "Succeeded",
"exitCode": 0,
"output": "Hello World!!!", --> Here is the output or the result that script ran
"startTime": "2023-05-02T14:28:20.057096+00:00",
"statuses": null
},
"location": "japaneast",
"name": "myScriptFileRunCommand",
"outputBlobUri": null,
"parameters": null,
"protectedParameters": null,
"provisioningState": "Succeeded",
"resourceGroup": "test-env-rg",
"runAsPassword": null,
"runAsUser": null,
"source": {
"commandId": null,
"script": "Write-Host Hello World!!!",
"scriptUri": null
},
"tags": null,
"timeoutInSeconds": 0,
"type": "Microsoft.Compute/virtualMachines/runCommands"
}

If you don’t want to wait for the output, add “--no-wait” in your AzCLI. It will automatically jump out of the command and still run in the backend.

Overriding your command

It is possible to revise the script in your command. The script can be updated using “az vm run-command create” or “az vm run-command update

az vm run-command create \
--name "myFirstRunCommand" \
--vm-name "runcommand-vm" \
--resource-group "test-env-rg" \
--script "Write-Host Hello World Revise Script..."

# ---- or ----

az vm run-command update \
--name "myFirstRunCommand" \
--vm-name "runcommand-vm" \
--resource-group "test-env-rg" \
--script "Write-Host Hello World Revise Script..."

Output:

az vm run-command show \
--name "myFirstRunCommand" \
--vm-name "runcommand-vm" \
--resource-group "test-env-rg" \
--instance-view
{
"asyncExecution": false,
"errorBlobUri": null,
"id": "/subscriptions/<subscription-id>/resourceGroups/test-env-rg/providers/Microsoft.Compute/virtualMachines/runcommand-vm/runCommands/RunCommand",
"instanceView": {
"endTime": "2023-05-03T16:37:10.423738+00:00",
"error": null,
"executionMessage": null,
"executionState": "Succeeded",
"exitCode": 0,
"output": "Hello World Revise Script...", --> The updated output
"startTime": "2023-05-03T16:37:08.658080+00:00",
"statuses": null
},
"location": "japaneast",
"name": "myFirstRunCommand",
"outputBlobUri": null,
"parameters": null,
"protectedParameters": null,
"provisioningState": "Succeeded",
"resourceGroup": "test-env-rg",
"runAsPassword": null,
"runAsUser": null,
"source": {
"commandId": null,
"script": "Write-Host Hello World Revise Script...",
"scriptUri": null
},
"tags": null,
"timeoutInSeconds": 0,
"type": "Microsoft.Compute/virtualMachines/runCommands"
}

No matter which AzCLI you choose (create or update), the backend seems to do the same action:

Create/Update → Provisioning/Updating runCommands resource → Run script

Last, Delete the runCommand resources

List all commands you created before, and copy the name and related info to delete it if not using anymore:

List all commands:

az vm run-command list \
--vm-name "runcommand-vm" \
--resource-group "test-env-rg"

Q: What if I want to know the numbers of runCommands resources I have in a resource group right now?

A: Use jq (a lightweight and flexible command-line JSON processor) to count the number.

az vm run-command list \
--vm-name "runcommand-vm" \
--resource-group "test-env-rg" | jq length

# output: <the numbers>

Delete specific command:

az vm run-command delete \
--name "myFirstRunCommand" \
--vm-name "runcommand-vm" \
--resource-group "test-env-rg" \
--yes #(optional) if you don't add --yes, the prompt will ask are you sure you want to perform delete operation.

To conclude, the AzCLI above is just a demo that you might encounter when using it. I still got some issues when doing the test of “az vm run-commands.” So, please ask your question and contact me if you don’t mind. We can discuss it or open an Issue on Github as well.

Reference: az vm run-command | Microsoft Learn

<P.S> 2023/05/04 update:
If you try to use “ — command-id” with “ — script” together in AzCLI, it will return error said it is “InvalidParameterConflictingProperties.” After I test all cases, you can only choose one of the arguments to become resource. For example, if you use

az vm run-command create \
--name "RunCommand_with_commandID" \
--vm-name "runcommand-vm" \
--resource-group "test-env-rg" \
--command-id "RunPowerShellScript" # which is a pre-defined type of action-oriented command
#--script "Write-Host Hello World Command ID" --> It will show an Error said properties conflict

The output:

{
"asyncExecution": false,
"errorBlobUri": null,
"id": "/subscriptions/<subscription-id>/resourceGroups/test-env-rg/providers/Microsoft.Compute/virtualMachines/runcommand-vm/runCommands/RunCommand_with_commandID",
"instanceView": {
"endTime": "2023-05-03T16:56:29.163625+00:00",
"error": null,
"executionMessage": null,
"executionState": "Succeeded",
"exitCode": 0,
"output": "This is a sample script with parameters ", --> I think it is the default output for command with command-id
"startTime": "2023-05-03T16:56:28.429244+00:00",
"statuses": null
},
"location": "japaneast",
"name": "RunCommand_with_commandID",
"outputBlobUri": null,
"parameters": null,
"protectedParameters": null,
"provisioningState": "Succeeded",
"resourceGroup": "test-env-rg",
"runAsPassword": null,
"runAsUser": null,
"source": {
"commandId": "RunPowerShellScript", --> Only Command ID comes out
"script": null,
"scriptUri": null
},
"tags": null,
"timeoutInSeconds": 0,
"type": "Microsoft.Compute/virtualMachines/runCommands"
}

If you try to update or create the same command, it will eliminate the value of commandId and replace script’s value with your new script:

az vm run-command update \
--name "RunCommand_with_commandID" \
--vm-name "runcommand-vm" \
--resource-group "test-env-rg" \
--script "Write-Host Hello World Command ID"

The output:

{
"asyncExecution": false,
"errorBlobUri": null,
"id": "/subscriptions/<subscription-id>/resourceGroups/test-env-rg/providers/Microsoft.Compute/virtualMachines/runcommand-vm/runCommands/RunCommand_with_commandID",
"instanceView": null,
"location": "japaneast",
"name": "RunCommand_with_commandID",
"outputBlobUri": null,
"parameters": null,
"protectedParameters": null,
"provisioningState": "Succeeded",
"resourceGroup": "test-env-rg",
"runAsPassword": null,
"runAsUser": null,
"source": {
"commandId": null, --> RunPowerShellScript disappeared
"script": "Write-Host Hello World Command ID", --> New script comes out
"scriptUri": null
},
"tags": null,
"timeoutInSeconds": 0,
"type": "Microsoft.Compute/virtualMachines/runCommands"
}

--

--