Remote Software Deployment on multiple Azure Linux and Windows VMs.

In this blog, we will discuss a real-time scenario of deploying software on multiple Linux and Windows Virtual machines simultaneously. Suppose you have 500 virtual machines both windows and Linux and you want to push the software to these virtual machines. Obviously, it is not a workable solution to perform the manual installation on these 500 VMs. But there is good news that Azure provides a custom script extension for remote command execution. Here is the YouTube Video to demonstrate this concept. In the subsequent section, I will explain this script.

In this blog, I will share the PowerShell code and walk-through to installing Adobe reader into multiple VM. You can customize this code for other software installations.

Approach for Software Installation

We will use this approach for software installations:

  1. Develop PowerShell script which installs the software on Windows Virtual Machine. We need to test this script on one of the Windows Virtual machines and make sure that the script works perfectly fine. you need to save this script with the .ps1 file extension (filename.ps1).
# Silent install Adobe Reader DC
# https://get.adobe.com/nl/reader/enterprise/

# Path for the workdir
$workdir = "c:\installer\"

# Check if work directory exists if not create it

If (Test-Path -Path $workdir -PathType Container)
{ Write-Host "$workdir already exists" -ForegroundColor Red}
else
{ New-Item -Path $workdir  -ItemType directory }

# Download the installer

$source = "http://ardownload.adobe.com/pub/adobe/reader/win/AcrobatDC/1502320053/AcroRdrDC1502320053_en_US.exe"
$destination = "$workdir\adobeDC.exe"

# Check if Invoke-Webrequest exists otherwise execute WebClient

if (Get-Command 'Invoke-Webrequest')
{
     Invoke-WebRequest $source -OutFile $destination
}
else
{
    $WebClient = New-Object System.Net.WebClient
    $webclient.DownloadFile($source, $destination)
}

# Start the installation

Start-Process -FilePath "$workdir\adobeDC.exe" -ArgumentList "/sPB /rs"

# Wait XX Seconds for the installation to finish

Start-Sleep -s 35

# Remove the installer

rm -Force $workdir\adobe*

2. Develop a shell script that installs the software on a Linux Virtual Machine and make sure to test this script on one of the Linux Virtual machines. Once your testing is over and you have finalized the script you will have to copy it into the Azure Storage account. So please note this important trick because even if you test the script it may not work as expected when you deploy it into UNIX Virtual machine with Azure custom script extension.

IMPORTANT TIP: While creating Shell script especially if you are working on Windows OS please use notepad++ and enable EOL conversion for UNIX as shown below:

Here is the shell script:

#!/bin/bash
sudo apt update
sudo apt install snapd
sudo snap install acrordrdc

Once you enable the EOL conversion for UNIX then start writing the script in Notepad++. UNIX EOL feature of Notepad++ will allow UNIX line feed.Save the script with the .sh extension (filename.sh).

3. Create an Azure Storage account and container and copy PowerShell and Shell scripts into the container.

4. Now copy the storage account access key so we can access the scripts from the storage account container and invoke them with Azure Custom Script extension.

5. Now we need to write a script that can access the storage account and invoke the scripts based on the condition of whether the machine is Windows or Linux. Let’s understand the part of the script here:

  1. The script will initialize variables so they can be used in the following steps. Please note that the script takes the server names from a CSV file and you need to define the path of the CSV file. It also stores the PowerShell and Shell script file names along with the Storage account container and access keys. Since we have different extension names for UNIX and windows it stores them in a separate variable.
### Installing AdobeReader  on Multiple virtual machines ##### 
#Login to Azure via PowerShell and Azure CLI

connect-azAccount
       az login
###################Script Variables###################################
#csv file path where servernames are fetched. use your own path here 
$serverFilePath="C:\script\servers.csv"
#Name of the PowerShell Script
$Powershell_Script_Name="AdobeInstaller.ps1"
# container name where the custom script is stored
$Container_Name="software"
# Name of the UNIX script 
$Linux_Script_Name="AdobeInstallationOnLinux.sh"
#Storage Account Name where Script is stored, Replace it with your storage account name
$storage_account_name = "demosoftware123"
# storage account key of where the custom script is stored. Please replace the storage account key from your storage account,
$storage_account_key = "RJ2A7rn1Hx325cj0zHqfE1L6ldX5/uurupRHhcrm3KvyYNkHGE40D9OCvSgtEj0EbEA9ohWfJ0cNpWn43gQVAg=="
# Assuming the state of the virtual machine is not de-allocated
$is_dellocated = $false
#Name of the Resource group where your servers are kept
$resource_group = "Demo"

$ExtensionTypeForWindows="CustomScriptExtension"
$ExtensionTypeForLinux="customScript"
#############################Script Variables End Here########################## 
# this part of the script uses variables declared above. 
$fileUri = @("https://"+$storage_account_name+".blob.core.windows.net/software/"+$Powershell_Script_Name)

$settings = @{"fileUris" = $fileUri};

$extensionName="Adobe_Reader_install_extension"
 
$protectedSettings = @{"storageAccountName" = $storage_account_name; "storageAccountKey" = $storage_account_key; "commandToExecute" = "powershell -ExecutionPolicy Unrestricted -File "+$Powershell_Script_Name};

$protectedSettingsLinux='{\"storageAccountName\":\"'+$storage_account_name+'\",\"storageAccountKey\":\"'+$storage_account_key+"\"+'"}'
#Replace it with your path
$serverFilePath="C:\Scripts\servers.csv"

$serverList = Get-Content $serverFilePath 
 
$uriParameterForLinux='{\"fileUris\": [\"https://'+$storage_account_name+".blob.core.windows.net/software/$Linux_Script_Name\""],"+" "+'\"commandToExecute\":'+"\""./$Linux_Script_Name\"""+'}'

2. Script fetches the server names from the CSV file and then verifies if the Machine is stopped or not. If the machine is stopped it starts the machine and checks if the machine is windows or Linux. In case the machine is a windows machine it uses a custom script extension for windows otherwise it uses the CLI command for UNIX. Script also verifies if the machine is in a Generalized state then it does not takes any action. If an extension is already installed script uninstalls the extension and then starts installing it again. It repeats all the steps for each VM. Here is how you need to specify all the server names in the CSV file.

Here is the script:

    #LIST OF VM comign from CSV file 
 
foreach ($server in $serverList)
{
$vm_name=$server
        
        # Checking if the  Resource Group is valid .

        if($resource_group -eq $null -or $vm_name -eq $null){

            "Either Resource Group or Virtual Machine name, not present. This could be because the input variables could be misspelled. Make sure the input names are - 'ResourceGroup' and 'VirtualMachine'. " | write-output

            exit

        }

        #### Checking if the Virtual Machine is a Windows machine or Linux Machine ########

        # Obtaining the Virtual Machine object

        $vm = get-AzVM -ResourceGroupName $resource_group -Name $vm_name

        
        # Obtaining the Virtual Machine status object

        $vm_status = get-AzVM -ResourceGroupName $resource_group -Name $vm_name -Status
        

        "Displaying the status of Virtual machine...." | write-output

        $vm_status.Statuses[1].DisplayStatus | write-output

        "" | write-output

        "" | write-output

        "Checking if the VM:$vm_name is Windows or Linux." | write-output

        $vm.OSProfile.WindowsConfiguration | write-output

      $VmOS="" 

        if($vm.OSProfile.WindowsConfiguration -eq $null){

            $VmOS="Linux"

        }else
        {
        
        $VmOS="Windows"
        }


<# IF THE VIRTUAL MACHINE IS STOPPED-DEALLOCATED, THIS SCRIPT WILL START THE VIRTUAL MACHINE, INSTALL AGENTS AND WILL DE-ALLOCATE IT
        ######## Checking the status of the Virtual Machine ########


           IF VM is Generalized --> Do not take any action. Exit Execution

            IF VM is Deallocated --> Start the Virtual Machine

            IF VM is Running --> Do not take any action, Proceed with Execution

        #>


        if($vm_status.Statuses[1].DisplayStatus -eq "VM Generalized"){

            "Virtual Machine:$vm_name is in the GENERALIZED state. Do not proceed further... " | write-output

            "" | write-output

            "" | write-output

exit

        }


        if($vm_status.Statuses[1].DisplayStatus -eq "VM deallocated"){

            "Virtual Machine:$vm_name is STOPPED. Starting the virtual machine... " | write-output

            $is_dellocated = $true

            $vm | Start-AzVM

            "Successfully started Virtual Machine:$vm_name.." | write-output

            ""| write-output

            "" | write-output

        }


        if($vm_status.Statuses[1].DisplayStatus -eq "VM running"){

            "Virtual Machine:$vm_name is already RUNNING. Proceeding with agents installation" | write-output

            "" | write-output

            "" | write-output

        }

        # Checking if the virtual machine already has a Custom Script Extension

        $vm = get-AzVM -ResourceGroupName $resource_group -Name $vm_name

        $vm_status = get-AzVM -ResourceGroupName $resource_group -Name $vm_name -Status

        $vm_extensions = $vm.Extensions


        foreach($vm_extensions_iterator in $vm_extensions){

            if($vm_extensions_iterator.VirtualMachineExtensionType -eq $ExtensionTypeForWindows -or $vm_extensions_iterator.VirtualMachineExtensionType -eq $ExtensionTypeForLinux ){
             "Extension Type for the VM:$vm_name is $vm_extensions_iterator.VirtualMachineExtensionType" | write-output
                "Removing the CSE from VM:$vm_name..." | write-output
                if($VmOS -eq"Windows"){
                #For windows Machine
                Remove-AzVMCustomScriptExtension -Name $vm_extensions_iterator.Name -ResourceGroupName $resource_group -VMName $vm_name -force
                }else
                {
                #For Linux Machine                 
                az vm extension delete `
                --resource-group $resource_group `
                --vm-name $vm_name --name $ExtensionTypeForLinux 
                }
                "Removed  the CSE from VM:$vm_name " | write-output

                "" | write-output

                "" | write-output

            }

                    }


        # Re-creating the Virtual Machine object, since one of the above condition - starts the virtual machine

        $vm = get-AzVM -ResourceGroupName $resource_group -Name $vm_name

        $vm_status = get-AzVM -ResourceGroupName $resource_group -Name $vm_name -Status


        ########### Installing Adobe Reader client via Azure Custom Script Extension ###########

        if($vm_status.Statuses[1].DisplayStatus -eq "VM running"){

            "Installing AdobeReader extension on VM:$vm_name..." | write-output
if($VmOS -eq "Windows"){
            # azure powershell cmdlet to  add the custom script extension  to execute the powershell file

 #For Windows
Set-AzVMExtension -ResourceGroupName $resource_group `
    -Location $vm.Location`
    -VMName $vm_name `
    -Name  $extensionName `
    -Publisher "Microsoft.Compute" `
    -ExtensionType $ExtensionTypeForWindows `
    -TypeHandlerVersion "1.10" `
    -Settings $settings    `
    -ProtectedSettings $protectedSettings `

    }else
    {

    #For Linux
az vm extension set `
   --resource-group $resource_group `
  --vm-name linuxvm --name $ExtensionTypeForLinux `
  --publisher Microsoft.Azure.Extensions `
  --settings $uriParameterForLinux `
  --protected-settings $protectedSettingsLinux
   
  }
  }

        "waiting for 10 seconds..." | write-output

        "" | write-output

        "" | write-output


        Start-Sleep -s 10


        ######## Stopping the Virtual machine that we had started ########



        if($is_dellocated -eq $true){

            "We had started the virtual machine:$vm_name before installing the Adobe Reader agent. STOPPING the virtual machine:$vm_name to preserve the initial state..." | write-output


            $vm | Stop-AzVM -force

            "Successfully stopped the virtual machine:$vm_name" | write-output

            "" | write-output

            "" | write-output

        }
}

You can customize this script for any other software. Here is the end-to-end Script.

   ### Installing AdobeReader  on Multiple virtual machines ##### 
#Login to Azure via PowerShell and Azure CLI

connect-azAccount
       az login
###################Script Variables###################################
#csv file path where servernames are fetched. use your own path here 
$serverFilePath="C:\script\servers.csv"
#Name of the PowerShell Script
$Powershell_Script_Name="AdobeInstaller.ps1"
# container name where the custom script is stored
$Container_Name="software"
# Name of the UNIX script 
$Linux_Script_Name="AdobeInstallationOnLinux.sh"
#Storage Account Name where Script is stored, Replace it with your storage account name
$storage_account_name = "demosoftware123"
# storage account key of where the custom script is stored. Please replace the storage account key from your storage account,
$storage_account_key = "RJ2A7rn1Hx325cj0zHqfE1L6ldX5/uurupRHhcrm3KvyYNkHGE40D9OCvSgtEj0EbEA9ohWfJ0cNpWn43gQVAg=="
# Assuming the state of the virtual machine is not de-allocated
$is_dellocated = $false
#Name of the Resource group where your servers are kept
$resource_group = "Demo"

$ExtensionTypeForWindows="CustomScriptExtension"
$ExtensionTypeForLinux="customScript"
#############################Script Variables End Here########################## 
# this part of the script uses variables declared above. 
$fileUri = @("https://"+$storage_account_name+".blob.core.windows.net/software/"+$Powershell_Script_Name)

$settings = @{"fileUris" = $fileUri};

$extensionName="Adobe_Reader_install_extension"
 
$protectedSettings = @{"storageAccountName" = $storage_account_name; "storageAccountKey" = $storage_account_key; "commandToExecute" = "powershell -ExecutionPolicy Unrestricted -File "+$Powershell_Script_Name};

$protectedSettingsLinux='{\"storageAccountName\":\"'+$storage_account_name+'\",\"storageAccountKey\":\"'+$storage_account_key+"\"+'"}'
#Replace it with your path
$serverFilePath="C:\Scripts\servers.csv"

$serverList = Get-Content $serverFilePath 
 
$uriParameterForLinux='{\"fileUris\": [\"https://'+$storage_account_name+".blob.core.windows.net/software/$Linux_Script_Name\""],"+" "+'\"commandToExecute\":'+"\""./$Linux_Script_Name\"""+'}'

   #LIST OF VM comign from CSV file 
 
foreach ($server in $serverList)
{
$vm_name=$server
        
        # Checking if the  Resource Group is valid .

        if($resource_group -eq $null -or $vm_name -eq $null){

            "Either Resource Group or Virtual Machine name, not present. This could be because the input variables could be misspelled. Make sure the input names are - 'ResourceGroup' and 'VirtualMachine'. " | write-output

            exit

        }

        #### Checking if the Virtual Machine is a Windows machine or Linux Machine ########

        # Obtaining the Virtual Machine object

        $vm = get-AzVM -ResourceGroupName $resource_group -Name $vm_name

        
        # Obtaining the Virtual Machine status object

        $vm_status = get-AzVM -ResourceGroupName $resource_group -Name $vm_name -Status
        

        "Displaying the status of Virtual machine...." | write-output

        $vm_status.Statuses[1].DisplayStatus | write-output

        "" | write-output

        "" | write-output

        "Checking if the VM:$vm_name is Windows or Linux." | write-output

        $vm.OSProfile.WindowsConfiguration | write-output

      $VmOS="" 

        if($vm.OSProfile.WindowsConfiguration -eq $null){

            $VmOS="Linux"

        }else
        {
        
        $VmOS="Windows"
        }


<# IF THE VIRTUAL MACHINE IS STOPPED-DEALLOCATED, THIS SCRIPT WILL START THE VIRTUAL MACHINE, INSTALL AGENTS AND WILL DE-ALLOCATE IT
        ######## Checking the status of the Virtual Machine ########


           IF VM is Generalized --> Do not take any action. Exit Execution

            IF VM is Deallocated --> Start the Virtual Machine

            IF VM is Running --> Do not take any action, Proceed with Execution

        #>


        if($vm_status.Statuses[1].DisplayStatus -eq "VM Generalized"){

            "Virtual Machine:$vm_name is in the GENERALIZED state. Do not proceed further... " | write-output

            "" | write-output

            "" | write-output

exit

        }


        if($vm_status.Statuses[1].DisplayStatus -eq "VM deallocated"){

            "Virtual Machine:$vm_name is STOPPED. Starting the virtual machine... " | write-output

            $is_dellocated = $true

            $vm | Start-AzVM

            "Successfully started Virtual Machine:$vm_name.." | write-output

            ""| write-output

            "" | write-output

        }


        if($vm_status.Statuses[1].DisplayStatus -eq "VM running"){

            "Virtual Machine:$vm_name is already RUNNING. Proceeding with agents installation" | write-output

            "" | write-output

            "" | write-output

        }

        # Checking if the virtual machine already has a Custom Script Extension

        $vm = get-AzVM -ResourceGroupName $resource_group -Name $vm_name

        $vm_status = get-AzVM -ResourceGroupName $resource_group -Name $vm_name -Status

        $vm_extensions = $vm.Extensions


        foreach($vm_extensions_iterator in $vm_extensions){

            if($vm_extensions_iterator.VirtualMachineExtensionType -eq $ExtensionTypeForWindows -or $vm_extensions_iterator.VirtualMachineExtensionType -eq $ExtensionTypeForLinux ){
             "Extension Type for the VM:$vm_name is $vm_extensions_iterator.VirtualMachineExtensionType" | write-output
                "Removing the CSE from VM:$vm_name..." | write-output
                if($VmOS -eq"Windows"){
                #For windows Machine
                Remove-AzVMCustomScriptExtension -Name $vm_extensions_iterator.Name -ResourceGroupName $resource_group -VMName $vm_name -force
                }else
                {
                #For Linux Machine                 
                az vm extension delete `
                --resource-group $resource_group `
                --vm-name $vm_name --name $ExtensionTypeForLinux 
                }
                "Removed  the CSE from VM:$vm_name " | write-output

                "" | write-output

                "" | write-output

            }

                    }


        # Re-creating the Virtual Machine object, since one of the above condition - starts the virtual machine

        $vm = get-AzVM -ResourceGroupName $resource_group -Name $vm_name

        $vm_status = get-AzVM -ResourceGroupName $resource_group -Name $vm_name -Status


        ########### Installing Adobe Reader client via Azure Custom Script Extension ###########

        if($vm_status.Statuses[1].DisplayStatus -eq "VM running"){

            "Installing AdobeReader extension on VM:$vm_name..." | write-output
if($VmOS -eq "Windows"){
            # azure powershell cmdlet to  add the custom script extension  to execute the powershell file

 #For Windows
Set-AzVMExtension -ResourceGroupName $resource_group `
    -Location $vm.Location`
    -VMName $vm_name `
    -Name  $extensionName `
    -Publisher "Microsoft.Compute" `
    -ExtensionType $ExtensionTypeForWindows `
    -TypeHandlerVersion "1.10" `
    -Settings $settings    `
    -ProtectedSettings $protectedSettings `

    }else
    {

    #For Linux
az vm extension set `
   --resource-group $resource_group `
  --vm-name linuxvm --name $ExtensionTypeForLinux `
  --publisher Microsoft.Azure.Extensions `
  --settings $uriParameterForLinux `
  --protected-settings $protectedSettingsLinux
   
  }
  }

        "waiting for 10 seconds..." | write-output

        "" | write-output

        "" | write-output


        Start-Sleep -s 10


        ######## Stopping the Virtual machine that we had started ########



        if($is_dellocated -eq $true){

            "We had started the virtual machine:$vm_name before installing the Adobe Reader agent. STOPPING the virtual machine:$vm_name to preserve the initial state..." | write-output


            $vm | Stop-AzVM -force

            "Successfully stopped the virtual machine:$vm_name" | write-output

            "" | write-output

            "" | write-output

        }
}

I hope this would be helpful!!

Leave a Reply