先日、AlwaysOn 可用性グループを Azure の仮想マシンとしてテンプレート展開ができるようになりました。
Microsoft Azure ポータルのギャラリーで SQL Server AlwaysOn テンプレートを提供開始
このテンプレートを展開することで、
- ドメインコントローラー × 2 台
- SQL Server × 2 台 (プライマリレプリカ/セカンダリレプリカ)
- ファイル共有監視用仮想マシン
の 5 台の仮想マシンが構築され、可用性グループが設定された状態となります。
この展開ですが、カスタムスクリプト + DSC が使用されて実施されているようです。
今回の投稿では 1 台目のドメインコントローラーを例にして、展開方法を確認してみたいと思います。
カスタムスクリプトについては、
- Understanding Azure Custom Script Extension
- Set-AzureVMCustomScriptExtension
- Get-AzureVMCustomScriptExtension
- Azure PowerShell で 仮想マシン作成時にカスタムスクリプト拡張を実行
が参考になります。
さすが世界を散歩する、僕らの TaaS です。既にいろいろと検証をされていたようです。
英語はしゃべれないですが、今度、かばん持ちとして雇ってもらいたいと思います。
ポータルから仮想マシンを作成する際に、カスタムスクリプトを指定することができますが、PowerShell から既に作成されている仮想マシンに対して、カスタムスクリプトの実行要求を出すことが可能です。
以下のような PowerShell を実行することでストレージのコンテナ内のスクリプトをダウンロードして、実行させることが可能です。
Add-AzureAccount
$VM = "VM Name"
$ServiceName = "Service Name"
$Subscription = Get-AzureSubscription | Out-GridView -OutputMode Single
Select-AzureSubscription $Subscription.SubscriptionName -Current
$Storage = Get-AzureStorageAccount | Out-GridView -OutputMode Single
$Container = Get-AzureStorageContainer | Out-GridView -OutputMode Single
$VM = Get-AzureVM -Name $VM -ServiceName $ServiceName
Set-AzureVMCustomScriptExtension -VM $VM -StorageAccountName $Storage.StorageAccountName -ContainerName $Container.Name -FileName "CustomScript.ps1" -Run "CustomScript.ps1" | Update-AzureVM
$Vm.ResourceExtensionStatusList.ExtensionSettingStatus.SubStatusList | Select Name, @{"Label"="Message";Expression = {$_.FormattedMessage.Message }}
今回使用している CustomScript.ps1 の内容は以下のようなものとなっています。
if ((Test-Path "C:Script") -eq $False){
New-Item "C:Script" -ItemType directory
}
Write-Output "Custom Script End."
カスタムスクリプトの実行が終わると C ドライブの下に Script ディレクトリが作成されます。
![]()
コンテナからダウンロードされたファイルは、[C:PackagesPluginsMicrosoft.Compute.CustomScriptExtension1.1Downloads] 以下にカスタムスクリプトの実行ごとに連番のディレクトリが作成され格納されます。
実行のログは、
- C:PackagesPluginsMicrosoft.Compute.CustomScriptExtension1.1RuntimeSettings
- C:PackagesPluginsMicrosoft.Compute.CustomScriptExtension1.1Status
などを見るとよさそうです。
さて、本題の AlwaysOn で展開された AD のサーバーを確認してみたいと思います。
カスタムスクリプトによりダウンロードされたファイルのディレクトリ構成は以下のようになっています。
![]()
0 のディレクトリは以下のようになっており、[InstallDSCModules.ps1] が格納されています。
![]()
PS1 の中身は以下のようになっています。
#
# Copyright="c Microsoft Corporation. All rights reserved."
#
[CmdletBinding()]
param
(
[Parameter(Mandatory)]
[String[]]$Modules,
[Switch]$Force
)
function DownloadFile
{
param
(
[Parameter(Mandatory)]
[String]$Uri
)
$retryCount = 1
while ($retryCount -le 3)
{
try
{
Write-Verbose "Downloading file from '$($Uri)', attempt $retryCount of 3 ..."
$file = "$env:TEMP$([System.IO.Path]::GetFileName((New-Object System.Uri $Uri).LocalPath))"
Invoke-WebRequest -Uri $Uri -OutFile $file
break
}
catch
{
if ($retryCount -eq 3)
{
Write-Error -Message "Error downloading file from '$($Uri)".
throw $_
}
Write-Warning -Message "Failed to download file from '$($Uri)', retrying in 30 seconds ..."
Start-Sleep -Seconds 30
$retryCount++
}
}
Write-Verbose "Successfully downloaded file from '$($Uri)' to '$($file)'."
return $file
}
function InstallDSCModule
{
param
(
[Parameter(Mandatory)]
[String]$ModuleSourcePath,
[Bool]$Force
)
$moduleInstallPath = "$env:ProgramFilesWindowsPowerShellModules"
$moduleName = [System.IO.Path]::GetFileNameWithoutExtension($ModuleSourcePath)
Write-Verbose "Installing module '$($moduleName)' ..."
$module = Get-Module -ListAvailable -Verbose:$false | Where-Object { $_.Name -eq $moduleName }
if (!$module -or $Force)
{
# System.IO.Compression.ZipFile is only available in .NET 4.5. We
# prefer it over shell.application since the former is available in
# Server Core.
if (![System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem"))
{
Write-Verbose "Installing the 'Net-Framework-45-Core' feature ..."
$featureResult = Install-WindowsFeature -Name "Net-Framework-45-Core"
if (-not $featureResult.Success)
{
throw "Failed to install the 'Net-Framework-45-Core' feature: $($featureResult.ExitCode)."
}
Write-Verbose "Successfully installed the 'Net-Framework-45-Core' feature."
[System.Reflection.Assembly]::LoadWithPartialName("System.IO.Compression.FileSystem")
}
Remove-Item -Path "$moduleInstallPath$moduleName" -Recurse -Force -ErrorAction Ignore
[System.IO.Compression.ZipFile]::ExtractToDirectory($ModuleSourcePath, $moduleInstallPath)
Write-Verbose "Successfully installed module '$($moduleName)' to '$("$moduleInstallPath$moduleName")'."
}
else
{
Write-Verbose "Module '$($moduleName)' is already installed at '$($module.Path)', skipping."
}
}
$VerbosePreference = 'Continue'
# Download the required DSC modules for this node.
foreach ($module in $Modules)
{
# We support fetching the DSC modules ourselves...
if (($module -as [System.Uri]).AbsoluteURI)
{
$module = DownloadFile -Uri $module
}
#... or having the Custom Script extension do it for us.
else
{
$module = "$PSScriptRoot$module"
# Coalesce the module name in case the extension was omitted.
if (-not $module.EndsWith(".zip"))
{
$module = "$module.zip"
}
}
InstallDSCModule -ModuleSourcePath $module -Force $Force
}
# WMF 4.0 is a prerequisite. We install this last since it will reboot the node.
if ($PSVersionTable.PSVersion.Major -lt 4)
{
# Check to see if the Custom Script extension has already downloaded
# WMF 4.0 for us.
$msu = "$PSScriptRootWindows8-RT-KB2799888-x64.msu"
if (-not (Test-Path $msu))
{
# If not, we fetch it ourselves.
Write-Verbose -Message "Downloading WMF 4.0 ..."
$msu = DownloadFile -Uri "http://download.microsoft.com/download/3/D/6/3D61D262-8549-4769-A660-230B67E15B25/Windows8-RT-KB2799888-x64.msu"
Write-Verbose -Message "Successfully downloaded WMF 4.0."
}
Write-Verbose -Message "Installing WMF 4.0 (KB2799888) ..."
Start-Process -FilePath "$env:SystemRootSystem32wusa.exe" -ArgumentList "$msu /quiet /norestart" -Wait
Write-Verbose "Successfully installed WMF 4.0, restarting the computer ..."
# Installing WMF 4.0 always requires a restart.
Start-Sleep -Seconds 5
Restart-Computer -Force
}
DSC を実行するためのモジュール群のダウンロードが行われているようですね。
実行時の引数については、RuntimeSettings ディレクトリの 0.settings から確認できます。
これで DSC の環境の設定が終わった後に、AD DS を構築するために 2 個めのカスタムスクリプトとして、[CreateADPrimaryDomainController.ps1] が実行されているようです。
# 1.settings を見る限り、カスタムスクリプト実行時には、CreateADPrimaryDomainController.ps1 と Common.ps1 をダウンロードしているようです。
![]()
CreateADPrimaryDomainController.ps1 の内容は以下のようになっています。
#
# Copyright="c Microsoft Corporation. All rights reserved."
#
param
(
[Parameter(Mandatory)]
[String]$DomainName,
[String]$DomainNetbiosName,
[Parameter(Mandatory)]
[String]$UserName,
[Parameter(Mandatory)]
[String]$Password,
[String]$SafeModeAdministratorPassword = $Password,
[String]$EncryptionCertificateThumbprint
)
. "$PSScriptRootCommon.ps1"
if ($EncryptionCertificateThumbprint)
{
Write-Verbose -Message "Decrypting parameters with certificate $EncryptionCertificateThumbprint..."
$Password = Decrypt -Thumbprint $EncryptionCertificateThumbprint -Base64EncryptedValue $Password
$SafeModeAdministratorPassword = Decrypt -Thumbprint $EncryptionCertificateThumbprint -Base64EncryptedValue $SafeModeAdministratorPassword
Write-Verbose -Message "Successfully decrypted parameters."
}
else
{
Write-Verbose -Message "No encryption certificate specified. Assuming cleartext parameters."
}
configuration ADDSForest
{
Import-DscResource -ModuleName xComputerManagement, xActiveDirectory
Node $env:COMPUTERNAME
{
xDisk ADDataDisk
{
DiskNumber = 2
DriveLetter = "F"
}
WindowsFeature ADDS
{
Name = "AD-Domain-Services"
Ensure = "Present"
}
xADDomain PrimaryDC
{
DomainAdministratorCredential = New-Object System.Management.Automation.PSCredential ("$DomainName$UserName", $(ConvertTo-SecureString $Password -AsPlainText -Force))
DomainName = $DomainName
DomainNetbiosName = $DomainNetbiosName
SafemodeAdministratorPassword = New-Object System.Management.Automation.PSCredential ("$DomainName$UserName", $(ConvertTo-SecureString $SafeModeAdministratorPassword -AsPlainText -Force))
DatabasePath = "F:NTDS"
LogPath = "F:NTDS"
SysvolPath = "F:SYSVOL"
DependsOn = "[xDisk]ADDataDisk", "[WindowsFeature]ADDS"
}
LocalConfigurationManager
{
CertificateId = $node.Thumbprint
}
}
}
if ($EncryptionCertificateThumbprint)
{
$certificate = dir Cert:LocalMachineMy$EncryptionCertificateThumbprint
$certificatePath = Join-Path -path $PSScriptRoot -childPath "EncryptionCertificate.cer"
Export-Certificate -Cert $certificate -FilePath $certificatePath | Out-Null
$configData = @{
AllNodes = @(
@{
Nodename = $env:COMPUTERNAME
CertificateFile = $certificatePath
Thumbprint = $EncryptionCertificateThumbprint
}
)
}
}
else
{
$configData = @{
AllNodes = @(
@{
Nodename = $env:COMPUTERNAME
PSDscAllowPlainTextPassword = $true
}
)
}
}
WaitForPendingMof
ADDSForest -ConfigurationData $configData -OutputPath $PSScriptRoot
$cimSessionOption = New-CimSessionOption -SkipCACheck -SkipCNCheck -UseSsl
$cimSession = New-CimSession -SessionOption $cimSessionOption -ComputerName $env:COMPUTERNAME -Port 5986
if ($EncryptionCertificateThumbprint)
{
Set-DscLocalConfigurationManager -CimSession $cimSession -Path $PSScriptRoot -Verbose
}
# Run Start-DscConfiguration in a loop to make it more resilient to network outages.
for ($count = 1; $count -le 10; $count++)
{
$error.Clear()
Write-Verbose -Message "Attempt $count of 10 ..."
Start-DscConfiguration -CimSession $cimSession -Path $PSScriptRoot -Force -Wait -Verbose *>&1 | Tee-Object -Variable output
if (!$error)
{
break;
}
Write-Warning -Message "An error has occurred, retrying in 60 seconds ..."
Start-Sleep -Seconds 60
}
CheckForPendingReboot -Output $output
DSC を使用してAD DS を構築するための処理が記述されているようですね。
common.ps1 の内容は以下のようになっています。
#
# Copyright="c Microsoft Corporation. All rights reserved."
#
function Decrypt
{
param
(
[Parameter(Mandatory)]
[String]$Thumbprint,
[Parameter(Mandatory)]
[String]$Base64EncryptedValue
)
# Decode Base64 string
$encryptedBytes = [System.Convert]::FromBase64String($Base64EncryptedValue)
# Get certificate from store
$store = new-object System.Security.Cryptography.X509Certificates.X509Store([System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine)
$store.open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)
$certificate = $store.Certificates | %{if($_.thumbprint -eq $Thumbprint){$_}}
# Decrypt
$decryptedBytes = $certificate.PrivateKey.Decrypt($encryptedBytes, $false)
$decryptedValue = [System.Text.Encoding]::UTF8.GetString($decryptedBytes)
return $decryptedValue
}
function WaitForPendingMof
{
# Check to see if there is a Pending.mof file to avoid a conflict with an
# already in-progress SendConfigurationApply invocation.
while ($true)
{
try
{
Get-Item -Path "$env:windirSystem32ConfigurationPending.mof" -ErrorAction Stop
Start-Sleep -Seconds 5
}
catch
{
break
}
}
}
function CheckForPendingReboot
{
param
(
[Parameter(Mandatory)]
[Object[]]$Output
)
# The LCM doesn't notify us when there's a pending reboot, so we have to check
# for it ourselves.
if ($Output -match "A reboot is required to progress further. Please reboot the system.")
{
Start-Sleep -Seconds 5
Restart-Computer -Force
}
}
function WaitForSqlSetup
{
# Wait for SQL Server Setup to finish before proceeding.
while ($true)
{
try
{
Get-ScheduledTaskInfo "ConfigureSqlImageTasksRunConfigureImage" -ErrorAction Stop
Start-Sleep -Seconds 5
}
catch
{
break
}
}
}
- VM の作成
- InstallDSCModules.ps1 の実行
- CreateADPrimaryDomainController.ps1 の実行
をどのように同期をとりながら実行しているのかが理解できていないのですが、処理の内容としてはカスタムスクリプトと DSC を使用して環境を構築するためのサンプルとして十分に使えそうですね。