PSDeployScripts/AzureAutomationModule.ps1
<# .SYNOPSIS Deploys a module to an Azure Automation account. .DESCRIPTION Deploys a PowerShell module to an Azure Automation account from a repository like the PowerShell Gallery. Supports credentials to access private repositories. Inspired by https://blog.tyang.org/2017/02/17/managing-azure-automation-module-assets-using-myget/ .EXAMPLE Sample snippet for public module configuration: Deploy PSDependModule { By AzureAutomationModule { FromSource "https://www.powershellgallery.com/api/v2" To "AAName" WithOptions @{ SourceIsAbsolute = $true ModuleName = "PSDepend" # ModuleVersion = '0.3.0' ResourceGroupName = "AAResourceGroupName" # Force = $true } } } .EXAMPLE Sample snippet for private module configuration: Deploy PrivateModule { By AzureAutomationModule { FromSource "https://pkgs.dev.azure.com/ORGANIZATION_NAME/PROJECT_NAME/_packaging/FEED_NAME/nuget/v2" To "AAName" WithOptions @{ SourceIsAbsolute = $true ModuleName = "PrivateModule" # ModuleVersion = '0.0.4' Force = $true ResourceGroupName = "AAResourceGroupName" StorageAccountName = "aadeploymentstor" Credential = $script:credential } WithPreScript { $user = 'user@contoso.com' $password = ConvertTo-SecureString 'PAT_TOKEN' -AsPlainText -Force # PAT with permissions to read from the Artifacts feed $script:credential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList $user, $password } } } .EXAMPLE Sample snippet for source module configuration: Deploy PSDependModule { By AzureAutomationModule { FromSource ".\PSDepend" To "AAName" WithOptions @{ ModuleName = "PSDepend" # ModuleVersion = '0.3.0' ResourceGroupName = "AAResourceGroupName" StorageAccountName = "aadeploymentstor" # Force = $true } } } .PARAMETER Deployment Deployment to run .PARAMETER ModuleName Module to deploy .PARAMETER ModuleVersion Specific module version to use for deployment .PARAMETER Credential Credential to use for accessing the PowerShell repository .PARAMETER Force Deploy the module even if the same module version is already imported into Azure Automation account .PARAMETER ResourceGroupName The resource group of target Azure Automation account .PARAMETER StorageAccountName The Storage account to use for module upload #> #Requires -modules Az.Automation, Az.Storage [CmdletBinding()] param( [ValidateScript( { $_.PSObject.TypeNames[0] -eq 'PSDeploy.Deployment' })] [psobject[]]$Deployment, [Parameter(Mandatory = $true)] [string]$ModuleName, [Parameter(Mandatory = $false)] [string]$ModuleVersion, [Parameter(Mandatory = $false)] [pscredential]$Credential, [Parameter(Mandatory = $false)] [switch]$Force, [Parameter(Mandatory = $true)] [string]$ResourceGroupName, [Parameter(Mandatory = $false)] [string]$StorageAccountName ) function Get-ModuleRepository { <# .SYNOPSIS Configures a repository module to use during the deployment .PARAMETER SourceLocation Source location of the target repository .PARAMETER ModuleName Module name to use for registering a new PS repository if required .PARAMETER Credential Credential to access private repository .OUTPUTS Microsoft.PowerShell.Commands.PSRepository #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $SourceLocation, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $ModuleName, [Parameter(Mandatory = $false)] [pscredential]$Credential ) begin { Write-Verbose "Starting the configuration of target module repository to work with..." } process { Write-Verbose "Searching for a registered PowerShell repository with SourceLocation '$SourceLocation'..." $existingPSRepository = Get-PSRepository | Where-Object -Property SourceLocation -eq $SourceLocation if ($existingPSRepository) { Write-Verbose "An already registered repository '$($existingPSRepository.Name)' with the same SourceLocation has been found." # Setting target repository name $targetRepositoryName = $existingPSRepository.Name } else { Write-Verbose "No registered repository has been found. Registering a new PowerShell repository..." # Setting target repository name $targetRepositoryName = $ModuleName + '-repository' # Register-PSRepository parameters $params = @{ Name = $targetRepositoryName SourceLocation = $SourceLocation Verbose = $VerbosePreference } if ($Credential) { $params['Credential'] = $Credential } # Register a new repository Register-PSRepository @params Write-Verbose "The following PowerShell repository has been registered:" Get-PSRepository -Name $targetRepositoryName | Write-Verbose } } end { # Return the target repository Get-PSRepository -Name $targetRepositoryName } } function Get-PublicModule { <# .SYNOPSIS Get module info from a public repository .PARAMETER ModuleName Name of the module to look for .PARAMETER Repository Registered PSRepository name to search for the module .PARAMETER RequiredVersion Specific module version to look for .OUTPUTS System.Management.Automation.PSCustomObject #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $ModuleName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Repository, [Parameter(Mandatory = $false)] [string] $RequiredVersion ) begin { if ($ModuleVersion) { Write-Verbose "Searching for version '$RequiredVersion' of module '$ModuleName' in the repository '$Repository'..." } else { Write-Verbose "Searching for the latest version of module '$ModuleName' in the repository '$Repository'..." } } process { #region Get source module repository # Get-ModuleRepository parameters $params = @{ ModuleName = $ModuleName SourceLocation = $Repository Verbose = $VerbosePreference } $sourceModuleRepository = Get-ModuleRepository @params #endregion if ($sourceModuleRepository) { # Find-Module parameters $params = @{ Name = $ModuleName Repository = $sourceModuleRepository.Name Verbose = $VerbosePreference } if ($ModuleVersion) { $params['RequiredVersion'] = $RequiredVersion } # Look for the module $sourceModule = Find-Module @params } else { throw "Cannot register source module repository." } } end { if ($sourceModule) { Write-Verbose "The version '$($sourceModule.Version)' of module '$($sourceModule.Name)' is found in the repository '$Repository'." # Create and return a source module object $result = [PSCustomObject]@{ Name = $sourceModule.Name Version = $sourceModule.Version ContentLink = "$($sourceModuleRepository.SourceLocation)/package/$($sourceModule.Name)/$($sourceModule.Version)/" } Write-Verbose "Content link: $($result.ContentLink)" Write-Output $result } else { Write-Verbose "No target version of module '$ModuleName' is found in the repository '$Repository'." } } } function Get-PrivateModule { <# .SYNOPSIS Get module info from a private repository .PARAMETER ModuleName Name of the module to look for .PARAMETER Repository Registered PSRepository name to search for the module .PARAMETER Credential Credential to access private repository .PARAMETER RequiredVersion Specific module version to look for .PARAMETER StorageAccount Azure Storage account to use for uploading the zipped module file .OUTPUTS System.Management.Automation.PSCustomObject #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $ModuleName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Repository, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [pscredential] $Credential, [Parameter(Mandatory = $false)] [string] $RequiredVersion, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Microsoft.Azure.Commands.Management.Storage.Models.PSStorageAccount] $StorageAccount ) begin { if ($ModuleVersion) { Write-Verbose "Searching for version '$RequiredVersion' of module '$ModuleName' in the repository '$Repository'..." } else { Write-Verbose "Searching for the latest version of module '$ModuleName' in the repository '$Repository'..." } } process { #region Get source module repository # Get-ModuleRepository parameters $params = @{ ModuleName = $ModuleName SourceLocation = $Repository Credential = $Credential Verbose = $VerbosePreference } $sourceModuleRepository = Get-ModuleRepository @params #endregion if ($sourceModuleRepository) { # Find-Module parameters $params = @{ Name = $ModuleName Repository = $sourceModuleRepository.Name Credential = $Credential Verbose = $VerbosePreference } if ($ModuleVersion) { $params['RequiredVersion'] = $RequiredVersion } # Look for the module $sourceModule = Find-Module @params if ($sourceModule) { Write-Verbose "The version '$($sourceModule.Version)' of module '$($sourceModule.Name)' is found in the repository '$Repository'." Write-Verbose "Saving the module locally..." $sourceModule | Save-Module -Path $env:TEMP -Credential $Credential Write-Verbose "Creating a module zip file..." $zippedModuleFile = New-ModuleZipFile -Path (Join-Path -Path $env:TEMP -ChildPath $sourceModule.Name) Write-Verbose "Module zip file: $zippedModuleFile" Write-Verbose "Uploading the module zip file to a storage account..." $contentLink = New-ContentLinkUri -FileInfo $zippedModuleFile -StorageAccount $StorageAccount # Create and return a source module object $result = [PSCustomObject]@{ Name = $sourceModule.Name Version = $sourceModule.Version ContentLink = $contentLink } Write-Output $result } else { Write-Verbose "No target version of module '$ModuleName' is found in the repository '$Repository'." } } else { throw "Cannot register source module repository." } } end { } } function Get-SourceModule { <# .SYNOPSIS Get module info from a local path .PARAMETER ModuleName Name of the module to look for .PARAMETER Path Path to target module location .PARAMETER RequiredVersion Specific module version to look for .PARAMETER StorageAccount Azure Storage account to use for uploading the zipped module file .OUTPUTS System.Management.Automation.PSCustomObject #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $ModuleName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter(Mandatory = $false)] [string] $RequiredVersion, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Microsoft.Azure.Commands.Management.Storage.Models.PSStorageAccount] $StorageAccount ) begin { if ($ModuleVersion) { Write-Verbose "Searching for version '$RequiredVersion' of module '$ModuleName' at '$Path'..." } else { Write-Verbose "Searching for the latest version of module '$ModuleName' at '$Path'..." } } process { # Get-Module parameters $params = @{ Name = $Path ListAvailable = $true Verbose = $VerbosePreference } # Get the module if ($ModuleVersion) { $sourceModule = Get-Module @params | Where-Object -Property Name -EQ $ModuleName | Where-Object -Property Version -EQ $ModuleVersion } else { $sourceModule = Get-Module @params | Where-Object -Property Name -EQ $ModuleName } if ($sourceModule) { Write-Verbose "The version '$($sourceModule.Version)' of module '$($sourceModule.Name)' is found in the repository '$Repository'." Write-Verbose "Creating a local module copy for processing..." $sourceModule.ModuleBase | Split-Path -Parent | Copy-Item -Destination $env:TEMP -Recurse -Force Write-Verbose "Creating a module zip file from ..." $zippedModuleFile = New-ModuleZipFile -Path (Join-Path -Path $env:TEMP -ChildPath $sourceModule.Name) Write-Verbose "Module zip file: $zippedModuleFile" Write-Verbose "Uploading the module zip file to a storage account..." $contentLink = New-ContentLinkUri -FileInfo $zippedModuleFile -StorageAccount $StorageAccount # Create and return a source module object $result = [PSCustomObject]@{ Name = $sourceModule.Name Version = $sourceModule.Version ContentLink = $contentLink } Write-Output $result } else { Write-Verbose "No target version of module '$ModuleName' is found in the repository '$Repository'." } } end { } } function Get-ModuleImportStatus { <# .SYNOPSIS Get the status of module import into an Automation account .PARAMETER ModuleImportJob Module import job to check status .OUTPUTS None #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] $ModuleImportJob ) begin { $importCompleted = $false } process { do { Write-Verbose 'Checking module import status...' $importedModule = Get-AzAutomationModule -Name $ModuleImportJob.Name -ResourceGroupName $ModuleImportJob.ResourceGroupName -AutomationAccountName $ModuleImportJob.AutomationAccountName if (($importedModule.ProvisioningState -eq 'Succeeded') -or ($importedModule.ProvisioningState -eq 'Failed')) { $importCompleted = $true } Start-Sleep -Seconds 5 } until ($importCompleted -eq $true) } end { Write-Verbose "Module import status is: $($importedModule.ProvisioningState)" # Return the import job status # Write-Output $importedModule } } function Get-ImportedModule { <# .SYNOPSIS Check for existing module in an Automation account .PARAMETER ModuleName Name of the module to look for .PARAMETER AutomationAccountName Azure Automation account to use .PARAMETER ResourceGroupName Resource group where the Automation account is located .OUTPUTS Microsoft.Azure.Commands.Automation.Model.Module #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $ModuleName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $AutomationAccountName, [Parameter(Mandatory = $false)] [string] $ResourceGroupName ) begin { Write-Verbose "Searching for an existing module '$ModuleName' in the Automation account '$AutomationAccountName'..." } process { # Get-AzAutomationModule parameters $params = @{ Name = $ModuleName AutomationAccountName = $AutomationAccountName ResourceGroupName = $ResourceGroupName Verbose = $VerbosePreference ErrorAction = 'SilentlyContinue' } $importedModule = Get-AzAutomationModule @params } end { if ($importedModule) { Write-Verbose "An existing module '$($importedModule.Name)' version '$($importedModule.Version)' was found in the Automation account '$($importedModule.AutomationAccountName)'." # Return the imported module Write-Output $importedModule } else { Write-Verbose "No existing module '$ModuleName' was found in the Automation account '$AutomationAccountName'." } } } function Import-SourceModule { <# .SYNOPSIS Imports the module specified in a module info object into an Automation account .PARAMETER Module Module info object to use for importing .PARAMETER AutomationAccount Azure Automation account object to use .OUTPUTS Microsoft.Azure.Commands.Automation.Model.Module #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [psobject] $Module, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Microsoft.Azure.Commands.Automation.Model.AutomationAccount] $AutomationAccount ) begin { } process { #region Searching for the source module in the Azure Automation account # Get-PublicModule parameters $params = @{ ModuleName = $Module.Name AutomationAccountName = $AutomationAccount.AutomationAccountName ResourceGroupName = $AutomationAccount.ResourceGroupName Verbose = $VerbosePreference } $targetModule = Get-ImportedModule @params #endregion #region Check the target Automation account for existing module and set import flag $startImport = $false if ($targetModule) { if ($Force) { Write-Warning "Forcing the target module import!" # Remove-AzAutomationModule parameters $removeParams = @{ Name = $targetModule.Name AutomationAccountName = $target ResourceGroupName = $deploy.DeploymentOptions.ResourceGroupName Force = $deploy.DeploymentOptions.Force Verbose = $VerbosePreference } Write-Warning "Removing the version '$($targetModule.Version)' of module '$($targetModule.Name)' from the the Automation Account '$target'..." Remove-AzAutomationModule @removeParams $startImport = $true } elseif ($Module.Version -gt ([version]::Parse($targetModule.Version))) { Write-Verbose "The source module version is '$($Module.Version)', which is greater than the existing version in the Automation Account. Updating now..." $startImport = $true } elseif ($Module.Version -eq ([version]::Parse($targetModule.Version))) { Write-Verbose "The source module version is '$($Module.Version)', which is the same as the existing version in the Automation Account. Update is not required." } else { Write-Verbose "The source module version is '$($Module.Version)', which is lower than the existing version '$($targetModule.Version)' in the Automation Account. Update is not required." } } else { $startImport = $true } #endregion #region Import the module if ($startImport) { Write-Verbose "Importing the version '$($Module.Version)' of module '$($Module.Name)' into the Automation Account '$target'..." # New-AzAutomationModule parameters $params = @{ Name = $Module.Name AutomationAccountName = $AutomationAccount.AutomationAccountName ResourceGroupName = $AutomationAccount.ResourceGroupName ContentLink = $Module.ContentLink Verbose = $VerbosePreference } $moduleImportJob = New-AzAutomationModule @params } #endregion } end { # Return module import job if ($moduleImportJob) { $moduleImportJob | Get-ModuleImportStatus } } } function New-ContentLinkUri { <# .SYNOPSIS Uploads a file to a Storage account and generates a URI link to access it .PARAMETER FileInfo Target file to upload .PARAMETER StorageAccount Storage account to use .OUTPUTS System.String #> [CmdletBinding()] [OutputType([String])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $True)] [ValidateNotNullOrEmpty()] [System.IO.FileInfo] $FileInfo, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [Microsoft.Azure.Commands.Management.Storage.Models.PSStorageAccount] $StorageAccount ) begin { } process { $context = $StorageAccount.Context # Check if a container with the same name exist in the Storage account $existingContainer = Get-AzStorageContainer -Name $FileInfo.BaseName.ToLower().Replace('.', '-') -Context $context -ErrorAction SilentlyContinue if ($existingContainer) { # Use the existing container $container = $existingContainer } else { # Create a new container $container = New-AzStorageContainer -Name $FileInfo.BaseName.ToLower().Replace('.', '-') -Context $context -Permission Container } # Set-AzStorageBlobContent parameters $params = @{ Container = $container.Name File = $FileInfo.FullName Blob = $FileInfo.Name Force = $true Context = $context Verbose = $VerbosePreference } # Upload the file $blob = Set-AzStorageBlobContent @params # Get secure context $key = (Get-AzStorageAccountKey -ResourceGroupName $storageAccount.ResourceGroupName -Name $storageAccount.StorageAccountName).Value[0] $context = New-AzStorageContext -StorageAccountName $storageAccount.StorageAccountName -StorageAccountKey $key # New-AzStorageBlobSASToken parameters $params = @{ Context = $context Container = $container.Name Blob = $blob.Name Permission = 'r' ExpiryTime = (Get-Date).AddHours(2.0) FullUri = $true Verbose = $VerbosePreference } # Generate a SAS token $contentLinkUri = New-AzStorageBlobSASToken @params } end { Write-Output $contentLinkUri } } function New-ModuleZipFile { <# .SYNOPSIS Create a zip file from a target local path .PARAMETER Path Path to a file or folder to compress .OUTPUTS System.IO.FileInfo #> [CmdletBinding()] [OutputType([System.IO.FileInfo])] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Path ) begin { } process { $moduleZipFilePath = "{0}.zip" -f (Get-Item -Path $Path).FullName Compress-Archive -Path $Path -DestinationPath $moduleZipFilePath -Force Get-Item -Path $moduleZipFilePath } end { } } foreach ($deploy in $Deployment) { foreach ($target in $deploy.Targets) { Write-Verbose "Starting deployment '$($deploy.DeploymentName)' to Azure Automation account '$target' in '$ResourceGroupName' resource group." #region Get the source module if ($deploy.DeploymentOptions.SourceIsAbsolute -and (-not $deploy.DeploymentOptions.Credential)) { Write-Verbose "Deploying from a public repository at '$($deploy.Source)'..." # Get-PublicModule parameters $params = @{ ModuleName = $deploy.DeploymentOptions.ModuleName Repository = $deploy.Source Verbose = $VerbosePreference } if ($ModuleVersion) { $params['RequiredVersion'] = $ModuleVersion } $sourceModule = Get-PublicModule @params } elseif ($deploy.DeploymentOptions.SourceIsAbsolute -and $deploy.DeploymentOptions.Credential) { Write-Verbose "Deploying from a private repository at '$($deploy.Source)'..." #region Get the Storage account for uploading the module # Get-PrivateModule parameters $params = @{ Name = $deploy.DeploymentOptions.StorageAccountName ResourceGroupName = $deploy.DeploymentOptions.ResourceGroupName Verbose = $VerbosePreference } $storageAccount = Get-AzStorageAccount @params #endregion if ($storageAccount) { # Get-PrivateModule parameters $params = @{ ModuleName = $deploy.DeploymentOptions.ModuleName Repository = $deploy.Source Credential = $deploy.DeploymentOptions.Credential StorageAccount = $storageAccount Verbose = $VerbosePreference } if ($ModuleVersion) { $params['RequiredVersion'] = $ModuleVersion } $sourceModule = Get-PrivateModule @params } else { throw "The '$($deploy.DeploymentOptions.StorageAccountName)' storage account was not found in the '$($deploy.DeploymentOptions.ResourceGroupName)' resource group" } } elseif (-not $deploy.DeploymentOptions.SourceIsAbsolute) { Write-Verbose "Deploying from a local path '$($deploy.Source)'..." #region Get the Storage account for uploading the module # Get-PrivateModule parameters $params = @{ Name = $deploy.DeploymentOptions.StorageAccountName ResourceGroupName = $deploy.DeploymentOptions.ResourceGroupName Verbose = $VerbosePreference } $storageAccount = Get-AzStorageAccount @params #endregion if ($storageAccount) { # Get-PrivateModule parameters $params = @{ ModuleName = $deploy.DeploymentOptions.ModuleName Path = $deploy.Source StorageAccount = $storageAccount Verbose = $VerbosePreference } if ($ModuleVersion) { $params['RequiredVersion'] = $ModuleVersion } $sourceModule = Get-SourceModule @params } else { throw "The '$($deploy.DeploymentOptions.StorageAccountName)' storage account was not found in the '$($deploy.DeploymentOptions.ResourceGroupName)' resource group" } } #endregion #region Importing the target module into an Azure Automation account if ($sourceModule) { $targetAzureAutomationAccount = Get-AzAutomationAccount -Name $target -ResourceGroupName $deploy.DeploymentOptions.ResourceGroupName if ($targetAzureAutomationAccount) { # Import-SourceModule parameters $params = @{ Module = $sourceModule AutomationAccount = $targetAzureAutomationAccount Verbose = $VerbosePreference } Import-SourceModule @params } else { throw "The target Azure Automation account '$target' was not found in '$($deploy.DeploymentOptions.ResourceGroupName)' resource group." } } else { if ($ModuleVersion) { throw "The version '$ModuleVersion' of source module '$($deploy.DeploymentOptions.ModuleName)' was not found at the source location '$($deploy.Source)'." } else { throw "The source module '$($deploy.DeploymentOptions.ModuleName)' was not found at the source location '$($deploy.Source)'." } } #endregion } } |