InstallAzModule.ps1

<#PSScriptInfo
 
.VERSION 1.2
 
.GUID 70e6f41b-5941-4ec7-b797-60b96a301319
 
.AUTHOR Ted Sdoukos
 
.COMPANYNAME
 
.COPYRIGHT
 
.TAGS AzureAutomation,Runbook
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
 
.PRIVATEDATA
 
#>


<#
.SYNOPSIS
Installs Az modules to automation account.
 
.DESCRIPTION
This Azure Automation runbook installs the Az modules selected into an
Azure Automation account with the module versions published to the PowerShell Gallery.
Prerequisite: an Azure Automation account with an Azure Run As account credential.
 
.PARAMETER ResourceGroupName
The Azure resource group name.
 
.PARAMETER AutomationAccountName
The Azure Automation account name.
 
.PARAMETER All
This will install the Az module and all dependancies
 
.NOTES
Credit to: https://stackoverflow.com/questions/60847861/how-to-import-modules-into-azure-automation-account-using-powershell
Credit to: https://github.com/microsoft/AzureAutomation-Account-Modules-Update
#>


[CmdletBinding()]
Param(
    [Parameter(Mandatory)]$ResourceGroupName,
    [Parameter(Mandatory)]$AutomationAccountName,
    [string[]]$AzModule,
    [string]$ModuleVersion,
    [bool]$All,
    [bool]$Wait,
    $PsGalleryApiUrl = 'https://www.powershellgallery.com/api/v2'
)
#region Functions
Function Get-AzModuleInfo {
    [CmdletBinding()]
    Param($ModuleName)
    $ModuleUrlFormat = "$PsGalleryApiUrl/Search()?`$filter={1}&searchTerm=%27{0}%27&targetFramework=%27%27&includePrerelease=false&`$skip=0&`$top=40"
    $CurrentModuleURL = $ModuleUrlFormat -f $ModuleName, 'IsLatestVersion'
    $SearchID = Invoke-RestMethod -Method Get -Uri $CurrentModuleURL -UseBasicParsing | 
    Where-Object -FilterScript { $_.Title.InnerText -eq $moduleName }
    $packageDetails = Invoke-RestMethod -Method Get -UseBasicParsing -Uri $SearchID.id
    $packageDetails
}

Function Get-AzModuleDependancy {
    [CmdletBinding()]
    Param($ModuleName)
    $output = $(Get-AZModuleInfo -ModuleName $ModuleName).entry.properties.Dependencies
    $output -replace ':\[\d+\.\d+\.\d, \d*\.*\d*\.*\d*\]*\)*:' -split '\|'
}

Function Install-AzModuleDependancy {
    [CmdletBinding()]
    Param(
        $ModuleName,
        $ModuleVersion
    )
    foreach ($M in $ModuleName) {
        Write-Verbose -Message "Getting information for $M"
        $module = (Get-AzModuleInfo -ModuleName $M).Entry.Properties
        If ($ModuleVersion) {
            $link = "$PsGalleryApiUrl/package/$($module.id)/$($module.Version)"
        }
        else {
            $link = "$PsGalleryApiUrl/package/$($module.id)"
        }
        # Find the actual blob storage location of the module
        do {
            $Link = (Invoke-WebRequest -Uri $link -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore).Headers.Location 
        } until ($link.Contains('.nupkg'))
        $status = Get-AzureRmAutomationModule -AutomationAccountName $AutomationAccountName -ResourceGroupName $ResourceGroupName -Name $module.id -ErrorAction SilentlyContinue

        If ( (-Not($status)) -or ($status.Version -ne $module.Version)) {
            Write-Verbose -Message "Currently installing the $($module.id) - Version-$($module.version) dependancy"
            $null = New-AzureRmAutomationModule -AutomationAccountName $automationAccountName -Name $module.id -ContentLink $link -ResourceGroupName $resourceGroupName
            Do {
                $State = Get-AzureRmAutomationModule -ResourceGroupName $resourceGroupName -AutomationAccountName $automationAccountName -Name $module.id
                Start-Sleep -Seconds 1
                Write-Verbose -Message "Waiting on install of $($module.id)"
                Write-Progress -Activity "Installing $($module.id)" -Status "Current status is: $($state.ProvisioningState)"
            }
            While ($state.ProvisioningState -eq 'Creating')
        }
        If ($state.ProvisioningState -eq 'Failed') { Throw "Unable to install $($module.id)" }
        While ($state.ProvisioningState -ne 'Succeeded') {
            $State = Get-AzureRmAutomationModule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $module.id
            Start-Sleep -Seconds 1
        }
        If ($state.ProvisioningState -eq 'Succeeded') { 
            Write-Progress -Activity "Installing $($module.id)" -Status "Current status is: $($state.ProvisioningState)"
            Write-Verbose -Message "Installation of $($module.id) successful" 
        }
    }
}

Function Install-AzAutomationModule {
    <#
    .Synopsis
        Installs Az modules in your azure automation account
    .DESCRIPTION
        Long description
    .EXAMPLE
        Install-AzAutomationModule -ResourceGroupName 'ContosoResourceGroup' -AutomationAccountName 'ContosoAutomationAccount' -All
    .EXAMPLE
        I'll do more examples later
    .NOTES
        Author: Ted Sdoukos
        Credit to: https://stackoverflow.com/questions/60847861/how-to-import-modules-into-azure-automation-account-using-powershell
        Credit to: https://github.com/microsoft/AzureAutomation-Account-Modules-Update
        REQUIEMENTS: Az.Automation module, PowerShell v5 or better
    #>

    [CmdletBinding(DefaultParameterSetName = 'All')]
    Param(
    )

    If ($All) {
        $AzModule = 'Az'
    }
    
    $DepList = New-Object -TypeName System.Collections.ArrayList
    $List = $AzModule | ForEach-Object { Get-AzModuleDependancy -ModuleName $_ }
    foreach ($item in $List) {
        Get-AzModuleDependancy -ModuleName $item | ForEach-Object {
            If (($_ -ne '') -and ($DepList -notcontains $_)) {
                $null = $DepList.Add($_)
            }
        }
    }
    Write-Verbose -Message "Dependant list:`n$DepList"
    If ($DepList) { Install-AzModuleDependancy -ModuleName $DepList }
    $PsGalleryApiUrl = 'https://www.powershellgallery.com/api/v2' 
    If ($All) {
        $AzModule = Get-AzModuleDependancy -ModuleName 'Az'
    }
    $AzModule | ForEach-Object {
        if (($_) -notin $DepList) {
            $InstallState = Get-AzureRmAutomationModule -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `
                -Name $_ -ErrorAction SilentlyContinue
            If (($InstallState.ProvisioningState -ne 'Succeeded') -or (-not($InstallState))) {
                $module = (Get-AzModuleInfo -ModuleName $_).Entry.Properties
                
                If ($ModuleVersion) {
                    $link = "$PsGalleryApiUrl/package/$($_)/$($module.Version)"
                }
                else {
                    $link = "$PsGalleryApiUrl/package/$($_)"
                }
                do {
                    $TryCount = 0
                    $Link = (Invoke-WebRequest -Uri $link -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore).Headers.Location 
                    $TryCount++
                } until ($link.Contains('.nupkg') -or $TryCount -gt 10)
                
                $ModName = $Module.Id
                Write-Verbose -Message "Currently installing $ModName"
                Write-Progress -Activity "Installing $ModName"
                $null = New-AzureRmAutomationModule -AutomationAccountName $automationAccountName -Name $ModName -ContentLink $link -ResourceGroupName $resourceGroupName
                If ($Wait) {
                    Do {
                        $Status = Get-AzureRmAutomationModule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $_ 
                        Start-Sleep -Seconds 1
                    }
                    While ($Status.ProvisioningState -eq 'Creating')
                    Write-Verbose -Message "Provisioning of $_ is complete. Current status is $($Status.ProvisioningState)"
                }
                #Added a sleep in here to alleviate errors
                # Most common error: Index was out of range. Must be non-negative and less than the size of the collection.
                Start-Sleep -Seconds 2
            }
        }
    }
    $AzModule | ForEach-Object {
        Get-AzureRmAutomationModule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -Name $_ |
        Select-Object -Property Name, ProvisioningState
    }
}

function Connect-AzureAutomation {
    try {
        $RunAsConnection = Get-AutomationConnection -Name 'AzureRunAsConnection'
        $RunAsConnection | Select-Object -Property *
        Write-Output "Logging in to Azure ($AzureEnvironment)..."
        
        if (!$RunAsConnection.ApplicationId) {
            $ErrorMessage = "Connection 'AzureRunAsConnection' is incompatible type."
            throw $ErrorMessage            
        }
        Add-AzureRmAccount -ServicePrincipal -TenantId $RunAsConnection.TenantId -ApplicationId $RunAsConnection.ApplicationId `
            -CertificateThumbprint $RunAsConnection.CertificateThumbprint 

        Select-AzureRmSubscription -SubscriptionId $RunAsConnection.SubscriptionID | Write-Verbose
    }
    catch {
        if (!$RunAsConnection) {
            $_.Exception
            $ErrorMessage = "Connection 'AzureRunAsConnection' not found."
            throw $ErrorMessage
        }

        throw $_.Exception
    }
}
#EndRegion Functions

#region main script
Connect-AzureAutomation 

Install-AzAutomationModule 
$failCheck = Get-AzureRmAutomationModule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName | 
Where-Object -FilterScript { $_.ProvisioningState -eq 'Failed' }
If ($failCheck) {
    Write-Warning -Message "The following modules failed to install: $($failCheck.Name | ForEach-Object {"`n$_"})"
    Write-Warning -Message `
        "Type the following to retry: `nInstall-AzAutomationModule -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccountName -AzModule $($failCheck.name -join ', ') -Wait"
}
#endRegion