Start, RDP, and Stop: Automating Azure VMs while optimizing cost
Tags: azure, powershell
Published: 2021-03-13 21:25:29

<meta name="image" property="og:image" content="https://res.cloudinary.com/practicaldev/image/fetch/s--q1SrFqAM--/c_imagga_scale,f_auto,fl_progressive,h_420,q_auto,w_1000/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/le1zl6m2kbfv7l32jzhk.png">

I've been wanting to write this article for weeks now and have finally prioritized some time to do so. Here's a broad overview of a nifty PowerShell Script I pieced together from multiple sources to accomplish a very specific goal.

## Problem 1: Workflow
For those who provision Windows 10 VMs in Azure, you will know that taking time to go to the Azure portal, allocate the VM, grab the public IP (if you don't have a static one assigned), and stick it into the Remote Desktop client of your choice... well this process is quite a few clicks for a simple task. If you are setting up a VM for an otherwise non-technical user, you also don't want them to have to log in to the Azure portal every time.

## Problem 2: Cost Optimization
Through the native Azure Portal VM Scheduler (as well as many other tools out there), you can [modify the startup and shutdown schedules of your VMs](https://docs.microsoft.com/en-us/azure/automation/automation-solution-vm-management-config#modify-the-startup-and-shutdown-schedules). This is a great feature, but as many of you Azure enthusiasts know, Stopping a VM is different from De-allocating, especially when looking at the bills. You can set schedules for your VMs, but if there is no robust schedule of VM usage, you could be faced with a costly bill by keeping your VMs running. Even when you stop them, you will also incur cost.

Chris Parlette from ParkMyCloud has a [perfect description](https://www.parkmycloud.com/blog/azure-deallocate-vm/) for what I'm talking about. I'll quote below:
> ### Azure’s Stopped State
When you are logged in to the operating system of an Azure VM, you can issue a command to shut down the server. This will kick you out of the OS and stop all processes, but will maintain the allocated hardware (including the IP addresses currently assigned). If you find the VM in the Azure console, you’ll see the state listed as “Stopped”. The biggest thing you need to know about this state is that you are still being charged by the hour for this instance.
### Azure’s Deallocated State
The other way to stop your virtual machine is through Azure itself, whether that’s through the console, Powershell, or the Azure CLI. When you stop a VM through Azure, rather than through the OS, it goes into a “Stopped (deallocated)” state.  This means that any non-static public IPs will be released, but you’ll also stop paying for the VM’s compute costs. This is a great way to save money on your Azure costs when you don’t need those VMs running, and is the state that ParkMyCloud puts your VMs in when they are parked.
### Which State to Choose?
The only scenario in which you should ever choose the stopped state instead of the deallocated state for a VM in Azure is if you are only briefly stopping the server and would like to keep the dynamic IP address for your testing. If that doesn’t perfectly describe your use case, or you don’t have an opinion one way or the other, then you’ll want to deallocate instead so you aren’t being charged for the VM.

I am not endorsing ParkMyCloud aside from their succinct descriptions in articles, but their service does look extremely useful (especially since they park your VMs in de-allocated mode off hours).

### A quick solution:
A client I was working with had both of the aforementioned problems. Their VM usage was sporadic enough that a scheduling system really wouldn't work. They also didn't want to have to train users to log in to the Azure portal, boot up the VM, grab the IP, and set up remote desktop. For a non-technical user, its a lot of steps. Even though authorization control can be implemented quite well in Azure, it was probably best to not have people logging into the portal all the time, when they just wanted to access a VM.

So I set out to scrap together a PowerShell script to do the heavy lifting. 

### These were the general requirements:
1) Start the VM without going to the portal.
2) Prompt user with credentials for VM access (Microsoft prompt).
3) Automatically start the RDP client and initialize a connection.
4) When the user closes the RDP session, automatically stop and de-allocate the VM.

Since this VM didn't get usage by multiple users at the same time, the script didn't need to account for other people being logged in. That being said, a solution [like this](https://gist.github.com/why-not/18c5c0c00ee4cddf5ca176f770918639) could be merged with the below PS script to listen for any RDP connections Azure-side. If there are no connections, it could de-allocate the VM. Otherwise, it wouldn't stop or de-allocate. This could basically follow a "last person off shuts down the VM" policy. That being said, I think the solution below is a good for one-off individual or sporadic VM usage.

Before I show the code, I would also like to note that this was written in a couple of hours. The clients just wanted a quick and dirty solution. I wouldn't say its the best code, but it definitely owns up to being a script.

### The general outline:
1) Self-elevate the script
2) Do some dependency checks, 
3) Gather credentials via Microsoft prompt.
4) gathering and booting of the VM.
5) Start Azure VM
7) Start RDP session to VM
8) Track session
9) De-allocate when RDP session is closed.

All you have to do is replace `RESOURCEGROUPNAMEHERE` and `VMNAMEHERE` with the Azure resource group name and VM name respectively. The rest should be agnostic.

```powershell
# Self-elevate the script if required
"`Self Elevating...`n"
if (-Not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) {
    if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
        $CommandLine = "-File `"" + $MyInvocation.MyCommand.Path + "`" " + $MyInvocation.UnboundArguments
        Start-Process -FilePath PowerShell.exe -Verb Runas -ArgumentList $CommandLine
        Exit
    }
}

"`nChecking/mitigating Powershell Execution Policy...`n"
if($(Get-ExecutionPolicy) -ne 'RemoteSigned')
{
`set-executionpolicy remotesigned
}

"`nInitiating settings and dependencies pre-check/installation`n"
"`nChecking/mitigating Remote Desktop...`n"
#Enable-PSRemoting -SkipNetworkProfileCheck -Force
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -name "fDenyTSConnections" -value 0
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -name "UserAuthentication" -Value 1


"`nChecking/mitigating Azure Powershell module...`n"
if ($PSVersionTable.PSEdition -eq 'Desktop' -and (Get-Module -Name AzureRM -ListAvailable)) {
   Write-Warning -Message ('Az module not installed. Having both the AzureRM and ' +
   'Az modules installed at the same time is not supported.')
} else {
     Install-Module -Name Az -AllowClobber -Scope CurrentUser
}

"`nGetting Azure Credentials and authenticating...`n"

Connect-AzAccount

"`nChecking Azure VM status...`n"

#$status =  Get-AzVM `
#   -ResourceGroupName "<RESOURCEGROUPNAMEHERE>" `
#   -Name "<VMNAMEHERE>" -Status

#Write-Output $status.Statuses.DisplayStatus

#if($($status.Statuses.DisplayStatus) -ne "VM deallocated")
#{
#    pause("Please try again later")
#    exit
#}

"`nStarting Azure VM <RESOURCEGROUPNAMEHERE> <VMNAMEHERE>...`n"

Start-AzVM `
   -ResourceGroupName "<RESOURCEGROUPNAMEHERE>" `
   -Name "<VMNAMEHERE>"

"`nGetting public IP of <VMNAMEHERE>...`n"


$ip = Get-AzPublicIpAddress `
   -ResourceGroupName "<RESOURCEGROUPNAMEHERE>"  | Select IpAddress

"`nStarting remote desktop session...`n"

mstsc /v:$($ip.{IpAddress})

Start-Sleep -s 60

$rdpsession = Get-Process mstsc -ErrorAction SilentlyContinue

Write-Output $rdpsession

"`nYour session is up and now being tracked. Listening for remote desktop closure...`n"

$hasd = "false"

for(;;)
{
   $rdpsession = Get-Process mstsc -ErrorAction SilentlyContinue
    if (!$rdpsession) {
        "`nRemote desktop session ended.`n"
        
        Stop-AzVM `
           -ResourceGroupName "<RESOURCEGROUPNAMEHERE>" `
           -Name "<VMNAMEHERE>"

         $hasd = "true"

        "`nStopping <VMNAMEHERE>...`n"

        break
    }
}

Register-EngineEvent PowerShell.Exiting –Action { if($hasd -eq "false")
{
    Stop-AzVM `
           -ResourceGroupName "<RESOURCEGROUPNAMEHERE>" `
           -Name "<VMNAMEHERE>"

        "`nStopping <VMNAMEHERE>...`n"
}}


Function pause ($message)
{
    # Check if running Powershell ISE
    if ($psISE)
    {
        Add-Type -AssemblyName System.Windows.Forms
        [System.Windows.Forms.MessageBox]::Show("$message")
    }
    else
    {
        Write-Host "$message" -ForegroundColor Yellow
        $x = $host.ui.RawUI.ReadKey("NoEcho,IncludeKeyDown")
    }
}
```

Notable fixes that could be implemented:
* Parameterize VMRESOURCENAME
* Parameterize VMNAME
* Currently installing the entire AZ module... which can take some time. Reduce to only necessary dependencies.
* Add functionality for "last off de-allocates" policy.

If you are a PowerShell enthusiast and would like to implement the fixes above, clean up the code, add error handling, or add your own features/fixes, you can go visit the Github Gist where I have posted the above code and collaborate on improvements. (quite frankly I don't have the time to allocate and make it perfect these days so any help improving this script for the community is greatly appreciated).

[Here is the GH Gist](https://gist.github.com/spencer741/9c930c52a991966ae8f69068752eadf9)

Thanks for reading! If there are any typos or grammatical issues, just comment below so I can amend them.

-----

Give me a follow on Dev.to, Github, or LinkedIn if you liked the article and want to see more! If you would like, you can also buy me a coffee. Any support is greatly appreciated!
```
> Connect with me
```
<p>
<a href="https://linkedin.com/in/spencer-arnold741" target="_blank" >
<img  src="https://img.shields.io/badge/linkedin%20-%230077B5.svg?&style=for-the-badge&logo=linkedin&logoColor=white" alt="My LinkedIn profile" width="100" height="50" />
</a>
<a href="https:/Github.com/spencer741" target="_blank">
<img alt="GitHub" src="https://img.shields.io/badge/github%20-%23121011.svg?&style=for-the-badge&logo=github&logoColor=white" width="100" height="50"/>
</a>
</p>

```
> Support me
```
<p>
<a href="https://www.buymeacoffee.com/spencer741" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="200" height="175" ></a>
</p>
```
> Support everybody
```
The ability to transfer money has been a long-standing omission from the web platform. As a result, the web suffers from a flood of advertising and corrupt business models. [Web Monetization](https://webmonetization.org/) provides an open, native, efficient, and automatic way to compensate creators, pay for API calls, and support crucial web infrastructure. [Learn More](https://dev.to/hacksultan/web-monetization-like-i-m-5-1418) about how you can help change the web.

-----