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

  • Tutorial
Создание новой виртуальной машины — это рутина, отнимающая много времени. И чем больше инфраструктура и организация, тем больше процедур, связанных с этим процессом. Мы автоматизировали это процесс с помощью 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 "vSphereSrv" -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, необходимо указать Thumbprint сертификата и в дальнейшем 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-ключей. Сгенерируем самоподписанный сертификат и временно будем работать с ним.

Теперь мы можем зарегистрироваться на 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 определен:
$MSSQL=$Configuration.Applications | where {$_.Application -eq "Microsoft SQL Server 2012"}


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

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

Service-Now


В Service-Now доступно несколько API. Мы используем Rest API.
Для получение списка машин со статусом Allocated используется запрос вида:
instance.service-now.com/cmdb_ci_server_list.do?sysparm_query=install_status=16^u_subtype=^ORDERBYname
В PowerShell это выглядит так:
$url="https://instance.service-now.com/api/now/table/cmdb_ci_server?sysparm_query=install_status=16^u_subtype=^ORDERBYname"
$uri= new-object System.Uri("https://instance.service-now.com/")
#берем учетные записи нашей сервисной учетной записи
$credentials = (Get-StoredCredential -Type Generic -Target DSC).GetNetworkCredential()
$credentials = new-object System.Net.NetworkCredential $credentials.UserName, $credentials.SecurePassword
Add-Type -AssemblyName System.Net.Http
$handler = New-Object  System.Net.Http.HttpClientHandler
$handler.CookieContainer = New-Object System.Net.CookieContainer
$handler.UseCookies=$true
$handler.Credentials=$credentials


$HttpClient = New-Object System.Net.Http.HttpClient($handler)
$HttpClient.BaseAddress= $uri

$Header = New-Object System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")
$HttpClient.DefaultRequestHeaders.Accept.Clear()
$HttpClient.DefaultRequestHeaders.Accept.Add($Header);

$response=$HttpClient.GetAsync($url)
$respStream=$response.Result.Content.ReadAsStringAsync()

$Servers = $respStream.Result | ConvertFrom-Json
#получаем объекты нашего Configuration Items каталога
$ServersCI=$Servers.result

Первый объект массива и есть нужный нам hostname.
Если машина готова, то можно изменить статус машины в Service-Now, для этого скрипт UpdateCI.ps1:
param(
    $CI,
    [ValidateSet("Allocated","In use","Pending install")]
    $NewStatus='In use'     
)
$url="https://instance.service-now.com/api/now/table/cmdb_ci_server?sysparm_query=name=$CI"
$uri= new-object System.Uri("https://instance.service-now.com/")
$credentials = (Get-StoredCredential -Type Generic -Target DSC).GetNetworkCredential()
$credentials = new-object System.Net.NetworkCredential $credentials.UserName, $credentials.SecurePassword
Add-Type -AssemblyName System.Net.Http
$handler = New-Object  System.Net.Http.HttpClientHandler
$handler.CookieContainer = New-Object System.Net.CookieContainer
$handler.UseCookies=$true
$handler.Credentials=$credentials


$HttpClient = New-Object System.Net.Http.HttpClient($handler)
$HttpClient.BaseAddress= $uri

$Header = New-Object System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")
$HttpClient.DefaultRequestHeaders.Accept.Clear()
$HttpClient.DefaultRequestHeaders.Accept.Add($Header);

$response=$HttpClient.GetAsync($url)
$respStream=$response.Result.Content.ReadAsStringAsync()




$Servers = $respStream.Result | ConvertFrom-Json

$ServerCI=$Servers.result[0]

$update=@{}
if($NewStatus -eq "In use"){
    $update.install_status=1
}
if($NewStatus -eq "Pending install"){
    $update.install_status=4
}

$stringcontent =  New-Object System.Net.Http.StringContent((ConvertTo-Json -InputObject $update -Depth 100),[System.Text.Encoding]::UTF8, "application/json");
$result=$HttpClient.PutAsync("https://instance.service-now.com/api/now/table/cmdb_ci_server/$($ServerCI.sys_id)", $stringcontent)

Для получения таблицы и записей используются REST API GET запросы, для изменения записи PUT/POST запросы, в теле которого поля которые необходимо изменить.

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

ICL Services

105,00

Управляемые ИТ-сервисы мирового уровня

Поделиться публикацией
Комментарии 30
    0
    с графическим инструментом
    Часть про ServiceNow пропущена, у вас там используется встроенный Service Catalog, или что-то кастомное? Заказчик принципиально требует работать только через ServiceNow? Иначе часто удобнее работать через какой-нибудь портал самообслуживания именно для server/service provisioning'га.
    А может вы репозиторий публичный откроете со своими наработками?

    PS: хостнейм машины в инфраструктуре заказчика из текста уберите.
      0
      Часть про Service-Now добавлена.
      Да, испольуется встроенный Service-Catalog. Дело в том, что в проекте уже много реализованных автоматизаций интегрированных с Service-Now, поэтому и данная автоматизация также реализована в интеграции с Service-Now.
        0
        А репозиторий откроете? В целях популяризации компании, улучшения HR-бренда.
          0
          Данное решение-собственность Заказчика. Я опубликовал общий подход:
          1. транслятор JSON-DSC Configuration
          2. JSON->Stage VM->Connect to DSC->DSC Management
      0
      И отключаемся:

      Disconnect-VIServer * -Force -Confirm:$false

      Странно, а для чего? После ведь создание виртуальной машины продолжаем через PowerCLI.

      Адрес можно взять проще:
      $vm_ip_address = (Get-VMGuest -VM $Name).ExtensionData.IpAddress


      Интересная статья. Спасибо!
        0
        В процессе отладки выяснилось, что не закрытые сессии ведут иногда к не предсказуемым результатам. Понятно, что .net c GC сам все закроет, но лучше сделать это самому.
        0

        А сами виртуальные машины на чем разворачиваются? В тексте упоминалась VMware, на ней? Или что-то ещё: Hyper-V или типа того?


        Ещё было бы полезно узнать немного о бизнес-задачах Заказчика, это во многом объяснило бы, почему вы реализовали схему именно так. Уверен. можно это сделать не засвечивая конф. информации)

          0
          Виртуальные машины разворачиваются в vSphere кластерах через PowerCLI. Все это делает BuildVM.ps1 скрипт и нет никаких проблем сделать ответвление на разворачивание машин в Hyper-V.

          Этот инструмент особенно удобен Заказчику, когда нужно создать ряд симметричных машины, например: для фермы терминальных серверов с SAP, Navision, etc., кластера принт-серверов.
          Этот инструмент удобен для всякого рода Disaster ситуаций-мы сможем сбилдить новую машину в короткие сроки, тот-же принт-сервер воссоздастся со всеми принтерами и драйверами. Доходило до смешного, что нам было быстрее сбилдить новую машину, чем пытаться доконфигурить старую.
          Этот инструмент удобен для создания SCCM/SCOM кластеров с SQL Server.
          IPAM позволяет рулить всеми IP все сети заказчика и даже DMZ машины имеют свою А-запись, что удобно.
          C DSC SQL Server настраивается ровно так, как нужно по Дизайну, со всеми скедул тасками, выключенным sa и прочее. DSC вообще много чего позволяет делать. А json лишь удобный формат хранения таких конфигурация-более читаемый.
          0
          Не очень понял, почему размеры диска и памяти проверяются как просто числа (даже не числа, а строки! )
          IF ($Datastore1.FreeSpaceGB -le "200"){
          Write-Host -foreground red "STOP: Not enough datastore capacity for DISK" $vdisk.Id
          Break
          }
          IF ($VMHost.MemoryUsageGB -le "20"){
          Write-Host -foreground red "STOP: No enough ESXi host capacity"
                  Break
          }
          
          а не как $Configuration.VM.RAM и т.д.

          а вот это, наверное, вы так с кодировкой боретесь или зачем?
          $Configuration=(Get-Content -Raw $File | Out-String | ConvertFrom-Json)
          


            0
            PowerShell в этом плане «умен», в данном случае «200» это Runtime type.
            Данно выражение работает:
            PS C:\Program Files\WindowsPowerShell\Scripts> $Datastore.FreeSpaceGB
            1265.4
            
            PS C:\Program Files\WindowsPowerShell\Scripts> $Datastore.FreeSpaceGB -le 1600
            True
            
            PS C:\Program Files\WindowsPowerShell\Scripts> $Datastore.FreeSpaceGB -le 1200
            False
            
            PS C:\Program Files\WindowsPowerShell\Scripts> $Datastore.FreeSpaceGB -le "1600"
            True
            
            PS C:\Program Files\WindowsPowerShell\Scripts> $Datastore.FreeSpaceGB -le "1200"
            False


            $VMHost.MemoryUsageGB -это память на ESXi хосте. У нас есть условие, что у ESXi хоста должно быть не менее 20Gb памяти до размещения машины. Понятно, что машина с 32Gb Ram туда «не влезет», но машина с 32Gb это также довольно редкое явление и требующее повышенного внимания.
            Ваше замечание верно. Я обновлю код.

            По пайплайну содержимое файла сразу попадает к ConvertFrom-Json и получаем PowerShell объект $Configuration. Возможно как-то можно это и улучшить, но данная строка отлично работает и пока никаких проблем.
              0
              Про то что Powershell умный — это понятно :)

              А про пайплайн я имел ввиду, почему не просто
              $Configuration = Get-Content $File | ConvertFrom-Json
              Так исторически сложилось, как я понимаю.

              Ну и максимальные/минимальные значения из массива объектов получать через «Measure-Object -Maximum -Minimum» раза в три быстрее и, главное, нагляднее.

              Особенно он пригодится, если размеры необходимого дискового пространства тоже считать, а не «200» брать :)
                0
                Да, так исторически сложилось. А все из-за отладки когда (Get-Content ukhqsv50876.json | Out-String ) использовалось для вывода в консоль содержимого json файла.
                $Configuration = Get-Content $File | ConvertFrom-Json

                так тоже работает.
                Measure-Object — это не самый популярный командлет, поэтому его и не использовали. Почти во всех статьях -le -eq etc и в большинстве случаев этого достаточно и многие сходу поймут что с чем сравнивается. Попробуем использовать Ваше предложение
            0
            Забыл ещё вот про это —
            $VMTemplate = Get-Template -Name 'Win2016_Std_x64_Template' 
            У вас же машинки по идее 2012 тоже бывают…

            а за идею установки PS через "DISM /online" — большое спрасибо. а то через schatasks/psexec вечно раздражало.
              0
              Тогда в коде ответвление по условию на
              $VMTemplate = Get-Template -Name 'Win2012r2_Std_x64_Template' 
              0
              Отличная статья.

              Я так понимаю, что это всё возможно только для vSphere, а в «десктопных» вариациях VmWare и VirtualBox подобного не сделать, верно?
                0

                Ну почему же, все до чего может дотянуться PowerShell- применимо. По большому счету, все что имеет CLI-применимо.

                0
                Disk 0 (C:\) имеет фиксированный размер
                Какой же? А, нашёл.
                A почему объём то в кавычках, то без?
                В образе сохранен локальный администратор с известным нам паролем
                У меня для вас плохая новость…

                P.S. Клиентам этих товарищей: ну вы поняли, да?
                  0

                  Нашлась "ошибка", для других дисков определен int тип данных. Я исправлю.


                  Локальными админами в доменной среде управляет LAPS, а в dmz-dsc выставляет пароль. Вклинится в процесс staging весьма сложно.

                    0
                    Про «вклиниться» никто и не говорил, но если образ можно получить «на руки», то узнать пароль — дело техники. Другими словами, пароль известен не только вам
                      0

                      VMware имеет свою защиту, образ не так-то и просто получить.
                      Потом, что даст этот пароль если на развернутых машинах этот пароль другой?

                        0
                        Если этот пароль всегда неактуален, какой смысл в его знании?
                        Помимо доступа к самому прообразу, прямой или опосредованный доступ возможен к файлу, в котором хранится хэш на «живой» VM
                          0

                          Доступ к VMware темплейту? Если кто-то сможет скопировать его и запустить то у нас и так — большие проблемы.
                          Расскажите подробнее за какой хэш вы говорите.

                            0
                            Хэш пароля учётной записи локального администратора (знание которой, если верить статье, имеет какое-то сакраментальное значение, а если верить вашим комментариям — полностью бесполезно) можно достать из безопасного режима ОС, если не ошибаюсь
                              0

                              В ос даже в безопасном режиме надо "зайти".
                              В частности у нас была ситуация когда был один локальный админ который имел флаги password expired и user cannot change password и машина потеряла domain trust. Чтобы восстановить доступ пришлось грузиться с диска, цеплять реестр и сбрасывать пароль. Но для этого нужен доступ к VMware консоли или к irmc. Не выцепить этот хэш без доступа к консоли машины.

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

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое