Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Azure Function vNext
  • Loading branch information
FriedrichWeinmann committed Jan 16, 2026
commit 2f9e3c1fefeaf41592b753e44797f27e915935e4
12 changes: 12 additions & 0 deletions PSModuleDevelopment/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## ???

+ Upd: Template AzureFunction - Update host.json extension bundle version to `[4.*, 5.0.0)`
+ Upd: Template AzureFunction - PSModulePath ensured to exist correctly
+ Upd: Template AzureFunction - Configuration for Flex Consumption added, changing build behavior
+ Upd: Template AzureFunction - Added Eventgrid Trigger folder to project module
+ Upd: Template AzureFunction - Added readme with setup instructions
+ Upd: Template AzureFunction - Added `-Restart` parameter to the build script, restarting the Function App after deployment
+ Upd: Template AzureFunction - Added psf-build.ps1 for faster build through PSFramework.Nuget
+ Upd: Template AzureFunction - Added option to deploy to Flex Consumption plans, disabling Managed Dependencies
+ Fix: Template AzureFunction - the folder "modules" should be "Modules"

## 2.2.13.176 (2025-05-04)

+ New: Template AzureFunctionEventGrid - dedicated endpoint template for an EventGrid function
Expand Down
7 changes: 7 additions & 0 deletions templates/AzureFunction/build/build.config.psd1
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
@{
General = @{
# Is this Function App deployed to a Flex COnsumption plan?
# If so, Managed Dependencies cannot be used and must be* disabled!
# *The build script will handle that for you, if setting this to $true
FlexConsumption = $false
}

TimerTrigger = @{
# Default Schedule for timed executions
Schedule = '0 5 * * * *'
Expand Down
80 changes: 73 additions & 7 deletions templates/AzureFunction/build/build.ps1
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
param (
[CmdletBinding()]
param (
[string]
$Repository = 'PSGallery',

[string]
$AppRg,

[string]
$AppName
$AppName,

[switch]
$Restart
)

$ErrorActionPreference = 'Stop'
trap {
Write-Warning "Script failed: $_"
throw $_
}

$workingDirectory = Split-Path $PSScriptRoot
$config = Import-PowerShellDataFile -Path "$PSScriptRoot\build.config.psd1"

Expand All @@ -16,17 +27,63 @@ Remove-Item -Path "$workingDirectory/publish" -Recurse -Force -ErrorAction Ignor
$buildFolder = New-Item -Path $workingDirectory -Name 'publish' -ItemType Directory -Force -ErrorAction Stop
Copy-Item -Path "$workingDirectory/function/*" -Destination $buildFolder.FullName -Recurse -Force

#region Handle Modules
# Process Dependencies
$requiredModules = (Import-PowerShellDataFile -Path "$workingDirectory/þnameþ/þnameþ.psd1").RequiredModules
foreach ($module in $requiredModules) {
Save-Module -Name $module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository
if ($module -is [string]) {
Save-Module -Name $module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository
continue
}

$saveParam = @{ Name = $module.ModuleName }
if ($module.RequiredVersion) { $saveParam.RequiredVersion = $module.RequiredVersion }
elseif ($module.ModuleVersion) { $saveParam.MinimumVersion = $module.ModuleVersion }

Save-Module @saveParam -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository
}

#region Handle the Requirements for Flex Consumption Plans
if ($config.General.FlexConsumption) {
# Resolve New Dependencies
$mdepsFile = Join-Path -Path $buildFolder.FullName -ChildPath 'requirements.psd1'
$mDeps = Import-PowerShellDataFile -Path $mdepsFile
$requiredModules = foreach ($name in $mDeps.Keys) {
if ($mDeps.$name -match '\*') {
@{
Name = $name
MinimumVersion = '{0}.0.0' -f $mDeps.$name.Split('.')[0]
MaximumVersion = '{0}.999.999' -f $mDeps.$name.Split('.')[0]
}
}
else {
@{
Name = $name
RequiredVersion = $mDeps.$name
}
}
}

# Inject New Dependencies
foreach ($module in $requiredModules) {
Save-Module @module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository
}

# Disable Managed Dependencies
$hostFile = Join-Path -Path $buildFolder.FullName -ChildPath 'host.json'
$hostCfg = Get-Content -Path $hostFile | ConvertFrom-Json
$hostCfg.managedDependency.enabled = $false
$hostCfg | ConvertTo-Json -Depth 99 | Set-Content -Path $hostFile
}
#endregion Handle the Requirements for Flex Consumption Plans

# Process Function Module
Copy-Item -Path "$workingDirectory/þnameþ" -Destination "$($buildFolder.FullName)/modules" -Force -Recurse
$commands = Get-ChildItem -Path "$($buildFolder.FullName)/modules/þnameþ/Functions" -Recurse -Filter *.ps1 | ForEach-Object BaseName
Update-ModuleManifest -Path "$($buildFolder.FullName)/modules/þnameþ/þnameþ.psd1" -FunctionsToExport $commands
#endregion Handle Modules

#region Triggers
# Generate Http Trigger
$httpCode = Get-Content -Path "$PSScriptRoot\functionHttp\run.ps1" | Join-String -Separator "`n"
$httpConfig = Get-Content -Path "$PSScriptRoot\functionHttp\function.json" | Join-String -Separator "`n"
Expand Down Expand Up @@ -74,13 +131,22 @@ foreach ($command in Get-ChildItem -Path "$workingDirectory\þnameþ\functions\t
$timerCode -replace '%COMMAND%',$command.BaseName | Set-Content -Path "$($endpointFolder.FullName)\run.ps1"
$timerConfig -replace '%SCHEDULE%', $schedule | Set-Content -Path "$($endpointFolder.FullName)\function.json"
}
#endregion Triggers

# Package & Cleanup
Remove-Item -Path "$workingDirectory/Function.zip" -Recurse -Force -ErrorAction Ignore
Compress-Archive -Path "$($buildFolder.FullName)/*" -DestinationPath "$workingDirectory/Function.zip"
Remove-Item -Path $buildFolder.FullName -Recurse -Force -ErrorAction Ignore

if ($AppRg -and $AppName) {
Write-Host "Publishing Function App to $AppRg/$AppName"
Publish-AzWebApp -ResourceGroupName $AppRG -Name $AppName -ArchivePath "$workingDirectory/Function.zip" -Confirm:$false -Force
}
if (-not $AppRg -or -not $AppName) { return }

Write-Host "Publishing Function App to $AppRg/$AppName"
$null = Publish-AzWebApp -ResourceGroupName $AppRG -Name $AppName -ArchivePath "$workingDirectory/Function.zip" -Confirm:$false -Force
Write-Host "Publishing Function App to $AppRg/$AppName - Done"

if (-not $Restart) { return }

Write-Host "Restarting Function App"
$null = Stop-AzWebApp -ResourceGroupName $AppRG -Name $AppName
$null = Start-AzWebApp -ResourceGroupName $AppRG -Name $AppName
Write-Host "Restarting Function App - Done"
152 changes: 152 additions & 0 deletions templates/AzureFunction/build/psf-build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
[CmdletBinding()]
param (
[string]
$Repository = 'PSGallery',

[string]
$AppRg,

[string]
$AppName,

[switch]
$Restart
)

$ErrorActionPreference = 'Stop'
trap {
Write-Warning "Script failed: $_"
throw $_
}

Invoke-WebRequest 'https://raw.githubusercontent.com/PowershellFrameworkCollective/PSFramework.NuGet/refs/heads/master/bootstrap.ps1' -UseBasicParsing | Invoke-Expression

$workingDirectory = Split-Path $PSScriptRoot
$config = Import-PowerShellDataFile -Path "$PSScriptRoot\build.config.psd1"

# Prepare output path and copy function folder
Remove-Item -Path "$workingDirectory/publish" -Recurse -Force -ErrorAction Ignore
$buildFolder = New-Item -Path $workingDirectory -Name 'publish' -ItemType Directory -Force -ErrorAction Stop
Copy-Item -Path "$workingDirectory/function/*" -Destination $buildFolder.FullName -Recurse -Force

#region Handle Modules
# Process Dependencies
$requiredModules = (Import-PowerShellDataFile -Path "$workingDirectory/þnameþ/þnameþ.psd1").RequiredModules
foreach ($module in $requiredModules) {
if ($module -is [string]) {
Save-PSFModule -Name $module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository
}
else {
$versionParam = @{}
if ($module.RequiredVersion) { $versionParam.Version = $module.RequiredVersion }
elseif ($module.ModuleVersion) { $versionParam.Version = "[$($module.ModuleVersion)-" }
Save-PSFModule -Name $module.ModuleName @versionParam -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository
}
}

#region Handle the Requirements for Flex Consumption Plans
if ($config.General.FlexConsumption) {
# Resolve New Dependencies
$mdepsFile = Join-Path -Path $buildFolder.FullName -ChildPath 'requirements.psd1'
$mDeps = Import-PowerShellDataFile -Path $mdepsFile
$requiredModules = foreach ($name in $mDeps.Keys) {
if ($mDeps.$name -match '\*') {
@{
Name = $name
Version = '[{0}.0.0-{1}.0.0)' -f $mDeps.$name.Split('.')[0], (1 + $mDeps.$name.Split('.')[0])
}
}
else {
@{
Name = $name
Version = $mDeps.$name
}
}
}

# Inject New Dependencies
foreach ($module in $requiredModules) {
Save-PSFModule @module -Path "$($buildFolder.FullName)/modules" -Force -Repository $Repository
}

# Disable Managed Dependencies
$hostFile = Join-Path -Path $buildFolder.FullName -ChildPath 'host.json'
$hostCfg = Get-Content -Path $hostFile | ConvertFrom-Json
$hostCfg.managedDependency.enabled = $false
$hostCfg | ConvertTo-Json -Depth 99 | Set-Content -Path $hostFile
}
#endregion Handle the Requirements for Flex Consumption Plans

# Process Function Module
Copy-Item -Path "$workingDirectory/þnameþ" -Destination "$($buildFolder.FullName)/modules" -Force -Recurse
$commands = Get-ChildItem -Path "$($buildFolder.FullName)/modules/þnameþ/Functions" -Recurse -Filter *.ps1 | ForEach-Object BaseName
Update-PSFModuleManifest -Path "$($buildFolder.FullName)/modules/þnameþ/þnameþ.psd1" -FunctionsToExport $commands
#endregion Handle Modules

#region Triggers
# Generate Http Trigger
$httpCode = Get-Content -Path "$PSScriptRoot\functionHttp\run.ps1" | Join-String -Separator "`n"
$httpConfig = Get-Content -Path "$PSScriptRoot\functionHttp\function.json" | Join-String -Separator "`n"
foreach ($command in Get-ChildItem -Path "$workingDirectory\þnameþ\functions\httpTrigger" -Recurse -File -Filter *.ps1) {
$authLevel = $config.HttpTrigger.AuthLevel
if ($config.HttpTrigger.AuthLevelOverrides.$($command.BaseName)) {
$authLevel = $config.HttpTrigger.AuthLevelOverrides.$($command.BaseName)
}
$methods = $config.HttpTrigger.Methods
if ($config.HttpTrigger.MethodOverrides.$($command.BaseName)) {
$methods = $config.HttpTrigger.MethodOverrides.$($command.BaseName)
}
$endpointFolder = New-Item -Path $buildFolder.FullName -Name $command.BaseName -ItemType Directory
$httpCode -replace '%COMMAND%', $command.BaseName | Set-Content -Path "$($endpointFolder.FullName)\run.ps1"
$httpConfig -replace '%AUTHLEVEL%', $authLevel -replace '%METHODS%', ($methods -join '", "') | Set-Content -Path "$($endpointFolder.FullName)\function.json"
}


# Generate Event Grid Trigger
$eventGridCode = Get-Content -Path "$PSScriptRoot\functionEventGrid\run.ps1" | Join-String -Separator "`n"
$eventGridConfig = Get-Content -Path "$PSScriptRoot\functionEventGrid\function.json" | Join-String -Separator "`n"
foreach ($command in Get-ChildItem -Path "$workingDirectory\þnameþ\functions\eventGridTrigger" -Recurse -File -Filter *.ps1) {
$authLevel = $config.EventGridTrigger.AuthLevel
if ($config.EventGridTrigger.AuthLevelOverrides.$($command.BaseName)) {
$authLevel = $config.EventGridTrigger.AuthLevelOverrides.$($command.BaseName)
}
$methods = $config.EventGridTrigger.Methods
if ($config.EventGridTrigger.MethodOverrides.$($command.BaseName)) {
$methods = $config.EventGridTrFigger.MethodOverrides.$($command.BaseName)
}
$endpointFolder = New-Item -Path $buildFolder.FullName -Name $command.BaseName -ItemType Directory
$eventGridCode -replace '%COMMAND%', $command.BaseName | Set-Content -Path "$($endpointFolder.FullName)\run.ps1"
$eventGridConfig -replace '%AUTHLEVEL%', $authLevel -replace '%METHODS%', ($methods -join '", "') | Set-Content -Path "$($endpointFolder.FullName)\function.json"
}

# Generate Timer Trigger
$timerCode = Get-Content -Path "$PSScriptRoot\functionTimer\run.ps1" | Join-String -Separator "`n"
$timerConfig = Get-Content -Path "$PSScriptRoot\functionTimer\function.json" | Join-String -Separator "`n"
foreach ($command in Get-ChildItem -Path "$workingDirectory\þnameþ\functions\timerTrigger" -Recurse -File -Filter *.ps1) {
$schedule = $config.TimerTrigger.Schedule
if ($config.TimerTrigger.ScheduleOverrides.$($command.BaseName)) {
$schedule = $config.TimerTrigger.ScheduleOverrides.$($command.BaseName)
}
$endpointFolder = New-Item -Path $buildFolder.FullName -Name $command.BaseName -ItemType Directory
$timerCode -replace '%COMMAND%', $command.BaseName | Set-Content -Path "$($endpointFolder.FullName)\run.ps1"
$timerConfig -replace '%SCHEDULE%', $schedule | Set-Content -Path "$($endpointFolder.FullName)\function.json"
}
#endregion Triggers

# Package & Cleanup
Remove-Item -Path "$workingDirectory/Function.zip" -Recurse -Force -ErrorAction Ignore
Compress-Archive -Path "$($buildFolder.FullName)/*" -DestinationPath "$workingDirectory/Function.zip"
Remove-Item -Path $buildFolder.FullName -Recurse -Force -ErrorAction Ignore

if (-not $AppRg -or -not $AppName) { return }

Write-Host "Publishing Function App to $AppRg/$AppName"
$null = Publish-AzWebApp -ResourceGroupName $AppRG -Name $AppName -ArchivePath "$workingDirectory/Function.zip" -Confirm:$false -Force
Write-Host "Publishing Function App to $AppRg/$AppName - Done"

if (-not $Restart) { return }

Write-Host "Restarting Function App"
$null = Stop-AzWebApp -ResourceGroupName $AppRG -Name $AppName
$null = Start-AzWebApp -ResourceGroupName $AppRG -Name $AppName
Write-Host "Restarting Function App - Done"
2 changes: 1 addition & 1 deletion templates/AzureFunction/function/host.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[3.*, 4.0.0)"
"version": "[4.*, 5.0.0)"
}
}
11 changes: 8 additions & 3 deletions templates/AzureFunction/function/profile.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ if ($env:MSI_SECRET -and (Get-Module -ListAvailable Az.Accounts))
Connect-AzAccount -Identity
}

# Uncomment the next line to enable legacy AzureRm alias in Azure PowerShell.
# Enable-AzureRmAlias
# You can also define functions or aliases that can be referenced in any of your PowerShell functions.
$pathDelimiter = ';'
if (-not $IsWindows) { $pathDelimiter = ':' }
$modulePaths = $env:PSModulePath -split $pathDelimiter
$ourModulePath = Join-Path -Path $PSScriptRoot -ChildPath Modules

if ($modulePaths -notcontains $ourModulePath) {
$env:PSModulePath = $ourModulePath, $env:PSModulePath -join $pathDelimiter
}
50 changes: 50 additions & 0 deletions templates/AzureFunction/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Azure Function Template

TODO: Replace me with actual readme for your project

## Template Instructions

### Layout

There are three folders with this project:

+ build: Where all the magic happens - do not touch, other than the config file (`build.config.psd1`)
+ function: Where the basic function app files are stored. Generally you only need to update the `requirements.psd1` for Managed Dependencies (do not use when running on Flex Consumption plan)
+ `<name>`: Folder with the PowerShell module that gets turned into a function app. This is where you add your content, usually.

### Flex Consumption and You

If you plan to deploy the function app code to an App running under the Flex Consumption plan, **you must configure the template for it!**
Not all features are available in that plan - specifically the Managed Dependencies feature does not work - and that changes the requirements we have to work with.

To make things work, open the project configuration file: `build\build.config.psd1`.
Enable the Flex Consumption behavior by setting `General > FlexConsumption` to `$true`.

### Adding your content

Your own code is usually placed in the `<name>` root level folder, which is a regular, lightweight PowerShell module structure.
Treat it as a module and add code as you would for a module.

Any RequiredModules you declare will automatically be downloaded and bundled with the function app during build.

There _is_ however one special aspect:
In the `functions` subfolder you will find several subfolders, that are special to this template:

+ `<name>/functions/eventGridTrigger`
+ `<name>/functions/httpTrigger`
+ `<name>/functions/nonPublished`
+ `<name>/functions/timerTrigger`

Functions placed in a particular trigger-folder will be published as that kind of trigger within the function app.
For example, if you place a `Get-EntraUser.ps1` file & PS-function under the `httpTrigger` subfolder, your function-app will have an http endpoint with the url `<function-app-baseurl>/api/Get-EntraUser` that accepts the same parameters (via body or query) as the PS-function you wrote.

The same applies to the other trigger kinds (though a Timer Trigger cannot receive any parameters, even if you add them to the PS-function).

> Configuring Details

Having the triggers generated automatically is all nice and useful, but some triggers might need some extra configuration.
For example, when defining a timer trigger, what is the actual schedule it triggers on?

All those configuration aspects can be found under `build/build.config.psd1`.
For each setting you can define a global default and overrides for specific, individual endpoints.
The individual settings and what they mean are documented in that file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Eventgrid Triggered Functions

Each function placed under this folder will be registered as an Eventgrid-trigger function endpoint on build.