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=Admin
groups=Administrators
inject_user_password=true
config_drive_raw_hhd=true
config_drive_cdrom=true
config_drive_vfat=true
bsdtar_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exe
mtools_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\
verbose=true
debug=true
logdir=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\
logfile=cloudbase-init-unattend.log
default_log_levels=comtypes=INFO,suds=INFO,iso8601=WARN,requests=WARN
logging_serial_port_settings=COM1,115200,N,8
mtu_use_dhcp_config=true
ntp_use_dhcp_config=true
local_scripts_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts\
metadata_services=cloudbaseinit.metadata.services.ec2service.EC2Service
plugins=cloudbaseinit.plugins.common.mtu.MTUPlugin,cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin,cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin,cloudbaseinit.plugins.common.userdata.UserDataPlugin
allow_reboot=false
stop_service_on_exit=false
check_latest_version=false
Файл cloudbase‑init‑unattend.conf
[DEFAULT]
username=Admin
groups=Administrators
inject_user_password=true
config_drive_raw_hhd=true
config_drive_cdrom=true
config_drive_vfat=true
bsdtar_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\bsdtar.exe
mtools_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\bin\
verbose=true
debug=true
logdir=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\log\
logfile=cloudbase-init-unattend.log
default_log_levels=comtypes=INFO,suds=INFO,iso8601=WARN,requests=WARN
logging_serial_port_settings=COM1,115200,N,8
mtu_use_dhcp_config=true
ntp_use_dhcp_config=true
local_scripts_path=C:\Program Files\Cloudbase Solutions\Cloudbase-Init\LocalScripts\
metadata_services=cloudbaseinit.metadata.services.ec2service.EC2Service
plugins=cloudbaseinit.plugins.common.mtu.MTUPlugin,cloudbaseinit.plugins.common.sethostname.SetHostNamePlugin,cloudbaseinit.plugins.windows.extendvolumes.ExtendVolumesPlugin,cloudbaseinit.plugins.common.userdata.UserDataPlugin
allow_reboot=false
stop_service_on_exit=false
check_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-WinrmCertificateThumbPrint

if ($CertificateThumbPrint) {
    Remove-Item -Path "Cert:\LocalMachine\My\$CertificateThumbPrint" -Force | Out-Null
}

# Remove old winrm listeners, if any
Remove-Item -Path WSMan:\Localhost\listener\listener* -Recurse| Out-Null

# Create Listeners
$Certificate = New-WinrmCertificate

New-Item -Path WSMan:\LocalHost\Listener -Transport HTTPS -Address * -CertificateThumbPrint $Certificate.ThumbPrint -HostName $ENV:COMPUTERNAME -Force | Out-Null
New-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 SilentlyContinue

if ($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 SilentlyContinue

if ($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 patches
Remove-Item "C:\Windows\SoftwareDistribution\Download\*" -Recurse -Force

# Start sysprep
& $env:SystemRoot\System32\Sysprep\Sysprep.exe /oobe /generalize /quiet /unattend:"$WorkDirectory\unattend.xml" /shutdown

Move-Item "C:\Windows\Setup\Scripts\SetupComplete-final.ps1" "C:\Windows\Setup\Scripts\SetupComplete.ps1" -Force -Confirm:$false

#Wait for correct system state
do {
    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 -Force
Get-ScheduledTask sysprep | Unregister-ScheduledTask -Confirm:$false
Remove-Item "C:\Windows\Setup\Scripts\Sysprep.ps1" -Force
Start-Sleep -s 10

# Wait for sysprep tag
while (-not (Test-Path 'C:\Windows\System32\Sysprep\Sysprep_succeeded.tag') ) {
    'Sysprep succeeded tag not yet exist...' 

    Start-Sleep -s 1
}

Start-Sleep -s 10

Stop-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>”