Working on Virtual Machines Start / Stop automation, that I have done as functions in my PowerShell Module (mrv_module)  that was running in Azure Automation Hybrid Worker quite efficiently, I decided to take a look into an improved solution that may be running as Azure Functions.

The reason for that was scalability, additional functionality, integration and much more – I just wanted to try something new and create a  scalable serverless solution, that may be seamlessly integrated with Azure.

As my activity was related to interaction with Azure Subscription or at least with Virtual Machines – the first thing I have checked was: “How I can authenticate against Azure Subscription”.

Doing my research I have found that there is a cool feature, that is currently in Preview, called “Managed Service Identity (MSI)”:

In few words – it allows you to get an authentication token and authenticate you session agains Azure Resources (Subscription, VM, Storage) as  the Service Principle that has been created for the Virtual Machine or Application Service.

MSI for Azure Function

To get this bit working – it was easy:

  • Create MSI for your Azure Function as per documentation:
    • Go to Your Function
    • Navigate to “Platform Features” and choose “Managed Service Identity”
    • Enable “Managed Service Identity”
    • Wait for the registration to complete
  • Now we need to grant some permissions to the MSI SPN that has been just created. While for the real implementation it is highly recommended to go with least privilege approach and even potentially create a custom role, I have assigned this SPN “Contributor” permissions, as this implementation is for testing purposes only. To do this you need:
    • Go to  the Subscriptions in Azure Portal. Choose subscription you want to assign permissions to and choose IAM. Press Add
    • Chose the role you want to assign, “Contributor” in my case, and looks for SPN with the name of your Azure Function.
    • Confirm Permissions.

The first part is done. Lets try to use is as per Microsoft guidance and try to login to Azure…

It didn’t work for me straight away for 2 reasons:

  • Failed to get a token
  • Failed to Load PowerShell Modules.

Get a token

Unfortunately documentation that describe login to the portal do not mention or doing any cross links to the fact that MSI Endpoint is dynamic and got changed in runtime.

It is stored within the environment variable MSI_ENDPOINT

Updated code from the article should look like below, as it was written in the main article:

Write-Output "PowerShell Timer trigger function executed at:$(get-date)";
# Get an access token for the MSI
Write-Output "Endpoint: [$($env:MSI_ENDPOINT)]" 
$apiVersion = "2017-09-01"
$resourceURI = "https://management.azure.com/"
$tokenAuthURI = $env:MSI_ENDPOINT + "?resource=$resourceURI&api-version=$apiVersion"
$tokenResponse = Invoke-RestMethod -Method Get -Headers @{"Secret" = "$env:MSI_SECRET"} -Uri $tokenAuthURI
$accessToken = $tokenResponse.access_token

Next we should be able to login with the following code:

# Use the access token to sign in under the MSI service principal. -AccountID can be any string to identify the session.
Login-AzureRmAccount -AccessToken $accessToken -AccountId "MSI@myfunc01"

But I have encouraged the following issue:

Endpoint: [http://127.0.0.1:41289/MSI/token/]
Parameter set cannot be resolved using the specified named parameters.
at run.ps1: line 11
+ 
+ 
    + CategoryInfo          : InvalidArgument: (:) [Add-AzureRmAccount], ParameterBindingException
    + FullyQualifiedErrorId : AmbiguousParameterSet,Microsoft.Azure.Commands.Profile.AddAzureRMAccountCommand,run.ps1
Exception while executing function: Functions.TimerTriggerPowerShell1. Microsoft.Azure.WebJobs.Script: PowerShell script error. System.Management.Automation: Parameter set cannot be resolved using the specified named parameters.
Function completed (Failure, Id=a31f6133-cd66-470d-84b2-9206190efda7, Duration=489ms)

It looks like [Add-AzureRmAccount], ParameterBindingException does not know how to login with token.

My first thought was it may be Azure Modules. And I was correct.

I have used information from this article to check what modules I have. They were very old…. So it looks like we need to Bring Our Own Modules.

There is a awesome suggestion around modules on GitHub as well as example how to load you modules without any additional errors.

I have tried – and it worked. But I have found one other thing – you need to load all your modules explicitly, other wise old modules can be partially loaded and you script will fail, as they would not share the same Context.

Good example could be that you have loaded only “AzureRM.Profile” module and you have successfully logged in, but Get-AzureRMVM will fail:

Get-AzureRMVM : Run Login-AzureRmAccount to login.
at run.ps1: line 37
+ Get-AzureRMVM
+ _____________
    + CategoryInfo          : InvalidOperation: (:) [Get-AzureRmVM], PSInvalidOperationException
    + FullyQualifiedErrorId : InvalidOperation,Microsoft.Azure.Commands.Compute.GetAzureVMCommand
Exception while executing function: Functions.VMPowerAssessment. Microsoft.Azure.WebJobs.Script: PowerShell script error. Microsoft.Azure.Commands.ResourceManager.Common: Run Login-AzureRmAccount to login.

To avoid this, as previously mentioned all modules need to be loaded. It kinda sounds like not a big deal, but measuring how much time it takes – I got scared:

@{Minutes=1; Seconds=16; Milliseconds=657}

Taking into account that on “Consumption” plan allows you to run your function up to 5 or 10 minutes – 1 minutes looks a lot.

I thought that I need to find a way to decrease it, and decided to try to prevent “Old preinstalled” modules from being loaded. So I decided to try to remove them from environment variable.

My working version of  the script looked like this:

Write-Output "PowerShell Timer trigger function executed at:$(get-date)";
# Get an access token for the MSI
Write-Output "Endpoint: [$($env:MSI_ENDPOINT)]" 
$apiVersion = "2017-09-01"
$resourceURI = "https://management.azure.com/"
$tokenAuthURI = $env:MSI_ENDPOINT + "?resource=$resourceURI&api-version=$apiVersion"
$tokenResponse = Invoke-RestMethod -Method Get -Headers @{"Secret" = "$env:MSI_SECRET"} -Uri $tokenAuthURI
$accessToken = $tokenResponse.access_token

Write-Output "Validating PSModulePath..."
$oldPSModulePath = $env:PSModulePath
$modulePathEntries = @($env:PSModulePath -split ';')
$localModulesFolder = "${env:HOME}\site\wwwroot\Modules\"
$builtinModulesFolderPattern = 'D:\Program Files (x86)\Microsoft SDKs\Azure\PowerShell\*'
if ($modulePathEntries -notcontains '$localModulesFolder')
{
    Write-Output "Updating PSModulePath to include [$localModulesFolder]"
    $env:PSModulePath += ";${localModulesFolder}"
}
else
{
    Write-Output "PSModulePath already includes [$localModulesFolder]. No modifications needed"
}

If (($modulePathEntries | % {$_ -like $builtinModulesFolderPattern}) -contains $true)
{
    Write-Output "Removing Standard Azure RM modules from PSENVPath"
    $env:PSModulePath = ($env:PSModulePath.Split(';') | Where-Object { $_ -notlike $builtinModulesFolderPattern }) -join ';'
}

Write-Output "Loading Modules"
Measure-Command { `
    Write-Output "Loading MRVModules"
    Import-Module "D:\home\site\wwwroot\Modules\mrv_module\mrv_module.psd1" -Global;
} | Select-Object Minutes, Seconds, Milliseconds
Write-Output "All Modules loaded."


Write-output "PSedition [$($PSVersionTable.PsEdition)]"
Login-AzureRmAccount -AccessToken $accessToken -AccountId "MSI@50342"

I have used this code as the beginning of all my functions to ensure that the only Azure Modules available through the environment variable would be the path defined in $localModulesFolder variable. Please update it with location you put your modules.