0 Comments

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.

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 course Azure Functions Fundamentals.
Vote on HN
comments powered by Disqus