Your Go-To PowerShell Template for HTTP-Triggered Azure Functions

Published:1 May 2024 - 9 min. read

Today’s sponsor is Automox. They have created a big toolbox of small Bash and PowerShell scripts called Worklets to automate your least favorite IT tasks. Here, take a look.

Quit reinventing the wheel and crafting snowflake PowerShell scripts to execute your PowerShell Azure Functions. Join me as I cover how to create a simple framework run script to include a a standard in all of your HTTP-triggered PowerShell functions!

The Anatomy of a PowerShell Script

In the PowerShell world, a script or a function has always has three areas of work; input, processing and output. Each area serves a distinct purpose:

  • To customize script behavior – input
  • To do thew work – processing
  • To tell you what happened – output
When writing a typical PowerShell script, your input will typically be parameters. Parameters define variables you can pass to functions at runtime.

Get-Thing -Name 'thing1' -Value 'value1'
Processing can be nearly anything from formatting strings, restarting services, messing with fil, whatever..

Output always returns objects whether that’s a simple string or some complex object.

PS> Get-Thing -Name 'thing1' -Value 'value1'
thingvalue

PS> Get-Thing -Name 'thing1' -Value 'value1'
[pscustomobject]@{
	Name = 'thing1'
	Value = 'value1'
}
PowerShell scripts/functions always have these major constructs and generally follow the same pattern every time of receiving the same kind of input and returning the same kind output.

The “Before” PowerShell Way

When you’re “converting” PowerShell scripts/functions for the web as in transforming them for Azure Functions, that anatomy changes. You still have the same input, processing and output areas of work but what your script takes as input and outputs is different.

On the web, everything is HTTP and your scripts must accept and speak HTTP.

For example, you may have a script you run locally that restarts a Windows service. That script might have a parameter, restart the requested service and return the service object.

[CmdletBinding()]
param(
	[Parameter(Mandatory)]
	[string]$ServiceName
)


## Returns the service object output from Restart-Service
Restart-Service -Name $ServiceName
The script may even perform the service restart over PowerShell remoting to restart a service on a remote computer.

[CmdletBinding()]
param(
	[Parameter(Mandatory)]
	[string]$ComputerName,
	
	[Parameter(Mandatory)]
	[string]$ServiceName
)


## Returns the service object output from Restart-Service
Invoke-Command -ComputerName $ComputerName -ScriptBlock { Restart-Service -Name $using:ServiceName }
You’d then call the script per the usual:

.restartmyservice.ps1 -ComputerName 'REMOTEPC' -ServiceName 'someservice'
PowerShell alleviates a lot of overhead for you. You can simply pass values to a parameter, have the script process and built-in commands like Restart-Service or Invoke-Command return some kind of object that’s then returned to you.

But now you need to restart a service on an Azure virtual machine, make a change to an Azure website or query an Azure SQL database; the location changes and so must your PowerShell script.

Serverless and Local: A Totally Different Environment

When you build a PowerShell script and intend to execute it on a machine on your network, you naturally make some assumptions:

  • It will run on a piece of hardware you control
  • The script will have access to internal network resources
  • It will execute under some kind of context like a domain user, local user, etc.
  • It will run as long as it’s needed to complete the job
Regardless of what that script does, you can assume a lot of environmental attributes. You have complete control over it’s execution and it’s state. The script could also run for days if you wanted it to. Total control is yours!

In the Azure Functions/serverless world, the stakes are a bit different. When you execute PowerShell in the cloud via HTTP webhooks/triggers, you relinquish some control to alleviate the pains of running your own compute.

You give up the freedom to execute PowerShell however you want in exchange to forego the worry of host reliability.

HTTP is stateless and is generally for short-lived executions.

Out with the Old Parameters

Taking the new environment your script is running in, let’s change up that restart service script you’ve been relying on and start transforming it into an Azure Function.

First, the parameters gotta go. No, you don’t have to get rid of input completely but you do have to remove PowerShell parameters and introduce “HTTP parameters”.

Previously, you had a couple of parameters; ComputerName and ServiceName. In Azure Functions, a function triggered via HTTP must only have two PowerShell parameters; $Request and $TriggerMetadata.

The $Request parameter contains all of the incoming HTTP stuff like the body, headers, source, etc. The $TriggerMetadata parameter contains various other metadata about the function invocation such as how the function was triggered, queue messages, etc.

That’s it. Two PowerShell parameters.

Now, how, you may ask, do you tell your script now which service you’d like to restart? You learn how to “embed” that parameter in the HTTP body.

Just like web APIs, the HTTP body contains the data payload; basically the stuff you want to pass to the endpoint. Sounds familiar? The HTTP body passes information to the script just like your parameters do.

Let’s adjust the script now to accept the required parameters.

[CmdletBinding()]
param(
	[Parameter(Mandatory)]
	$Request,
	
	[Parameter(Mandatory)]
	$TriggerMetadata
)

💡 You don’t even need to make the parameters mandatory because when the script is invoked, those will always be the two parameters passed to it.

Invoking the Azure Functions Script

We have the $Request parameter defined in the new script but what does it even look like? How do you even invoke this script to begin with to tell? HTTP!

Using Invoke-WebRequest or Invoke-RestMethod, you can invoke this script that’s embedded in an Azure Function. The URI will follow the format https://<function app name>.azurewebsites.net/<function name>?code=optionalcodehereforprivateauthentication.

Let’s say we have an Azure Function App created called MyApp and a function inside of it called RestartService. I could call the function like this:

Invoke-RestMethod -Uri 'https://myapp.azurewebsites.net/restartservice?code=.....'

💡 You can get any Azure function’s base URL with PowerShell by running:

PS> $functionApp = Get-AzFunctionApp -ResourceGroupName $resourceGroupName -Name $functionAppName
PS> $functionApp.HostName | ForEach-Object { "https://$_/$functionName }

I want to see what the Request parameter holds when I call this function so I’ll add a Write-Host reference like I usually do in the script and deploy the app (with the script) to Azure.

[CmdletBinding()]
param(
	[Parameter(Mandatory)]
	$Request,
	
	[Parameter(Mandatory)]
	$TriggerMetadata
)

Write-Host $Request
Deployment in Visual Studio Code
Once the app has been zipped up, sent up to Azure and is ready, I’ll invoke the script with Invoke-RestMethod and see what happens….

Absolutely nothing.

Why? Because all of your common “Write” cmdlets don’t work how you’re used to in the web world. There’s no verbose, information, error, output stream anymore. Your communication must go over the web; not over PowerShell.

Let’s Get Some Output with HTTP Bindings

To get some kind of output back, you must “bind” that output to what Azure Functions calls an HTTP binding. That essentially means you need to attach any output from your script to the HTTP response going out once the script is finished executing.

To bind your script output to HTTP, you must use the Push-OutputBinding cmdlet. This cmdlet references a pre-existing output binding and adds a value to it.

Function bindings are always stored in the function.json file that sits inside of your Azure function.

A single input binding called Request and an output binding called Response.
To “bind” PowerShell output to function output, you must use Push-OutputBinding to attach the PowerShell output to the output binding name. In this case, it’s Response.

But, you can’t just assign the output directly to the Value parameter of Push-OutputBinding.

[CmdletBinding()]
param(
	[Parameter(Mandatory)]
	$Request,
	
	[Parameter(Mandatory)]
	$TriggerMetadata
)


Push-OutputBinding -Name Response -Value $Request
If you do, you will receive a nice HTTP/500 error indicating failure. Push-OutputBinding can’t full translate the PowerShell object to HTTP-speak.

To ensure your output comes back correctly, you must use the System.Net.HttpResponseContext object. You can create that by creating a hashtable with a Body key and assign your output to the Body key. Then, casting that hashtable to the HttpResponseContext and assigning that as the Value.

using namespace System.Net

[CmdletBinding()]
param(
[Parameter(Mandatory)]
$Request,

[Parameter(Mandatory)]
$TriggerMetadata
)

$response = @{
Body = $Request
}

Push-OutputBinding -Name Response -Value ([HttpResponseContext]$response)

💡 Don’t forget the using namespace System.Net line. This loads the .NET assembly that the HTTPResponseContext type needs.

Deploy changes, invoke the URI again and you’ll see that the request returns the contents of the Request parameter! This is starting to look like a PowerShell function again!

Output from PowerShell Azure function

Adding “PowerShellEsque” Parameters

In a PowerShell script, we have the luxury of defining parameters and adding features like mandatory, parameter types, parameter validation, dynamic parameters and so on. We can do a lot with parameters but in an Azure Function, when your input is an HTTP request represented by a single $request variable, your options are limited. All “parameters” to the script are held in the $request hashtable. But, with some ingenuity, you can mimic that same functionality. By enumerating the key/value pairs in the $request hashtable, you can process the parameters however you’d like.

For example, I know I only want to allow certain parameters in this script. Since my official PowerShell parameters are just $request and $TriggerMetadata, I can enumerate the key/value pairs in the $request hashtable to mimic those parameters.

Creating Variables from HTTP Query Parameters

Maybe I want this Azure Function to have two parameters; Name and Value. In a typical PowerShell environment, you could reference a $Name and $Value variable inside of the script.

[CmdletBinding()]
param(
	[Parameter()]
	$Name,
	
	[Parameter()]
	$Value
)

Write-Host "The Name parameter is $Name"
Write-Host "The Value parameter is $Value"
In an Azure Function, you must “convert” hashtable keys to variables to reference them the same way by enumerating the hashtable and creating new variables with the New-Variable cmdlet.

$Request.Query.GetEnumerator() | ForEach-Object {
	New-Variable -Name $_.Key -Value $_.Value
}
Then, you’re free to reference $Name and $Value anywhere in the script.

using namespace System.Net

[CmdletBinding()]
param(
	[Parameter(Mandatory)]
	$Request,
	
	[Parameter(Mandatory)]
	$TriggerMetadata
)

$Request.Query.GetEnumerator() | ForEach-Object {
	New-Variable -Name $_.Key -Value $_.Value
}

$response = @{
	Body = "The name passed was [$Name] with value of [$Value]
}

Push-OutputBinding -Name Response -Value ([HttpResponseContext]$response)
But, you don’t want to accept all HTTP query parameters. Just like in a typical PowerShell script, you only want to allow certain parameters and mimic mandatory parameters. In that case, you can perform some checks against known parameters and throw an error if you want any of them to be mandatory.


using namespace System.Net

[CmdletBinding()]
param(
	[Parameter(Mandatory)]
	$Request,
	
	[Parameter(Mandatory)]
	$TriggerMetadata
)

$response = @{
	Body = $null
}

## Define all parameters (mandatory or optional)
$allowedQueryParams = @('Name','Value')

## Define all of the parameters that must have a value
$requiredParams = @('Name')

## If any HTTP query params passed are not recognized, stop
[array]$notRecognizedParams = Compare-Object $allowedQueryParams @($request.Query.Keys) | Select-Object -ExpandProperty InputObject
if ($notRecognizedParams.Count -gt 0) {
	throw "The parameter(s) [$($notRecognizedParams -join ',')] were not recognized!"
}

## Ensure all mandatory parameters were passed
[array]$missingMandatoryParams = $requiredParams | Where-Object { $_ -notin @($request.Query.Keys) }
if ($missingMandatoryParams.Count -gt 0) {
	throw "Missing mandatory parameter(s) [$($missingMandatoryParams -join ',')]!"
}

## If all parametere were recognized and all mandatory parameters were used, now create variables from them
$Request.Query.GetEnumerator() | ForEach-Object {
	New-Variable -Name $_.Key -Value $_.Value
}

$response = @{
	Body = "The name passed was [$Name] with value of [$Value]"
}

Push-OutputBinding -Name Response -Value ([HttpResponseContext]$response)
When you execute the function now, you’ll see the function will only accept the parameters you defined and even has “mandatory” parameters.

Validating Azure Functions parameter validation

The Final Touch: Creating Descriptive HTTP Errors

Now that we have a great script built out to add to an Azure Function, let’s add a little bit of error handling to it. You saw from the screenshot above that when the script doesn’t have the right parameters, it returns an HTTP/500 error but just says Internal Server Error. We can make this more descriptive by sending our own HTTP status code.

The final script below:

  • Wraps all of the code inside of a try/catch block.
  • Defines a custom HTTP status code in the response
  • Defines the exception message as the response body
  • Returns the HTTP response in the finally block to return the response whether or not an exception was thrown

using namespace System.Net

[CmdletBinding()]
param(
	[Parameter(Mandatory)]
	$Request,
	
	[Parameter(Mandatory)]
	$TriggerMetadata
)

$response = @{}

try {

	## Define all parameters (mandatory or optional)
	$allowedQueryParams = @('Name','Value')
	
	## Define all of the parameters that must have a value
	$requiredParams = @('Name')
	
	## If any HTTP query params passed are not recognized, stop
	[array]$notRecognizedParams = Compare-Object $allowedQueryParams @($request.Query.Keys) | Select-Object -ExpandProperty InputObject
	if ($notRecognizedParams.Count -gt 0) {
		throw "The parameter(s) [$($notRecognizedParams -join ',')] were not recognized!"
	}
	
	## Ensure all mandatory parameters were passed
	[array]$missingMandatoryParams = $requiredParams | Where-Object { $_ -notin @($request.Query.Keys) }
	if ($missingMandatoryParams.Count -gt 0) {
		throw "Missing mandatory parameter(s) [$($missingMandatoryParams -join ',')]!"
	}
	
	## If all parametere were recognized and all mandatory parameters were used, now create variables from them
	$Request.Query.GetEnumerator() | ForEach-Object {
		New-Variable -Name $_.Key -Value $_.Value
	}
	
	$response.Body = "The name passed was [$Name] with value of [$Value]"

} catch {
	$response.StatusCode = [HttpStatusCode]::InternalServerError
	$response.Body = ($_.Exception | Out-String)
} finally {
	Push-OutputBinding -Name Response -Value ([HttpResponseContext]$response)
}
Returning HTTP status code and messages
You should now have a great template to use with your Azure Functions PowerShell functions!

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!