Use Azure Function to start VMs

An use-case came up recently to provide a little bit of self-service to some users with an occasionally used Virtual Machine in Azure. I have configured Auto-Shutdown on the VM, but want an ability for users to turn the VM on at their convenience.

Traditionally, this would be done by using Role-Based Access Control (RBAC) over the resource group or VM to allow users to enter portal.azure.com and start the VM with the GUI.

However I wanted to streamline this process without having to manage individual permissions, due to the low-risk of the resource. To do so, I’m using an Azure Function (v2. PowerShell) to start all the VMs in a resource group.

 

First create your function app (Microsoft Docs link) as a PowerShell app – this is still in preview as a Function V2 stack, but it is effective.

The next thing I did was create a Managed Identity in my directory for this Function app. I wanted to ensure that the code the Function runs is able to communicate with the Azure Resource Manager, but did not want to create and manage a dedicated Service Principal.

Within the Function App Platform Features section, I created a Managed Identity for it to authenticate against my directory to access resources:

Go to “Identity”:

Switch “System Assigned” to ON and click Save:

With the Managed Identity now created, you can go to your Subscription or Resource Group, and add a Role assignment under “Access control (IAM)”:

Lastly, I developed the following code to place into the function (github gist):

using namespace System.Net

# Input bindings are passed in via param block.
param($Request, $TriggerMetadata)

# Interact with query parameters or the body of the request.
$rgname = $Request.Query.resourcegroup
if (-not $rgname) {
    $rgname = $Request.Body.resourcegroup
}
$action = $Request.Query.action
if (-not $action) {
    $action = $Request.Body.action
}
$subscriptionid = $Request.Query.subscriptionid
if (-not $subscriptionid) {
    $subscriptionid = $Request.Body.subscriptionid
}
$tenantid = $Request.Query.tenantid
if (-not $tenantid) {
    $tenantid = $Request.Body.tenantid
}

#Proceed if all request body parameters are found
if ($rgname -and $action -and $subscriptionid -and $tenantid) {
    $status = [HttpStatusCode]::OK
    Select-AzSubscription -SubscriptionID $subscriptionid -TenantID $tenantid
    if ($action -ceq "get"){
        $body = Get-AzVM -ResourceGroupName $rgname -status | select-object Name,PowerState
    }
    if ($action -ceq "start"){
        $body = $action
        $body = Get-AzVM -ResourceGroupName $rgname | Start-AzVM
    }
}
else {
    $status = [HttpStatusCode]::BadRequest
    $body = "Please pass a name on the query string or in the request body."
}

# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = $status
    Body = $body
})

To provide secure access, I left my Function app with anonymous authentication, but added a new Function Key which I could use and control when calling this function. This is found under the “Manage” options of the function:

To test, I called the function externally like this, passing in my request body parameters and using the Function Key that was generated. You can grab the URL for your function right near the top “Save” button when you’re editing it in the Portal.

$Body = @"
{
    "resourcegroup": "source-rg",
    "action": "start",
    "subscriptionid": "SUBID",
    "tenantid": "TENANTID"
}
"@
$URI = "https://hostname.azurewebsites.net/api/startVMs?code=FUNCTIONKEY"
Invoke-RestMethod -Uri $URI -Method Post -body $body

 

If I run this with the “get” action, then the Function will return the status of each VM in the resource group:

3 thoughts to “Use Azure Function to start VMs”

  1. Quick comment,
    Thank you this was awesome. it is still hard to find good examples of this.
    with the latest v2 functions I had a lot of trouble with the parameters.

    I had to add the following after the param statement to convert the body back from Json to use the body parameters. they were blank/null trying to use $request.body.variable

    $ReqBody = $Request.Body | ConvertFrom-Json
    then it will find the parameter(s)
    $ReqBody.tenantid
    for example

  2. Hi Jeff,

    I have the same requirement, but my function is not getting authorization, it is not working and getting the following errors, highly appreciate if any help on these errors.

    2022-02-23T16:09:34.742 [Error] ERROR: Cannot validate argument on parameter ‘Subscription’. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.Exception :Type : System.Management.Automation.ParameterBindingValidationExceptionMessage : Cannot validate argument on parameter ‘Subscription’. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.ParameterName : SubscriptionParameterType : stringErrorId : ParameterArgumentValidationErrorLine : 29Offset : 43CommandInvocation :MyCommand : Set-AzContextScriptLineNumber : 29OffsetInLine : 5HistoryId : 1ScriptName : C:\home\site\wwwroot\HttpTrigger1\run.ps1Line : Select-AzSubscription -SubscriptionID $subid -TenantID $tenantidPositionMessage : At C:\home\site\wwwroot\HttpTrigger1\run.ps1:29 char:5+ Select-AzSubscription -SubscriptionID $subid -TenantID $tenantid+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~PSScriptRoot :

    1. Hi Harry,

      I took a look at your error message, and then the original code on my post. When the “Select-AzSubscription” cmdlet is called about halfway down, I was passing a variable of $subid which is non-existent (perhaps a leftover from when I sanitized the code for posting).

      I’m guessing if you copied/pasted from my post, you can update that variable to $subscriptionid and it will function.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.