Automating Azure VM Size Changes with PowerShell and CSV

Jaydeep DuttaJaydeep Dutta
12 min read

Lets understand with why Azure VM size needs to be changed and how many types of VM family are there in the Azure cloud

Cost Savings with Azure VM Size Changes

Changing the size of your Azure Virtual Machines (VMs) can lead to significant cost savings, especially if your current VM sizes are over-provisioned for your workloads. By resizing VMs to more appropriate sizes, you can optimize resource usage and reduce costs.

Example of Cost Savings

Let's consider an example where you have a VM running with the Standard_D4s_v3 size, which costs approximately $0.20 per hour. If your workload doesn't require such high specifications, you could resize it to a Standard_B2s size, which costs around $0.05 per hour.

  • Standard_D4s_v3: $0.20 per hour

  • Standard_B2s: $0.05 per hour

If the VM runs 24/7 for a month (730 hours), the cost difference would be:

  • Standard_D4s_v3: $0.20 * 730 = $146 per month

  • Standard_B2s: $0.05 * 730 = $36.50 per month

By resizing the VM, you save:

  • Savings: $146 - $36.50 = $109.50 per month

This example demonstrates how resizing VMs to more appropriate sizes can lead to substantial cost savings.

Azure VM Families

Azure offers a variety of VM families, each optimized for different types of workloads. Here are some of the main VM families:

  1. General Purpose:

    • Balanced CPU-to-memory ratio.

    • Suitable for testing and development, small to medium databases, and low to medium traffic web servers.

    • Examples: B-series, D-series, Av2-series.

  2. Compute Optimized:

    • High CPU-to-memory ratio.

    • Ideal for medium traffic web servers, network appliances, batch processes, and application servers.

    • Examples: F-series.

  3. Memory Optimized:

    • High memory-to-CPU ratio.

    • Great for relational database servers, medium to large caches, and in-memory analytics.

    • Examples: E-series, M-series.

  4. Storage Optimized:

    • High disk throughput and IO.

    • Suitable for big data, SQL, and NoSQL databases.

    • Examples: L-series.

  5. GPU:

    • Specialized VMs with Graphics Processing Units (GPUs).

    • Ideal for heavy graphics rendering, video editing, and model training for AI and machine learning.

    • Examples: NC-series, NV-series, ND-series.

  6. High Performance Compute (HPC):

    • Designed for high-performance computing workloads.

    • Suitable for molecular modeling, fluid dynamics, and other compute-intensive applications.

    • Examples: H-series, HB-series, HC-series.

By selecting the appropriate VM family and size for your workloads, you can optimize performance and cost efficiency.

Automating Azure VM Size Changes with PowerShell and CSV

Managing Azure Virtual Machines (VMs) can be a complex task, especially when you need to update the size of multiple VMs across different subscriptions. In this blog post, we'll walk through a PowerShell script that automates the process of changing VM sizes using a CSV file for input. This script leverages Azure Service Principal credentials for authentication and ensures robust error handling.

Prerequisites

Before we dive into the script, make sure you have the following:

  • Azure subscription ID

  • Resource group name

  • Path to a CSV file containing VM names and their desired sizes

  • Service Principal credentials (tenant ID, client ID, and client secret)

The PowerShell Script

# Parameters
$subscriptionId = "your-subscription-id"
$resourceGroupName = "your-resource-group-name"
$csvFilePath = "path-to-your-csv-file"  # Path to the CSV file

# Service Principal credentials
$tenantId = "your-tenant-id"
$clientId = "your-client-id"
$clientSecret = "your-client-secret"

# Login to Azure using SPN
$secureClientSecret = ConvertTo-SecureString $clientSecret -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential ($clientId, $secureClientSecret)
Connect-AzAccount -ServicePrincipal -TenantId $tenantId -Credential $credential

try {
    # Validate the subscription
    $subscription = Get-AzSubscription -SubscriptionId $subscriptionId
    if (-not $subscription) {
        Write-Host "Invalid subscription ID: $subscriptionId"
        exit
    } else {
        Write-Host "Subscription ID validated: $subscriptionId"
    }

    # Set the subscription
    Set-AzContext -SubscriptionId $subscriptionId

    # Read the CSV file
    try {
        $vmList = Import-Csv -Path $csvFilePath
    } catch {
        Write-Host "Error reading CSV file: $_"
        exit
    }

    foreach ($vm in $vmList) {
        $vmName = $vm.VMName
        $newVMSize = $vm.NewVMSize

        # Validate CSV data
        if (-not $vmName -or -not $newVMSize) {
            Write-Host "Invalid data in CSV file for VM: $vmName. Skipping this entry."
            continue
        }

        try {
            # Validate the VM name
            $vmObject = Get-AzVM -ResourceGroupName $resourceGroupName -Name $vmName -ErrorAction SilentlyContinue
            if (-not $vmObject) {
                Write-Host "Invalid VM name: $vmName"
                continue
            } else {
                Write-Host "VM name validated: $vmName"
            }

            # Validate the new VM size
            $availableSizes = Get-AzVMSize -ResourceGroupName $resourceGroupName -VMName $vmName
            if ($availableSizes.Name -notcontains $newVMSize) {
                Write-Host "Invalid VM size: $newVMSize for VM: $vmName. Please choose a valid size."
                continue
            } else {
                Write-Host "VM size validated: $newVMSize for VM: $vmName"
            }

            # Check the VM state
            $vmState = (Get-AzVM -ResourceGroupName $resourceGroupName -Name $vmName -Status).Statuses[1].Code
            if ($vmState -eq "PowerState/running") {
                # Stop the VM if it is running
                Stop-AzVM -ResourceGroupName $resourceGroupName -Name $vmName -Force
                Write-Host "VM $vmName was running and has been stopped."
            } elseif ($vmState -eq "PowerState/deallocated") {
                Write-Host "VM $vmName is already stopped."
            }

            # Update the VM size
            $vmObject.HardwareProfile.VmSize = $newVMSize
            Update-AzVM -ResourceGroupName $resourceGroupName -VM $vmObject

            # Start the VM if it was running before
            if ($vmState -eq "PowerState/running") {
                Start-AzVM -ResourceGroupName $resourceGroupName -Name $vmName
                Write-Host "VM $vmName has been started with the new size $newVMSize."
            } else {
                Write-Host "VM $vmName size changed to $newVMSize but remains stopped."
            }
        } catch {
            Write-Host "An error occurred with VM: $vmName - $_"
        }
    }
} catch {
    Write-Host "An error occurred: $_"
}

Steps Explained

  1. Login to Azure using SPN: Authenticate your Azure account using Service Principal credentials.

  2. Validate Subscription: Check if the provided subscription ID is valid. If not, exit the script with an error message. If valid, proceed.

  3. Set Subscription: Switch to the appropriate subscription.

  4. Read CSV File: Import the CSV file containing VM names and their desired sizes. If there's an error reading the file, catch the exception and exit the script.

  5. Loop Through VMs: Iterate through each VM in the CSV file.

  6. Validate CSV Data: Check if the VM name and new VM size are present in the CSV file. If not, skip the entry.

  7. Validate VM Name: Check if each VM name exists in the specified resource group. If not, continue to the next VM.

  8. Validate New VM Size: Check if the new VM size is available for each VM. If not, continue to the next VM.

  9. Check VM State: Determine if the VM is running or stopped.

    • If the VM is running, stop it.

    • If the VM is already stopped, proceed without stopping.

  10. Update VM Size: Retrieve each VM, change its size, and update the VM configuration.

  11. Start VM if Necessary: If the VM was running before, start it again. If it was already stopped, leave it stopped.

  12. Error Handling: Use try-catch blocks to handle any errors that occur during the process and display appropriate error messages for each VM and the CSV file.

Script Overview

This script is designed to automate the process of changing the size of multiple Azure VMs using a CSV file for input. It uses Service Principal credentials for authentication and includes robust error handling.

Detailed Explanation

Parameters

# Parameters
$subscriptionId = "your-subscription-id"
$resourceGroupName = "your-resource-group-name"
$csvFilePath = "path-to-your-csv-file"  # Path to the CSV file

# Service Principal credentials
$tenantId = "your-tenant-id"
$clientId = "your-client-id"
$clientSecret = "your-client-secret"
  • $subscriptionId: The ID of the Azure subscription where the VMs are located.

  • $resourceGroupName: The name of the resource group containing the VMs.

  • $csvFilePath: The path to the CSV file that lists the VM names and their desired sizes.

  • Service Principal credentials: These are used to authenticate with Azure.

Login to Azure using SPN

$secureClientSecret = ConvertTo-SecureString $clientSecret -AsPlainText -Force
$credential = New-Object System.Management.Automation.PSCredential ($clientId, $secureClientSecret)
Connect-AzAccount -ServicePrincipal -TenantId $tenantId -Credential $credential
  • ConvertTo-SecureString: Converts the client secret to a secure string.

  • New-Object System.Management.Automation.PSCredential: Creates a credential object using the client ID and secure client secret.

  • Connect-AzAccount: Authenticates to Azure using the Service Principal credentials.

Validate the Subscription

try {
    $subscription = Get-AzSubscription -SubscriptionId $subscriptionId
    if (-not $subscription) {
        Write-Host "Invalid subscription ID: $subscriptionId"
        exit
    } else {
        Write-Host "Subscription ID validated: $subscriptionId"
    }
  • Get-AzSubscription: Retrieves the subscription details.

  • Validation: Checks if the subscription ID is valid. If not, it exits the script with an error message.

Set the Subscription

    Set-AzContext -SubscriptionId $subscriptionId
  • Set-AzContext: Sets the Azure context to the specified subscription.

Read the CSV File

    try {
        $vmList = Import-Csv -Path $csvFilePath
    } catch {
        Write-Host "Error reading CSV file: $_"
        exit
    }
  • Import-Csv: Reads the CSV file and imports the data into a variable.

  • Error Handling: Catches any errors that occur while reading the CSV file and exits the script with an error message.

Loop Through VMs

    foreach ($vm in $vmList) {
        $vmName = $vm.VMName
        $newVMSize = $vm.NewVMSize
  • foreach: Iterates through each VM in the CSV file.

  • $vmName and $newVMSize: Extracts the VM name and desired size from the CSV file.

Validate CSV Data

        if (-not $vmName -or -not $newVMSize) {
            Write-Host "Invalid data in CSV file for VM: $vmName. Skipping this entry."
            continue
        }
  • Validation: Checks if the VM name and new VM size are present in the CSV file. If not, it skips the entry.

Validate the VM Name

        try {
            $vmObject = Get-AzVM -ResourceGroupName $resourceGroupName -Name $vmName -ErrorAction SilentlyContinue
            if (-not $vmObject) {
                Write-Host "Invalid VM name: $vmName"
                continue
            } else {
                Write-Host "VM name validated: $vmName"
            }
  • Get-AzVM: Retrieves the VM details.

  • Validation: Checks if the VM name exists in the specified resource group. If not, it skips the entry.

Validate the New VM Size

            $availableSizes = Get-AzVMSize -ResourceGroupName $resourceGroupName -VMName $vmName
            if ($availableSizes.Name -notcontains $newVMSize) {
                Write-Host "Invalid VM size: $newVMSize for VM: $vmName. Please choose a valid size."
                continue
            } else {
                Write-Host "VM size validated: $newVMSize for VM: $vmName"
            }
  • Get-AzVMSize: Retrieves the available sizes for the VM.

  • Validation: Checks if the new VM size is available. If not, it skips the entry.

Check VM State

            $vmState = (Get-AzVM -ResourceGroupName $resourceGroupName -Name $vmName -Status).Statuses[1].Code
            if ($vmState -eq "PowerState/running") {
                Stop-AzVM -ResourceGroupName $resourceGroupName -Name $vmName -Force
                Write-Host "VM $vmName was running and has been stopped."
            } elseif ($vmState -eq "PowerState/deallocated") {
                Write-Host "VM $vmName is already stopped."
            }
  • Get-AzVM -Status: Retrieves the current state of the VM.

  • Stop-AzVM: Stops the VM if it is running.

  • Logging: Logs the state of the VM.

Update the VM Size

            $vmObject.HardwareProfile.VmSize = $newVMSize
            Update-AzVM -ResourceGroupName $resourceGroupName -VM $vmObject
  • Update-AzVM: Updates the VM size.

Start VM if Necessary

            if ($vmState -eq "PowerState/running") {
                Start-AzVM -ResourceGroupName $resourceGroupName -Name $vmName
                Write-Host "VM $vmName has been started with the new size $newVMSize."
            } else {
                Write-Host "VM $vmName size changed to $newVMSize but remains stopped."
            }
        } catch {
            Write-Host "An error occurred with VM: $vmName - $_"
        }
    }
} catch {
    Write-Host "An error occurred: $_"
}
  • Start-AzVM: Starts the VM if it was running before.

  • Logging: Logs the result of the size change.

  • Error Handling: Catches any errors that occur during the process and logs them.

Integration with Jenkins Pipeline -

Integrating your PowerShell script with a Jenkins Pipeline can streamline your automation process and make it easier to manage and execute your scripts. Here's a step-by-step guide to help you integrate the provided PowerShell script into a Jenkins Pipeline.

Prerequisites

  1. Jenkins Installed: Ensure you have Jenkins installed and running.

  2. PowerShell Plugin: Install the PowerShell plugin for Jenkins.

  3. Service Principal Credentials: Ensure you have the necessary Service Principal credentials for Azure authentication.

Step-by-Step Guide

1. Install the PowerShell Plugin

  • Go to Manage Jenkins > Manage Plugins.

  • Search for PowerShell Plugin and install it.

2. Create a New Pipeline Job

  • Go to New Item in Jenkins.

  • Enter a name for your job and select Pipeline.

  • Click OK to create the job.

3. Configure the Pipeline Script

  • In the Pipeline section, select Pipeline script.

  • Enter the following Groovy script to integrate your PowerShell script:

pipeline {
    agent any

    environment {
        AZURE_SUBSCRIPTION_ID = "your-subscription-id"
        AZURE_RESOURCE_GROUP = "your-resource-group-name"
        CSV_FILE_PATH = "path-to-your-csv-file"
        AZURE_TENANT_ID = "your-tenant-id"
        AZURE_CLIENT_ID = "your-client-id"
        AZURE_CLIENT_SECRET = "your-client-secret"
    }

    stages {
        stage('Run PowerShell Script') {
            steps {
                script {
                    def psScript = """
                    # Parameters
                    \$subscriptionId = "\${AZURE_SUBSCRIPTION_ID}"
                    \$resourceGroupName = "\${AZURE_RESOURCE_GROUP}"
                    \$csvFilePath = "\${CSV_FILE_PATH}"

                    # Service Principal credentials
                    \$tenantId = "\${AZURE_TENANT_ID}"
                    \$clientId = "\${AZURE_CLIENT_ID}"
                    \$clientSecret = "\${AZURE_CLIENT_SECRET}"

                    # Login to Azure using SPN
                    \$secureClientSecret = ConvertTo-SecureString \$clientSecret -AsPlainText -Force
                    \$credential = New-Object System.Management.Automation.PSCredential (\$clientId, \$secureClientSecret)
                    Connect-AzAccount -ServicePrincipal -TenantId \$tenantId -Credential \$credential

                    try {
                        # Validate the subscription
                        \$subscription = Get-AzSubscription -SubscriptionId \$subscriptionId
                        if (-not \$subscription) {
                            Write-Host "Invalid subscription ID: \$subscriptionId"
                            exit
                        } else {
                            Write-Host "Subscription ID validated: \$subscriptionId"
                        }

                        # Set the subscription
                        Set-AzContext -SubscriptionId \$subscriptionId

                        # Read the CSV file
                        try {
                            \$vmList = Import-Csv -Path \$csvFilePath
                        } catch {
                            Write-Host "Error reading CSV file: \$_"
                            exit
                        }

                        foreach (\$vm in \$vmList) {
                            \$vmName = \$vm.VMName
                            \$newVMSize = \$vm.NewVMSize

                            # Validate CSV data
                            if (-not \$vmName -or -not \$newVMSize) {
                                Write-Host "Invalid data in CSV file for VM: \$vmName. Skipping this entry."
                                continue
                            }

                            try {
                                # Validate the VM name
                                \$vmObject = Get-AzVM -ResourceGroupName \$resourceGroupName -Name \$vmName -ErrorAction SilentlyContinue
                                if (-not \$vmObject) {
                                    Write-Host "Invalid VM name: \$vmName"
                                    continue
                                } else {
                                    Write-Host "VM name validated: \$vmName"
                                }

                                # Validate the new VM size
                                \$availableSizes = Get-AzVMSize -ResourceGroupName \$resourceGroupName -VMName \$vmName
                                if (\$availableSizes.Name -notcontains \$newVMSize) {
                                    Write-Host "Invalid VM size: \$newVMSize for VM: \$vmName. Please choose a valid size."
                                    continue
                                } else {
                                    Write-Host "VM size validated: \$newVMSize for VM: \$vmName"
                                }

                                # Check the VM state
                                \$vmState = (Get-AzVM -ResourceGroupName \$resourceGroupName -Name \$vmName -Status).Statuses[1].Code
                                if (\$vmState -eq "PowerState/running") {
                                    # Stop the VM if it is running
                                    Stop-AzVM -ResourceGroupName \$resourceGroupName -Name \$vmName -Force
                                    Write-Host "VM \$vmName was running and has been stopped."
                                } elseif (\$vmState -eq "PowerState/deallocated") {
                                    Write-Host "VM \$vmName is already stopped."
                                }

                                # Update the VM size
                                \$vmObject.HardwareProfile.VmSize = \$newVMSize
                                Update-AzVM -ResourceGroupName \$resourceGroupName -VM \$vmObject

                                # Start the VM if it was running before
                                if (\$vmState -eq "PowerState/running") {
                                    Start-AzVM -ResourceGroupName \$resourceGroupName -Name \$vmName
                                    Write-Host "VM \$vmName has been started with the new size \$newVMSize."
                                } else {
                                    Write-Host "VM \$vmName size changed to \$newVMSize but remains stopped."
                                }
                            } catch {
                                Write-Host "An error occurred with VM: \$vmName - \$_"
                            }
                        }
                    } catch {
                        Write-Host "An error occurred: \$_"
                    }
                    """
                    powershell script: psScript
                }
            }
        }
    }
}

Explanation of the Jenkins Pipeline Script

  1. Pipeline Block: Defines the Jenkins Pipeline.

  2. Environment Variables: Sets environment variables for the Azure subscription, resource group, CSV file path, and Service Principal credentials.

  3. Stages Block: Defines the stages of the pipeline.

  4. Run PowerShell Script Stage: Executes the PowerShell script.

    • script Block: Contains the PowerShell script to be executed.

    • powershell Step: Runs the PowerShell script within the Jenkins Pipeline.

By integrating your PowerShell script with Jenkins Pipeline, you can automate the process of changing Azure VM sizes efficiently. This setup ensures that your script runs in a controlled environment with proper error handling and logging.

Conclusion

This PowerShell script provides a robust and automated way to manage Azure VM sizes using a CSV file for input. By leveraging Service Principal credentials, it ensures secure and efficient authentication. The script includes comprehensive error handling to manage various scenarios, making it a reliable tool for Azure administrators.

Make sure to replace the placeholder values with your actual subscription ID, resource group name, path to the CSV file, and Service Principal credentials. Happy scripting!

If you have any questions or need further assistance, feel free to leave a comment below.

0
Subscribe to my newsletter

Read articles from Jaydeep Dutta directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jaydeep Dutta
Jaydeep Dutta

Proficient and skilled Cloud and DevOps Engineer with 6+ years of hands-on experience Cloud Infra, automating, orchestrating end to end Containerized application deployment in Cloud environment (Azure, AWS) , with Docker, Kubernetes, Ansible, Terraform IAC, Jenkins CI/CD, and DevOps methodologies and Automating Cloud solutions with Boto3 and Architecting cloud service based solutions