Azure WebApp - version controlled configuration/user data deployed separately from app - azure

I've developed an WebApp (API) which is hosted in Azure & uses VSTS for CI/CD/version control.
I'd like to give the customer (owner) of this API the ability to update various configuration/data files under wwwroot, however I'd like those files to be under version control (source of truth - a separate repository to the API source code). Creating/Updating/Deleting one of those files in the repository should cause the file to be uploaded/removed in the WebApp (in a folder under wwwroot).
Modifying (creating/deleting) one of these files should not trigger a full redeployment (of the WebApp)
How can I achieve this? So far I've thought about a VSTS release pipeline for a GIT artefact however I couldn't see a low-friction way to make the changes in the Azure webapp (KUDU API seems a bit complex and heavy-handed)
**EDIT: ** Sample PowerShell script to sync Configuration files in WebApp w/ a build-artefact (PUT/DELETE only invoked when necessary).
# The idea behind this script is to synchronize the configuration files on the server with what's in the repo, only updating files where necessary
param (
[string]$resourceGroupName = "XXX",
[Parameter(Mandatory=$true)][string]$webAppName,
[Parameter(Mandatory=$true)][string]$latestConfigFilesPath
)
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 Get-KuduInode($kuduHref) {
return Invoke-RestMethod -Uri $kuduHref `
-Headers #{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} `
-Method GET `
-ContentType "application/json"
}
function Get-AllFilesUnderKuduHref($kuduHref) {
$result = #()
$inodes = (Get-KuduInode $kuduHref)
Foreach ($inode in $inodes) {
if ($inode.mime -eq "inode/directory") {
$result += (Get-AllFilesUnderKuduHref $inode.href)
} else {
$result += $inode.href
}
}
return $result
}
function Get-LocalPathForUri([System.Uri]$uri) {
$latestConfigFilesUri = [System.Uri]$latestConfigFilesPath
$localFileUri = [System.Uri]::new($latestConfigFilesUri, $uri)
return $localFileUri.LocalPath
}
function Get-RemoteUri([System.Uri]$uri) {
return [System.Uri]::new($configurationHref, $uri)
}
function Files-Identical($uri) {
$localFilePath = Get-LocalPathForUri $uri
$localFileHash = Get-FileHash $localFilePath -Algorithm MD5
# Download the remote file so that we can calculate the hash. It doesn't matter that it doesn't get cleaned up, this is running on a temporary build server anyway.
$temporaryFilePath = "downloded_kudu_file"
$remoteFileUri = [System.Uri]::new($configurationHref, $uri)
Invoke-RestMethod -Uri $remoteFileUri `
-Headers #{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} `
-Method GET `
-OutFile $temporaryFilePath `
-ContentType "multipart/form-data"
$remoteFileHash = Get-FileHash $temporaryFilePath -Algorithm MD5
return $remoteFileHash.Hash -eq $localFileHash.Hash
}
function CalculateRelativePath([System.Uri]$needle, [System.Uri]$haystack) {
return $haystack.MakeRelativeUri($needle).ToString();
}
function Put-File([System.Uri]$uri) {
Write-Host "Uploading file $uri"
$localFilePath = Get-LocalPathForUri $uri
$remoteFileUri = Get-RemoteUri $uri
Invoke-RestMethod -Uri $remoteFileUri `
-Headers #{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} `
-Method PUT `
-InFile $localFilePath `
-ContentType "multipart/form-data"
}
function Delete-File([System.Uri]$uri) {
Write-Host "Deleting file $uri"
$remoteFileUri = Get-RemoteUri $uri
Invoke-RestMethod -Uri $remoteFileUri `
-Headers #{"Authorization"=$kuduApiAuthorisationToken;"If-Match"="*"} `
-Method DELETE `
}
# Script begins here
$configurationHref = [System.Uri]"https://$webAppName.scm.azurewebsites.net/api/vfs/site/wwwroot/Configuration/"
$kuduApiAuthorisationToken = Get-KuduApiAuthorisationHeaderValue -resourceGroupName $resourceGroupName -webAppName $webAppName
$filenamesOnServer = Get-AllFilesUnderKuduHref $configurationHref $kuduApiAuthorisationToken | % { $configurationHref.MakeRelativeUri($_).OriginalString }
Write-Host "Files currently on server" $filenamesOnServer
$filesCurrentlyInRepo = Get-ChildItem -Path $latestConfigFilesPath -Recurse -File
$filenamesInRepo = $filesCurrentlyInRepo | Select-Object -ExpandProperty FullName | % { CalculateRelativePath $_ $latestConfigFilesPath}
Write-Host "Files currently in repo" $filenamesInRepo
$intersection = $filenamesOnServer | ?{$filenamesInRepo -contains $_}
Write-Host "Intersection: " $intersection
$onlyOnServer = $filenamesOnServer | ?{-Not($filenamesInRepo -contains $_)}
$onlyInRepo = $filenamesInRepo | ?{-Not($filenamesOnServer -contains $_)}
Write-Host "Only on server" $onlyOnServer
Write-Host "Only in repo" $onlyInRepo
Write-Host
Foreach ($uri in $onlyInRepo) {
Put-File $uri
}
Foreach ($uri in $onlyOnServer) {
Delete-File $uri
}
Foreach ($uri in $intersection) {
if (-Not (Files-Identical $uri)) {
Write-Host "Configuration file $uri needs updating"
Put-File $uri
} else {
Write-Host "Configuration file $uri is identical, skipping"
}
}
Write-Host "Sync complete"

With Azure App Service deploy task, you can upload files to app service, but can’t delete files (Uncheck Publish using Web Deploy optioin and specify the folder path in Package or folder input box).
So, the better way is using Kudu API to delete/upload files during build/release.
There is the thread and a blog about using Kudu API:
How to access Kudu in Azure using power shell script
Interacting with Azure Web Apps Virtual File System using PowerShell and the Kudu API

Related

Do we need to dispose azure powershell commands?

We have over 400 app services and we are creating a deployment tool to deploy to all of these instances as soon as possible.
We have 4 projects running in separate app service and each customer need to have all 4 projects up and running so we have distributed the instances by customer name and each instance deploy to 4 app services internally (See the code snippet below)
We've created a powershell function app with queue trigger that is calling azure powershell commands to do the deployment. The deployment steps are:
Create deployment slot
Publish to the deployment slot
Version check (Have an endpoint in all projects which returns assembly version)
Swap slot
Version check again
Delete slot
The steps are straight forward but when I deploy to single instance it runs pretty quickly but when I am trying to deploy to multiple instances (5+) then it starts becoming really slow and eventually stops execution. My assumption is that there is a memory issue. I am using premium (EP1) plan for my function so it shouldn't be that slow.
I am totally new to powershell so the question is do I need to dispose the azure powershell stuff? Or anything else to freed the memory. If yes, how can I call dispose to azure powershell commands?
Update
Adding the code snippets.
Some more context:
So the run.ps1 file has the code that downloads the ps1 script file from blob storage and dynamically executes it:
run.ps1
# Input bindings are passed in via param block.
param($QueueItem, $TriggerMetadata)
# Write out the queue message and insertion time to the information log.
Write-Host "PowerShell queue trigger function processed work item: $QueueItem"
Write-Host "Queue item insertion time: $($TriggerMetadata.InsertionTime)"
$currentContext = Get-AzContext -ListAvailable | Where-Object {$_.Name -contains 'Subscriptionname'}
$storageAccount = Get-AzStorageAccount -ResourceGroupName "<ResourceGroup>" -Name "<ResourceName>" -DefaultProfile $currentContext
$Context = $storageAccount.Context
$queue = Get-AzStorageQueue -Name logs -Context $Context
function Log {
param (
[string]$message, [string]$isDetailed, [string]$hasError = "false"
)
$jsonMessge = #"
{
"InstanceId": "$instanceId",
"TaskId": "$taskId",
"Note": "$message",
"IsDetailed": $isDetailed,
"HasError": $hasError
}
"#
# Create a new message using a constructor of the CloudQueueMessage class
$queueMessage = [Microsoft.Azure.Storage.Queue.CloudQueueMessage]::new($jsonMessge)
# # Add a new message to the queue
$queue.CloudQueue.AddMessageAsync($queueMessage)
}
try {
#Extracting data from Queue message
$queueMessage = $QueueItem
$instanceId = $queueMessage.instanceId
$taskId = $queueMessage.taskId
$siteName = $queueMessage.siteName
$buildNo = $queueMessage.buildNo
$pass = $queueMessage.password
$dbPassword = convertto-securestring "$pass" -asplaintext -force
$servicePlanName = $queueMessage.servicePlanName
$subscription = $queueMessage.subscription
$key = $queueMessage.key
$rg = $queueMessage.resourceGroup
$sqlServerName = $queueMessage.sqlServerName
Log -message "Deployment started for $($siteName)" -isDetailed "false"
$tempFolder = $env:temp
# $artifactFolderPath = "$($tempFolder)\$($siteName)\$($buildNo)"
#$artifactFilePath = "$($tempFolder)\$($siteName)\$($buildNo).zip"
$tempDownloadPath = "$($tempFolder)\$($siteName)"
$scriptFileName = "DeployScripts.ps1"
if (Test-Path $tempDownloadPath) {
Remove-Item $tempDownloadPath -Force -Recurse
}
$storageAccount = Get-AzStorageAccount -ResourceGroupName "KineticNorthEurope" -Name "KineticDeployment" -DefaultProfile $currentContext
New-Item -Path $tempFolder -Name $siteName -ItemType "directory"
$Context = $storageAccount.Context
$blobContent = #{
Blob = "DeployScripts.ps1"
Container = 'builds'
Destination = "$($tempDownloadPath)\$($scriptFileName)"
Context = $Context
}
Get-AzStorageBlobContent #blobContent -DefaultProfile $currentContext
#[System.IO.Compression.ZipFile]::ExtractToDirectory($artifactFilePath, $artifactFolderPath)
$arguments = "-rg $rg -site $siteName -buildNo $buildNo -dbPassword `$dbPassword -instanceId $instanceId -taskId $taskId -sqlServerName $sqlServerName -subscription $subscription"
$path = "$($tempDownloadPath)\$($scriptFileName)"
Unblock-File -Path $path
"$path $ScriptFilePath $arguments" | Invoke-Expression
if (Test-Path $tempDownloadPath) {
Remove-Item $tempDownloadPath -Force -Recurse
}
Log -message "Resources cleaned up" -isDetailed "false"
}
catch {
Log -message $_.Exception.message -isDetailed "false" -hasError "true"
if (Test-Path $tempDownloadPath) {
Remove-Item $tempDownloadPath -Force -Recurse
}
}
And here is the actual DeployScripts.ps1 file:
param($rg, $site, $buildNo, [SecureString] $dbPassword, $instanceId, $taskId, $sqlServerName, $subscription)
$siteNameWeb = "${site}"
$siteNamePortal = "${site}Portal"
$siteNamePortalService = "${site}PortalService"
$siteNameSyncService = "${site}SyncService"
$kineticNorthContext = Get-AzContext -ListAvailable | Where-Object {$_.Name -contains 'SubscriptionName'}
$storageAccount = Get-AzStorageAccount -ResourceGroupName "<ResourceGroup>" -Name "<ResourceName>" -DefaultProfile $kineticNorthContext
$Context = $storageAccount.Context
$queue = Get-AzStorageQueue -Name logs -Context $Context
Set-AzContext -SubscriptionName $subscription
Function CreateDeploymentSlots() {
Log -message "Creating deployment slots" -isDetailed "false"
Log -message "Creating deployment slot for web" -isDetailed "true"
New-AzWebAppSlot -ResourceGroupName $rg -name $siteNameWeb -slot develop
Log -message "Creating deployment slot for portal" -isDetailed "true"
New-AzWebAppSlot -ResourceGroupName $rg -name $siteNamePortal -slot develop
Log -message "Creating deployment slot for portal service" -isDetailed "true"
New-AzWebAppSlot -ResourceGroupName $rg -name $siteNamePortalService -slot develop
Log -message "Creating deployment slot for sync service" -isDetailed "true"
New-AzWebAppSlot -ResourceGroupName $rg -name $siteNameSyncService -slot develop
Log -message "Deployment slots created" -isDetailed "false"
}
Function DeleteDeploymentSlots() {
Log -message "Deleting deployment slots" -isDetailed "false"
Log -message "Deleting slot web" -isDetailed "true"
Remove-AzWebAppSlot -ResourceGroupName $rg -Name $siteNameWeb -Slot "develop"
Log -message "Deleting slot portal" -isDetailed "true"
Remove-AzWebAppSlot -ResourceGroupName $rg -Name $siteNamePortal -Slot "develop"
Log -message "Deleting slot portal service" -isDetailed "true"
Remove-AzWebAppSlot -ResourceGroupName $rg -Name $siteNamePortalService -Slot "develop"
Log -message "Deleting slot sync service" -isDetailed "true"
Remove-AzWebAppSlot -ResourceGroupName $rg -Name $siteNameSyncService -Slot "develop"
Log -message "Slots deployment deleted" -isDetailed "false"
}
Function SwapDeploymentSlots {
Log -message "Switching deployment slots" -isDetailed "false"
Log -message "Switch slot web" -isDetailed "true"
Switch-AzWebAppSlot -SourceSlotName "develop" -DestinationSlotName "production" -ResourceGroupName $rg -Name $siteNameWeb
Log -message "Switch slot portal" -isDetailed "true"
Switch-AzWebAppSlot -SourceSlotName "develop" -DestinationSlotName "production" -ResourceGroupName $rg -Name $siteNamePortal
Log -message "Switch slot portal service" -isDetailed "true"
Switch-AzWebAppSlot -SourceSlotName "develop" -DestinationSlotName "production" -ResourceGroupName $rg -Name $siteNamePortalService
Log -message "Switch slot sync service" -isDetailed "true"
Switch-AzWebAppSlot -SourceSlotName "develop" -DestinationSlotName "production" -ResourceGroupName $rg -Name $siteNameSyncService
Log -message "Deployment slots switched" -isDetailed "false"
}
function Log {
param (
[string]$message, [string]$isDetailed, [string]$isDeployed = "false", [string]$hasError = "false"
)
$jsonMessge = #"
{
"InstanceId": "$instanceId",
"TaskId": "$taskId",
"Note": "$message",
"IsDetailed": $isDetailed,
"IsDeployed": $isDeployed,
"HasError": $hasError
}
"#
# Create a new message using a constructor of the CloudQueueMessage class
$queueMessage = [Microsoft.Azure.Storage.Queue.CloudQueueMessage]::new($jsonMessge)
# # Add a new message to the queue
$queue.CloudQueue.AddMessageAsync($queueMessage)
}
function VersionCheckWeb() {
$tls = Invoke-WebRequest -URI "https://${site}.net/Home/Info" -UseBasicParsing
$content = $tls.Content | ConvertFrom-Json
$versionCheck = $content.VersionNo -eq $buildNo
return $versionCheck
}
function VersionCheckPortal() {
$tls = Invoke-WebRequest -URI "https://${site}portal.net/Home/Info" -UseBasicParsing
$content = $tls.Content | ConvertFrom-Json
$versionCheck = $content.VersionNo -eq $buildNo
return $versionCheck
}
function VersionCheckPortalService() {
$tls = Invoke-WebRequest -URI "https://${site}portalservice.net/Home/Info" -UseBasicParsing
$content = $tls.Content | ConvertFrom-Json
$versionCheck = $content.VersionNo -eq $buildNo
return $versionCheck
}
function VersionCheckSyncService() {
$tls = Invoke-WebRequest -URI "https://${site}syncservice.net/SyncService.svc/Info" -UseBasicParsing
$tls = $tls -replace '[?]', ""
$content = "$tls" | ConvertFrom-Json
$versionCheck = $content.VersionNo -eq $buildNo
return $versionCheck
}
function VersionCheck() {
$versionCheckWeb = VersionCheckWeb
$versionCheckPortal = VersionCheckPortal
$versionCheckPortalService = VersionCheckPortalService
$versionCheckSyncService = VersionCheckSyncService
if (($versionCheckWeb -eq "True") -and ($versionCheckPortal -eq "True") -and ($versionCheckPortalService -eq "True") -and ($versionCheckSyncService -eq "True")) {
Log -message "Version correct" -isDetailed "false"
}
else {
Log -message "Version check failed, exception not thrown" -isDetailed "false"
}
}
function VersionCheckWebSlot() {
$tls = Invoke-WebRequest -URI "https://${site}-develop.azurewebsites.net/Home/Info" -UseBasicParsing
$content = $tls.Content | ConvertFrom-Json
$versionCheck = $content.VersionNo -eq $buildNo
return $versionCheck
}
function VersionCheckPortalSlot() {
$tls = Invoke-WebRequest -URI "https://${site}portal-develop.azurewebsites.net/Home/Info" -UseBasicParsing
$content = $tls.Content | ConvertFrom-Json
$versionCheck = $content.VersionNo -eq $buildNo
return $versionCheck
}
function VersionCheckPortalServiceSlot() {
$tls = Invoke-WebRequest -URI "https://${site}portalservice-develop.azurewebsites.net/Home/Info" -UseBasicParsing
$content = $tls.Content | ConvertFrom-Json
$versionCheck = $content.VersionNo -eq $buildNo
return $versionCheck
}
function VersionCheckSyncServiceSlot() {
$tls = Invoke-WebRequest -URI "https://${site}syncservice-develop.azurewebsites.net/SyncService.svc/Info" -UseBasicParsing
$tls = $tls -replace '[?]', ""
$content = "$tls" | ConvertFrom-Json
$versionCheck = $content.VersionNo -eq $buildNo
return $versionCheck
}
function VersionCheckSlot() {
$versionCheckWeb = VersionCheckWebSlot
$versionCheckPortal = VersionCheckPortalSlot
$versionCheckPortalService = VersionCheckPortalServiceSlot
$versionCheckSyncService = VersionCheckSyncServiceSlot
if (($versionCheckWeb -eq "True") -and ($versionCheckPortal -eq "True") -and ($versionCheckPortalService -eq "True") -and ($versionCheckSyncService -eq "True")) {
Log -message "Slot version correct" -isDetailed "false"
}
else {
Log -message "Slot version check failed, exception not thrown" -isDetailed "false"
}
}
function PublishToAzure() {
$webPublishProfile = Get-AzWebAppPublishingProfile -ResourceGroupName $rg -Name $siteNameWeb
$webXml = $webPublishProfile -as [Xml]
$webUserName = $webXml.publishData.publishProfile[0].userName
$webUserPwd = $webXml.publishData.publishProfile[0].userPWD
$webpair = "$($webUserName):$($webUserPwd)"
$webencodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($webpair))
$webauthHeader = "Basic $webencodedCreds"
$webHeaders = #{
Authorization = $webauthHeader
}
$portalPublishProfile = Get-AzWebAppPublishingProfile -ResourceGroupName $rg -Name $siteNamePortal
$portalXml = $portalPublishProfile -as [Xml]
$portalUserName = $portalXml.publishData.publishProfile[0].userName
$portalUserPwd = $portalXml.publishData.publishProfile[0].userPWD
$portalpair = "$($portalUserName):$($portalUserPwd)"
$portalencodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($portalpair))
$portalauthHeader = "Basic $portalencodedCreds"
$portalHeaders = #{
Authorization = $portalauthHeader
}
$portalServicePublishProfile = Get-AzWebAppPublishingProfile -ResourceGroupName $rg -Name $siteNamePortalService
$portalServiceXml = $portalServicePublishProfile -as [Xml]
$portalServiceUserName = $portalServiceXml.publishData.publishProfile[0].userName
$portalServiceUserPwd = $portalServiceXml.publishData.publishProfile[0].userPWD
$portalServicepair = "$($portalServiceUserName):$($portalServiceUserPwd)"
$portalServiceencodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($portalServicepair))
$portalServiceauthHeader = "Basic $portalServiceencodedCreds"
$portalServiceHeaders = #{
Authorization = $portalServiceauthHeader
}
$syncServicePublishProfile = Get-AzWebAppPublishingProfile -ResourceGroupName $rg -Name $siteNameSyncService
$syncServiceXml = $syncServicePublishProfile -as [Xml]
$syncServiceUserName = $syncServiceXml.publishData.publishProfile[0].userName
$syncServiceUserPwd = $syncServiceXml.publishData.publishProfile[0].userPWD
$syncServicepair = "$($syncServiceUserName):$($syncServiceUserPwd)"
$syncServiceencodedCreds = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($syncServicepair))
$syncServiceauthHeader = "Basic $syncServiceencodedCreds"
$syncServiceHeaders = #{
Authorization = $syncServiceauthHeader
}
$bodyWeb = '{"packageUri": "<blobUrl>"}'
$bodyPortal = '{"packageUri": "blobUrl"}'
$bodyPortalService = '{"packageUri": "blobUrl"}'
$bodySyncService = '{"packageUri": "blobUrl"}'
$web = Invoke-RestMethod -URI "https://${siteNameWeb}.scm.azurewebsites.net/api/publish?type=zip" -Method POST -Body $bodyWeb -Headers $webHeaders -ContentType "application/json"
Log -message "Published to Web" -isDetailed "false"
$portal = Invoke-RestMethod -URI "https://${siteNamePortal}.scm.azurewebsites.net/api/publish?type=zip" -Method POST -Body $bodyPortal -Headers $portalHeaders -ContentType "application/json"
Log -message "Published to Portal" -isDetailed "false"
$portalService = Invoke-RestMethod -URI "https://${siteNamePortalService}.scm.azurewebsites.net/api/publish?type=zip" -Method POST -Body $bodyPortalService -Headers $portalServiceHeaders -ContentType "application/json"
Log -message "Published to PortalService" -isDetailed "false"
$syncService = Invoke-RestMethod -URI "https://${siteNameSyncService}.scm.azurewebsites.net/api/publish?type=zip" -Method POST -Body $bodySyncService -Headers $syncServiceHeaders -ContentType "application/json"
Log -message "Published to SyncService" -isDetailed "false"
}
try {
CreateDeploymentSlots
PublishToAzure
VersionCheckSlot
SwapDeploymentSlots
VersionCheck
DeleteDeploymentSlots
Log -message "Instance deployed successfully" -isDetailed "false" -isDeployed "true"
}
catch {
Log -message $_.Exception.message -isDetailed "false" -hasError "true"
}
I know the code is little mess right now and that's because I've changed the implementation to faster the process but no major luck.
Question2
I actually have another question as well. We've multiple subscriptions and appservices are distributed accordingly. So when function app tries to deploy to a instance exists in another subscription, it throws error. I'm setting AzContext before starting the processing and it seems to be working fine. But i also have to put message into a queue after each step and the queue exists in a storage account that belongs to same subscription as of the function app. Currently i'm getting the AzContent and passing it as -DefaultProfile while getting the storage account. Is there a better way to handle this?
Also I am writing powershell for the first time so any suggestion would be appreciated. Thanks.

How to make an Azure app registration with platform SPA via Powershell

We use PowerShell to set up an Azure deployment, which, among other Azure resources, creates an app registration.
The simplified code is as follows:
$appRegistration = New-AzADApplication `
-DisplayName $applicationName `
-HomePage "$webAppUrl" `
-IdentifierUris "api://$webAppName";
To it, we add redirect uris, like this:
if ($redirectUris -notcontains "$webAppUrl") {
$redirectUris.Add("$webAppUrl");
Write-Host "Adding $webAppUrl to redirect URIs";
}
if ($redirectUris -notcontains "$webAppUrl/aad-auth") {
$redirectUris.Add("$webAppUrl/aad-auth");
Write-Host "Adding $webAppUrl/aad-auth to redirect URIs";
}
Update-AzADApplication `
-ApplicationId $applicationId `
-IdentifierUris "api://$applicationId" `
-ReplyUrl $redirectUris | Out-Null
This works great, and an app registration with the "web" platform is created. It looks like this:
My question is how can we get these redirect uris to be under the "SPA" platform, using PowerShell? Like in the image below, which was done manually on the Portal.
Looks there is no feature in the built-in command to do that, you could call the MS Graph - Update application in the powershell directly.
You could refer to the sample below work for me, make sure your service principal/user acount logged in Az via Connect-AzAccount has the permission to call the API.
$objectId = "xxxxxxxxxxxxxxxx"
$redirectUris = #()
$webAppUrl = "https://joyweb.azurewebsites.net"
if ($redirectUris -notcontains "$webAppUrl") {
$redirectUris += "$webAppUrl"
Write-Host "Adding $webAppUrl to redirect URIs";
}
if ($redirectUris -notcontains "$webAppUrl/aad-auth") {
$redirectUris += "$webAppUrl/aad-auth"
Write-Host "Adding $webAppUrl/aad-auth to redirect URIs";
}
$accesstoken = (Get-AzAccessToken -Resource "https://graph.microsoft.com/").Token
$header = #{
'Content-Type' = 'application/json'
'Authorization' = 'Bearer ' + $accesstoken
}
$body = #{
'spa' = #{
'redirectUris' = $redirectUris
}
} | ConvertTo-Json
Invoke-RestMethod -Method Patch -Uri "https://graph.microsoft.com/v1.0/applications/$objectId" -Headers $header -Body $body
Check the result in the portal:
There was a similar thread where someone was trying to programmatically add the redirect URIs for SPA and could not do it because it defaults under the Web section.
He was able to resolve this by posting with Azure CLI to the Graph API:
az rest `
--method PATCH `
--uri 'https://graph.microsoft.com/v1.0/applications/{id}' `
--headers 'Content-Type=application/json' `
--body "{spa:{redirectUris:['http://localhost:3000']}}"

Azure Web Job Zip deployment Error due to Size

I am deploying a web job through powershell script and can manage to get the publishing credentials and then add the access token in the authorization header. All is fine until it uploads the zip file when I receive file size error: The remote server returned an error: (413) Request Entity Too Large.
#Function to get Publishing credentials for the WebApp :
function Get-PublishingProfileCredentials($resourceGroupName, $AppServiceNameToDeployWebJobs) {
$resourceType = "Microsoft.Web/sites/config"
$resourceName = "$AppServiceNameToDeployWebJobs/publishingcredentials"
$publishingCredentials = Invoke-AzResourceAction -ResourceGroupName $resourceGroupName -ResourceType `
$resourceType -ResourceName $resourceName -Action list -ApiVersion $Apiversion -Force
return $publishingCredentials
}
#Pulling authorization access token :
function Get-KuduApiAuthorisationHeaderValue($resourceGroupName, $AppServiceNameToDeployWebJobs) {
$publishingCredentials = Get-PublishingProfileCredentials $resourceGroupName $AppServiceNameToDeployWebJobs
return ("Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f `
$publishingCredentials.Properties.PublishingUserName, $publishingCredentials.Properties.PublishingPassword))))
}
$accessToken = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $AppServiceNameToDeployWebJobs
#Generating header to create and publish the Webjob :
$Header = #{
'Content-Disposition' = 'attachment; attachment; filename=Copy.zip'
'Authorization' = $accessToken
}
$apiUrl = "http://xxxx.scm.azurewebsites.net/app_data/jobs/triggered/Test/"
$result = Invoke-RestMethod -Uri $apiUrl -Headers $Header -Method put `
-InFile "D:\Work\WebJobs\WebJobsBuild\Test.zip" -ContentType 'application/zip' `
-TimeoutSec 600
The zip file size is only 43MB. How can I check the upper limit of file size allowed and how can I increase it? I've tried both Invoke-WebRequest and Invoke-RestMethod but the result is the same
I modify $apiUrl and it works for me.
It should be like
$apiUrl = "https://$AppServiceNameToDeployWebJobs.scm.azurewebsites.net/api/triggeredwebjobs/MyWebJob1"
Step 1. My test webjob in portal, and I will create MyWebJob1 later.
Step 2. Before running cmd.
Step 3. Modify the web job name as MyWebJob1.
Step 4. Check the webjob in portal.
Sample Code
$resourceGroupName='***';
$AppServiceNameToDeployWebJobs='jas***pp';
$Apiversion='2019-08-01';
#Function to get Publishing credentials for the WebApp :
function Get-PublishingProfileCredentials($resourceGroupName, $AppServiceNameToDeployWebJobs) {
$resourceType = "Microsoft.Web/sites/config"
$resourceName = "$AppServiceNameToDeployWebJobs/publishingcredentials"
$publishingCredentials = Invoke-AzResourceAction -ResourceGroupName $resourceGroupName -ResourceType `
$resourceType -ResourceName $resourceName -Action list -ApiVersion $Apiversion -Force
return $publishingCredentials
}
#Pulling authorization access token :
function Get-KuduApiAuthorisationHeaderValue($resourceGroupName, $AppServiceNameToDeployWebJobs) {
$publishingCredentials = Get-PublishingProfileCredentials $resourceGroupName $AppServiceNameToDeployWebJobs
return ("Basic {0}" -f [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f `
$publishingCredentials.Properties.PublishingUserName, $publishingCredentials.Properties.PublishingPassword))))
}
$accessToken = Get-KuduApiAuthorisationHeaderValue $resourceGroupName $AppServiceNameToDeployWebJobs
#Generating header to create and publish the Webjob :
$Header = #{
'Content-Disposition' = 'attachment; attachment; filename=test.zip'
'Authorization' = $accessToken
}
$apiUrl = "https://$AppServiceNameToDeployWebJobs.scm.azurewebsites.net/api/triggeredwebjobs/MyWebJob1"
$result = Invoke-RestMethod -Uri $apiUrl -Headers $Header -Method put `
-InFile "E:\test.zip" -ContentType 'application/zip' `
-TimeoutSec 600

Disable AFD Backend Pool's Backend Host with Azure Cli or Azure REST API

Im trying to update an existing backend host of a AFD backend pool to have its status from Enabled to Disabled.
Is there a way to update existing backend host of Front Door backend pools?
currently, i can only see add, list and remove in the following azure front door cli docs:
az network front-door backend-pool backend add
az network front-door backend-pool backend list
az network front-door backend-pool backend remove
Is there one for update?
I've also looked into the Azure REST API docs and have not found an endpoint to update a backend host of AFD backend pools.
I am able to achieve your ask using PowerShell.
Here is the script:
$resourceGroup1 = "frontdoor"
$frontDoor1 = "msrini"
$afd = Get-AzFrontDoor -ResourceGroupName $resourceGroup1 -name $frontDoor1
$loadBalancingSetting1=$afd.LoadBalancingSettings
$afd.BackendPools.backends[0].EnabledState = "Disabled"
$backendpool1=$afd.BackendPools
$frontendEndpoint1 = $afd.FrontendEndpoints
$healthProbeSetting1= $afd.HealthProbeSettings
$routingrule1 = $afd.RoutingRules
$backendpoolsettings1 = $afd.BackendPoolsSetting
Set-AzFrontDoor -Name $frontDoor1 -ResourceGroupName $resourceGroup1 -RoutingRule $routingrule1 -BackendPool $backendpool1 -FrontendEndpoint $frontendEndpoint1 -LoadBalancingSetting $loadBalancingSetting1 -HealthProbeSetting $healthProbeSetting1 -BackendPoolsSetting $backendpoolsettings1
I was able to resolve my issue, the below script requires using azure cli with a logged in service principle.
Param(
[string]$desiredState, #"Enabled" or "Disabled"
[string]$frontDoorName,
[string]$resourceGroupName,
[string]$targetBackendPool,
[string]$targetBackendHost
)
# Get Access Token
$token = az account get-access-token | ConvertFrom-Json
$accessToken = $token.accessToken
$subscriptionId = $token.subscription
$tenantId = $token.tenant
$uri = "https://management.azure.com/subscriptions/$($subscriptionId)/resourceGroups/$($resourceGroupName)/providers/Microsoft.Network/frontDoors/$($frontDoorName)?api-version=2019-05-01"
$headers = #{ "Authorization" = "Bearer $accessToken" }
$contentType = "application/json"
# Get AFD Configuration.
Write-Host "Invoking Azure REST API to get AFD configuration"
$afdConfig = Invoke-WebRequest -method get -uri $uri -headers $headers | ConvertFrom-Json
# Edit AFD Configuration to toggle backend host state
Write-Host "Checking BackendHost: $targetBackendHost In Backend Pool: $targetBackendPool"
foreach ($backendPool in $afdConfig.properties.backendPools) {
if ($backendPool.name -eq $targetBackendPool) {
foreach ($backends in $backendPool.properties.backends) {
if ($backends.address -eq $targetBackendHost) {
$currentState = $backends.enabledState
Write-Host "Current State of $targetBackendHost is $currentState"
if ($currentState -eq $desiredState) {
Write-Host "$targetBackendHost is already in the desired state of $desiredState"
exit
}
$backends.enabledState = $desiredState
Write-Host "$targetBackendHost Updated to $desiredState."
}
}
}
}
$updateRequest = $afdConfig | ConvertTo-Json -Depth 100
# Update AFD Configuration.
Write-Host "Invoking Azure REST API to update AFD configuration"
Invoke-WebRequest -method put -uri $uri -headers $headers -ContentType $contentType -body $updateRequest
# Poll current state until the change has been fully propogated in azure
Write-Host "Polling AFD until update is complete."
do {
$afdConfig = Invoke-WebRequest -method get -uri $uri -headers $headers | ConvertFrom-Json
foreach ($backendPool in $afdConfig.properties.backendPools) {
if ($backendPool.name -eq $targetBackendPool) {
foreach ($backends in $backendPool.properties.backends) {
if ($backends.address -eq $targetBackendHost) {
$currentState = $backends.enabledState
}
}
}
}
Write-Host "$targetBackendHost is currently in $currentState"
Start-Sleep -Seconds 1
} until ($currentState -eq $desiredState)
Write-Host "$targetBackendHost has successfully been updated to $desiredState"
I did it using Powershell
$FrontDoor = Get-AzFrontDoor -ResourceGroupName "FrontDoorResourceGroupName" -Name "FrontDoorName"
$FrontDoor.BackendPools.Backends[0].EnabledState = "Disabled"
Set-AzFrontDoor -InputObject $FrontDoor

Azure Function Key Management API - Authentication error

I have two Azure function apps, both in the same Azure subscription. I can retrieve keys for one but not for the other. As far as I can see there is no difference between the two function apps.
I'm using this Powershell code:
function GetHostKey
{
param($webAppName, $resourceGroupName)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "Getting master key from $webAppName"
$xml = [xml](Get-AzureRmWebAppPublishingProfile -Name $webAppName -ResourceGroupName $resourceGroupName -Format WebDeploy -OutputFile null)
$msdeployUsername = $xml.SelectNodes("//publishProfile[#publishMethod=`"MSDeploy`"]/#userName").value
$msdeployPassword = $xml.SelectNodes("//publishProfile[#publishMethod=`"MSDeploy`"]/#userPWD").value
$apiBaseUrl = "https://$webAppName.scm.azurewebsites.net/api"
$siteBaseUrl = "https://$webAppName.azurewebsites.net"
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $msdeployUsername,$msdeployPassword)))
$jwt = Invoke-RestMethod -Uri "$apiBaseUrl/functions/admin/token" -Headers #{Authorization=("Basic {0}" -f $base64AuthInfo)} -Method GET
$uri = "$siteBaseUrl/admin/host/systemkeys/_master"
$response = Invoke-RestMethod -Uri $uri -Headers #{Authorization=("Bearer {0}" -f $jwt)} -Method GET
return $response.value
}
The call to $siteBaseUrl/admin/host/systemkeys/_master returns the expected json for one function app, but the other one returns a login screen.
Compare both functions settings, versions. Try to get the keys manually from the portal to isolate the source of the problem. I had the same problem till i deleted and recreated the functions.

Resources