Yandex Cloud Marketplace упрощает запуск виртуальных машин из готовых образов: можно выбрать операционную систему и решение с уже установленным набором компонентов. Но в корпоративной инфраструктуре нередко требуется Windows Server с определённой редакцией, политиками безопасности, обновлениями и прикладным ПО.
В таком случае удобнее подготовить собственный образ и использовать его как стандарт для новых ВМ. Ниже — пошаговый процесс сборки и загрузки образа Windows Server в Yandex Cloud.
Сборку необходимо осуществлять на базе Windows Server (2016,2019,2022) английской версии. Cloud init не понимает названия групп на русском.
-
Развернуть и Windows Server внутри VirtualBox нужно версии (не Evaluation). Прописать KMS ключ для нужной редакции и указать KMS сервер для активации.
-
Установите последние обновления ОС через Центр обновлений Windows.
-
Открыть 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" && 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" && 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/