Export/New-ScaleSetImage.ps1

function Global:New-ScaleSetImage {
    [CmdletBinding()]
    <#
    .SYNOPSIS
        Creates a new Application Server, installs Business Central, sys-preps it and saves it as an image
    .DESCRIPTION
        This CmdLet will create a new VM, initialize it (install this and other modules to it), download the desired Business Central DVD and install it locally.
        Afterwards it will generalize the created VM and set it as an Image to be used for future ScaleSets
    #>

    param(        
        [Parameter(Mandatory = $true)]
        [string]
        $ResourceGroupName,        
        [Parameter(Mandatory = $true)]
        [string]
        $ResourceLocation,
        [Parameter(Mandatory = $true)]
        [string]
        $Name,
        [Alias("Version")]
        [Parameter(Mandatory = $false)]
        [string]
        $BCVersion,
        [Alias("CumulativeUpdate")]
        [Parameter(Mandatory = $false)]
        [string]
        $BCCumulativeUpdate,
        [Alias("Language")]
        [Parameter(Mandatory = $false)]
        [string]
        $BCLanguage,
        [ValidateSet('App', 'Web')]
        [string]
        $InstallationType = "App",
        [Parameter(Mandatory = $false)]
        [string]
        $DownloadDirectory,
        [Parameter(Mandatory = $false)]
        [string]
        $ConfigurationFile,
        [Parameter(Mandatory = $false)]
        [string]
        $LicenseFilename,
        [Alias("VirtualNetworkName")]
        [Parameter(Mandatory = $true)]
        [string]
        $VNetName,
        [Parameter(Mandatory = $true)]
        [string]
        $VMName,
        [Parameter(Mandatory = $true)]
        [string]
        $VMImagePublisher,
        [Parameter(Mandatory = $true)]
        [string]
        $VMImageOffer,
        [Parameter(Mandatory = $true)]
        [string]
        $VMImageSku,
        [Parameter(Mandatory = $true)]
        [string]
        $VMSize,
        [Parameter(Mandatory = $true)]
        [PSCredential]
        $VMCredentials,
        [Parameter(Mandatory = $true)]
        [int]
        $VMDiskSizeInGb,
        [Parameter(Mandatory = $true)]
        [string]
        $VMStorageAccountType,
        [Parameter(Mandatory = $true)]
        [string]
        $VMDataDiskName,
        [Alias("NetworkInterfaceName")]
        [Parameter(Mandatory = $true)]
        [string]
        $NicName,
        [Alias("NetworkInterfacePrivateIP")]
        [Parameter(Mandatory = $false)]
        [string]
        $NicPrivateIP,
        [Parameter(Mandatory = $false)]
        [string]
        $SubnetName,
        [Alias("PublicIPName")]
        [Parameter(Mandatory = $false)]
        [string]
        $PipName,
        [Alias("PublicIPDomainNameLabel")]
        [Parameter(Mandatory = $false)]
        [string]
        $PipDnsLabel,
        [Parameter(Mandatory = $true)]
        [string]
        $LocalPropScaleSetName,
        [Parameter(Mandatory = $true)]
        [string]
        $LocalPropStorageAccountName,
        [Parameter(Mandatory = $false)]
        [string]
        $GalleryName,
        [Parameter(Mandatory = $false)]
        [string]
        $GalleryDefinitionName,
        [Parameter(Mandatory = $false)]
        [string]
        $GalleryPublisher,
        [Parameter(Mandatory = $false)]
        [string]
        $GalleryOffer,
        [Parameter(Mandatory = $false)]
        [string]
        $GallerySku,
        [HashTable]
        $Tags
    )
    process {
        
        $image = Get-AzImage -ResourceGroupName $ResourceGroupName -ImageName $Name -ErrorAction SilentlyContinue
        if ($image) {                        
            Write-CustomHost -Message "Image $Name already exists. Stopping here."
            return
        }
        # Copy arguments for NIC creation from parent call
        $VMArguments = @{ }
        foreach ($param in $PsBoundParameters.GetEnumerator() | Where-Object { ($_.Key -notlike "BC*" ) -and ($_.Key -notlike "LocalProp*" ) -and ($_.Key -notlike "InstallationType*" ) -and ($_.Key -notlike "DownloadDirectory*" ) -and ($_.Key -notlike "ConfigurationFile*" ) -and ($_.Key -notlike "LicenseFilename*" )}) {
            
            $VMArguments.Add($param.Key, $param.Value)
        }
        
        # Create new VM (as Application Server), might be a job, might be a VM-object (if VM already exists)
        $vm = New-CustomAzVm @VMArguments -AsJob
        if ($vm.GetType().ToString() -ne "Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine") {
            # Means: Job is currently running / VM is currently being deploy
            # use
            Write-CustomHost -Message "Receiving job..."
            $vm = Receive-Job -Job $vm -Wait
            Write-CustomHost -Message "Job received"
            $vmJustCreated = $true
            # to get the VM-object
        }
        
        # Assign created VM the Contributor role for the resource group (to be able to read data from storage account)
        # TODO: Check if lesser role is a possibility
        # TODO: Check if role is needed here anyway (maybe only for Scale Set relevant)
        $vm = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $VMName -ErrorAction Stop
        if (-not(Get-AzRoleAssignment -ObjectId $vm.Identity.PrincipalId -ResourceGroupName $ResourceGroupName -RoleDefinitionName Contributor -ErrorAction SilentlyContinue)) {
            Write-CustomHost -Message "Assigning access role to managed identity of VM..."
            New-AzRoleAssignment -ObjectId $vm.Identity.PrincipalId -RoleDefinitionName Contributor -ResourceGroupName $ResourceGroupName | Out-Null
            Write-CustomHost -Message "Done."
        }

        if ($vmJustCreated) {
            # TODO: Check if there is a way to find out if network connection is already available (to not sleep that long)
            # Hint: Sometimes the installation of the module during command InstallD365Module failed because it couldn't connect to gallery
            # Waiting some more minutes solved it
            Write-CustomHost -Message "Sleeping for 8 minutes to give the VM a chance to completely start (network might be unavailable if we start right away)..."
            Start-Sleep -Seconds (8 * 60)
            Write-CustomHost -Message "Done sleeping."
        }
        
        $vmPreparationParams = @{
            ResourceGroupName = $ResourceGroupName
            ResourceLocation = $ResourceLocation
            VMName = $VMName
            LocalPropScaleSetName = $LocalPropScaleSetName
            LocalPropStorageAccountName = $LocalPropStorageAccountName
            Version = $BCVersion
            CumulativeUpdate = $BCCumulativeUpdate
            Language = $BCLanguage
        }
        if (-not([string]::IsNullOrEmpty($InstallationType))) {
            $vmPreparationParams.Add('InstallationType', $InstallationType)
        }
        if (-not([string]::IsNullOrEmpty($DownloadDirectory))) {
            $vmPreparationParams.Add('DownloadDirectory', $DownloadDirectory)
        }
        if (-not([string]::IsNullOrEmpty($ConfigurationFile))) {
            $vmPreparationParams.Add('ConfigurationFile', $ConfigurationFile)
        }
        if (-not([string]::IsNullOrEmpty($LicenseFilename))) {
            $vmPreparationParams.Add('LicenseFilename', $LicenseFilename)
        }

        Set-VMPreparation @vmPreparationParams        
        
        Write-CustomHost -Message "Deallocating VM..."
        Stop-AzVm -ResourceGroupName $ResourceGroupName -Name $VMName -Force | Out-Null
        Write-CustomHost -Message "Generalizing VM..."
        Set-AzVM -ResourceGroupName $ResourceGroupName -Name $VMName -Generalized | Out-Null
        
        Write-CustomHost -Message "Preparing VM-image..."
        $vm = Get-AzVM -Name $VMName -ResourceGroupName $ResourceGroupName
        $image = New-AzImageConfig -Location $ResourceLocation -SourceVirtualMachineId $vm.ID
        New-AzImage -Image $image -ImageName $Name -ResourceGroupName $ResourceGroupName | Out-Null
        Write-CustomHost -Message "Done."
        
        Clear-ScaleSetPreparationResources -ResourceGroupName $ResourceGroupName -Tag $tags

        if (-not([string]::IsNullOrEmpty($GalleryName))) {
            Write-CustomHost -Message "Adding Image to Gallery..."
            Add-CustomImageToGallery -ResourceGroupName $ResourceGroupName -ResourceLocation $ResourceLocation `
                -GalleryName $GalleryName -GalleryDefinitionName $GalleryDefinitionName -GalleryPublisher $GalleryPublisher `
                -GalleryOffer $GalleryOffer -GallerySku $GallerySku -SourceImageName $Name
        }
    }
}
Export-ModuleMember -Function New-ScaleSetImage