Posted in:

In my Azure Functions Fundamentals Pluralsight course, I focused on deploying functions using Git. In many ways this should be thought of as the main way to deploy your Azure Functions apps because it really is so straightforward to use.

Having said that, there are sometimes reasons why you might not want to take this approach. For example, your Git repository may contain source code for several different applications, and while there are ways to help Kudu understand which folder contains your functions, it may feel like overkill to clone loads of code unrelated to the deployment.

Another reason is that you might want to have a stricter separation of pushing code to Git and deploying it. Of course you can already do this when deploying with Git – you can have a separate remote that you push to for deployment, or even configure a certain branch to be the one that is monitored. But nevertheless you might want to protect yourself from a developer issuing the wrong Git command and accidentally sending some experimental code live.

So in this post, I want to show how we can use PowerShell to call the Kudu REST API to deploy your function app on demand by pushing a zip file.

Update: There is now a new and improved "zip deploy" Kudu API that should be used in preference to the one discussed in this post. I've blogged about it here

First of all, we want to create a zip file containing our function app. To help me do that, I’ve created a simple helper function that zips up the contents of our functions folder with a few exclusions specified:

Function ZipAzureFunction(
    [Parameter(Mandatory = $true)]
    [String]$functionPath,
    [Parameter(Mandatory = $true)]
    [String]$outputPath
)
{
  $excluded = @(".vscode", ".gitignore", "appsettings.json", "secrets")
  $include = Get-ChildItem $functionPath -Exclude $excluded
  Compress-Archive -Path $include -Update -DestinationPath $outputPath
}

And now we can use this to zip up our function:

$functionAppName = "MyFunction"
$outFolder = ".\deployassets"
New-Item -ItemType Directory -Path $outFolder -Force
$deployzip = "$outFolder\$functionAppName.zip"

If (Test-Path $deployzip) {
    Remove-Item $deployzip # delete if already exists
}

ZipAzureFunction "..\funcs" $deployzip

Next, we need to get hold of the credentials to deploy our app. Now you could simply download the publish profile from the Azure portal and extract the username and password from that. But you can also use Azure Resource Manager PowerShell commands to get them. In order to do this, we do need to sign into Azure, which you can do like this:

# sign in to Azure
Login-AzureRmAccount

# find out the current selected subscription
Get-AzureRmSubscription | Select SubscriptionName, SubscriptionId

# select a particular subscription
Select-AzureRmSubscription -SubscriptionName "My Subscription"

Note that this does prompt you to enter your credentials, so if you want to use this unattended, you would need to set up a service principal instead or just use the credentials from the downloaded publish profile file.

But having done this, we can now get hold of the username and password needed to call Kudu:

$resourceGroup = "MyResourceGroup"
$functionAppName = "MyFunctionApp"
$creds = Invoke-AzureRmResourceAction -ResourceGroupName $resourceGroup -ResourceType Microsoft.Web/sites/config `
            -ResourceName $functionAppName/publishingcredentials -Action list -ApiVersion 2015-08-01 -Force

$username = $creds.Properties.PublishingUserName
$password = $creds.Properties.PublishingPassword

Now we have the deployment credentials, and the zip file to deploy. The next step is to actually call the Kudu REST API to upload our zip. We can do that using this helper function:

Function DeployAzureFunction(
    [Parameter(Mandatory = $true)]
    [String]$username,
    [Parameter(Mandatory = $true)]
    [String]$password,
    [Parameter(Mandatory = $true)]
    [String]$functionAppName,
    [Parameter(Mandatory = $true)]
    [String]$zipFilePath    
)
{
  $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))
  $apiUrl = "https://$functionAppName.scm.azurewebsites.net/api/zip/site/wwwroot"
  Invoke-RestMethod -Uri $apiUrl -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -Method PUT -InFile $zipFilePath -ContentType "multipart/form-data"
}

Which we can then easily call:

DeployAzureFunction $username $password $functionAppName $deployzip

This works great, but there is one caveat to bear in mind. It won’t delete any existing files in the site/wwwroot folder. It simply unzips the file and overwrites what’s already there. Normally this is fine, but if you had deleted a function so it wasn’t in your zip file, the version already uploaded would remain in place and stay active.

There are a couple of options here. One is to use the VFS part of the Kudu API to specifically delete a single function. Unfortunately, it won’t let you delete a folder with its contents, so you have to recurse through and delete each file individually before deleting the folder. Here’s a function I made to do that:

Function DeleteAzureFunction(
    [Parameter(Mandatory = $true)]
    [String]$username,
    [Parameter(Mandatory = $true)]
    [String]$password,
    [Parameter(Mandatory = $true)]
    [String]$functionAppName,
    [Parameter(Mandatory = $true)]
    [String]$functionName
)
{
  $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))
  $apiUrl = "https://$functionAppName.scm.azurewebsites.net/api/vfs/site/wwwroot/$functionName/"
  
  $files = Invoke-RestMethod -Uri $apiUrl -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -Method GET
  $files | foreach { 
    
    $fname = $_.name
    Write-Host "Deleting $fname"
    # don't know how to get the etag, so tell it to ignore by using If-Match header
    Invoke-RestMethod -Uri $apiUrl/$fname -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo); "If-Match"="*"} -Method DELETE
  }

  # might need a delay before here as it can think the directory still contains some data
  Invoke-RestMethod -Uri $apiUrl -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -Method DELETE
}

It sort of works, but is a bit painful due to the need to recurse through all the contents of the function folder.

Another approach I found here is to make use of the command Kudu API and instruct it to delete either our whole function app folder at site/wwwroot, or a specific function as I show in this example:

Function DeleteAzureFunction2(
    [Parameter(Mandatory = $true)]
    [String]$username,
    [Parameter(Mandatory = $true)]
    [String]$password,
    [Parameter(Mandatory = $true)]
    [String]$functionAppName,
    [Parameter(Mandatory = $true)]
    [String]$functionName
)
{
  $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))
  $apiUrl = "https://$functionAppName.scm.azurewebsites.net/api/command"
  
  $commandBody = @{
    command = "rm -d -r $functionName"
    dir = "site\\wwwroot"
  }

  Invoke-RestMethod -Uri $apiUrl -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -Method POST `
        -ContentType "application/json" -Body (ConvertTo-Json $commandBody) | Out-Null
}

This is nicer as it’s just one REST method, and you could use it to clear out the whole wwwroot folder if you wanted a completely clean start before deploying your new zip.

The bottom line is that Azure Functions gives you loads of deployment options, so there’s bound to be something that meets your requirements. Have a read of this article by Justin Yoo for a summary of the main options at your disposal.

Want to learn more about how easy it is to get up and running with Azure Functions? Be sure to check out my Pluralsight courses Azure Functions Fundamentals and Microsoft Azure Developer: Create Serverless Functions

Comments

Comment by Knut

Hey, Is there a way to create a Azure Function App with powershell? seen it done with json template, but wondering if it is possible to do with just script, like new-AzureRmResource . Just need an exsample if there is one :)

Knut
Comment by Mark Heath

it's not something I've done myself, but really a function app is just a special type of web app, so maybe the powershell commandlets to create an App Service Plan and a WebApp have some flags that can specify a function app

Mark Heath
Comment by DisqusTech

What would be the command to just zap all the folders and files at "site\wwwroot". I tried
$commandBody = @{
command = "rm .\* -recurse"
dir = "site\\wwwroot"
}
but that didn't work.

DisqusTech
Comment by Mark Heath

hmm not sure what the exact options you'd need for rm are. You might need to escape the backslash before *. Or you could try something like "del /S /F /Q .\\"

Mark Heath
Comment by Gayatri

Hello,
I am successfully uploaded my azure function zip file but may be it not deployed into the azure function but files are uploaded into the "site\wwwroot" path of the azure function what will be i do for deployed or hookup the timer with the my function?

Gayatri
Comment by Mark Heath

I'm not sure I understand. The functions are supposed to get uploaded to site\wwwroot. One way to learn how it works, is to first do a manual deployment with Visual Studio of your function app, and then use the Kudu site (myfuncapp.scm.azurewebsites...) to have a look and see where your code/binaries get placed on disk.

Mark Heath
Comment by David Ebbo

Note that we are not recommending the use of the zip API to deploy functions, as it does not sync triggers (in Consumption mode), and does not propagate deletions. Instead, the newer 'zipdeploy' API is the way to go: https://github.com/projectk...

David Ebbo
Comment by Mark Heath

yes, the new zipdeploy api is really nice. I've got a post on that too - https://markheath.net/post/...
I'll update this post to recommend the newer technique.

Mark Heath