2021-07-08 01:22:20 +03:00
|
|
|
function deploy {
|
|
|
|
|
|
|
|
#Requires -Version 3.0
|
|
|
|
#Requires -Module Az.Resources
|
|
|
|
#Requires -Module Az.Storage
|
|
|
|
|
|
|
|
Param(
|
|
|
|
[string] [Parameter(Mandatory = $true)] $ArtifactStagingDirectory,
|
|
|
|
[string] [Parameter(Mandatory = $true)][alias("ResourceGroupLocation")] $Location,
|
|
|
|
[string] $ResourceGroupName = (Split-Path $ArtifactStagingDirectory -Leaf),
|
|
|
|
[switch] $UploadArtifacts,
|
|
|
|
[string] $StorageAccountName,
|
|
|
|
[string] $StorageContainerName = $ResourceGroupName.ToLowerInvariant() + '-stageartifacts',
|
|
|
|
[string] $TemplateFile = 'mainTemplate.json',
|
|
|
|
[string] $TemplateParametersFile = 'azuredeploy.parameters.json',
|
|
|
|
[string] $DSCSourceFolder = 'DSC',
|
|
|
|
[switch] $BuildDscPackage,
|
|
|
|
[switch] $ValidateOnly,
|
|
|
|
[string] $DebugOptions = "None",
|
|
|
|
[string] $Mode = "Incremental",
|
|
|
|
[string] $DeploymentName = ((Split-Path $TemplateFile -LeafBase) + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')),
|
|
|
|
[string] $ManagementGroupId,
|
|
|
|
[switch] $Dev,
|
|
|
|
[switch] $bicep
|
|
|
|
)
|
|
|
|
|
|
|
|
try {
|
|
|
|
[Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("AzQuickStarts-$UI$($host.name)".replace(" ", "_"), "1.0")
|
|
|
|
}
|
|
|
|
catch { }
|
|
|
|
|
|
|
|
$currentFolder = $(Get-Location).path
|
|
|
|
# Convert relative paths to absolute paths if needed
|
|
|
|
$ArtifactStagingDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($currentFolder, $ArtifactStagingDirectory))
|
|
|
|
$DSCSourceFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($currentFolder, $DSCSourceFolder))
|
|
|
|
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
Set-StrictMode -Version 3
|
|
|
|
|
|
|
|
function Format-ValidationOutput {
|
|
|
|
param ($ValidationOutput, [int] $Depth = 0)
|
|
|
|
Set-StrictMode -Off
|
|
|
|
return @($ValidationOutput | Where-Object { $_ -ne $null } | ForEach-Object { @(' ' * $Depth + ': ' + $_.Message) + @(Format-ValidationOutput @($_.Details) ($Depth + 1)) })
|
|
|
|
}
|
|
|
|
|
|
|
|
$OptionalParameters = New-Object -TypeName Hashtable
|
|
|
|
$TemplateArgs = New-Object -TypeName Hashtable
|
|
|
|
$ArtifactStagingDirectory = ($ArtifactStagingDirectory.TrimEnd('/')).TrimEnd('\')
|
|
|
|
|
|
|
|
# if the bicep switch is set, and the templateFile arg was the default, swap .json for .bicep
|
|
|
|
$isBicep = ($bicep -or $TemplateFile.EndsWith('.bicep'))
|
|
|
|
if ($isBicep) {
|
2021-07-09 17:04:45 +03:00
|
|
|
$defaultTemplateFile = 'main.bicep'
|
2021-07-08 01:22:20 +03:00
|
|
|
}
|
|
|
|
else {
|
2021-07-09 17:04:45 +03:00
|
|
|
$defaultTemplateFile = 'azuredeploy.json'
|
2021-07-08 01:22:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
# if the template file isn't found, try another default
|
|
|
|
if (!(Test-Path "$ArtifactStagingDirectory\$TemplateFile")) {
|
2021-07-09 17:04:45 +03:00
|
|
|
$TemplateFile = $defaultTemplateFile
|
2021-07-08 01:22:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
# build the bicep file
|
|
|
|
if ($isBicep) {
|
|
|
|
bicep build "$ArtifactStagingDirectory\$TemplateFile"
|
|
|
|
# now point the deployment to the json file that was just build
|
|
|
|
$TemplateFile = $TemplateFile.Replace('.bicep', '.json')
|
|
|
|
}
|
|
|
|
|
|
|
|
Write-Host "Using template file: $TemplateFile"
|
|
|
|
|
|
|
|
#try a few different default options for param files when the -dev switch is use
|
|
|
|
if ($Dev) {
|
|
|
|
$TemplateParametersFile = $TemplateParametersFile.Replace('azuredeploy.parameters.json', 'azuredeploy.parameters.dev.json')
|
|
|
|
if (!(Test-Path $TemplateParametersFile)) {
|
|
|
|
$TemplateParametersFile = $TemplateParametersFile.Replace('azuredeploy.parameters.dev.json', 'azuredeploy.parameters.1.json')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Write-Host "Using parameter file: $TemplateParametersFile"
|
|
|
|
|
|
|
|
if (!$ValidateOnly) {
|
|
|
|
$OptionalParameters.Add('DeploymentDebugLogLevel', $DebugOptions)
|
|
|
|
}
|
|
|
|
|
|
|
|
$TemplateFile = "$ArtifactStagingDirectory\$TemplateFile"
|
|
|
|
$TemplateParametersFile = "$ArtifactStagingDirectory\$TemplateParametersFile"
|
|
|
|
|
|
|
|
$TemplateJSON = Get-Content $TemplateFile -Raw | ConvertFrom-Json
|
|
|
|
|
|
|
|
$TemplateSchema = $TemplateJson | Select-Object -expand '$schema' -ErrorAction Ignore
|
|
|
|
|
|
|
|
switch -Wildcard ($TemplateSchema) {
|
|
|
|
'*tenantDeploymentTemplate.json*' {
|
|
|
|
$deploymentScope = "Tenant"
|
|
|
|
}
|
|
|
|
'*managementGroupDeploymentTemplate.json*' {
|
|
|
|
$deploymentScope = "ManagementGroup"
|
|
|
|
}
|
|
|
|
'*subscriptionDeploymentTemplate.json*' {
|
|
|
|
$deploymentScope = "Subscription"
|
|
|
|
}
|
|
|
|
'*/deploymentTemplate.json*' {
|
|
|
|
$deploymentScope = "ResourceGroup"
|
|
|
|
$OptionalParameters.Add('Mode', $Mode)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Write-Host "Running a $deploymentScope scoped deployment..."
|
|
|
|
|
|
|
|
$ArtifactsLocationName = '_artifactsLocation'
|
|
|
|
$ArtifactsLocationSasTokenName = '_artifactsLocationSasToken'
|
|
|
|
$ArtifactsLocationParameter = $TemplateJson | Select-Object -expand 'parameters' -ErrorAction Ignore | Select-Object -Expand $ArtifactsLocationName -ErrorAction Ignore
|
|
|
|
$ArtifactsLocationSasTokenParameter = $TemplateJson | Select-Object -expand 'parameters' -ErrorAction Ignore | Select-Object -Expand $ArtifactsLocationSasTokenName -ErrorAction Ignore
|
|
|
|
$useAbsolutePathStaging = $($ArtifactsLocationParameter -ne $null)
|
|
|
|
|
|
|
|
# if the switch is set or the standard parameter is present in the template, upload all artifacts
|
|
|
|
if ($UploadArtifacts -Or $useAbsolutePathStaging -or $ArtifactsLocationSasTokenParameter) {
|
|
|
|
|
|
|
|
# Parse the parameter file and update the values of artifacts location and artifacts location SAS token if they are present
|
|
|
|
if (Test-Path $TemplateParametersFile) {
|
|
|
|
$JsonParameters = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json
|
|
|
|
if (($JsonParameters | Get-Member -Type NoteProperty 'parameters') -ne $null) {
|
|
|
|
$JsonParameters = $JsonParameters.parameters
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$JsonParameters = @{ }
|
|
|
|
}
|
|
|
|
|
|
|
|
# if using _artifacts* parameters, add them to the optional params and get the value from the param file (if any)
|
|
|
|
if ($useAbsolutePathStaging) {
|
|
|
|
$OptionalParameters[$ArtifactsLocationName] = $JsonParameters | Select-Object -Expand $ArtifactsLocationName -ErrorAction Ignore | Select-Object -Expand 'value' -ErrorAction Ignore
|
|
|
|
$OptionalParameters[$ArtifactsLocationSasTokenName] = $JsonParameters | Select-Object -Expand $ArtifactsLocationSasTokenName -ErrorAction Ignore | Select-Object -Expand 'value' -ErrorAction Ignore
|
|
|
|
}
|
|
|
|
|
|
|
|
# Create DSC configuration archive
|
|
|
|
if ((Test-Path $DSCSourceFolder) -and ($BuildDscPackage)) {
|
|
|
|
$DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter '*.ps1' | ForEach-Object -Process { $_.FullName })
|
|
|
|
foreach ($DSCSourceFilePath in $DSCSourceFilePaths) {
|
|
|
|
$DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + '.zip'
|
|
|
|
Publish-AzVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -Force -Verbose
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Create a storage account name if none was provided
|
|
|
|
if ($StorageAccountName -eq '') {
|
|
|
|
$StorageAccountName = 'stage' + ((Get-AzContext).Subscription.Id).Replace('-', '').substring(0, 19)
|
|
|
|
}
|
|
|
|
|
|
|
|
$StorageAccount = (Get-AzStorageAccount | Where-Object { $_.StorageAccountName -eq $StorageAccountName })
|
|
|
|
|
|
|
|
# Create the storage account if it doesn't already exist
|
|
|
|
if ($StorageAccount -eq $null) {
|
|
|
|
$StorageResourceGroupName = 'ARM_Deploy_Staging'
|
|
|
|
if ((Get-AzResourceGroup -Name $StorageResourceGroupName -Verbose -ErrorAction SilentlyContinue) -eq $null) {
|
|
|
|
New-AzResourceGroup -Name $StorageResourceGroupName -Location $Location -Verbose -Force -ErrorAction Stop
|
|
|
|
}
|
|
|
|
$StorageAccount = New-AzStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $StorageResourceGroupName -Location "$Location"
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($StorageContainerName.length -gt 63) {
|
|
|
|
$StorageContainerName = $StorageContainerName.Substring(0, 63)
|
|
|
|
}
|
|
|
|
$ArtifactStagingLocation = $StorageAccount.Context.BlobEndPoint + $StorageContainerName + "/"
|
|
|
|
|
|
|
|
# Generate the value for artifacts location if it is not provided in the parameter file
|
|
|
|
if ($useAbsolutePathStaging -and $OptionalParameters[$ArtifactsLocationName] -eq $null) {
|
|
|
|
#if the defaultValue for _artifactsLocation is using the template location, use the defaultValue, otherwise set it to the staging location
|
|
|
|
$defaultValue = $ArtifactsLocationParameter | Select-Object -Expand 'defaultValue' -ErrorAction Ignore
|
|
|
|
if ($defaultValue -like '*deployment().properties.templateLink.uri*') {
|
|
|
|
$OptionalParameters.Remove($ArtifactsLocationName) # just use the defaultValue if it's using the template language function
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$OptionalParameters[$ArtifactsLocationName] = $ArtifactStagingLocation
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Copy files from the local storage staging location to the storage account container
|
|
|
|
New-AzStorageContainer -Name $StorageContainerName -Context $StorageAccount.Context -ErrorAction SilentlyContinue *>&1
|
|
|
|
|
|
|
|
$ArtifactFilePaths = Get-ChildItem $ArtifactStagingDirectory -Recurse -File | ForEach-Object -Process { $_.FullName }
|
|
|
|
foreach ($SourcePath in $ArtifactFilePaths) {
|
|
|
|
|
|
|
|
if ($SourcePath -like "$DSCSourceFolder*" -and $SourcePath -like "*.zip" -or !($SourcePath -like "$DSCSourceFolder*")) {
|
|
|
|
#When using DSC, just copy the DSC archive, not all the modules and source files
|
|
|
|
$blobName = ($SourcePath -ireplace [regex]::Escape($ArtifactStagingDirectory), "").TrimStart("/").TrimStart("\")
|
|
|
|
Set-AzStorageBlobContent -File $SourcePath -Blob $blobName -Container $StorageContainerName -Context $StorageAccount.Context -Force
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# Generate a 4 hour SAS token for the artifacts location if one was not provided in the parameters file
|
|
|
|
# first check to see if we need a sasToken (if it was not already provided in the param file or we're using relativePath)
|
|
|
|
if ($useAbsolutePathStaging -or $OptionalParameters[$ArtifactsLocationSasTokenName] -eq $null) {
|
|
|
|
$sasToken = (New-AzStorageContainerSASToken -Container $StorageContainerName -Context $StorageAccount.Context -Permission r -ExpiryTime (Get-Date).AddHours(4))
|
|
|
|
}
|
|
|
|
|
|
|
|
# now set the parameter value for the QueryString or _artifactsLocationSasToken as appropriate
|
|
|
|
if ($OptionalParameters[$ArtifactsLocationSasTokenName] -eq $null -and $useAbsolutePathStaging) {
|
|
|
|
$OptionalParameters[$ArtifactsLocationSasTokenName] = ConvertTo-SecureString $sasToken -AsPlainText -Force
|
|
|
|
$TemplateArgs.Add('TemplateUri', $ArtifactStagingLocation + (Get-ChildItem $TemplateFile).Name + $sasToken)
|
|
|
|
}
|
|
|
|
elseif (!$useAbsolutePathStaging) {
|
|
|
|
$OptionalParameters['QueryString'] = $sasToken.TrimStart("?") # remove leading ? as it is not part of the QueryString
|
|
|
|
$TemplateArgs.Add('TemplateUri', $ArtifactStagingLocation + (Get-ChildItem $TemplateFile).Name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
|
|
|
|
$TemplateArgs.Add('TemplateFile', $TemplateFile)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Test-Path $TemplateParametersFile) {
|
|
|
|
$TemplateArgs.Add('TemplateParameterFile', $TemplateParametersFile)
|
|
|
|
}
|
2021-09-01 18:22:56 +03:00
|
|
|
|
|
|
|
$OptionalParameters.Add('Name', $DeploymentName)
|
|
|
|
|
2021-07-08 01:22:20 +03:00
|
|
|
Write-Host ($TemplateArgs | Out-String)
|
|
|
|
Write-Host ($OptionalParameters | Out-String)
|
|
|
|
|
|
|
|
# Create the resource group only when it doesn't already exist - and only in RG scoped deployments
|
|
|
|
if ($deploymentScope -eq "ResourceGroup") {
|
|
|
|
if ((Get-AzResourceGroup -Name $ResourceGroupName -Location $Location -Verbose -ErrorAction SilentlyContinue) -eq $null) {
|
|
|
|
New-AzResourceGroup -Name $ResourceGroupName -Location $Location -Verbose -Force -ErrorAction Stop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($ValidateOnly) {
|
|
|
|
|
|
|
|
switch ($deploymentScope) {
|
|
|
|
"resourceGroup" {
|
|
|
|
$ErrorMessages = Format-ValidationOutput (Test-AzResourceGroupDeployment -ResourceGroupName $ResourceGroupName @TemplateArgs @OptionalParameters)
|
|
|
|
}
|
|
|
|
"Subscription" {
|
|
|
|
$ErrorMessages = Format-ValidationOutput (Test-AzDeployment -Location $Location @TemplateArgs @OptionalParameters)
|
|
|
|
}
|
|
|
|
"managementGroup" {
|
|
|
|
$ErrorMessages = Format-ValidationOutput (Test-AzManagementGroupDeployment -Location $Location @TemplateArgs @OptionalParameters)
|
|
|
|
}
|
|
|
|
"tenant" {
|
|
|
|
$ErrorMessages = Format-ValidationOutput (Test-AzTenantDeployment -Location $Location @TemplateArgs @OptionalParameters)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($ErrorMessages) {
|
|
|
|
Write-Output '', 'Validation returned the following errors:', @($ErrorMessages), '', 'Template is invalid.'
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
Write-Output '', 'Template is valid.'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
|
|
|
|
|
|
|
$ErrorActionPreference = 'Continue' # Switch to Continue" so multiple errors can be formatted and output
|
|
|
|
|
|
|
|
switch ($deploymentScope) {
|
|
|
|
"resourceGroup" {
|
2021-09-01 18:22:56 +03:00
|
|
|
New-AzResourceGroupDeployment `
|
2021-07-08 01:22:20 +03:00
|
|
|
-ResourceGroupName $ResourceGroupName `
|
|
|
|
@TemplateArgs `
|
|
|
|
@OptionalParameters `
|
|
|
|
-Force -Verbose `
|
|
|
|
-ErrorVariable ErrorMessages
|
|
|
|
}
|
|
|
|
"Subscription" {
|
2021-09-01 18:22:56 +03:00
|
|
|
New-AzDeployment `
|
2021-07-08 01:22:20 +03:00
|
|
|
-Location $Location `
|
|
|
|
@TemplateArgs `
|
|
|
|
@OptionalParameters `
|
|
|
|
-Verbose `
|
|
|
|
-ErrorVariable ErrorMessages
|
|
|
|
}
|
|
|
|
"managementGroup" {
|
2021-09-01 18:22:56 +03:00
|
|
|
New-AzManagementGroupDeployment `
|
2021-07-08 01:22:20 +03:00
|
|
|
-ManagementGroupId $managementGroupId `
|
|
|
|
-Location $Location `
|
|
|
|
@TemplateArgs `
|
|
|
|
@OptionalParameters `
|
|
|
|
-Verbose `
|
|
|
|
-ErrorVariable ErrorMessages
|
|
|
|
}
|
|
|
|
"tenant" {
|
2021-09-01 18:22:56 +03:00
|
|
|
New-AzTenantDeployment `
|
2021-07-08 01:22:20 +03:00
|
|
|
-Location $Location `
|
|
|
|
@TemplateArgs `
|
|
|
|
@OptionalParameters `
|
|
|
|
-Verbose `
|
|
|
|
-ErrorVariable ErrorMessages
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
if ($ErrorMessages) {
|
|
|
|
Write-Output '', 'Template deployment returned the following errors:', '', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message })
|
|
|
|
Write-Error "Deployment failed."
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|