loading...

PowerShell Core and Azure Functions, how-to

omiossec profile image Olivier Miossec ・6 min read

PowerShell in Azure Functions v2 can be simple. You open the portal, write a few lines of code and that all.
It's true for a POC or some tests. But once you need real resources it's not the same.

Let's start by the beginning, the name of the function App where the functions will be run unless you plan to create a serverless API in PowerShell the name of the function App doesn't have so much importance. It just needs to be explicit. But remember the name, is also the part of the function App URL (xxx.azurewebsite.com) and it must be globally unique.

To deploy a new function App, I use an ARM Template like this one

{
    "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "functionAppName": {
            "type": "string"
        },
        "location": {
            "type": "string",
            "defaultValue": "[resourceGroup().location]"
        }
    },
    "variables": {
        "hostingPlanName": "[concat(parameters('functionAppName'), '-plan')]",
        "storageAccountName": "[concat('stofunc', uniquestring(resourceGroup().id))]",
        "runtimeStack": "powershell",
        "timezone": "UTC"
    },
    "resources": [
        {
            "name": "[parameters('functionAppName')]",
            "type": "Microsoft.Web/sites",
            "apiVersion": "2018-02-01",
            "location": "[parameters('location')]",
            "kind": "functionapp",            
            "dependsOn": [
                "[resourceId('Microsoft.Web/serverfarms/', variables('hostingPlanName'))]",
                "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
            ],
            "identity": {
                "type": "SystemAssigned"
            },
            "properties": {
                "siteConfig": {
                    "appSettings": [
                        {
                            "name": "FUNCTIONS_WORKER_RUNTIME",
                            "value": "[variables('runtimeStack')]"
                        },
                        {
                            "name": "AzureWebJobsStorage",
                            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=',variables('storageAccountName'),';AccountKey=',listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2017-06-01').keys[0].value)]"
                        },
                        {
                            "name": "FUNCTIONS_EXTENSION_VERSION",
                            "value": "~2"
                        },
                        {
                            "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
                            "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2017-06-01').keys[0].value)]"
                        },
                        {
                            "name": "WEBSITE_CONTENTSHARE",
                            "value": "[toLower(parameters('functionAppName'))]"
                        },
                        {
                            "name": "WEBSITE_TIME_ZONE",
                            "value": "[variables('timezone')]"
                        }
                    ]
                },
                "name": "[parameters('functionAppName')]",
                "clientAffinityEnabled": false,
                "serverFarmId": "[resourceId('Microsoft.Web/serverfarms/', variables('hostingPlanName'))]"
            }

        },
        {
            "type": "Microsoft.Web/serverfarms",
            "apiVersion": "2018-11-01",
            "name": "[variables('hostingPlanName')]",
            "location": "[parameters('location')]",
            "properties": {
                "name": "[variables('hostingPlanName')]"
            },
            "sku": {
                "name": "Y1",
                "tier": "Dynamic",
                "size": "Y1",
                "family": "Y",
                "capacity": 0
            }
        },
        {
            "name": "[variables('storageAccountName')]",
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2018-07-01",
            "location": "[parameters('location')]",
            "kind": "StorageV2",
            "tags": {
                "type": "Function Storage",
                "FunctionName": "[parameters('functionAppName')]"
              },
            "sku": {
                "name": "Standard_LRS",
                "tier": "Standard"
            },
            "properties": {
                "accessTier": "Hot"
            }
        }
    ],
    "outputs": {
    }
}

Few things to say about that. You can notice that a Function App is just a kind of Web App. So, everything related to a web app can work with a Function. You need a hosting plan and a storage account to store the functions app files (functions, configuration, logs,…)

You will notice an extra App Settings, WEBSITE_TIME_ZONE. By default, the function App runs with the UTC timezone. If you need to run the Timer trigger function it can be a problem UTC is 2 hours away from Paris. This setting changes the time zone of the function. The Value can be found here

Locally the function app is just a folder with 3 files host.json, profile.ps1 and Requirements.psd1.

Host.json, defines the function App, extensions, log, timeout... By default, it contains the runtime version (version= “2.0”) and the ManagedDependency field. This file control all the configuration options in the Function App and so in all the functions.

  • Functiontimeout, the default timeout for an instance of a function is 5 minutes you can go up to 10 minutes in a consumption plan and 30 minutes in a dedicated plan.

  • Hosthealthmonitor, active by default, The Health monitor check performance counters (connections, process, and threads). If a counter is near the threshold, the host stops starting new function until the counter return to the normal. You can control the check interval and the check windows and you can disable it (but it's a bad idea)

  • Http, to control route prefix, the number of concurrent request and the number of requests in the queue

  • Logging, to control the level of log, to disable console logging and application Insights settings. You can control logging by function.

  • ExtensionBundle, to install extensions needed by the functions in the app

There are a few more parameters. Here's a sample host.json. It define a timeout at 10 minutes and logging properties for all functions except one, OnFunction.

{
  "version": "2.0",
  "managedDependency": {
    "enabled": true
  },
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[1.*, 2.0.0)"
  }
  ,
  "logging": {
      "logLevel": {
          "default": "Information",
          "MyFunction.OnFunction": "Trace"
      },
      "fileLoggingMode": "always"
  },
  "functionTimeout": "00:10:00"
}

The two other files are

Profile.ps1, it runs when the function app start and by default it creates a connection to Azure using connect-azAccount if managed identity is enabled

Requirements.psd1, to load the Azure PowerShell module.

Now, that we have a Function App we can talk about functions. A function is just a folder inside the function app root path. This folder only needs 2 files, function.json defining the function and run.ps1 for the code. The name of the folder is the name of the function once deployed in the Function App.

You don't do anything else. Just create a folder with these two files inside the function App folder.

Function.json defines the configuration of the function, the schema is simple. You define one and only one trigger and zero to multiple binding. You can check my last post about Azure Functions for more detail about Binding for PowerShell Azure Functions.

But there is something you need to understand, binding and trigger, except timer, need extensions. When you are on the portal, you should notice that when you add a trigger or binding, you have an option to install an extension. Of course, you don’t have such option when you work on your function locally but we have the option to edit the host.json and add a extension Bundle object

    "extensionBundle": { 

      "id": "Microsoft.Azure.Functions.ExtensionBundle", 

      "version": "[1.*, 2.0.0)" 

    } 

At the next deployment, the function App will install extensions needed by the binding.

But when writing Azure functions, you may want to reuse some part of your code or use some kind of unit testing.

Azure functions let you add a folder named modules inside the function app. The folder will not count as a function and the system import every PowerShell from this directory.

If you need a module from a public repository you can use the save-module cmdlet

Save-module name "<Module Name>" -path "x:\FunctionAppFolder\modules\" 

You can write your module to use in Azure Functions. It helps to separate core logic, execution logic and data structure.

Writing a PowerShell core module, especially with Class can help a lot. You can create complexes objects and use functions as interfaces.

You can have situations where you need to execute the same tasks triggered by different events. With classes as Data structure and function as interfaces in a module, you only have to write these tasks once.

One advantage with powershell modules is the facility we have to do unit testing with Pester.

Testing a module is easier than testing an azure function. It’s difficult to reproduce the different states of an Azure function and it’s more difficult to do it automatically in a CI/CD chain. Module testing is easier. Pester is the perfect tool for that. The inModuleScope command allows the testing of internal functions of a module.

Pester let you mock command, so you can concentrate on the function itself. You can even emulate Azure storage behavior by using new-AzStorageContext -local.

Using unit testing with Pester is important in PowerShell tools making. It ensures that change to the code base doesn't break anything and help to create a CI/CD tooling for each Azure functions you need to write.

We have several options to deploy a function app to Azure. You can use GitHub and Azure DevOps Pipeline. Azure functions will install any new content from a branch in your repository and Azure DevOps is used to run unit tests before merging content in this branch.

Another way is to use the Publish-AzWebApp cmdlet. It takes a zip file and sends it to the Function App in Azure.

Files in the zip file need to be at the first level. All the necessary files need to be in the zip file, host.json, profile.ps1, Requirements.psd1, the modules folder, and all your functions. The content of the zip file will replace the current function in Azure.

One way to build it is to use this kind of script

$TmpFuncZipDeployPath = c:\temp\mytmpFunction.zip 

$excludeFilesAndFolders = @(".git",".vscode","bin","Microsoft",".funcignore",".gitignore") 

$FileToSendArray = @() 

foreach ($file in get-childitem -Path $this.FunctionAppPath) { 

    if ($file.name -notin $excludeFilesAndFolders) { 

        $FileToSendArray += $file.fullname 

    } 

} 

compress-archive -Path $FileToSendArray -DestinationPath $TmpFuncZipDeployPath 

Publish-AzWebapp -ResourceGroupName $RessourceGroup -Name $FunctionAppName ArchivePath $TmpFuncZipDeployPath -force 

Posted on by:

omiossec profile

Olivier Miossec

@omiossec

Microsoft Azure MVP, Passionate about Cloud and DevOps. Co-organizers of the French PowerShell UG and Paris PowerShell & WinOps UG. I live in Paris.

Discussion

markdown guide
 

Thanks, Olivier. I spend 8 hours trying to figure out, why my Azure Function would not work after being deployed. Shaved it to look like this and voila!