Deploy an Azure Function app with PowerShell

AI generated image of people writing code

Deploying updated code to an Azure Function App can be done through a variety of methods, and one of those methods is by uploading a zip file to the Function App via the Kudu REST API.

To start, go to your Function App in the Azure Portal and download the publish profile. This contains the ZipDeploy URL and the username and password that will be needed. You can do this from the Overview page of the Function App:

Location showing where the publish profile can be downloaded
Download the publish profile for your Function App

This will download an xml file that contains a few different publish profiles that you can use. In this case, look for the ZipDeploy profile and take note of the publishURL, userName and userPWD. The URL will take the form of FunctionAppName.scm.azurewebsites.net, the username will be $FunctionAppName and the password will be a long, randomised string.

The ZipDeploy publish profile
The ZipDeploy publish profile

Next you will need to make sure that SCM Basic Auth Publishing Credentials are enabled in the General Settings for your Function App. This can be found under Settings > Configuration > General Settings:

Updating the configuration of your Function App to enable SCM Basic Auth Publishing Credentials
Enable SCM Basic Auth Publishing Credentials

With this done let’s start coding the deployment, starting with the creation of the zip file. The root directory that you zip should contain the files that you want in the wwwroot folder of your Function App. Typically this will include your functions, a Modules folder, and a host.json file (and perhaps other files depending on the scripting language you selected for your Function App).

Let’s also say you have a .funcignore file that has a list of files or folders that are in the folder structure of the Function App but should not be included in the deployment.

So, your Function App source folder might look something like this:

Typical Function App folder structure
Typical Function App folder structure

Here’s the code to create the zip file:

$FolderPath = "C:\FunctionApps\KevinStreetsFunctionApp"
$Exclude = Import-Csv -Path "$FolderPath\.funcignore" -Header "Exclude"
$Destination = "$FolderPath\KevinStreetsFunctionApp.zip"
$Files = Get-ChildItem -Path $FolderPath -Exclude $Exclude.Exclude
# Create the zip file
Compress-Archive -Path $Files -DestinationPath $Destination -CompressionLevel Fastest

Now that the zip file has been created it is time to deploy it. Start by defining some variables with the publishURL, userName and userPWD you noted down earlier. Also, update the publishURL to include the correct API and include “isAsync=true” which will immediately return a response that you can use to monitor the deployment status.

$DeploymentUrl = 'https://kevinstreetsfunctionapp.scm.azurewebsites.net'
$Username = '$KevinStreetsFunctionApp'
$Password = 'Hnsl114TmGlpmEis9afDe5KEZoqvJDpcNZyri8ucAJx2b2uSoKhd4ovlCg9Q'
$ApiUrl = "$($DeploymentUrl):443/api/zipdeploy?isAsync=true"

Create the base-64 encoded string to pass in the Authorization header when we make the request:

$Base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $Username, $Password)))

Next create a variable to define the zip file location:

$ZipFileLocation = "C:\FunctionApps\KevinStreetsFunctionApp\KevinStreetsFunctionApp.zip"

And finally deploy!

$Result = Invoke-WebRequest -Uri $ApiUrl -Headers @{Authorization=("Basic {0}" -f $Base64AuthInfo)} -Method POST -InFile $ZipFileLocation -ContentType "multipart/form-data"

As mentioned above, this should return immediately, and you can this by checking the content of the $Result variable. If all has gone well, you will get a StatusCode 202 meaning it has been accepted.

Feedback from the deployment showing that the deployment has been accepted and the URL that can be used to monitor the deployment
Deployment has been accepted

As previously mentioned, you can now monitor the status of the deployment. The URL you use to monitor the deployment is contained in the Headers in the return of the previous request. You can view it by looking in $Result.Headers.Location:

The URL to monitor the deployment
$Result.Headers.Location

To query the status of the deployment, run the following command:

$Complete = Invoke-WebRequest -Uri $($Result.Headers.Location) -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -Method GET
($Complete.Content | ConvertFrom-Json).provisioningState

This will likely return one of two statuses: InProgress or Succeeded. In my experience it takes very little time to complete the deployment. I like to create a small loop that checks the status every 5 seconds until the status is completed:

do {
    $Complete = Invoke-WebRequest -Uri $($Result.Headers.Location) -Headers @{Authorization=("Basic {0}" -f $Base64AuthInfo)} -Method GET
    Write-Host -Object "Current deployment progress: $(($Complete.Content | ConvertFrom-Json).provisioningState)"
    if ($(($Complete.Content | ConvertFrom-Json).provisioningState) -ne "Succeeded") {
        Start-Sleep -Seconds 5
    }
}
until ($(($Complete.Content | ConvertFrom-Json).provisioningState) -eq "Succeeded")

This gives me a nice output that shows when my deployment has completed:

Monitoring status of deployment by querying the monitoring URL every 5 seconds until the deployment has succeeded
Monitoring status of deployment

That is the foundation needed to deploy a Function App with PowerShell. If you have multiple Function Apps you could wrap this code around a loop to deploy each in turn or include other configuration changes as part of a deployment pipeline.

Unable to permanently delete mail enabled user. The mail enabled user has litigation hold or In-Place hold applied on it. Please remove the hold before trying to delete

AI generated image of a frustrated user in front of a terminal

Here’s a fun little problem that had me stumped for a while until I figured out the correct sequence of commands to run.

Take the following scenario:

You created a MailUser in Exchange Online but for some reason you need to delete it. You attempt to remove it with the command:

Get-MailUser -Identity <MailUser> | Remove-MailUser

But you get the following error: “This mail enabled user cannot be permanently deleted since there is a user associated with this mail enabled user in Azure Active Directory. You will first need to delete the user in Azure Active Directory. Please refer to documentation for more details.”

Oh… okay then. So you remove the user in Microsoft Entra ID (Azure AD) and also make sure it has been removed from Deleted users. Now you try to remove the MailUser in Exchange Online again, this time running:

Get-MailUser -Identity <MailUser> -SoftDeletedMailUser | Remove-MailUser -PermanentlyDelete

But now you get a new error: “Unable to permanently delete mail enabled user. The mail enabled user has litigation hold or In-Place hold applied on it. Please remove the hold before trying to delete”

This is probably due to a default policy that is applied to all UserMailbox / MailUser objects in your organisation. Not a problem… except, how do you remove litigation hold / in-place hold from an object that is now soft-deleted? Especially considering that you cannot restore it because you already deleted the user in Entra ID.

Thankfully the answer is fairly straightforward, just not completely intuitive in my opinion. Run the following command:

Get-MailUser -Identity <MailUser> -SoftDeletedMailUser | Set-MailUser -RemoveLitigationHoldEnabled

Once that is done you can run your original command again to remove the MailUser:

Get-MailUser -Identity <MailUser> | Remove-MailUser

Easy when you know how!