I am trying to access Kudu through power shell script. Link looks like: https://adc-dev.scm.azurewebsites.net. I need to copy war file located in D:\home\site\wwwroot\bin\apache-tomcat-8.0.33\webapps location in above link.
Currently I am deploying the war file using VSTS by adding FTP task. But before deploying the latest war I would like to take backup of the old war in some location in Azure Kudu location say like: D:\home\site\wwwroot\bin\apache-tomcat-8.0.33 (root folder to the war location). So after that I can remove the war and deploy the latest war file in Kudu.
How to do this? I mean how to access kudu using power shell script. Please suggest me.
To access Kudu API, get your WebApp:
$app = Get-AzWebApp -ResourceGroupName "your RG" -Name "your App"
Then get publishing credentials for the app:
$resourceName = "$($app.Name)/publishingcredentials";
$resourceType = "Microsoft.Web/sites/config";
$publishingCredentials = Invoke-AzResourceAction `
-ResourceGroupName $app.ResourceGroup `
-ResourceType $resourceType `
-ResourceName $resourceName `
-Action list `
-ApiVersion $apiVersion `
-Force;
Format the username/password suitable for a HTTP-request:
$user = $publishingCredentials.Properties.PublishingUserName;
$pass = $publishingCredentials.Properties.PublishingPassword;
$creds = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("${user}:${pass}")));
Finally, you can access the Kudu API:
$header = #{
Authorization = "Basic $creds"
};
$kuduApiBaseUrl = "https://$($app.Name).scm.azurewebsites.net";
Example, check if extension is installed:
$extensionName = "Microsoft.AspNetCore.AzureAppServices.SiteExtension";
$kuduApiUrl = "$kuduApiBaseUrl/api/siteextensions/$extensionName";
$response = Invoke-RestMethod -Method 'Get' -Uri $kuduApiUrl -Headers $header;
Example, get list of available extensions:
$kuduApiUrl = "$kuduApiBaseUrl/api/extensionfeed";
$response = Invoke-RestMethod -Method 'Get' -Uri $kuduApiUrl -Headers $header;
Example, install an extension:
$kuduApiUrl = "$kuduApiBaseUrl/api/siteextensions";
$response = Invoke-RestMethod -Method 'Put' -Uri $kuduApiUrl -Headers $header;
API-details are at https://github.com/projectkudu/kudu/wiki/REST-API
Also deployment slots can be accessed. App config needs to be retrieved for the slot and a minor modification of the base URL is needed.
You can refer to this thread below to know how to call Kudu API through Azure PowerShell in VSTS build/release:
Remove files and foldes on Azure before a new deploy from VSTS
Regarding copy file through Kudu, you can use Command Kudu API (Post /api/command):
Kudu REST API
Update:
Simple sample to call Command through Kudu API:
function RunCommand($dir,$command,$resourceGroupName, $webAppName, $slotName = $null){
$kuduApiAuthorisationToken = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppName $slotName
$kuduApiUrl="https://$webAppName.scm.azurewebsites.net/api/command"
$Body =
#{
"command"=$command;
"dir"=$dir
}
$bodyContent=#($Body) | ConvertTo-Json
Write-Host $bodyContent
Invoke-RestMethod -Uri $kuduApiUrl `
-Headers #{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} `
-Method POST -ContentType "application/json" -Body $bodyContent
}
RunCommand "site\wwwroot\bin\apache-tomcat-8.0.33\webapps" "copy xx.war ..\xx.war /y" "[resource group]" "[web app]"
You can use below code to access kudu apis from powershell -
//function to Get webapp's publishing credentials
function Get-AzWebAppPublishingCredentials($resourceGroupName, $webAppName, $slotName = $null) {
if ([string]::IsNullOrWhiteSpace($slotName) -or $slotName.ToLower() -eq "production") {
$resourceType = "Microsoft.Web/sites/config"
$resourceName = "$webAppName/publishingcredentials"
}
else {
$resourceType = "Microsoft.Web/sites/slots/config"
$resourceName = "$webAppName/$slotName/publishingcredentials"
}
$publishingCredentials = Invoke-AzResourceAction -ResourceGroupName $resourceGroupName -ResourceType $resourceType -ResourceName $resourceName -Action list -ApiVersion 2015-08-01 -Force
return $publishingCredentials
}
//function to get authorization header from publishing credentials
function Get-KuduApiAuthorisationHeaderValue($resourceGroupName, $webAppName, $slotName = $null) {
$publishingCredentials = Get-AzWebAppPublishingCredentials $resourceGroupName $webAppName $slotName
$ret = #{ }
$ret.header = ("Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $publishingCredentials.Properties.PublishingUserName, $publishingCredentials.Properties.PublishingPassword))))
$ret.url = $publishingCredentials.Properties.scmUri
return $ret
}
//function to call kudu api e.g. to get a file from webapp
function Get-FileFromWebApp($resourceGroupName, $webAppName, $slotName = "", $kuduPath) {
$KuduAuth = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $webAppName $slotName
$kuduApiAuthorisationToken = $KuduAuth.header
$kuduApiUrl = $KuduAuth.url + "/api/vfs/$kuduPath"
Write-Host " Downloading File from WebApp. Source: '$kuduApiUrl'." -ForegroundColor DarkGray
$tmpPath = "$($env:TEMP)\$([guid]::NewGuid()).json"
$null = Invoke-RestMethod -Uri $kuduApiUrl `
-Headers #{"Authorization" = $kuduApiAuthorisationToken; "If-Match" = "*" } `
-Method GET `
-ContentType "application/json" `
-OutFile $tmpPath
$ret = (Get-Content $tmpPath) | Out-String | ConvertFrom-Json
Remove-Item $tmpPath -Force
return $ret
}
Related
I have Runbook under Azure automation account that should collect results from Resource graph query and pass it to Log analytics as custom log. I have managed to create a script that works fine.
$customerId = "xxxxxxxxx"
$SharedKey = "xxxxxxxxxxxxxxxx"
$LogType = "MyRecord"
$TimeStampField = ""
#function block
Function Connect-ToAzure {
$connectionName = "AzureRunAsConnection"
$automationAccountName = Get-AutomationVariable -Name 'automationAccountName'
Write-Output "Azure Automation Account Name - $automationAccountName"
$connectionName = "AzureRunAsConnection"
Write-Output "Connection Name - $connectionName"
$servicePrincipalConnection = Get-AutomationConnection -Name $connectionName
Write-Output "Logging in to Azure..."
Connect-AzAccount -ServicePrincipal -Tenant $servicePrincipalConnection.TenantId `
-ApplicationId $servicePrincipalConnection.ApplicationId `
-CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint `
-Subscription $servicePrincipalConnection.SubscriptionId
}
Function Build-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource)
{
$xHeaders = "x-ms-date:" + $date
$stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource
$bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash)
$keyBytes = [Convert]::FromBase64String($sharedKey)
$sha256 = New-Object System.Security.Cryptography.HMACSHA256
$sha256.Key = $keyBytes
$calculatedHash = $sha256.ComputeHash($bytesToHash)
$encodedHash = [Convert]::ToBase64String($calculatedHash)
$authorization = 'SharedKey {0}:{1}' -f $customerId,$encodedHash
return $authorization
}
# Create the function to create and post the request
Function Post-LogAnalyticsData($customerId, $sharedKey, $body, $LogType)
{
$method = "POST"
$contentType = "application/json"
$resource = "/api/logs"
$rfc1123date = [DateTime]::UtcNow.ToString("r")
$contentLength = $body.Length
$signature = Build-Signature `
-customerId $customerId `
-sharedKey $sharedKey `
-date $rfc1123date `
-contentLength $contentLength `
-method $method `
-contentType $contentType `
-resource $resource
$uri = "https://" + $customerId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01"
$headers = #{
"Authorization" = $signature;
"Log-Type" = $LogType;
"x-ms-date" = $rfc1123date;
"time-generated-field" = $TimeStampField;
}
Write-Output "Sending a request"
$response = Invoke-WebRequest -Uri $uri -Method $method -ContentType $contentType -Headers $headers -Body $body -UseBasicParsing
return $response.StatusCode
Write-Output "Request has been sent"
}
try {
Write-Output "Starting runbook"
$customerId = "xxxxxxxxx"
$SharedKey = "xxxxxxxxxxxxxxxx"
$LogType = "MyRecord"
$Query = #'
resources
| where type == "microsoft.compute/disks"
| extend diskState = properties.diskState
| extend diskSizeGB = properties.diskSizeGB
| where properties['encryptionSettingsCollection'] != "enabled"
| where diskState == "Attached" or diskState == "Reserved"
| extend diskCreationTime = properties.timeCreated
| extend hostVM = split(managedBy,"/")[-1]
| project diskCreationTime, name, resourceGroup, hostVM, diskState
'#
Connect-ToAzure
$Result = (Search-AzGraph -Query $Query -First 1000)
$jsonResult = $Result | ConvertTo-Json
Post-LogAnalyticsData -customerId $customerId -sharedKey $sharedKey -body $jsonResult -logType $LogType
Write-Output "Runbook has been finished"
}
catch {
Write-Error -Message $_
break
}
However, I have an issue with collection of logs. I gather logs only from one subscription.
Can someone please help with code adjustment? How can I gather results from all subscriptions not just from one? I assume it should be foreach ($sub in $subs), but not sure how to do it combined with this graph query.
As the run as account will retire by Sep'23 so I would recommend using managed identities approach in your runbook i.e., configure role assignment for managed identity across multiple subscriptions as explained here and update your Connect-ToAzure function block something like shown below. For more context with regards to it, refer this Azure document.
$automationAccountName = Get-AutomationVariable -Name 'automationAccountName'
Write-Output "Azure Automation Account Name - $automationAccountName"
Disable-AzContextAutosave -Scope Process | Out-Null
Write-Output "Logging in to Azure..."
$AzureContext = (Connect-AzAccount -Identity).context
Also, you may use UseTenantScope parameter in your Search-AzGraph cmdlet to run the query across all available subscriptions in the current tenant.
As part of my release pipeline in Azure DevOps, I wish to rename some .config files to .config.disabled, after my "Deploy Azure App Service" task has finished.
I have tried to add an additional "Deploy Azure App Service" task, but that seems to overwrite the previous one, leaving only the .config.disabled files in the wwwroot.
Is there any other way (other than FTP upload task), which can be used to rename/deploy a subset of files, in an Azure web app?
If you don't want to use FTP, you might have some success trying to use the Kudu service that backs the Azure Web App.
To view the Kudu service, navigate to https://<your-web-app-name>.scm.azurewebsites.net
After you've authenticated, you'll discover there's a way to browse files, view running processes and an interactive console that allows you to run basic DOS or PowerShell commands against the file system.
The interactive console is great, but to automate it you can issue commands against the file system using a REST API.
There are several examples of issuing commands to the server. In PowerShell:
# authenticate with Azure
Login-AzureRmAccount
$resoureGroupName = "your-web-app-name"
$websiteName = "your-resource-group-name"
$env = #{
command= 'Set COMPUTERNAME'
dir= 'site'
}
$json = $env | ConvertTo-Json
$env2 = #{
command= 'copy filename.config file.config.notused'
dir= 'site\wwwroot'
}
$json2 = $env2 | ConvertTo-Json
$env3 = #{
command= 'delete filename.config'
dir= 'site\wwwroot'
}
$json3 = $env3 | ConvertTo-Json
# setup auth header
$website = Get-AzureWebsite -Name $websiteName
$username = $website.PublishingUsername
$password = $website.PublishingPassword
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))
$apiBaseUrl = "https://$($website.Name).scm.azurewebsites.net/api"
[System.Uri]$Uri = $apiBaseUrl
# get all the vms in the web-app
$instances = Get-AzureRmResource -ResourceGroupName $resoureGroupName `
-ResourceType Microsoft.Web/sites/instances `
-ResourceName $websiteName `
-ApiVersion 2018-02-01
#loop through instances
foreach($instance in $instances)
{
$instanceName = $instance.Name
Write-Host "`tVM Instance ID `t`t: " $instanceName
#Now execute 'SET COMPUTER' cmd
$cookie= New-Object System.Net.Cookie
$cookie.Name = "ARRAffinity"
$cookie.Value = $instanceName
$Cookie.Domain = $uri.DnsSafeHost
$session=New-Object Microsoft.Powershell.Commands.WebRequestSession
$session.Cookies.add($cookie)
$response = Invoke-RestMethod -Uri "$apiBaseUrl/command" `
-Headers #{Authorization=("Basic {0}" `
-f $base64AuthInfo)} `
-Method Post -Body $json `
-ContentType 'application/json' `
-WebSession $session
Write-Host "`tVM Instance Name `t: " $response
# perform copy file
$response = Invoke-RestMethod -Uri "$apiBaseUrl/command" `
-Headers #{Authorization=("Basic {0}" `
-f $base64AuthInfo)} `
-Method Post -Body $json2 `
-ContentType 'application/json' `
-WebSession $session
Write-Host "`tCopy file result `t: " $response
# perform delete file
$response = Invoke-RestMethod -Uri "$apiBaseUrl/command" `
-Headers #{Authorization=("Basic {0}" `
-f $base64AuthInfo)} `
-Method Post -Body $json3 `
-ContentType 'application/json' `
-WebSession $session
Write-Host "`tCopy file result `t: " $response
}
The file system supports basic commands, like copy and delete so you could easily rename the file.
I'm using Save-AzrWebApp function to downloads files from an Azure Web App.
How to do it is described here: https://blog.ipswitch.com/how-to-copy-files-from-an-azure-app-service-with-powershell
My problem is: it doesn't matter which SourcePath I set, it always downloads me files from wwwroot folder.
Code example that I use:
$syncParams = #{
SourcePath = '\wwwroot\history'
TargetPath = $TargetPath
ComputerName = "https://$Name.scm.azurewebsites.net:443/msdeploy.axd?site=$Name"
Credential = $Credential
}
Sync-Website #syncParams
Get-Item -Path $TargetPath
Actually it doesn't matter what I put into SourcePath (even not existing path) it will download content of wwwroot.
How to use it in a proper way?
If you want to download file from web app, you could use this web app kudu api via powershell.
Try the command below, it works fine on my side.
$creds = Invoke-AzureRmResourceAction -ResourceGroupName joywebapp -ResourceType Microsoft.Web/sites/config -ResourceName joywebapp2/publishingcredentials -Action list -ApiVersion 2015-08-01 -Force
$username = $creds.Properties.PublishingUserName
$password = $creds.Properties.PublishingPassword
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username, $password)))
$apiUrl = "https://joywebapp2.scm.azurewebsites.net/api/vfs/site/wwwroot/Content/Site.css"
Invoke-RestMethod -Uri $apiUrl -Headers #{Authorization=("Basic {0}" -f $base64AuthInfo);"If-Match"="*"} -Method GET -ContentType "multipart/form-data" -OutFile "C:\Users\joyw\Desktop\test.css"
Test result:
Update:
If you want to download a folder, you can use the Zip api in the doc I mentioned, it allows downloading folder as a zip file.
Sample command:
$creds = Invoke-AzureRmResourceAction -ResourceGroupName joywebapp -ResourceType Microsoft.Web/sites/config -ResourceName joywebapp2/publishingcredentials -Action list -ApiVersion 2015-08-01 -Force
$username = $creds.Properties.PublishingUserName
$password = $creds.Properties.PublishingPassword
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username, $password)))
$apiUrl = "https://joywebapp2.scm.azurewebsites.net/api/zip/site/wwwroot/Scripts/"
Invoke-RestMethod -Uri $apiUrl -Headers #{Authorization=("Basic {0}" -f $base64AuthInfo);"If-Match"="*"} -Method GET -ContentType "multipart/form-data" -OutFile "C:\Users\joyw\Desktop\Scripts.zip"
Note:The zip doesn't include the top folder itself. Make sure you include
the trailing slash, e.g, I download the Scripts folder, we need to use Scripts/ in the apiUrl.
Now, before you throw out the super simple answer of Get-ChildItem -Recurse, here is my unique issue:
I am connecting remotely to Azure with Powershell, grabbing the site (slot) list, then looping over the sites and finding all images in the directory structure, using Kudu API. Since Kudu has no notion of recursion, I have to build my own recursive function to grab all images from the root directory, then recurse through all children and chidren's children, etc, to also find the image files in those directories.
Here is my code for connecting to Azure and grabbing the root directory:
function Get-AzureRmWebAppPublishingCredentials($resourceGroupName, $webAppName, $slotName = $null){
if ([string]::IsNullOrWhiteSpace($slotName)){
$resourceType = "Microsoft.Web/sites/config"
$resourceName = "$webAppName/publishingcredentials"
}
else{
$resourceType = "Microsoft.Web/sites/slots/config"
$resourceName = "$webAppName/$slotName/publishingcredentials"
}
$publishingCredentials = Invoke-AzureRmResourceAction -ResourceGroupName $resourceGroupName -ResourceType $resourceType -ResourceName $resourceName -Action list -ApiVersion 2015-08-01 -Force
return $publishingCredentials
}
function Get-KuduApiAuthorisationHeaderValue($resourceGroupName, $webAppName, $slotName = $null){
$publishingCredentials = Get-AzureRmWebAppPublishingCredentials $resourceGroupName $webAppName $slotName
return ("Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $publishingCredentials.Properties.PublishingUserName, $publishingCredentials.Properties.PublishingPassword))))
}
function Fill-MimeTypes(){
return #("image/gif", "image/x-icon", "image/jpeg", "image/png", "image/tiff", "image/bmp")
}
$MimeTypes = Fill-MimeTypes
[System.Collections.ArrayList]$Directories = #()
#Login to Azure Account
Login-AzureRmAccount
#Get the Azure subscription
Select-AzureRmSubscription -SubscriptionName [Subscription Name]
#Get the resource group name
$resourceGroupName = [Resource Group Name]
#Get the WebApp name
$Resources = Find-AzureRmResource -ResourceType Microsoft.Web/sites -ResourceGroupNameContains $resourceGroupName
ForEach($Resource in $Resources)
{
#Get the WebAppName
$WebAppName = $Resource.Name
#Now, get the publishing creds
$publishingCredentialsHeader = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $WebAppName $null
$ApiUrl = "https://$WebAppName.scm.azurewebsites.net/api/vfs/site/wwwroot/"
#Now get the list of files in the wwwroot directory
$InitialList = Invoke-RestMethod -Uri $ApiUrl -Headers #{Authorization=$publishingCredentialsHeader} -Method GET -ContentType "application/json"
ForEach($Item in $InitialList)
{
If($MimeTypes -Contains $Item.mime)
{
#Add image file data to a collection
}
If ($Item.mime -eq "inode/directory")
{
#So, here is where I need to recurse...
#The mime type of inode/directory means it's a directory ;)
#I now need to call the Api again with the Url and get the contents of the current directory and rinse and repeat until done
#But I cannot forget about the other directories in the root directory and their children.
}
}
}
How can I write the recursive function?
I would write it as follows. Some is psuedo-code as I am not versed in PS syntax or keywords:
function Collect-Files($apiUrl, $creds, $currentDir)
{
$list = Invoke-RestMethod -Uri $apiUrl/$currentDir/ -Headers #{Authorization=$creds} -Method GET -ContentType "application/json"
If($MATCHLIST -eq $null)
{
$MATCHLIST = #() #initialize array
}
ForEach($Item in $list)
{
If($MimeTypes -Contains $Item.mime)
{
#Add image file data to a collection
$MATCHLIST += $Item #add to array
}
If ($Item.mime -eq "inode/directory")
{
$nextDir = $Item.name
$MATCHLIST = Collect-Files $apiUrl $creds $currentDir/$nextDir
}
}
return ($MATCHLIST)
}
Then, your previous code would call this function as follows:
#Get the WebApp name
$Resources = Find-AzureRmResource -ResourceType Microsoft.Web/sites -ResourceGroupNameContains "Nav-Inventory"
ForEach($Resource in $Resources)
{
#Get the WebAppName
$WebAppName = $Resource.Name
#Now, get the publishing creds
$publishingCredentialsHeader = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $WebAppName $null
$ApiUrl = "https://$WebAppName.scm.azurewebsites.net/api/vfs/site/"
#Now get the list of files in the wwwroot directory
$InitialList = Invoke-RestMethod -Uri $ApiUrl -Headers #{Authorization=$publishingCredentialsHeader} -Method GET -ContentType "application/json"
$MATCHES += Collect-Files $ApiUrl $publishingCredentialsHeader "wwwroot"
}
Basic recursion.
I can use the PowerShell cmdlet Get-AzureRMResource to list all Azure resources.
Is there a cmdlet that takes a ResourceGroupName and a SiteName and it returns all the functions in that "Site".
Or, a combination of cmdlets that I can use to get these details.
As Fabio Cavalcante said, Azure PowerShell does not support this, you could use Rest API to get it. Here is a example how to get Functions with PowerShell.
#
#get token
$TENANTID="<tenantid>"
$APPID="<application id>"
$PASSWORD="<app password>"
$result=Invoke-RestMethod -Uri https://login.microsoftonline.com/$TENANTID/oauth2/token?api-version=1.0 -Method Post -Body #{"grant_type" = "client_credentials"; "resource" = "https://management.core.windows.net/"; "client_id" = "$APPID"; "client_secret" = "$PASSWORD" }
$token=$result.access_token
##set Header
$Headers=#{
'authorization'="Bearer $token"
'host'="management.azure.com"
}
$functions = Invoke-RestMethod -Uri "https://management.azure.com/subscriptions/<subscriptions id>/resourceGroups/<group name>/providers/Microsoft.Web/sites/<function name>/functions?api-version=2015-08-01" -Headers $Headers -ContentType "application/json" -Method GET
$functions.value
In 2021, you can use func
func azure functionapp list-functions FUNCTION_NAME
available in azure-functions-core-tools#3
This is possible using the Get-AzureRmResource cmdlet.
$Params = #{
ResourceGroupName = $ResourceGroupName
ResourceType = 'Microsoft.Web/sites/functions'
ResourceName = $AppName
ApiVersion = '2015-08-01'
}
Get-AzureRmResource #Params
Not a PowerShell cmdlet, but you can use the ListingFunctions API as described here
Listing functions
get /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Web/sites/{functionapp}/functions?api-version=2015-08-01
Response:
{
"value": [
{
...
}
]
}
you can use below
Disable
Update-AzFunctionAppSetting -Name <FUNCTION_APP_NAME> -ResourceGroupName <RESOURCE_GROUP_NAME> -AppSetting #{"AzureWebJobs.<Function_Name>.Disabled" = "true"}
Enable
Update-AzFunctionAppSetting -Name <FUNCTION_APP_NAME> -ResourceGroupName <RESOURCE_GROUP_NAME> -AppSetting #{"AzureWebJobs.QueueTrigger.Disabled" = "false"}
PowerShell to get all the Functions in all ResourceGroup. If only certain RG needed, this can be filtered by uncomment the code where RG filter is.
In my case, I'm looking for Azure Function v3, so I print out only the function V3, but you can filter the list or update the Functions however you want.
$subscriptionId = "some-guid-subscription"
Set-AzContext -SubscriptionId $subscriptionId
$apps = Get-AzFunctionApp -SubscriptionId $subscriptionId
Write-Output ("Checking subscription " + $subscriptionId)
foreach ($app in $apps) {
# Filter RG here if needed
#if ($app.ResourceGroupName -ne "a-resource-group") { continue }
$appsetting = Get-AzFunctionAppSetting -Name $app.Name -ResourceGroupName $app.ResourceGroupName
if ($appsetting.FUNCTIONS_EXTENSION_VERSION -eq "~3") {
Write-Output ($app.Name + " in RG: " + $app.ResourceGroupName + " has version" + $appsetting.FUNCTIONS_EXTENSION_VERSION)
}
}