pf-azFuncDeploy.psm1
$ErrorActionPreference = 'stop' function Ensure-Module ($name) { if (Get-Module -name $name ) { return } if (-not (Get-Module -name $name -ListAvailable )) { Set-PSRepository -name PsGallery -InstallationPolicy Trusted Install-Module -Name $name -Repository PsGallery } Import-Module $name } function Get-ScriptTopCallerFolder() { $scriptPath = Get-PSCallStack | where ScriptName | select -Last 1 $result = Split-Path $scriptPath.ScriptName -Parent return $result; } function New-TemporaryDirectory { $parent = [System.IO.Path]::GetTempPath() $name = [System.IO.Path]::GetRandomFileName() New-Item -ItemType Directory -Path (Join-Path $parent $name) } function Prepare-Git_CLI { # Git return messages in sdterr by default, which can be interpreted as # an execution error even when the call succeded. # The following ensures output is redirected to stdout. # Execution error should be checked using $LASTEXITCODE $env:GIT_REDIRECT_STDERR = '2>&1' } function Get-GitBranchName { git symbolic-ref HEAD | % { $_.TrimStart("refs/heads/") } } function Get-GitTopFolder { $folder = git rev-parse --show-toplevel if ($LASTEXITCODE -eq 0) { $folder = $folder -replace '/', '\' return $folder } } function Get-GitRepositoryName($remote = 'origin') { $remoteUrl = git remote get-url --push $remote if ($LASTEXITCODE -eq 0) { $result = Split-Path -Path $remoteUrl -Leaf return $result } } function Get-GitInfo($path) { $result = @{} try { if ($path) { Push-Location $path } Prepare-Git_CLI $result.Name = Get-GitRepositoryName $result.Folder = Get-GitTopFolder $result.Branch = Get-GitBranchName $result.LastAuthor = git log -1 --pretty=format:'%an' $result.CommitCount = git rev-list HEAD --count return $result } finally { if ($path) { Pop-Location } } } $azLocationAbbreviationMap = @{ UKS = 'UK South' UKW = 'UK West' } function Get-AzLocationAbbreviation($location){ $item = $azLocationAbbreviationMap.GetEnumerator() | ? { $_.Value -eq $location } return $item.Key } function Get-AzLocationAbbreviation:::Example{ Get-AzLocationAbbreviation 'UK South' } function Ensure-Member { Param ( [Parameter(ValueFromPipeline=$true)] $InputObject, $Name, $value, [Switch]$force, [Switch]$PassThru ) Process { if (!$InputObject) { return } $doAssignValue = (-not $InputObject.$Name) -or $force.IsPresent $member = Get-Member -InputObject $InputObject -Name $Name if (!$member) { $doAssignValue = $true $member = Add-Member -InputObject $InputObject -Name $Name -Value $null ` -MemberType NoteProperty -PassThru } if ($doAssignValue) { $valueResult = if ($Value -is [ScriptBlock]) { $Value.InvokeReturnAsIs($InputObject) } else { $Value } $InputObject.$Name = $valueResult } if ($PassThru) { return $InputObject } } } function Ensure-Member:::Example { $obj = ConvertFrom-Json "{ A : 1, B : 2}" $obj | Ensure-Member -Name D -Value { 4 } $obj | Ensure-Member -Name D -Value { 5 } $obj | Ensure-Member -Name D -Value { 6 } -force $obj | Ensure-Member -Name E -Value 7 $obj | Ensure-Member -Name F -Value { $_.D } } function Ensure-DeployManifest { $scriptFolder = Get-ScriptTopCallerFolder $gitInfo = Get-GitInfo -path $scriptFolder $content = gc -Path "$($gitInfo.Folder)\deploy\deployManifest.json" -Raw $result = ConvertFrom-Json $content $result | Ensure-Member -Name ScriptFolder -value $scriptFolder $result | Ensure-Member -Name ServiceName -value $gitInfo.Name $result | Ensure-Member -Name Folder -value $gitInfo.Folder $result | Ensure-Member -Name ResourceGroup -value { $azLocationAbbreviation = Get-AzLocationAbbreviation $_.Location $ownerAndEnvironment = $gitInfo.Branch.Split('!\/')[0] return "$ownerAndEnvironment-$($_.ServiceName)-$azLocationAbbreviation" } $result | Ensure-Member -Name AppInsights -value { $_.ResourceGroup + "-Insights" } $result | Ensure-Member -Name TemplateFile -value { $_.ScriptFolder + "\template.json" } return $result } function Dev-Prepare { npm install -g azure-functions-core-tools } function Ensure-AzContext { $azSubscription = $deployManifest.Subscription $azContext = Get-AzContext if (-not $azContext) { Connect-AzAccount -Subscription $azSubscription | Out-Null } else { Set-AzContext -Subscription $azSubscription | Out-Null } } function Get-AzStorageRandomName { Get-RandomString -seed 'AnyObject' -validChars (Get-Chars -LowerCase -number ) } function Deploy-AzService { # Install-Module -Name Az -AllowClobber -Scope CurrentUser Ensure-AzContext $azFunctionApp = $deployManifest.ResourceGroup function Get-AzDeploymentMode { $resourceGroup = Get-AzResourceGroup -Name $deployManifest.ResourceGroup -ErrorAction SilentlyContinue if(!$resourceGroup) { $resourceGroup = New-AzResourceGroup -Name $deployManifest.ResourceGroup -Location $deployManifest.Location return 'Complete' } return 'Incremental' } $DeploymentMode = Get-AzDeploymentMode if ($deployManifest.TemplateFile) { if (Test-Path ($deployManifest.TemplateFile)) { New-AzResourceGroupDeployment -Name $deployManifest.TemplateName ` -Mode $DeploymentMode -Force -Confirm:$false ` -ResourceGroupName $deployManifest.ResourceGroup ` -TemplateFile $deployManifest.TemplateFile #` # -TemplateParameterFile $deployManifest.ParametersFile; $pv = Get-AzResourceGroupDeployment -Name $deployManifest.TemplateName ` -ResourceGroupName $deployManifest.ResourceGroup write-host $pv.ProvisioningState } } function Generate-AzStorageName([string]$sourceName) { [string]$result = $sourceName.ToLowerInvariant() $result = $result.replace('-','') return $result + "st" } $azFuncStorage = Generate-AzStorageName -sourceName $azFunctionApp function Ensure-AzApplicationInsights { $applicationInsights = Get-AzApplicationInsights -Name $deployManifest.AppInsights ` -ResourceGroupName $deployManifest.ResourceGroup -ErrorAction SilentlyContinue if (!$applicationInsights) { $applicationInsights = New-AzApplicationInsights -Name $deployManifest.AppInsights ` -ResourceGroupName $deployManifest.ResourceGroup ` -location $deployManifest.Location } } Ensure-AzApplicationInsights $azWebApp = Get-AzWebApp -Name $azFunctionApp -ResourceGroupName ` $deployManifest.ResourceGroup -ErrorAction SilentlyContinue if (!$azWebApp) { $storage = Get-AzStorageAccount -Name $azFuncStorage ` -ResourceGroupName $deployManifest.ResourceGroup -ErrorAction SilentlyContinue if (!$storage) { $storage = New-AzStorageAccount -Name $azFuncStorage -Kind StorageV2 -AccessTier Hot ` -ResourceGroupName $deployManifest.ResourceGroup -AssignIdentity ` -SkuName Standard_LRS -Location $deployManifest.Location } $output = az functionapp create --name $azFunctionApp -g $deployManifest.ResourceGroup ` -s $storage.StorageAccountName ` --disable-app-insights ` --consumption-plan-location uksouth } # az functionapp delete --name $azFunctionApp -g $deployManifest.ResourceGroup $output = az functionapp config appsettings set --name $azFunctionApp ` --resource-group $deployManifest.ResourceGroup ` --settings FUNCTIONS_EXTENSION_VERSION=~2 function Ensure-AzFunctionAppInsightKey { $appInsights = Get-AzApplicationInsights ` -ResourceGroupName $deployManifest.ResourceGroup ` -Name $deployManifest.AppInsights $appInsightKey = $appInsights.InstrumentationKey if ($appInsightKey) { $output = az functionapp config appsettings set --name $azFunctionApp ` --resource-group $deployManifest.ResourceGroup ` --settings APPINSIGHTS_INSTRUMENTATIONKEY=$appInsightKey } } Ensure-AzFunctionAppInsightKey Publish-AzFunction Ensure-AzKeyVault } function Get-AzFunctionProjects($path) { $funcProjectList = gci -Path $path -Filter *.csproj -Recurse | Select-String -SimpleMatch '<AzureFunctionsVersion>' $funcProjectList.Path | % { Split-Path $_ -Parent } } function Publish-AzFunction($projectPath) { if (!$projectPath) { $projectPath = Get-AzFunctionProjects -path $deployManifest.Folder } try { $tmpFolder = New-TemporaryDirectory $tmpFolderPublish = $tmpFolder.FullName + "\publish" $zipPath = $tmpFolder.FullName + "\deploy.zip" mkdir -Path "$tmpFolderPublish\bin" -Force | Out-Host dotnet publish $projectPath --configuration debug -o $tmpFolderPublish | Out-Host Compress-Archive -Path "$tmpFolderPublish\*" -DestinationPath $zipPath ` -CompressionLevel Optimal -Force -Confirm:$false | Out-Host <# az functionapp deployment source config-zip ` -g $deployManifest.ResourceGroup -n $azFunctionApp ` --src $zipPath az functionapp deployment source config-zip ` -g djl02_drawioAzureBridge -n PBITOpsDrawIO20191108073052 ` --src $zipPath #> Publish-AzWebapp -ArchivePath $zipPath -ResourceGroupName $deployManifest.ResourceGroup ` -Name $azFunctionApp -Force -Confirm:$false } finally { if ($tmpFolder) { $tmpFolder | Remove-Item -Recurse -Force -Confirm:$false -ErrorAction SilentlyContinue } } } function Ensure-AzKeyVault { #https://gist.github.com/pascalnaber/75412a97a0d0b059314d193c3ab37c4c Set-AzWebApp -AssignIdentity $true -Name $azFunctionApp -ResourceGroupName $deployManifest.ResourceGroup $app = Get-AzWebApp -ResourceGroupName $deployManifest.ResourceGroup -Name $azFunctionApp $azVaultName = $deployManifest.ServiceName + "-kv" $azVault = Get-AzKeyVault -Name $azVaultName if (-not $azVault) { $azVault = Get-AzKeyVault -Name $azVaultName -InRemovedState -Location $deployManifest.Location if (-not $azVault) { $azVault = New-AzKeyVault -Name $azVaultName -ResourceGroupName $deployManifest.ResourceGroup ` -Location $deployManifest.Location -EnablePurgeProtection -EnableSoftDelete } else { Undo-AzKeyVaultRemoval -VaultName $azVaultName -ResourceGroupName $deployManifest.ResourceGroup ` -Location $deployManifest.Location } } az keyvault set-policy --secret-permissions get -n $azVaultName -g $deployManifest.ResourceGroup ` --object-id $app.Identity.PrincipalId | Out-Host $localSettingsFile = gci -Path $deployManifest.Folder -Filter local.settings.json -Recurse $settingsPath = $localSettingsFile[0].FullName function Get-AppLocalSettings($path, $name) { $settings = gc -path $path -Raw | ConvertFrom-Json $SecretSettings = $settings.Values | Get-Member -MemberType NoteProperty | where Name -Like $name $result = @{} $SecretSettings.Name | % { $result.Add($_ , $settings.Values.$_ ) } return $result } $vaultSecrets = Get-AppLocalSettings -path $settingsPath -name 'Secret-*' $vaultSecrets.GetEnumerator() | Set-AzSecretSetting -VaultName $azVaultName } function Set-AzSecretSetting { Param ( $VaultName, [parameter(ValueFromPipelineByPropertyName=$true,Position=0)] [Alias('Name')] [String]$SecretName, [parameter(ValueFromPipelineByPropertyName=$true,Position=1)] [Alias('Value')] [String]$SecretValue ) begin { $app = Get-AzWebApp -ResourceGroupName $deployManifest.ResourceGroup -Name $azFunctionApp $appSettings = $app.SiteConfig.AppSettings | % { $_tmpHash = @{} } { $_tmpHash[$_.Name] = $_.Value } { $_tmpHash } } process { $SecureValue = ConvertTo-SecureString -String $SecretValue -AsPlainText -Force $azVaultSecret = Set-AzKeyVaultSecret -VaultName $VaultName -Name $SecretName ` -SecretValue $SecureValue $settingValue = "@Microsoft.KeyVault(SecretUri=$($azVaultSecret.Id))" $appSettings[$SecretName] = $settingValue <# # The following does not work as it can truncate the last character az functionapp config appsettings set --name $azFunctionApp ` --resource-group $deployManifest.ResourceGroup --settings $settingValue #> } end { Set-AzWebApp -ResourceGroupName $deployManifest.ResourceGroup -Name $azFunctionApp ` -AppSettings $appSettings } } function Export-AzResources { Export-AzResourceGroup -ResourceGroupName $deployManifest.ResourceGroup -Path $deployManifest.TemplateFile ` -IncludeParameterDefaultValue -Force -Confirm:$false $armContent = Get-Content $deployManifest.TemplateFile -Raw | ConvertFrom-Json $armResourcesTypesToRemove = @( # Ignored in order to support SoftDelete "Microsoft.KeyVault/vaults", # Ignored as require values that are not obtained during Export "Microsoft.KeyVault/vaults/secrets", # Previous deployments are not relevant "Microsoft.Web/sites/deployments" ) $armContent.resources = $armContent.resources | ? { $_.type -notin $armResourcesTypesToRemove } $jsonContent = $armContent | ConvertTo-Json -Depth 100 $jsonContent = Format-Json -Json $jsonContent -Indentation 2 $jsonContent | Out-File -FilePath $deployManifest.TemplateFile -Encoding ascii } function Remove-AzService { Remove-AzResourceGroup $deployManifest.ResourceGroup -Force -Confirm:$false } function Setup-Planuml_Container { # docker run -d --name plantuml -p 8080:8080 plantuml/plantuml-server:tomcat $containerName = "$azEnv-plantuml" $containerGroup = New-AzureRmContainerGroup -ResourceGroupName $deployManifest.ResourceGroup ` -Name $containerName ` -Image 'plantuml/plantuml-server:tomcat' -DnsNameLabel plantuml -Port 8080 $basepath = "http://" + $containerGroup.Fqdn + ":8080" $puml = "https://raw.githubusercontent.com/anoff/devradar/master/assets/editor-build.puml" start "$basepath/proxy?src=$puml" # $containerGroup | Remove-AzureRmContainerGroup } function Get-FunctionsLocal { $scriptPath = Get-PSCallStack | where ScriptName | select -first 1 $functions = Get-Command | ? { $_.ScriptBlock } | ? { $_.ScriptBlock.File } | ? { $_.ScriptBlock.File -eq $scriptPath.ScriptName } return $functions } $toExport = Get-FunctionsLocal Export-ModuleMember -Function $toExport |