使用DSC和ARM模板传递凭据以及使用Visual Studio 2017进行部署

时间:2018-11-30 17:18:40

标签: azure powershell azure-virtual-machine azure-resource-manager dsc

我正在努力制作DSC(所需状态配置)文件与ARM(Azure资源管理器)模板一起部署Windows Server 2016,到目前为止,在我尝试传递用户名之前,一切工作都很好/ password,以便我可以创建本地Windows用户帐户。我似乎根本无法使用此功能(请参见下面的错误消息)。

我的问题是,如何使用ARM模板从Azure密钥库中提取密码并将其传递给DSC powershell扩展?

这是我当前的设置:

azuredeploy.json

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "deployExecUsername": {
      "type": "string",
      "defaultValue": "DeployExec"
    },
    "deployExecPassword": {
      "type": "securestring"
    },
    "_artifactsLocation": {
      "type": "string",
      "metadata": {
        "description": "Auto-generated container in staging storage account to receive post-build staging folder upload"
      }
    },
    "_artifactsLocationSasToken": {
      "type": "securestring",
      "metadata": {
        "description": "Auto-generated token to access _artifactsLocation"
      }
    },
    "virtualMachineName": {
      "type": "string",
      "defaultValue": "web-app-server"
    }
  },
  "variables": {
    "CreateLocalUserArchiveFolder": "DSC",
    "CreateLocalUserArchiveFileName": "CreateLocalUser.zip"},
  "resources": [
    {
      "name": "[concat(parameters('virtualMachineName'), '/', 'Microsoft.Powershell.DSC')]",
      "type": "Microsoft.Compute/virtualMachines/extensions",
      "location": "eastus2",
      "apiVersion": "2016-03-30",
      "dependsOn": [ ],
      "tags": {
        "displayName": "CreateLocalUser"
      },
      "properties": {
        "publisher": "Microsoft.Powershell",
        "type": "DSC",
        "typeHandlerVersion": "2.9",
        "autoUpgradeMinorVersion": true,
        "settings": {
          "configuration": {
            "url": "[concat(parameters('_artifactsLocation'), '/', variables('CreateLocalUserArchiveFolder'), '/', variables('CreateLocalUserArchiveFileName'))]",
            "script": "CreateLocalUser.ps1",
            "function": "Main"
          },
          "configurationArguments": {
            "nodeName": "[parameters('virtualMachineName')]"
          }
        },
        "protectedSettings": {
          "configurationArguments": {
            "deployExecCredential": {
              "UserName": "[parameters('deployExecUsername')]",
              "Password": "[parameters('deployExecPassword')]"
            }
          },
          "configurationUrlSasToken": "[parameters('_artifactsLocationSasToken')]"
        }
      }
    }],
  "outputs": {}
}

azuredeploy.parameters.json

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "deployExecPassword": {
      "reference": {
        "keyVault": {
          "id": "/subscriptions/<GUID>/resourceGroups/<Resource Group Name>/providers/Microsoft.KeyVault/vaults/<Resource Group Name>-key-vault"
        },
        "secretName": "web-app-server-deployexec-password"
      }
    }
  }
}

DSC / CreateLocalUser.ps1

Configuration Main
{
    Param ( 
        [string] $nodeName,
        [PSCredential]$deployExecCredential 
    )

    Import-DscResource -ModuleName PSDesiredStateConfiguration

    Node $nodeName
    {
        User DeployExec
        {
            Ensure = "Present"
            Description = "Deployment account for Web Deploy"
            UserName = $deployExecCredential.UserName
            Password = $deployExecCredential
            PasswordNeverExpires = $true
            PasswordChangeRequired = $false
            PasswordChangeNotAllowed = $true
        }
    }
}

Deploy-AzureResourceGroup.ps1(Azure资源组模板中的默认值)

#Requires -Version 3.0

Param(
    [string] [Parameter(Mandatory=$true)] $ResourceGroupLocation,
    [string] $ResourceGroupName = 'AzureResourceGroup2',
    [switch] $UploadArtifacts,
    [string] $StorageAccountName,
    [string] $StorageContainerName = $ResourceGroupName.ToLowerInvariant() + '-stageartifacts',
    [string] $TemplateFile = 'azuredeploy.json',
    [string] $TemplateParametersFile = 'azuredeploy.parameters.json',
    [string] $ArtifactStagingDirectory = '.',
    [string] $DSCSourceFolder = 'DSC',
    [switch] $ValidateOnly
)

try {
    [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("VSAzureTools-$UI$($host.name)".replace(' ','_'), '3.0.0')
} catch { }

$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
$TemplateFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateFile))
$TemplateParametersFile = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $TemplateParametersFile))

if ($UploadArtifacts) {
    # Convert relative paths to absolute paths if needed
    $ArtifactStagingDirectory = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $ArtifactStagingDirectory))
    $DSCSourceFolder = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($PSScriptRoot, $DSCSourceFolder))

    # Parse the parameter file and update the values of artifacts location and artifacts location SAS token if they are present
    $JsonParameters = Get-Content $TemplateParametersFile -Raw | ConvertFrom-Json
    if (($JsonParameters | Get-Member -Type NoteProperty 'parameters') -ne $null) {
        $JsonParameters = $JsonParameters.parameters
    }
    $ArtifactsLocationName = '_artifactsLocation'
    $ArtifactsLocationSasTokenName = '_artifactsLocationSasToken'
    $OptionalParameters[$ArtifactsLocationName] = $JsonParameters | Select -Expand $ArtifactsLocationName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore
    $OptionalParameters[$ArtifactsLocationSasTokenName] = $JsonParameters | Select -Expand $ArtifactsLocationSasTokenName -ErrorAction Ignore | Select -Expand 'value' -ErrorAction Ignore

    # Create DSC configuration archive
    if (Test-Path $DSCSourceFolder) {
        $DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter '*.ps1' | ForEach-Object -Process {$_.FullName})
        foreach ($DSCSourceFilePath in $DSCSourceFilePaths) {
            $DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + '.zip'
            Publish-AzureRmVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -Force -Verbose
        }
    }

    # Create a storage account name if none was provided
    if ($StorageAccountName -eq '') {
        $StorageAccountName = 'stage' + ((Get-AzureRmContext).Subscription.SubscriptionId).Replace('-', '').substring(0, 19)
    }

    $StorageAccount = (Get-AzureRmStorageAccount | Where-Object{$_.StorageAccountName -eq $StorageAccountName})

    # Create the storage account if it doesn't already exist
    if ($StorageAccount -eq $null) {
        $StorageResourceGroupName = 'ARM_Deploy_Staging'
        New-AzureRmResourceGroup -Location "$ResourceGroupLocation" -Name $StorageResourceGroupName -Force
        $StorageAccount = New-AzureRmStorageAccount -StorageAccountName $StorageAccountName -Type 'Standard_LRS' -ResourceGroupName $StorageResourceGroupName -Location "$ResourceGroupLocation"
    }

    # Generate the value for artifacts location if it is not provided in the parameter file
    if ($OptionalParameters[$ArtifactsLocationName] -eq $null) {
        $OptionalParameters[$ArtifactsLocationName] = $StorageAccount.Context.BlobEndPoint + $StorageContainerName
    }

    # Copy files from the local storage staging location to the storage account container
    New-AzureStorageContainer -Name $StorageContainerName -Context $StorageAccount.Context -ErrorAction SilentlyContinue *>&1

    $ArtifactFilePaths = Get-ChildItem $ArtifactStagingDirectory -Recurse -File | ForEach-Object -Process {$_.FullName}
    foreach ($SourcePath in $ArtifactFilePaths) {
        Set-AzureStorageBlobContent -File $SourcePath -Blob $SourcePath.Substring($ArtifactStagingDirectory.length + 1) `
            -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
    if ($OptionalParameters[$ArtifactsLocationSasTokenName] -eq $null) {
        $OptionalParameters[$ArtifactsLocationSasTokenName] = ConvertTo-SecureString -AsPlainText -Force `
            (New-AzureStorageContainerSASToken -Container $StorageContainerName -Context $StorageAccount.Context -Permission r -ExpiryTime (Get-Date).AddHours(4))
    }
}

# Create or update the resource group using the specified template file and template parameters file
New-AzureRmResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -Force

if ($ValidateOnly) {
    $ErrorMessages = Format-ValidationOutput (Test-AzureRmResourceGroupDeployment -ResourceGroupName $ResourceGroupName `
                                                                                  -TemplateFile $TemplateFile `
                                                                                  -TemplateParameterFile $TemplateParametersFile `
                                                                                  @OptionalParameters)
    if ($ErrorMessages) {
        Write-Output '', 'Validation returned the following errors:', @($ErrorMessages), '', 'Template is invalid.'
    }
    else {
        Write-Output '', 'Template is valid.'
    }
}
else {
    New-AzureRmResourceGroupDeployment -Name ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm')) `
                                       -ResourceGroupName $ResourceGroupName `
                                       -TemplateFile $TemplateFile `
                                       -TemplateParameterFile $TemplateParametersFile `
                                       @OptionalParameters `
                                       -Force -Verbose `
                                       -ErrorVariable ErrorMessages
    if ($ErrorMessages) {
        Write-Output '', 'Template deployment returned the following errors:', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message.TrimEnd("`r`n") })
    }
}

请注意,我的原始模板部署了整个服务器,但是我能够重现上述模板和任何旧的Windows Server 2016 VM所遇到的问题。

我正在通过Visual Studio 2017社区运行模板:

enter image description here

enter image description here

模板可以验证并运行,但是当我运行它时,出现以下错误消息:

22:26:41 - New-AzureRmResourceGroupDeployment : 10:26:41 PM - VM has reported a failure when processing extension 
22:26:41 - 'Microsoft.Powershell.DSC'. Error message: "The DSC Extension received an incorrect input: Compilation errors occurred 
22:26:41 - while processing configuration 'Main'. Please review the errors reported in error stream and modify your configuration 
22:26:41 - code appropriately. System.InvalidOperationException error processing property 'Password' OF TYPE 'User': Converting 
22:26:41 - and storing encrypted passwords as plain text is not recommended. For more information on securing credentials in MOF 
22:26:41 - file, please refer to MSDN blog: http://go.microsoft.com/fwlink/?LinkId=393729
22:26:41 - At C:\Packages\Plugins\Microsoft.Powershell.DSC\2.77.0.0\DSCWork\CreateLocalUser.4\CreateLocalUser.ps1:12 char:3
22:26:41 - +   User Converting and storing encrypted passwords as plain text is not recommended. For more information on securing 
22:26:41 - credentials in MOF file, please refer to MSDN blog: http://go.microsoft.com/fwlink/?LinkId=393729 Cannot find path 
22:26:41 - 'HKLM:\SOFTWARE\Microsoft\PowerShell\3\DSC' because it does not exist. Cannot find path 
22:26:41 - 'HKLM:\SOFTWARE\Microsoft\PowerShell\3\DSC' because it does not exist.
22:26:41 - Another common error is to specify parameters of type PSCredential without an explicit type. Please be sure to use a 
22:26:41 - typed parameter in DSC Configuration, for example:
22:26:41 -     configuration Example {
22:26:41 -         param([PSCredential] $UserAccount)
22:26:41 -         ...
22:26:41 -     }.
22:26:41 - Please correct the input and retry executing the extension.".
22:26:41 - At F:\Users\Shad\Documents\Visual Studio 2017\Projects\AzureResourceGroup2\AzureResourceGroup2\bin\Debug\staging\AzureR
22:26:41 - esourceGroup2\Deploy-AzureResourceGroup.ps1:108 char:5
22:26:41 - +     New-AzureRmResourceGroupDeployment -Name ((Get-ChildItem $Templat ...
22:26:41 - +     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22:26:41 -     + CategoryInfo          : NotSpecified: (:) [New-AzureRmResourceGroupDeployment], Exception
22:26:41 -     + FullyQualifiedErrorId : Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.NewAzureResourceGroupDep 
22:26:41 -    loymentCmdlet

我尝试过的事情

我已经尝试着看这个问题:

但是它似乎正在使用旧的ARM模板格式来构造DSC调用,并且由于我不熟悉它,因此无法计算出额外参数的用途。

我也看了这个问题:

,可接受的答案是仅使用PsDSCAllowPlainTextPassword = $true。虽然这似乎不是最好的方法,但我尝试添加以下配置数据文件。

CreateLocalUser.psd1

#
# CreateLocalUser.psd1
#
@{
    AllNodes = @(
        @{
            NodeName = '*'
            PSDscAllowPlainTextPassword = $true
        }
    )
}

并更改Deploy-AzureResourceGroup.ps1,以将这些设置传递给DSC配置,如下所示。

# Create DSC configuration archive
if (Test-Path $DSCSourceFolder) {
    $DSCSourceFilePaths = @(Get-ChildItem $DSCSourceFolder -File -Filter '*.ps1' | ForEach-Object -Process {$_.FullName})
    foreach ($DSCSourceFilePath in $DSCSourceFilePaths) {
        $DSCArchiveFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + '.zip'
        $DSCConfigDataFilePath = $DSCSourceFilePath.Substring(0, $DSCSourceFilePath.Length - 4) + '.psd1'
        Publish-AzureRmVMDscConfiguration $DSCSourceFilePath -OutputArchivePath $DSCArchiveFilePath -ConfigurationDataPath $DSCConfigDataFilePath -Force -Verbose
    }
}

但是,这样做时我的错误消息没有任何变化。

我已经阅读了很多Azure文档,以尝试解决此问题。错误消息中的链接完全无济于事,因为没有使用ARM模板使用加密的示例。大多数示例都显示了正在运行的Powershell脚本而不是ARM模板。而且,文档中没有一个示例说明如何从密钥库中检索密码并将其从ARM模板传递到DSC扩展文件中。这有可能吗?

  

请注意,如果我可以使用Visual Studio进行部署,我会很高兴。但是我已经在这个问题上工作了几天,似乎找不到一个可行的解决方案。因此,我想我会问这里,然后再使用管理Windows帐户进行Web部署。

更新2018-12-21

通过Visual Studio 2017运行deploy命令时,我注意到日志包含错误消息:

20:13:43 - Build started.
20:13:43 - Project "web-app-server.deployproj" (StageArtifacts target(s)):
20:13:43 - Project "web-app-server.deployproj" (ContentFilesProjectOutputGroup target(s)):
20:13:43 - Done building project "web-app-server.deployproj".
20:13:43 - Done building project "web-app-server.deployproj".
20:13:43 - Build succeeded.
20:13:43 - Launching PowerShell script with the following command:
20:13:43 - 'F:\Projects\thepath\web-app-server\bin\Debug\staging\web-app-server\Deploy-AzureResourceGroup.ps1' -StorageAccountName 'staged<xxxxxxxxxxxxxxxxx>' -ResourceGroupName 'web-app-server' -ResourceGroupLocation 'eastus2' -TemplateFile 'F:\Projects\thepath\web-app-server\bin\Debug\staging\web-app-server\web-app-server.json' -TemplateParametersFile 'F:\Projects\thepath\web-app-server\bin\Debug\staging\web-app-server\web-app-server.parameters.json' -ArtifactStagingDirectory '.' -DSCSourceFolder '.\DSC' -UploadArtifacts
20:13:43 - Deploying template using PowerShell script failed.
20:13:43 - Tell us about your experience at https://go.microsoft.com/fwlink/?LinkId=691202

错误消息发生后,它将继续。我怀疑它可能会转而使用另一种方法,正是这种方法导致了失败,以及为什么其他人看不到我是什么。

2 个答案:

答案 0 :(得分:1)

这是我最近使用的一种:

配置:

Param(
    [System.Management.Automation.PSCredential]$Admincreds,
)

在模板中,我这样做:

"publisher": "Microsoft.Powershell",
"type": "DSC",
"typeHandlerVersion": "2.20",
"autoUpgradeMinorVersion": true,
"settings": {
    "configuration": {
        "url": "url.zip",
        "script": "file.ps1",
        "function": "configuration"
    }
},
"protectedSettings": {
    "configurationArguments": {
        "adminCreds": {
            "userName": "username",
            "password": "password"
        }
    }
}

您不需要PSDscAllowPlainTextPassword,因为它们是通过powershell.dsc扩展名自动加密的。

答案 1 :(得分:0)

可悲的是,无论我尝试什么,这都不适用于DSC。

解决方法

目前,我正在使用自定义脚本扩展来解决此问题,例如:

    {
      "name": "[concat(parameters('virtualMachineName'), '/addWindowsAccounts')]",
      "type": "Microsoft.Compute/virtualMachines/extensions",
      "apiVersion": "2018-06-01",
      "location": "[resourceGroup().location]",
      "dependsOn": [
        "[concat('Microsoft.Compute/virtualMachines/', parameters('virtualMachineName'))]"
      ],
      "properties": {
        "publisher": "Microsoft.Compute",
        "type": "CustomScriptExtension",
        "typeHandlerVersion": "1.9",
        "autoUpgradeMinorVersion": true,
        "settings": {
          "fileUris": []
        },
        "protectedSettings": {
          "commandToExecute": "[concat('powershell -ExecutionPolicy Unrestricted -Command \"& { $secureDeployExecPassword = ConvertTo-SecureString -String ', variables('quote'), parameters('deployExecPassword'), variables('quote'), ' -AsPlainText -Force; New-LocalUser -AccountNeverExpires -UserMayNotChangePassword -Name ', variables('quote'), parameters('deployExecUsername'), variables('quote'), ' -Password $secureDeployExecPassword -FullName ', variables('quote'), parameters('deployExecUsername'), variables('quote'), ' -Description ', variables('quote'), 'Deployment account for Web Deploy', variables('quote'), ' -ErrorAction Continue ', '}\"')]"
        }
      }
    }

然后using dependsOn通过在DSC扩展名上进行设置来强制自定义脚本扩展名在DSC之前运行。

      "dependsOn": [
        "[resourceId('Microsoft.Compute/virtualMachines', parameters('virtualMachineName'))]",
        "addWindowsAccounts"
      ],

这不是理想的解决方案,但是它是安全的,可让我克服此阻塞问题,而无需使用管理员密码来进行网站部署。