Как автоматизировать создание виртуальных машин? Рассказываем подробно

от автора

Создание новой виртуальной машины — это рутина, отнимающая много времени. И чем больше инфраструктура и организация, тем больше процедур, связанных с этим процессом. Мы автоматизировали это процесс с помощью PowerShell.

Добро пожаловать под кат, если вам это интересно.


Программисты не любят делать двойную работу, сисадмины тоже.

Ниже пример автоматизации одного из наших заказчиков.

Мы хотели сделать так, чтобы любой инженер или project-менеджер смог создать новую виртуальную машину с минимальными усилиями и за минимальный срок. У нашего заказчика есть ITSM-система, в данном примере это ServiceNow, мы создали соответствующую web-форму в сервисном каталоге. Для «заказа» новой машины менеджеру необходимо заполнить поля и подтвердить «заказ», после этого запускается цепочка процессов, и на выходе получаем готовую к использованию машину.

Итак, давайте рассмотрим, что нужно определить менеджеру, чтобы создать новую виртуальную машину:

VM Description: описание виртуальной машины
Тут нужны некоторые пояснения. В нашем решении активно используется PowerShell 5.1, поэтому пока Windows-only, в будущем мы постараемся добавить поддержку Unix-машин и перейдем на PowerShell Core.

OS, операционная система. Никаких особых препятствий использовать Windows 2008 (R2) нет, но мы используем 2012R2 или 2016.

VM Size, размер виртуальной машины. У каждого это может быть определено по-своему, в данном примере Small 1CPU-4Gb Ram, Medium 2CPU-8Gb, Large 4-16.

VM Storage, Disk 0 (C:\) имеет фиксированный размер, который вы не можете изменить, доступен только селектор Fast/Slow storage. «Fast» — это может быть Storage Tier с SSD, а «Slow» — это storage на «обычных» HDD (конечно — SAN). Disk1 (Disk2 и далее) также имеют селектор выбора типа Storage, а также поля для ввода желаемого размера в гигабайтах, Letter для раздела и размер кластера (что важно для SQL Server).

Trust, определяем, что машина должна быть Domain-joined или нет, с доступом из Public Network или нет.

Type, тип машины. Почти каждую машину можно определить, как front-end или back-end приложения или же other во всех оставшихся случаях. На основе выбранного типа мы сможем в дальнейшем определить наиболее подходящую подсеть для машины.

Environment, в инфраструктуре заказчика есть два дата центра: Primary (Production) и Secondary (Dev/test), DC связаны между собой быстром каналом связи и обеспечивают отказоустойчивость. По договоренности все виртуальные машины в Primary DC имеют IP-адрес, начинающийся на 10.230, а в Secondary DC — на 10.231.

(SLA) Service Level Agreement, этот параметр влияет на качество обслуживания данной машины.

Приложения. Мы добавили возможность установки и настройки SQL Server. Необходимо выбрать издание, instance name и collation. Также возможно настроить и Web Server роль и многое другое.

Теперь нам нужно определить, как хранить выбранные значения. Мы решили, что наиболее удобный формат — JSON-файл. Как я говорил ранее, в среде заказчика используется ITSM ServiceNow; менеджер, после того как выбрал все необходимы значения, нажимает кнопку «order» и после этого ServiceNow передает все параметры нашему PowerShell-скрипту (на back-end ServiceNow), который и создаст JSON-файл. Выглядит это примерно так:

.\CreateConfiguration.ps1 -SecurityZone trusted -VMDescription "VM for CRM System" -Requestor "evgeniy.vpro" -OSVersion 2k16 -OSEdition Standard -BuildNewVM -VMEnvironment Prod -VMServiceLevel GOLD -VMSize Medium -Disk0Tier Fast -Disk1Size 50 -Disk1Tier Eco -Disk1Letter D -MSSQLServer -MSSQLInstanceName "Instance1" -SQLCollation Latin1_General_CI_AS -SQLEdition Standard -Disk2Size 35 -Disk3Size 65 

В теле CreateConfiguration .ps1 скрипта:

#создаем PowerShell-объект $config = [ordered]@{}  #И заполняем его входными параметрами.  $config.SecurityZone=$SecurityZone 

В конце экспортируем наш объект в JSON-файл:

$ServerConfig = New-Object –TypeName PSObject  $config ConvertTo-Json -InputObject $ServerConfig -Depth 100 | Out-File "C:\Configs\TargetNodes\Build\$($Hostname.ToLower()).json" -Force  

Примерный образец конфигурации:

{     "Hostname":  "dsctest552",     "SecurityZone":  "trusted",     "Domain":  "testdomain",     "Requestor":  "evgeniy.vpro",     "VM":  {                "Size":  "Medium",                "Environment":  "Prod",                "SLR":  "GOLD",                "DbEngine":  "MSSQL",                "RAM":  8,                "Storage":  [                                {                                    "Id":  0,                                    "Tier":  "Fast",                                    "Size":  "100",                                    "Allocation":  4,                                    "Letter":  "C"                                },                                {                                    "Id":  1,                                    "Tier":  "Eco",                                    "Size":  50,                                    "Label":  "Data",                                    "Allocation":  64,                                    "Letter":  "D"                                },                                {                                    "Id":  2,                                    "Tier":  "Fast",                                    "Size":  35,                                    "Label":  "Data",                                    "Allocation":  64,                                    "Letter":  "E"                                },                                {                                    "Id":  3,                                    "Tier":  "Fast",                                    "Size":  65,                                    "Label":  "Data",                                    "Allocation":  64,                                    "Letter":  "F"                                }                            ]            },     "Network":  {                     "MAC":  "",                     "IP":  "10.230.168.50",                     "Gateway":  "10.230.168.1",                     "VLAN":  “VLAN168”                 },     "OS":  {                "Version":  "2k16",                "Edition":  "Standard",                "Administrators":  [                                       "LocaAdmin",                                       "testdomain\\ Security-LocalAdmins"                                   ]            },     "OU":  "OU=Servers,OU=Staging,DC=testdomain",     "Applications":  [                          {                              "Application":  "Microsoft SQL Server 2016",                              "InstanceName":  "vd",                              "Collation":  "Latin1_General_CI_AS",                              "Edition":  "Standard",                              "Features":  "SQLENGINE",                              "Folders":  {                                              "DataRoot":  "F:\\MSSQL",                                              "UserDB":  "F:\\MSSQL\\MSSQL11.vd\\MSSQL\\Data",                                              "UserLog":  "E:\\MSSQL\\MSSQL11.vd\\MSSQL\\Log",                                              "TempDB":  "D:\\MSSQL\\MSSQL11.vd\\MSSQL\\TempDB",                                              "TempDBLog":  "D:\\MSSQL\\MSSQL11.vd\\MSSQL\\TempDB",                                              "Backup":  "E:\\MSSQL\\MSSQL11.vd\\MSSQL\\Backup"                                          },                              "MaxMemory":  2147483647                          }                      ],     "Description":  "VM for CRM",     "Certificate":  {                         "File":  null,                         "Thumbprint":  null                     },     "Version":  0 } 

Вы могли заметить, что в веб-форме отсутствовало имя виртуальной машины и IP-адрес. Мы получаем эти значения автоматически следующим образом:

Имя машины, в ITSM ServiceNow есть специальный раздел: CMDB (Configuration Management Data Base), в этой базе хранятся все записи о существующих виртуальных машинах, их статус, команда поддержки и прочее. Мы создали порядка 200 резервных записей со статусом Allocated. Чтобы получить имя для виртуальной машины мы делаем REST-запрос к CMDB и получаем первую «свободную» запись и меняем её статус с Allocated на Pending install.

IP адрес и VLAN, мы развернули IPAM в нашей сети — это встроенная feature в Windows Server 2016, которая позволяет управлять IP-адресами в вашей сети. Вовсе не обязательно использовать все возможности IPAM (DHCP, DNS, AD), а использовать её только как базу данных IP-адресов с потенциальным расширением функционала. Скрипт, который создает JSON файл, делает запрос к IPAM на предмет первого свободного IP адреса в подсети. А подсеть VLAN (х/24 подсеть) определяется на основе выбранных значений SLA, Environment, Trust и Type.
Файл-конфигурация готов, все поля на месте, можно создавать машину. Встает вопрос «как хранить учетные данные для всех наших скриптов?». Мы используем пакет CredentialManager. Этот пакет работает со встроенным Windows Credential Manager API для хранения паролей. Пример создания пароля:

New-StoredCredential -Target "ESXi" -UserName "testdomain.eu\vmwareadm" -Password "veryultraP@ssw00rd." -Type Generic -Persist LocalMachine

Пароль будет доступен для чтения в пределах данной машины и учетной записи.

$ESXiAdmin = Get-StoredCredential -Type Generic -Target ESXi

У нас есть сервер, на котором хранятся все конфигурации c GIT, теперь мы можем надежно отслеживать все изменения в конфигурациях: кто, что, где и когда.

На этом сервере настроен scheduled task: проверять папку c конфигурациями и писать в Windows Event Log обо всех изменениях.

Через 15 минут scheduled task напишет в Windows EventLog, что обнаружен новый файл-конфигурация.

Пришло время проверить эту конфигурацию. В первую очередь нам нужно убедиться, что файл имеет корректное форматирование:

$Configuration=(Get-Content -Raw $File | Out-String | ConvertFrom-Json) 

Если все хорошо, пора приступать к созданию машины и запусить BuildVM.ps1 скрипт.

В BuildVM.ps1 мы проверяем, что файл-конфигурация имеет описание всех характеристик виртуальной машины: size, env, sla, type, storage, ram, network.

Обязательно проверим, есть ли в инфраструктуре машина с таким же именем (CheckVM.ps1).
Подключаемся через VMWare PowerShell CLI к нашей vSphere:

$VmWareAdmin = Get-StoredCredential -Type Generic -Target ESXi Connect-VIServer -Server "UKHQSV50001" -Credential $VmWareAdmin | Out-Null  

Проверяем, есть ли машина с таким же именем в инфраструктуре

$VM=Get-VM $server -ErrorAction SilentlyContinue

И отключаемся:

Disconnect-VIServer * -Force -Confirm:$false 

Убедимся, что машина также не доступна по WinRM

$ping=Test-NetConnection -ComputerName $Configuration.Hostname -CommonTCPPort WINRM -InformationLevel Quiet -ErrorAction SilentlyContinue 

Если в $VM и $ping пусто, то можно создавать новую машину. (Мы обрабатываем ситуации, когда машина уже создана в ESXi вручную или же эта машина в другом дата-центре.)

Пару слов о машине. Это подготовленный образ виртуальный машины, который был финализирован sysprep и сконвертирован в template в нашем vSphere. В образе сохранен локальный администратор с известным нам паролем, эта учетная запись «не слетает» после sysprep, что позволит нам получить доступ к каждой машине из этого темплейта, а позже мы сможем заменить этот пароль в целях безопасности.

Создание виртуальной машины

Найдем соответствующий SLR-кластер:

$Cluster=Get-Cluster -Name $Configuration.VM.SLR 

Проверим, что у нас достаточно места на Datastore:

$DatastoreCluster = Get-DatastoreCluster |Where-Object {$_.Name -like $Datastore1Name} $Datastore1 = Get-Datastore -Location $DatastoreCluster |sort -Property "FreeSpaceGB" |select -Last 1  IF ($Datastore1.FreeSpaceGB -le "200"){ Write-Host -foreground red "STOP: Not enough datastore capacity for DISK" $vdisk.Id Break }  

И достаточно памяти:

$VMHost = Get-VMHost -Location $Cluster |sort -Property "MemoryUsageGB" |select -First 1  IF ($VMHost.MemoryUsageGB -le "20"){ Write-Host -foreground red "STOP: No enough ESXi host capacity"         Break }  

Берем наш темплейт

$VMTemplate = Get-Template -Name 'Win2016_Std_x64_Template' 

И создаем новую виртуальную машину

New-VM -Name $Configuration.Hostname.ToUpper() -VMHost $VMHost -ResourcePool $ResourcePool -Datastore $Datastore -Template $VMTemplate -Location "AutoDeployed VMs" 

Важно подключить сетевой интерфейс к подсети с включенным DHCP.

Запускаем виртуальную машину

Start-VM $VM

И сохраняем описание машины, чтобы потом можно было определить машину на уровне VMWare.

Set-Annotation -Entity $VM -CustomAttribute "Change request" -Value $Configuration.Request -Confirm:$false Set-VM $VM -Notes $Configuration.Description -Confirm:$false 

Машина запустилась и теперь мы можем узнать полученный MAC-адрес:

$vMAC = (($VM | Get-NetworkAdapter | Select-Object -Property "MacAddress").MacAddress).Replace(':','')

Сохраним это значение в наш JSON-файл

$Configuration.Network.MAC=$VMAC ConvertTo-Json -InputObject $Configuration -Depth 100 | Out-File "C:\Configs\TargetNodes\Build\$Hostname.json" -Force 

Здесь самое время сделать commit в наш Git, что машина создана и имеет свой уникальный MAC.

Машина начинает инициализироваться (после sysprep), настраивать оборудование и начальную конфигурацию.

Давайте дождемся, когда будет доступна наша машина по WinRM c скриптом EstablishConnection.ps1.

Сначала узнаем какой IP машина получила от DHCP:

#Здесь $MAC = $vMAC while($isOnline -ne $true){      if((Get-DhcpServerv4Lease -ClientId $MAC -ScopeId $StagingDHCPScope -ComputerName $DHCPServer -ErrorAction Ignore).IPAddress.IPAddressToString){         $tempIP=(Get-DhcpServerv4Lease -ClientId $MAC -ScopeId $StagingDHCPScope -ComputerName $DHCPServer).IPAddress.IPAddressToString         break     }     else{             if($isOnline -ne $true){             Write-Host "`r$i`t" -NoNewline             $i++         }      } } 

А теперь дождемся, когда машина будет доступна по WinRM:

$LocalAdmin = Get-StoredCredential -Type Generic -Target LocalAdmin $i=0 $isOnline=$false while($isOnline -ne $true){     if(Invoke-Command -ComputerName $tempIP -ScriptBlock{ Get-ItemProperty -Path "Registry::\HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing" } -Credential $LocalAdmin -ErrorAction SilentlyContinue){         $isOnline=$true                 break             }     else{         if($isOnline -ne $true){             Write-Host "`r$i" -NoNewline              $i++             Start-Sleep -Seconds 1         }     }      }   

Машина готова к управлению.

Desired State Configuration

Для настройки желаемой конфигурации мы используем часть PowerShell — DSC (Desired State Configuration). В сети есть настроенный DSC Pull Server: dscpull.testdomain.eu.
Ниже конфигурация нашего DSC Pull Server. Хорошая статья по настройке DSC Pull.

Node $NodeName     {         WindowsFeature DSCServiceFeature         {             Ensure = "Present"             Name   = "DSC-Service"                     }          xDscWebService PSDSCPullServer         {             Ensure                  = "Present"             EndpointName            = "PSDSCPullServer"             Port                    =  8080                         PhysicalPath            = "$env:SystemDrive\inetpub\PSDSCPullServer"             CertificateThumbPrint   =  $certificateThumbPrint                      ModulePath              = "$env:PROGRAMFILES\WindowsPowerShell\DscService\Modules"             ConfigurationPath       = "$env:PROGRAMFILES\WindowsPowerShell\DscService\Configuration"                         State                   = "Started"             DependsOn               = "[WindowsFeature]DSCServiceFeature"              RegistrationKeyPath     = "$env:PROGRAMFILES\WindowsPowerShell\DscService"                AcceptSelfSignedCertificates = $true             UseSecurityBestPractices = $true                                          }          File RegistrationKeyFile         {             Ensure          = 'Present'             Type            = 'File'             DestinationPath = "$env:ProgramFiles\WindowsPowerShell\DscService\RegistrationKeys.txt"             Contents        = $RegistrationKey         }     }  

Он доступен по адресу: https://dscpull.testdomain.eu:8080

Его Endpoint: https://dscpull.testdomain.eu:8080/PSDSCPullserver.svc

На всех клиентах pull сервера должен быть установлен PowerShell 5.1
Если установлен не PowerShell 5.1:

$PSVersionTable.PSVersion.Major –lt 5

установить PowerShell 5.1:

Write-Host "Download PowerShell 5.1" Invoke-Command -ComputerName $Node -ScriptBlock { [System.Net.ServicePointManager]::SecurityProtocol=[System.Net.SecurityProtocolType]::Tls12;Invoke-WebRequest -Uri "https://dscpull.testdomain.eu:8080/Files/Updates/WMF.msu" -OutFile C:\TEMP\WMF.MSU  } Write-Host "Extract PowerShell 5.1"     Invoke-Command -ComputerName $Node -ScriptBlock {Start-Process -FilePath 'wusa.exe' -ArgumentList "C:\temp\WMF.msu /extract:C:\temp\" -Wait -PassThru   }     Write-Host "Apply PowerShell 5.1"     Invoke-Command -ComputerName $Node -ScriptBlock {Start-Process -FilePath 'dism.exe' -ArgumentList "/online /add-package /PackagePath:C:\temp\WindowsBlue-KB3191564-x64.cab /Quiet" -Wait -PassThru }      Write-Host "PowerShell 5.1 has been installed"  

В нашей сети также развернут PKI-сервер. Это условие для безопасного шифрование учетных данных сохранённых в DSC mof файлах (Mof файлы — это «язык» на котором общаются Pull Server и его клиенты). Когда клиент пытается зарегистрироваться на Pull Server, необходимо указать Thumprint сертификата и в дальнейшем Pull Server будет использовать этот сертификат для шифрования паролей. Ниже мы рассмотрим, как это работает.

Импортируем Root CA нашей новой машине:

  Invoke-Command -ComputerName $server -ScriptBlock{         $PKI="-----BEGIN CERTIFICATE----- MIIF2TCCA8GgAwIBAgIQSPIjcff9rotNdxbg3+ygqDANBgkqhkiG9w0BAQUFADAe **************************************************************** znafMvVx0B4tGEz2PFss/FviGdC3RohBHG0rF5jO50J4nS/3cGGm+HGdn1w/tZd0 a0FWpn9VCOSmXM2It+tSW1f4nZVt6T2kr1ZlTxkDhT7HMSGsrX/XJswzCkDGe3dE qrVVjNUkhVTaeeBWdujB5J6mcx7YkNsAUhODiS9Cf7FnYnxLFA72M0pijI48P5F0 ShM9HWAAUIrLkv13ug== -----END CERTIFICATE-----"         $PKI  | Out-File RootCA.cer         Import-Certificate RootCA.cer -CertStoreLocation Cert:\LocalMachine\Root | select Thumbprint | Out-Null      }  -Credential $LocalAdmin | Out-Null   

Для дальнейшей работы нам нужна пара RSA-ключей. Сгенерируем самоподписанный сертификат и временно будем работать с ним:

param(     $server,     $LocalAdmin  ) Write-Verbose "Try to generate certificate" $generated=Invoke-Command -ComputerName $server -ScriptBlock{   function New-SelfSignedCertificateEx { [OutputType('[System.Security.Cryptography.X509Certificates.X509Certificate2]')] [CmdletBinding(DefaultParameterSetName = '__store')] 	param ( 		[Parameter(Mandatory = $true, Position = 0)] 		[string]$Subject, 		[Parameter(Position = 1)] 		[datetime]$NotBefore = [DateTime]::Now.AddDays(-1), 		[Parameter(Position = 2)] 		[datetime]$NotAfter = $NotBefore.AddDays(365), 		[string]$SerialNumber, 		[Alias('CSP')] 		[string]$ProviderName = "Microsoft Enhanced Cryptographic Provider v1.0", 		[string]$AlgorithmName = "RSA", 		[int]$KeyLength = 2048, 		[validateSet("Exchange","Signature")] 		[string]$KeySpec = "Exchange", 		[Alias('EKU')] 		[Security.Cryptography.Oid[]]$EnhancedKeyUsage, 		[Alias('KU')] 		[Security.Cryptography.X509Certificates.X509KeyUsageFlags]$KeyUsage, 		[Alias('SAN')] 		[String[]]$SubjectAlternativeName, 		[bool]$IsCA, 		[int]$PathLength = -1, 		[Security.Cryptography.X509Certificates.X509ExtensionCollection]$CustomExtension, 		[ValidateSet('MD5','SHA1','SHA256','SHA384','SHA512')] 		[string]$SignatureAlgorithm = "SHA1", 		[string]$FriendlyName, 		[Parameter(ParameterSetName = '__store')] 		[Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation = "CurrentUser", 		[Parameter(Mandatory = $true, ParameterSetName = '__file')] 		[Alias('OutFile','OutPath','Out')] 		[IO.FileInfo]$Path, 		[Parameter(Mandatory = $true, ParameterSetName = '__file')] 		[Security.SecureString]$Password, 		[switch]$AllowSMIME, 		[switch]$Exportable 	) 	$ErrorActionPreference = "Stop" 	if ([Environment]::OSVersion.Version.Major -lt 6) { 		$NotSupported = New-Object NotSupportedException -ArgumentList "Windows XP and Windows Server 2003 are not supported!" 		throw $NotSupported 	} 	$ExtensionsToAdd = @()  #region constants 	# contexts 	New-Variable -Name UserContext -Value 0x1 -Option Constant 	New-Variable -Name MachineContext -Value 0x2 -Option Constant 	# encoding 	New-Variable -Name Base64Header -Value 0x0 -Option Constant 	New-Variable -Name Base64 -Value 0x1 -Option Constant 	New-Variable -Name Binary -Value 0x3 -Option Constant 	New-Variable -Name Base64RequestHeader -Value 0x4 -Option Constant 	# SANs 	New-Variable -Name OtherName -Value 0x1 -Option Constant 	New-Variable -Name RFC822Name -Value 0x2 -Option Constant 	New-Variable -Name DNSName -Value 0x3 -Option Constant 	New-Variable -Name DirectoryName -Value 0x5 -Option Constant 	New-Variable -Name URL -Value 0x7 -Option Constant 	New-Variable -Name IPAddress -Value 0x8 -Option Constant 	New-Variable -Name RegisteredID -Value 0x9 -Option Constant 	New-Variable -Name Guid -Value 0xa -Option Constant 	New-Variable -Name UPN -Value 0xb -Option Constant 	# installation options 	New-Variable -Name AllowNone -Value 0x0 -Option Constant 	New-Variable -Name AllowNoOutstandingRequest -Value 0x1 -Option Constant 	New-Variable -Name AllowUntrustedCertificate -Value 0x2 -Option Constant 	New-Variable -Name AllowUntrustedRoot -Value 0x4 -Option Constant 	# PFX export options 	New-Variable -Name PFXExportEEOnly -Value 0x0 -Option Constant 	New-Variable -Name PFXExportChainNoRoot -Value 0x1 -Option Constant 	New-Variable -Name PFXExportChainWithRoot -Value 0x2 -Option Constant #endregion 	 #region Subject processing 	# http://msdn.microsoft.com/en-us/library/aa377051(VS.85).aspx 	$SubjectDN = New-Object -ComObject X509Enrollment.CX500DistinguishedName 	$SubjectDN.Encode($Subject, 0x0) #endregion  #region Extensions  #region Enhanced Key Usages processing 	if ($EnhancedKeyUsage) { 		$OIDs = New-Object -ComObject X509Enrollment.CObjectIDs 		$EnhancedKeyUsage | ForEach-Object { 			$OID = New-Object -ComObject X509Enrollment.CObjectID 			$OID.InitializeFromValue($_.Value) 			# http://msdn.microsoft.com/en-us/library/aa376785(VS.85).aspx 			$OIDs.Add($OID) 		} 		# http://msdn.microsoft.com/en-us/library/aa378132(VS.85).aspx 		$EKU = New-Object -ComObject X509Enrollment.CX509ExtensionEnhancedKeyUsage 		$EKU.InitializeEncode($OIDs) 		$ExtensionsToAdd += "EKU" 	} #endregion  #region Key Usages processing 	if ($KeyUsage -ne $null) { 		$KU = New-Object -ComObject X509Enrollment.CX509ExtensionKeyUsage 		$KU.InitializeEncode([int]$KeyUsage) 		$KU.Critical = $true 		$ExtensionsToAdd += "KU" 	} #endregion  #region Basic Constraints processing 	if ($PSBoundParameters.Keys.Contains("IsCA")) { 		# http://msdn.microsoft.com/en-us/library/aa378108(v=vs.85).aspx 		$BasicConstraints = New-Object -ComObject X509Enrollment.CX509ExtensionBasicConstraints 		if (!$IsCA) {$PathLength = -1} 		$BasicConstraints.InitializeEncode($IsCA,$PathLength) 		$BasicConstraints.Critical = $IsCA 		$ExtensionsToAdd += "BasicConstraints" 	} #endregion  #region SAN processing 	if ($SubjectAlternativeName) { 		$SAN = New-Object -ComObject X509Enrollment.CX509ExtensionAlternativeNames 		$Names = New-Object -ComObject X509Enrollment.CAlternativeNames 		foreach ($altname in $SubjectAlternativeName) { 			$Name = New-Object -ComObject X509Enrollment.CAlternativeName 			if ($altname.Contains("@")) { 				$Name.InitializeFromString($RFC822Name,$altname) 			} else { 				try { 					$Bytes = [Net.IPAddress]::Parse($altname).GetAddressBytes() 					$Name.InitializeFromRawData($IPAddress,$Base64,[Convert]::ToBase64String($Bytes)) 				} catch { 					try { 						$Bytes = [Guid]::Parse($altname).ToByteArray() 						$Name.InitializeFromRawData($Guid,$Base64,[Convert]::ToBase64String($Bytes)) 					} catch { 						try { 							$Bytes = ([Security.Cryptography.X509Certificates.X500DistinguishedName]$altname).RawData 							$Name.InitializeFromRawData($DirectoryName,$Base64,[Convert]::ToBase64String($Bytes)) 						} catch {$Name.InitializeFromString($DNSName,$altname)} 					} 				} 			} 			$Names.Add($Name) 		} 		$SAN.InitializeEncode($Names) 		$ExtensionsToAdd += "SAN" 	} #endregion  #region Custom Extensions 	if ($CustomExtension) { 		$count = 0 		foreach ($ext in $CustomExtension) { 			# http://msdn.microsoft.com/en-us/library/aa378077(v=vs.85).aspx 			$Extension = New-Object -ComObject X509Enrollment.CX509Extension 			$EOID = New-Object -ComObject X509Enrollment.CObjectId 			$EOID.InitializeFromValue($ext.Oid.Value) 			$EValue = [Convert]::ToBase64String($ext.RawData) 			$Extension.Initialize($EOID,$Base64,$EValue) 			$Extension.Critical = $ext.Critical 			New-Variable -Name ("ext" + $count) -Value $Extension 			$ExtensionsToAdd += ("ext" + $count) 			$count++ 		} 	} #endregion  #endregion  #region Private Key 	# http://msdn.microsoft.com/en-us/library/aa378921(VS.85).aspx 	$PrivateKey = New-Object -ComObject X509Enrollment.CX509PrivateKey 	$PrivateKey.ProviderName = $ProviderName 	$AlgID = New-Object -ComObject X509Enrollment.CObjectId 	$AlgID.InitializeFromValue(([Security.Cryptography.Oid]$AlgorithmName).Value) 	$PrivateKey.Algorithm = $AlgID 	# http://msdn.microsoft.com/en-us/library/aa379409(VS.85).aspx 	$PrivateKey.KeySpec = switch ($KeySpec) {"Exchange" {1}; "Signature" {2}} 	$PrivateKey.Length = $KeyLength 	# key will be stored in current user certificate store 	switch ($PSCmdlet.ParameterSetName) { 		'__store' { 			$PrivateKey.MachineContext = if ($StoreLocation -eq "LocalMachine") {$true} else {$false} 		} 		'__file' { 			$PrivateKey.MachineContext = $false 		} 	} 	$PrivateKey.ExportPolicy = if ($Exportable) {1} else {0} 	$PrivateKey.Create() #endregion  	# http://msdn.microsoft.com/en-us/library/aa377124(VS.85).aspx 	$Cert = New-Object -ComObject X509Enrollment.CX509CertificateRequestCertificate 	if ($PrivateKey.MachineContext) { 		$Cert.InitializeFromPrivateKey($MachineContext,$PrivateKey,"") 	} else { 		$Cert.InitializeFromPrivateKey($UserContext,$PrivateKey,"") 	} 	$Cert.Subject = $SubjectDN 	$Cert.Issuer = $Cert.Subject 	$Cert.NotBefore = $NotBefore 	$Cert.NotAfter = $NotAfter 	foreach ($item in $ExtensionsToAdd) {$Cert.X509Extensions.Add((Get-Variable -Name $item -ValueOnly))} 	if (![string]::IsNullOrEmpty($SerialNumber)) { 		if ($SerialNumber -match "[^0-9a-fA-F]") {throw "Invalid serial number specified."} 		if ($SerialNumber.Length % 2) {$SerialNumber = "0" + $SerialNumber} 		$Bytes = $SerialNumber -split "(.{2})" | Where-Object {$_} | ForEach-Object{[Convert]::ToByte($_,16)} 		$ByteString = [Convert]::ToBase64String($Bytes) 		$Cert.SerialNumber.InvokeSet($ByteString,1) 	} 	if ($AllowSMIME) {$Cert.SmimeCapabilities = $true} 	$SigOID = New-Object -ComObject X509Enrollment.CObjectId 	$SigOID.InitializeFromValue(([Security.Cryptography.Oid]$SignatureAlgorithm).Value) 	$Cert.SignatureInformation.HashAlgorithm = $SigOID 	# completing certificate request template building 	$Cert.Encode() 	 	# interface: http://msdn.microsoft.com/en-us/library/aa377809(VS.85).aspx 	$Request = New-Object -ComObject X509Enrollment.CX509enrollment 	$Request.InitializeFromRequest($Cert) 	$Request.CertificateFriendlyName = $FriendlyName 	$endCert = $Request.CreateRequest($Base64) 	$Request.InstallResponse($AllowUntrustedCertificate,$endCert,$Base64,"") 	switch ($PSCmdlet.ParameterSetName) { 		'__file' { 			$PFXString = $Request.CreatePFX( 				[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)), 				$PFXExportEEOnly, 				$Base64 			) 			Set-Content -Path $Path -Value ([Convert]::FromBase64String($PFXString)) -Encoding Byte 		} 	} 	[Byte[]]$CertBytes = [Convert]::FromBase64String($endCert) 	New-Object Security.Cryptography.X509Certificates.X509Certificate2 @(,$CertBytes) } New-SelfsignedCertificateEx `     -Subject "CN=${ENV:ComputerName}" `     -EKU 'Document Encryption' `     -KeyUsage 'KeyEncipherment, DataEncipherment' `     -SAN ${ENV:ComputerName} `     -FriendlyName 'DSC Credential Encryption certificate' `     -Exportable `     -StoreLocation 'LocalMachine' `     -KeyLength 2048 `     -ProviderName 'Microsoft Enhanced Cryptographic Provider v1.0' `     -AlgorithmName 'RSA' `     -SignatureAlgorithm 'SHA256'  } -ErrorAction Ignore -Credential $LocalAdmin Write-Host "SelfSigned Certificate has been generated" if($generated){     return $true } else{     return $false }  

Теперь мы можем зарегистрироваться на Pull Server:

$DscHostFQDN = [System.Net.Dns]::GetHostEntry([string]$env:computername).HostName $DscPullServerURL = "https://$($DscHostFQDN):8080/PSDSCPullserver.svc" $DscWebConfigChildPath = '\inetpub\psdscpullserver\web.config' $DscWebConfigPath = Join-Path -Path $env:SystemDrive -ChildPath $DscWebConfigChildPath $DscWebConfigXML = [xml](Get-Content $DscWebConfigPath) $DscRegKeyName = 'RegistrationKeys.txt' $DscRegKeyXMLNode = "//appSettings/add[@key = 'RegistrationKeyPath']" $DscRegKeyParentPath = ($DscWebConfigXML.SelectNodes($DscRegKeyXMLNode)).value $DscRegKeyPath = Join-Path -Path $DscRegKeyParentPath -ChildPath $DscRegKeyName $DscRegKey = Get-Content $DscRegKeyPath   [DSCLocalConfigurationManager()] configuration RegisterOnPull {     Node $Node     {         Settings         {                   ConfigurationModeFrequencyMins =   1440             CertificateID = $Thumbprint             RefreshMode          ='Pull'             RefreshFrequencyMins = 1440             RebootNodeIfNeeded   = $true                       ConfigurationMode ='ApplyAndAutoCorrect'             AllowModuleOverwrite = $true             DebugMode = 'None'             StatusRetentionTimeInDays = 1                      }          ConfigurationRepositoryWeb $([string]$env:computername)         {             ServerURL =  $DscPullServerURL             RegistrationKey = $DscRegKey             CertificateID = $Thumbprint                           ConfigurationNames = @("$hostx")         }      } }  RegisterOnPull -OutputPath $MetaConfigsStorage  Set-DscLocalConfigurationManager -ComputerName $Node  -Path $MetaConfigsStorage  -Verbose -Force -Credential $LocalAdmin 

Отправим первую конфигурацию нашей машине

Configuration Rename {     param     (         [Parameter()]         [System.String[]]         $Node,         $hostname     )      Import-DscResource -ModuleName xComputerManagement         Import-DscResource –ModuleName PSDesiredStateConfiguration      Node $Node     {                  xComputer JoinDomain         {             Name       = $hostname         }     } }  Rename -Node $Node -OutputPath $DscConfigPath -hostname $hostname  New-DscChecksum $DscConfigPath -Force Invoke-Command -ComputerName $Node -ScriptBlock{Update-DscConfiguration -Verbose -Wait } -Credential $LocalAdmin -Verbose  

Сервер автоматически переименуется и перезагрузится. Теперь мы можем выполнить Join Domain.

Configuration JoinAD {     param     (         [Parameter()]         [System.String[]]         $Node,         [Parameter(Mandatory = $true)]         [ValidateNotNullorEmpty()]         [System.Management.Automation.PSCredential]         $DomainAdmin,         $hostname,         $domain     )      Import-DscResource -ModuleName xComputerManagement         Import-DscResource –ModuleName PSDesiredStateConfiguration      Node $Node     {                  xComputer JoinDomain         {             Name       = $hostname             DomainName = $domain             Credential = $DomainAdmin             JoinOU = "OU=Servers,OU=Staging,OU=Provider,DC=testdomain,DC=eu"         }                  GroupSet LocalAdmins         {             GroupName = @( 'Administrators')             Ensure = 'Present'             MembersToInclude = @( 'testdomain-eu\srv_dscstaging_001' )         }     } } $cd = @{     AllNodes = @(         @{             NodeName = $Node             PSDscAllowPlainTextPassword = $false             PSDscAllowDomainUser=$true             Certificatefile = $CertFile             Thumbprint = $Certificate.ToString()         }     ) }  JoinAD -Node $Node -OutputPath $DscConfigPath -DomainAdmin $DomainAdmin -hostname $hostname -ConfigurationData $cd -domain $domain New-DscChecksum $DscConfigPath -Force Invoke-Command -ComputerName $Node -ScriptBlock{Update-DscConfiguration -Verbose -Wait } -Credential $LocalAdmin -Verbose   

Вот как выглядит наш mof-файл:

instance of MSFT_Credential as $MSFT_Credential1ref { Password = "-----BEGIN CMS-----\nMIIBsgYJKoZIhvcNAQcDoIIBozCCAZ8CAQAxggFKMIIBRgIBADAuMBoxGDAWBgNVBAMMD1dJTi1H\nNFFKTFFQME4xNQIQOQN77pxew75HU6l7GPn99TANBgkqhkiG9w0BAQcwAASCAQAlhFf7Zs2gJsrw\ngvQ0OGDRsVQMr5jZHIa9bAAcl3+V+5dLaN1GA/Jl06YrLJpnulyuivIJWU34SNTkeRCfxpzPwACV\n2RJHdYIqpFApIxTmSh5zhilC515aDukGchCrFsHayNQsr8vAjIALkRvvtECHgIOREaiwdF2WsKUU\nkbeSDAE2FDx6HBZDxrMG8OCxeiNMgLKeB4rwbmx7ZUiABu5OIcTtHOvMaXp4vNWX5jXStsdQ/Ylt\njPNt2FE6CAnMabC256wnXJIBQpTdqqmc2qmzlz/hpSEUMDbJEnc1DEK2yWbKcO+BEyD2cr6vKHdn\nQ9TrjvbysEOvYjT15o6MccwkMEwGCSqGSIb3DQEHATAdBglghkgBZQMEASoEEEdKJT+GX4IkPezR\nwYncyQiAIAFKxwJocH4ufRsq9L2Ipkp+VQCx2ljlwif6ac4X/PqG\n-----END CMS-----";  UserName = "testdomain.eu\\service_DomainJoin_001";  };  instance of MSFT_xComputer as $MSFT_xComputer1ref { ResourceID = "[xComputer]JoinDomain";  Credential = $MSFT_Credential1ref;  DomainName = "testdomain.eu";  SourceInfo = "C:\\Program Files\\WindowsPowerShell\\Scripts\\JoinAD.ps1::34::9::xComputer";  Name = "dsctest51";  JoinOU = "OU=Servers,OU=Staging,DC=testdomain,DC=eu";  ModuleName = "xComputerManagement";  ModuleVersion = "4.1.0.0";   ConfigurationName = "JoinAD";  }; 

DSC зашифровал учетные данные от сервисной учетки с правами Domain Admin: testdomain.eu\\service_DomainJoin_001 самоподписанным сертификатом. DSC Client своим Private Key расшифровывает учетные данные и применяет все модули конфигурации c указанными доменным учетными данными. В данном случае выполняет Domain Join в указанную organization unit.

GroupSet LocalAdmins         {             GroupName = @( 'Administrators')             Ensure = 'Present'             MembersToInclude = @( testdomain-eu\srv_dscstaging_001' )         } 

Этот модуль добавляет srv_dscstaging_001 в локальные администраторы для дальнейшей настройки.

После перезагрузки, мы сможем зайти на машину с доменными учетными данными.

Ждем, когда сервер получит сертификат от нашего PKI (у нас настроен auto enrollment) и в дальнейшем будем работать с сертификатом, выпущенным нашим PKI.

$vmcert=Invoke-Command -ComputerName $server -ScriptBlock{ return Get-ChildItem -Path cert:\LocalMachine\My  | where {$_.EnhancedKeyUsageList.FriendlyName -eq "Document Encryption"-and $_.Issuer -eq "CN=TestDomain Issuing CA, DC=testdomain, DC=eu"} } -ErrorAction Ignore  

Теперь снова зарегистрируемся на Pull Server с обновленным thumbprint.

Всё, машина domain-joined, и мы можем использовать её так, как нам удобно.

Установка SQL Server

В JSON- файле описаны требования по MS SQL Server, для установки и настройки SQL Server мы также используем DSC. Вот как выглядит конфигурация:

Configuration $Node{      WindowsFeature "NetFramework35"{                 Name = "NET-Framework-Core"                 Ensure = "Present"                 Source = "\\$DscHostFQDN\Files\Updates"             } 			             WindowsFeature "NetFramework45"{                 Name = "NET-Framework-45-Core"                 Ensure= "Present"             }       SqlSetup "MSSQL2012NamedInstance"{ 					    InstanceName          = $MSSQL.InstanceName 					    Features              = $MSSQL.Features 					    ProductKey            = $ProductKey 					    SQLCollation          = $MSSQL.Collation 					    SQLSysAdminAccounts   = @('testdomain-EU\SQLAdmins',' testdomain-EU\SRV_Backup_001') 					    InstallSharedDir      = "C:\Program Files\Microsoft SQL Server" 					    InstallSharedWOWDir   = "C:\Program Files (x86)\Microsoft SQL Server"					 					    InstallSQLDataDir     = $MSSQL.DataRoot 					    SQLUserDBDir          = $MSSQL.UserDBDir 					    SQLUserDBLogDir       = $MSSQL.UserLogDir 					    SQLTempDBDir          = $MSSQL.TempDBDir 					    SQLTempDBLogDir       = $MSSQL.TempDBLogDir 					    SQLBackupDir          = $MSSQL.BackupDir 					    SourcePath            = $SQLSource 					    SAPwd                 = $SA 					    SecurityMode          = 'SQL' 					    UpdateSource          = ".\Updates" 					    Action                = "Install" 					    ForceReboot           = $True                         		    SQLSvcAccount         = $SqlServiceCredential                                      AgtSvcAccount         = $SqlServiceCredential                                      ISSvcAccount          = $SqlServiceCredential 					    BrowserSvcStartupType = "Automatic" 					    DependsOn             = '[WindowsFeature]NetFramework35', '[WindowsFeature]NetFramework45' }  

$MSSQL.InstanceName – всё это указано в нашем Json файле. Применение данной конфигурации выполнит установку и перезагрузку MS SQL Server cо всеми обновлениями в папке Updates.

Машина готова.

Мы создали удобный инструмент с графическим инструментом подобный Azure Portal, который позволяет управлять on-premises инфраструктурой максимально удобно нам и нашему заказчику.


ссылка на оригинал статьи https://habr.com/post/425129/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *