public/New-ArmGroupDeployment.ps1

<#
.Synopsis
Starts an Azure ARM Template deployment for a single resource group.
.Description
This CmdLet will wrap the complete logic and preparation for a deployment in a single command. It uses New-AzResourceGroupDeployment internally.
.Parameter TenantId
The GUID of the Azure Tenant in which the subscription resides.
.Parameter SubscriptionId
The GUID of the subscription to which the deployment should be applied.
.Parameter Stage
The short name of the stage with a capitalized first letter (e.g. 'Test', 'Prod', 'Demo', 'Int')
.Parameter ResourceGroupTags
The tag that shall be assigned as purpose to the created resource group.
.Parameter ResourceGroupName
Optional explicit name of the resource group (turns off auto-generation of the name).
.Parameter DeploymentDebugLogLevel
The debug log level for the deployment (defaults to All).
.Parameter DeleteOnFailure
Indicates if the resource group should be deleted on any error.
.Parameter DoNotSetResourceGroupLock
Indicates if a no-delete-lock should NOT be applied to the ressource group.
.Parameter ProjectName
The name of the project which will be used to build the name of the resource group and the resources. Leave this empty if your template parameter
file contains one of the following keys defining the name: project-name, projectName, ProjectName, project or Project.
.Parameter ResourceGroupLocation
The Azure location for the resource group (defaults to 'West Europe').
.Parameter TemplateFile
The path to the template file (ARM-JSON or BICEP) (if empty the script searches for 'azuredeploy.(json)' in the current directory).
.Parameter TemplateParameterFile
Optional path to the template parameter file in JSON format (if empty the script searches for the matching file built from template file and stage).
.Parameter NoParams
If set to $true the script will assume that no template parameter file is needed.
.Example
New-AzdArmGroupmDeployment -Stage Test -TenantId 00000-00000-00000 -SubscriptionId 000000-00000-000000-00000 -WhatIf -TemplateFile c:\temp\azuredeploy.json
Execute an ARM deployment for the Test stage using a deployment file in c:\temp folder
#>

Function New-ArmGroupDeployment {
    [CmdLetBinding()]
    Param (                
        [Parameter(Mandatory = $true)] [string] $TenantId,
        [Parameter(Mandatory = $true)] [string] $SubscriptionId,
        [string] $Stage,
        [string]
        [ValidateSet("RequestContent", "ResponseContent", "All", "None")]
        $DeploymentDebugLogLevel = "All",
        [Hashtable] $ResourceGroupTags,
        [string] $ResourceGroupName,
        [switch] $DoNotSetResourceGroupLock,
        [switch] $DeleteOnFailure,
        [string] $ProjectName,
        [string] $ResourceGroupLocation = "West Europe",
        [string] $TemplateFile = '.\azuredeploy.json',
        [string] $TemplateParameterFile,
        [switch] $NoParams,
        [switch] $NoLogo
    )
    begin {
        if (!$NoLogo.IsPresent) {
            Write-Logo $MyInvocation.InvocationName        
        }
        New-FunctionStartup
        try {
            [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("VSAzureTools-$UI$($host.name)".replace(' ', '_'), '3.0.0')
        } 
        catch {
        }    
        Set-StrictMode -Version 3
        # if stage is empty AND no group name was defined this script cannot work
        if ([string]::IsNullOrEmpty($Stage) -and [string]::IsNullOrEmpty($ResourceGroupName)) {
            throw "If you want to deploy without staging you have to define the resource group name explicitely." 
        }
        # check if deployment file exists
        $exists = Test-Path $TemplateFile -PathType Leaf
        if (!$exists) {
            throw "Template file $TemplateFile not found." 
        }        
        if (!$NoParams.IsPresent) {
            # get base directory and parameter file name for stage
            $item = Get-Item $TemplateFile         
            $path = $item.DirectoryName        
            $fileName = $item.Name.Substring(0, $item.Name.Length - $item.Extension.Length)    
            if ([string]::IsNullOrEmpty($TemplateParameterFile)) {
                $TemplateParameterFile = "$path\$fileName.parameters.$Stage.json"
            }
            # check if parameter file exists
            $exists = Test-Path $TemplateParameterFile -PathType Leaf
            if (!$exists) {
                throw "File $TemplateParameterFile not found." 
            }
        }
        # build resource group tags
        $tagsDefined = $false
        if ($ResourceGroupTags) {
            if ($ResourceGroupTags.Count -eq 0) {                
                $ResourceGroupTags = @{ purpose = $Stage }
                $tagsDefined = $true
            }
        }
        if ([string]::IsNullOrEmpty($Stage) -and !$tagsDefined) {
            throw "You must define a stage or tags."
        }
        # build deployment name
        $DeploymentName = ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm'));
        # ensure Azure context
        Write-Information 'Setting Azure context...'
        Set-SubscriptionContext -Subscription $SubscriptionId -Tenant $TenantId -NoLogo
        if (!$?) {        
            Write-HostError "Could not set subscription context."
            return
        }
        # build resource group name
        Write-HostDebug 'Determining resource group name...'    
        if ($ProjectName.Length -eq 0) {
            # try to read project name from the parameter file
            Write-HostInfo "Trying to read project name from $TemplateParameterFile"
            $json = Get-Content $TemplateParameterFile -Raw  | ConvertFrom-Json            
            $projectNameKeys = @( "project-name", "projectName", "ProjectName", "project", "Project" )
            foreach ($key in $projectNameKeys) {
                $tmp = $null
                try {
                    $tmp = $json.parameters.$key.value
                }
                catch {
                    Write-HostDebug "Key '$key' not found in parameter file" 
                }
                if ($tmp) {
                    Write-HostInfo "Found entry with key '$key' and value '$tmp'"
                    $ProjectName = $tmp
                    break
                }
            }
        }
        if ($ProjectName.Length -eq 0 -and $ResourceGroupName.Length -eq 0) {    
            throw "No project name or resource group name set."
        }
        if ($ResourceGroupName.Length -eq 0) {
            # build resource group name
            $ResourceGroupName = "rg-$ProjectName-$Stage".ToLowerInvariant()    
        }
        Write-HostDebug "Using resource group name $ResourceGroupName."                
        # Create the resource group only when it doesn't already exist
        if ($null -eq (Get-AzResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -ErrorAction SilentlyContinue)) {
            Write-HostDebug "Creating resource group $ResourceGroupName..."
            New-AzResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Tags $ResourceGroupTags -Verbose -Force -ErrorAction Stop                 
            Write-HostInfo "Resource group created."                            
        } 
        else {
            Write-HostDebug "Resource group $ResourceGroupName already exists."
        }
        if (!$DoNotSetResourceGroupLock.IsPresent) {
            # caller wants us to ensure the delete lock
            $noDeleteLocks = (Get-AzResourceLock -ResourceGroupName $ResourceGroupName -AtScope |  Where { $_.Properties.level -eq 'CanNotDelete' })
            if ($noDeleteLocks -eq $null -or $noDeleteLocks -eq 0) {
                Write-HostDebug "Set No-Delete-Lock for $ResourceGroupName..."
                New-AzResourceLock -LockName no-delete -LockLevel CanNotDelete -ResourceGroupName $ResourceGroupName -Force -ErrorAction Stop
                Write-HostInfo "No-Delete Lock is set."
            }
            else {
                Write-HostDebug "No-Delete Lock found."
            }
        }                
    }
    process {
        Write-HostDebug "Starting template deployment with template $TemplateParameterFile ..."
        try {            
            New-AzResourceGroupDeployment `
                -Name $DeploymentName `
                -ResourceGroupName $ResourceGroupName `
                -TemplateFile $TemplateFile `
                -TemplateParameterFile $TemplateParameterFile `
                -DeploymentDebugLogLevel $DeploymentDebugLogLevel `
                -Force `
                -Verbose                
        }
        catch {    
            Write-Host "Error: $_" -ForegroundColor Red        
            if (!$DeleteOnFailure) {                
                Write-HostInfo "Are you sure you want to delete the resource group now? (y/n)" -NoNewline
                $hostInput = Read-Host
                if ($hostInput -eq 'y') {
                    # user wants us to delete the resource group if the deployment failed
                    Write-HostDebug "Removing resource group $ResourceGroupName..."
                    Get-AzResourceLock -ResourceGroupName $ResourceGroupName -AtScope | Remove-AzResourceLock -Force
                    Remove-AzResourceGroup -Name $ResourceGroupName -Force
                    Write-HostInfo "Resource group deleted" 
                }
            }            
            return -1
        }    
    }
}