Сборка образа на базе Windows Server для Яндекс облака

от автора

Yandex Cloud Marketplace упрощает запуск виртуальных машин из готовых образов: можно выбрать операционную систему и решение с уже установленным набором компонентов. Но в корпоративной инфраструктуре нередко требуется Windows Server с определённой редакцией, политиками безопасности, обновлениями и прикладным ПО.

В таком случае удобнее подготовить собственный образ и использовать его как стандарт для новых ВМ. Ниже — пошаговый процесс сборки и загрузки образа Windows Server в Yandex Cloud.

Сборку необходимо осуществлять на базе Windows Server (2016,2019,2022) английской версии. Cloud init не понимает названия групп на русском.

  1. Развернуть и Windows Server внутри VirtualBox нужно версии (не Evaluation). Прописать KMS ключ для нужной редакции и указать KMS сервер для активации.

  2. Установите последние обновления ОС через Центр обновлений Windows.

  3. Открыть cmd от имени администратора и последовательно выполнить команды:

Установите менее строгую политику выполнения PS скриптов:

Set‑ExecutionPolicy ‑ExecutionPolicy Bypass ‑Force ‑Confirm:$false

Активируйте serial console загрузчика ОС:

bcdedit /ems “{current}” on bcdedit /emssettings EMSPORT:2 EMSBAUDRATE:115200

Отключите функции энергосбережения:

powercfg ‑setactive “8c5e7fda‑e8bf-4a96-9a85-a6e23a8c635c” powercfg ‑change ‑monitor‑timeout‑ac 0 powercfg ‑change ‑standby‑timeout‑ac 0 powercfg ‑change ‑hibernate‑timeout‑ac 0

Для виртуализованных аппаратных часов установите формат времени UTC:

Set‑ItemProperty ‑Path “HKLM:\SYSTEM\CurrentControlSet\Control\TimeZoneInformation” ‑Name “RealTimeIsUniversal” ‑Value 1 ‑Type DWord ‑Force

Отключите автоматическое назначение локальных IPv4-адресов для сетевых интерфейсов, которым не удалось назначить IP‑адрес (APIPA):

Set‑ItemProperty ‑Path “HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters” ‑Name “IPAutoconfigurationEnabled” ‑Value 0 ‑Type DWord ‑Force

Разрешите выключение ОС без активных пользовательских сессий:

Set‑ItemProperty ‑Path “HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System” ‑Name “ShutdownWithoutLogon” ‑Value 1

Установите минимальное время показа сообщения о выключении ОС при наличии активных пользовательских процессов:

Set‑ItemProperty ‑Path “HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows” ‑Name “ShutdownWarningDialogTimeout” ‑Value 1

Отключите автоматическую оптимизацию дисков (дефрагментация/TRIM):

Get‑ScheduledTask ‑TaskName “ScheduledDefrag” | Disable‑ScheduledTask

Разрешите трафик ICMPv4 (если Windows Firewall не отключен):

Get‑NetFirewallRule ‑Name “vm‑monitoring‑icmpv4” | Enable‑NetFirewallRule

(Опционально) Разрешите подключение по RDP с обязательной аутентификацией пользователей при подключении:

Get‑CimInstance ‑ClassName Win32_TSGeneralSetting ‑Namespace root\cimv2\terminalservices | Invoke‑CimMethod ‑MethodName SetUserAuthenticationRequired ‑Arguments @{ UserAuthenticationRequired = 1 } Set‑ItemProperty ‑Path “HKLM:\System\CurrentControlSet\Control\Terminal Server” ‑Name “fDenyTSConnections” ‑Value 0 Enable‑NetFirewallRule ‑DisplayGroup “Remote Desktop”

3. Скачайте и установите последнюю версию драйверов virtio‑win‑guest‑tools.exe.

4. Скачайте и установите последнюю версию Cloudbase‑init агента. После установки проверьте, что галочка sysprep НЕ установлена.

После установки в папку C:\Program Files\Cloudbase Solutions\Cloudbase‑Init\conf нужно положить конфигурационные файлы.

Файл cloudbase‑init.conf
[DEFAULT]username=Admingroups=Administratorsinject_user_password=trueconfig_drive_raw_hhd=trueconfig_drive_cdrom=trueconfig_drive_vfat=truebsdtar_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exemtools_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\verbose=truedebug=truelogdir=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\logfile=cloudbase-init-unattend.logdefault_log_levels=comtypes=INFO,suds=INFO,iso8601=WARN,requests=WARNlogging_serial_port_settings=COM1,115200,N,8mtu_use_dhcp_config=truentp_use_dhcp_config=truelocal_scripts_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts\metadata_services=cloudbaseinit.metadata.services.ec2service.EC2Serviceplugins=cloudbaseinit.plugins.common.mtu.MTUPlugin,cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin,cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin,cloudbaseinit.plugins.common.userdata.UserDataPluginallow_reboot=falsestop_service_on_exit=falsecheck_latest_version=false
Файл cloudbase‑init‑unattend.conf
[DEFAULT]username=Admingroups=Administratorsinject_user_password=trueconfig_drive_raw_hhd=trueconfig_drive_cdrom=trueconfig_drive_vfat=truebsdtar_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exemtools_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\verbose=truedebug=truelogdir=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\logfile=cloudbase-init-unattend.logdefault_log_levels=comtypes=INFO,suds=INFO,iso8601=WARN,requests=WARNlogging_serial_port_settings=COM1,115200,N,8mtu_use_dhcp_config=truentp_use_dhcp_config=truelocal_scripts_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts\metadata_services=cloudbaseinit.metadata.services.ec2service.EC2Serviceplugins=cloudbaseinit.plugins.common.mtu.MTUPlugin,cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin,cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin,cloudbaseinit.plugins.common.userdata.UserDataPluginallow_reboot=falsestop_service_on_exit=falsecheck_latest_version=false
Unattend.xml
<?xml version="1.0" encoding="utf-8"?><unattend xmlns="urn:schemas-microsoft-com:unattend">    <settings pass="generalize">        <component name="Microsoft-Windows-PnpSysprep" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">            <PersistAllDeviceInstalls>true</PersistAllDeviceInstalls>        </component>    </settings>    <settings pass="oobeSystem">        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">            <OOBE>                <HideEULAPage>true</HideEULAPage>                <HideLocalAccountScreen>true</HideLocalAccountScreen>                <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>                <HideOnlineAccountScreens>true</HideOnlineAccountScreens>                <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>                <NetworkLocation>Work</NetworkLocation>                <ProtectYourPC>1</ProtectYourPC>                <SkipMachineOOBE>true</SkipMachineOOBE>                <SkipUserOOBE>true</SkipUserOOBE>            </OOBE>            <TimeZone>UTC</TimeZone>        </component>    </settings>    <settings pass="specialize">        <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">            <RunSynchronous>                <RunSynchronousCommand wcm:action="add">                    <Order>1</Order>                    <Path>cmd.exe /c ""C:\Program Files\Cloudbase Solutions\Cloudbase-Init\Python\Scripts\cloudbase-init.exe" --config-file "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\conf\cloudbase-init-unattend.conf" &amp;&amp; exit 1 || exit 2"</Path>                    <Description>Run Cloudbase-Init to set the hostname</Description>                </RunSynchronousCommand>                            </RunSynchronous>        </component>    </settings></unattend>

5. Установите агент сброса пароля.

Получите номер последней версии программы:

$YCAgentUpdaterBaseUri = “https://storage.yandexcloud.net/yandexcloud‑guestagent‑updater” $YCAgentUpdaterVersion = (Invoke‑RestMethod “$YCAgentUpdaterBaseUri/release/stable”).Trim()

Скачайте программу и проверьте ее контрольную сумму:

$YCAgentUpdaterDir = “C:\Program Files\Yandex.Cloud\Guest Agent Updater” New‑Item ‑Path $YCAgentUpdaterDir ‑ItemType “directory” $p = @{   Uri = “$YCAgentUpdaterBaseUri/release/$YCAgentUpdaterVersion/windows/amd64/guest‑agent‑updater.exe”   OutFile = “$YCAgentUpdaterDir\guest‑agent‑updater.exe” } Invoke‑RestMethod @p $YCAgentUpdaterHashOrig = (Invoke‑RestMethod “$YCAgentUpdaterBaseUri/release/$YCAgentUpdaterVersion/windows/amd64/guest‑agent‑updater.exe.sha256”).Trim() $YCAgentUpdaterHashCopy = (Get‑Filehash ‑Path “$YCAgentUpdaterDir\guest‑agent‑updater.exe” ‑Algorithm SHA256 | Select‑Object ‑ExpandProperty Hash).ToLower() if ($YCAgentUpdaterHashOrig ‑eq $YCAgentUpdaterHashCopy) {   Write‑Host “Agent updater checksum verified” } else {   Write‑Host “Agent updater checksum NOT verified” }

Результат:

Agent updater checksum verified

Установите агент:

& $YCAgentUpdaterDir\guest‑agent‑updater.exe update

Проверьте, что агент установлен в качестве службы, а служба запущена:

Get‑Service “yc‑guest‑agent”

Результат:

Status  Name           DisplayName ‑‑‑     ‑‑‑              ‑‑‑‑‑‑‑ Running yc‑guest‑agent yc‑guest‑agent

Служба должна иметь статус Running.

Если служба не запущена, запустите ее:

Start‑Service “yc‑guest‑agent”

Чтобы проверить, что служба запустилась, повторите шаг 4.

Настройте задание на обновление агента каждую неделю в случайное время:

$YCAgentUpdaterLogFilepath = “C:\Windows\Temp\guest‑agent‑updater.log” $p = @{   Execute = 'C:\Windows\System32\cmd.exe'   Argument = “/c "$YCAgentUpdaterDir\guest-agent-updater.exe" update ‑log‑level debug > $YCAgentUpdaterLogFilepath" } $YCAgentUpdaterAction = New‑ScheduledTaskAction @p $RandomWeekdayNumber = Get‑Random ‑Minimum 0 ‑Maximum 6 $DaysOfWeek = @(“Sunday”, “Monday”, “Tuesday”, “Wednesday”, “Thursday”, “Friday”, “Saturday”) $RandomWeekday = $DaysOfWeek[$RandomWeekdayNumber] $RandomHour = Get‑Random ‑Minimum 0 ‑Maximum 23 $RandomMinute = Get‑Random ‑Minimum 0 ‑Maximum 59 $RandomSecond = Get‑Random ‑Minimum 0 ‑Maximum 59 $p = @{   Weekly = $true   At = ([datetime]::Today).AddHours($RandomHour).AddMinutes($RandomMinute).AddSeconds($RandomSecond)   RandomDelay = New‑TimeSpan ‑Hours 24 # with huge random delay   DaysOfWeek = $RandomWeekday }$YCAgentUpdaterTrigger = New‑ScheduledTaskTrigger @p $YCAgentUpdaterTaskName = “yc‑guest‑agent‑updater” $p = @{   TaskName = $YCAgentUpdaterTaskName   Action = $YCAgentUpdaterAction User = 'System'   RunLevel = 'Highest' Trigger = $YCAgentUpdaterTrigger } Register‑ScheduledTask @p | Out‑Null

Запустите задание:

Get‑ScheduledTask ‑TaskName $YCAgentUpdaterTaskName | Start‑ScheduledTask$Timeout = 30 $Deadline = ([datetime]::Now).AddSeconds($timeout) while ((Get‑ScheduledTask $YCAgentUpdaterTaskName).State ‑ne “Ready”) {  Start‑Sleep ‑Seconds 1 if ([datetime]::Now ‑gt $Deadline) {  Write‑Host “Deadline exceeded”   break  } }

6. Создайте папку C:\Windows\Setup\Scripts\ и положите в неё следующие скрипты:

SetupComplete.cmd
Powershell.exe -ExecutionPolicy Bypass -File "C:\Windows\Setup\Scripts\Setupcomplete.ps1"
SetupComplete‑final.ps1
<#    .FUNCTIONS        Functions, used in script below.#>function Get-WinrmCertificateThumbPrint {    $Listner = Get-WinrmHTTPSListener    return Get-ChildItem "WSMan:\localhost\Listener\$Listner\CertificateThumbPrint" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Value}function Get-WinrmHTTPSListener {    return Get-ChildItem WSMan:\localhost\Listener\ | Where-Object Keys -contains "Transport=HTTPS" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name}function New-WinrmCertificate {    $DnsName = [System.Net.Dns]::GetHostByName($env:computerName).Hostname    $WindowsVersion = Get-WindowsVersion    if ($WindowsVersion -match "Windows Server 2012 R2*") {        # yup, black sheep        return New-SelfSignedCertificate -CertStoreLocation "Cert:\LocalMachine\My" -DnsName $DnsName    }    return New-SelfSignedCertificate -CertStoreLocation "Cert:\LocalMachine\My" -DnsName $DnsName -Subject $ENV:COMPUTERNAME}function Get-WindowsVersion {    return Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows NT\CurrentVersion" -Name ProductName | Select-Object -ExpandProperty ProductName}<#    .SCRIPT        Actual script#># Remove old winrm certificate, if any$CertificateThumbPrint = Get-WinrmCertificateThumbPrintif ($CertificateThumbPrint) {    Remove-Item -Path "Cert:\LocalMachine\My\$CertificateThumbPrint" -Force | Out-Null}# Remove old winrm listeners, if anyRemove-Item -Path WSMan:\Localhost\listener\listener* -Recurse| Out-Null# Create Listeners$Certificate = New-WinrmCertificateNew-Item -Path WSMan:\LocalHost\Listener -Transport HTTPS -Address * -CertificateThumbPrint $Certificate.ThumbPrint -HostName $ENV:COMPUTERNAME -Force | Out-NullNew-Item -Path WSMan:\LocalHost\Listener -Transport HTTP -Address * -HostName $ENV:COMPUTERNAME -Force | Out-Null# Create allow firewall rules$WINRMHTTPS = Get-NetFirewallRule -Name "WINRM-HTTPS-In-TCP" -ErrorAction SilentlyContinueif ($WINRMHTTPS) {    $WINRMHTTPS | Enable-NetFirewallRule  }else {    $NetFirewallRuleParams = @{        Group = "Windows Remote Management"        DisplayName = "Windows Remote Management (HTTPS-In)"        Name = "WINRM-HTTPS-In-TCP"        LocalPort = 5986        Action = "Allow"        Protocol = "TCP"        Program = "System"    }    New-NetFirewallRule @NetFirewallRuleParams}$WINRMHTTPS = Get-NetFirewallRule -Name "WINRM-HTTP-In-TCP" -ErrorAction SilentlyContinueif ($WINRMHTTPS) {    $WINRMHTTPS | Enable-NetFirewallRule  }else {    $NetFirewallRuleParams = @{        Group = "Windows Remote Management"        DisplayName = "Windows Remote Management (HTTP-In)"        Name = "WINRM-HTTP-In-TCP"        LocalPort = 5985        Action = "Allow"        Protocol = "TCP"        Program = "System"    }    New-NetFirewallRule @NetFirewallRuleParams}Remove-Item "C:\Windows\Setup\Scripts\SetupComplete*" -Force
Sysprep.ps1
# Global vars$WorkDirectory = "C:\sysprep-temp"# Await if update process TiWorker running and finalizing updates after system reboot$UpdateFinishing = Get-WmiObject Win32_Process | where {$_.Name -eq "TiWorker.exe"}while($UpdateFinishing) {    Start-Sleep 5    $UpdateFinishing = Get-WmiObject Win32_Process | where {$_.Name -eq "TiWorker.exe"}}# Cleaning up downloaded patchesRemove-Item "C:\Windows\SoftwareDistribution\Download\*" -Recurse -Force# Start sysprep& $env:SystemRoot\System32\Sysprep\Sysprep.exe /oobe /generalize /quiet /unattend:"$WorkDirectory\unattend.xml" /shutdownMove-Item "C:\Windows\Setup\Scripts\SetupComplete-final.ps1" "C:\Windows\Setup\Scripts\SetupComplete.ps1" -Force -Confirm:$false#Wait for correct system statedo {    Start-Sleep -s 5    $SetupState = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\State"    $ImageState = $SetupState | Select-Object -ExpandProperty ImageState        $ImageState | Out-Default} while ($ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE')Remove-Item $WorkDirectory -Recurse -ForceGet-ScheduledTask sysprep | Unregister-ScheduledTask -Confirm:$falseRemove-Item "C:\Windows\Setup\Scripts\Sysprep.ps1" -ForceStart-Sleep -s 10# Wait for sysprep tagwhile (-not (Test-Path 'C:\Windows\System32\Sysprep\Sysprep_succeeded.tag') ) {    'Sysprep succeeded tag not yet exist...'     Start-Sleep -s 1}Start-Sleep -s 10Stop-Computer -Force& shutdown -s -f -t 0

7. Создайте папку C:\sysprep‑temp и файл Unattend.xml со следующим содержимым:

Unattend.xml
<?xml version="1.0" encoding="utf-8"?><unattend xmlns="urn:schemas-microsoft-com:unattend">    <settings pass="generalize">        <component name="Microsoft-Windows-PnpSysprep" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">            <PersistAllDeviceInstalls>true</PersistAllDeviceInstalls>        </component>    </settings>    <settings pass="oobeSystem">        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">            <OOBE>                <HideEULAPage>true</HideEULAPage>                <HideLocalAccountScreen>true</HideLocalAccountScreen>                <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>                <HideOnlineAccountScreens>true</HideOnlineAccountScreens>                <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>                <NetworkLocation>Work</NetworkLocation>                <ProtectYourPC>1</ProtectYourPC>                <SkipMachineOOBE>true</SkipMachineOOBE>                <SkipUserOOBE>true</SkipUserOOBE>            </OOBE>            <TimeZone>UTC</TimeZone>        </component>    </settings>    <settings pass="specialize">        <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">            <RunSynchronous>                <RunSynchronousCommand wcm:action="add">                    <Order>1</Order>                    <Path>cmd.exe /c ""C:\Program Files\Cloudbase Solutions\Cloudbase-Init\Python\Scripts\cloudbase-init.exe" --config-file "C:\Program Files\Cloudbase Solutions\Cloudbase-Init\conf\cloudbase-init-unattend.conf" &amp;&amp; exit 1 || exit 2"</Path>                    <Description>Run Cloudbase-Init to set the hostname</Description>                </RunSynchronousCommand>                            </RunSynchronous>        </component>    </settings></unattend>

8. Выполняем финальный этап — переходим в папку C:\Windows\Setup\Scripts\ и запускаем Sysprep скрипт. После его выполнения система выключится. Больше её НЕ включаем!

cd C:\Windows\Setup\Scripts\.\Sysprep.ps1

9. В VirtualBox нажимаем в верхнем меню Файл — Экспорт конфигураций. В качестве формата выберите “Open Virtualization Format 0.9” (OVF) или “Open Virtualization Format 1.0” (OVF). На самом деле VirtualBox упакует VMDK/VHD внутрь OVF, что Yandex.Cloud понимает. Яндекс облако в качестве образов поддерживает Qcow2, VMDK, RAW и VHD.

10. Загружаем получившийся образ в бакет Object Storage через WinSCP или cli. После загрузки получаем прямую ссылку на образ.

11. Создаем образ ОС Windows Server в Compute Cloud. Открываем cli в нужном folder и выполняем команду:

yc compute image create \ ‑name <имя_образа> \ ‑description <описание_образа> \ ‑os‑type windows \ ‑source‑uri “<ссылка_на_образ_в_Object_Storage>”

ссылка на оригинал статьи https://habr.com/ru/articles/1063550/