Как стать автором
Обновить

Опыт применения Terraform для управления сервером Hyper-V через HTTPS WinRM

Уровень сложностиСредний
Время на прочтение32 мин
Количество просмотров1.3K
Гибрид зайца и воробья (по версии Kandinsky 3.1)
Гибрид зайца и воробья (по версии Kandinsky 3.1)

Какие инструменты приходят вам на ум, когда заходит разговор о создании виртуальных машин под управлением Microsoft Hyper-V? Уверен что ими окажутся - Hyper-V Manager и командлеты PowerShell, т.к. они поставляются в комплекте с ролью Hyper-V и суммарно дают достаточно мощный инструментарий для управления виртуальными машинами и виртуальными сетями.

Однако, в какой-то момент мне стало интересно, а можно ли подружить Hyper-V с таким инструментом как Terraform? Этот вопрос и стал отправной точкой, за которой последовало несколько дней поисков, размышлений, экспериментов и набивания шишек.

В настоящей статье я попробую рассказать о своём опыте работы с Hyper-V провайдером для Terraform, поделиться опытом настройки WinRM для HTTPS, затронуть не самые очевидные механизмы, которые могут быть задействованы при администрировании операционных систем семейства Windows и описать свои ошибки и неудачи. Статья не претендует на то, чтобы стать полноценным руководством к действию, но может дать новые идеи энтузиастам и сэкономить время новичкам.

Оглавление

1) Справка о продуктах и терминах

Terraform - программный продукт созданный компанией HashiCorp для декларативного управления инфраструктурой, согласно концепции "инфраструктура как код (IaC)". Для работы с различными инфраструктурами через их REST API используются различные Terraform-провайдеры. Полный список провайдеров для множества сервисов и инфраструктур можно найти в Terraform Registry.

При работе с Terraform вы пишете код на языке Hashicorp Configuration Language (HCL), которым описываете то, как должна выглядеть ваша инфраструктура. Например сколько и каких виртуальных машин вам требуется, к каким виртуальным сетям они должны быть подключены, образ с какой предустановленной операционной системой вы хотите использовать и какие учётные записи на этих виртуальных компьютерах должны быть созданы. Проверяете код и применяете конфигурацию. Terraform считывает конфигурацию. Подгружает указанный вам провайдер, предназначенный для работы с той инфраструктурой, где происходит развертывание. Подключается к заданной инфраструктуре и применяет описанные изменения. Например разворачивает необходимые виртуальные машины. Затем сохраняет информацию о состоянии развертывания в файле tfstate и завершает свою работу. В дальнейшем, если вы решите внести какие-то изменения в инфраструктуру, например поменять количество виртуальных машин, то вам будет необходимо внести изменения в код и заново его применить. При этом Terraform считает предыдущее состояние инфраструктуры из файла tfstate и применит только изменившуюся часть. Например увеличит количество виртуальных машин.

Примечание: С марта 2022 года HashiCorp закрыла доступ к своим продуктам пользователям из России. Для доступа к продуктам необходимо использовать либо VPN, либо местные зеркала. Я, в своих экспериментах, использую зеркало от компании Яндекс.

10 августа 2023 года HashiCorp объявила о переводе всех своих продуктов с лицензии Mozilla Public License v2.0 (MPL v2.0) на Business Source License v1.1 (BSL v1.1).

С 27 февраля 2025 года HashiCorp является частью компании IBM.

WinRM - служба в операционных системах Windows, предназначенная для удалённого мониторинга, администрирования и выполнения команд. Для соединения используются протоколы HTTP (5985/TCP) и HTTPS (5986/TCP). По-умолчанию служба выключена. Для использования требуется её включение и конфигурирование.

Служба WinRM поддерживает следующие механизмы аутентификации:

  • Basic - аутентификация открытым текстом при помощи HTTP-заголовков;

  • Digest - аутентификация при помощи HTTP-заголовков с использованием функций хэширования;

  • Kerberos - взаимная аутентификация клиента и сервера, являющихся членами общей доменной инфраструктуры;

  • Negotiate - стандартная NTLM-аутентификация по логину и паролю для клиента и сервера не являющихся участниками общей доменной инфраструктуры;

  • Certificate - для проверки легитимности сервера и клиента при использовании протокола HTTPS. Этот механизм аутентификации обеспечивает безопасность передаваемых данных посредством шифрования;

  • CredSSP - делегирование учетных данных серверу, который может их использовать для удалённой проверки подлинности и выполнения задач на удаленных компьютерах.

WinRM позволяет создать список доверенных узлов. При помощи включённой и настроенной службы WinRM администратор может выполнять на удалённом компьютере отдельные команды и целые скрипты. Может обращаться к WMI (CIM) и выполнять командлеты PowerShell.

2) Описание и подготовка стенда

В качестве стенда я буду использовать два стареньких, но еще бодрых компьютера HP ProDesk 600 G1 SFF следующей конфигурации: процессор - Intel Core i5 4590 3,30 GHz, оперативная память - DDR3 32 Gb, накопитель - Patriot P210 512Gb SSD. В BIOS включена поддержка аппаратной виртуализации.

Сервер:

На компьютер произведена чистая установка Windows Server 2022 Standard 21H2 RU. Имя компьютера HP-SRV1, IP-адрес: 192.168.1.15/24, шлюз по-умолчанию: 192.168.1.1, DNS-сервер: 192.168.1.1. Установлены необходимые драйвера и последние обновления. Для удобства на сервере включен "Удаленный рабочий стол". Созданы каталоги: D:\Certificates\, D:\Hyper-V\, D:\ISO\, C:\Temp\.

Примечание: Впоследствии я убедился что использовать сервер с русским интерфейсом - было ошибкой, т.к. при работе с Terraform регулярно возникали ошибки, которые было трудно прочитать из-за неправильной кодировки. Скриношоты я оставил на русском языке, но вам я настоятельно советую сразу использовать Windows Server с английским интерфейсом.

Производим установку роли Hyper-V . Виртуальный коммутатор не создаём, так как планируется его создать позже при помощи Terraform.

Отказываемся от создания виртуального коммутатора.
Отказываемся от создания виртуального коммутатора.

Миграции виртуальных машин также не включаем, т.к. для экспериментов используется только один сервер.

Не включаем миграцию виртуальных машин и используем протокол CredSSP.
Не включаем миграцию виртуальных машин и используем протокол CredSSP.

Указываем путь D:\Hyper-V в качестве расположения по-умолчанию для хранения виртуальных жестких дисков и конфигураций виртуальных машин. Завершаем установку роли и перезагружаем сервер.

Указываем путь D:\Hyper-V в качестве расположения по-умолчанию.
Указываем путь D:\Hyper-V в качестве расположения по-умолчанию.

Рабочая станция:

На рабочую станцию установлена Windows 10 Enterprise LTSC 21H2 RU. Имя компьютера HP-WST1, IP-адрес: 192.168.1.18/24, шлюз по-умолчанию: 192.168.1.1, DNS-сервер: 192.168.1.1. Установлены необходимые драйвера и последние обновления. Для удобства на рабочей станции включен "Удаленный рабочий стол". Созданы каталоги: D:\Certificates\, D:\Terraform\, C:\Temp\.

В целом, операционная система установленная на рабочей станции не так важна - Terraform является кросплатформенным приложением и может быть установлен в операционных системах Windows, Linux, MacOS, FreeBSD, OpenBSD и Solaris.

3) Установка Terraform и добавление Hyper-V провайдера

Для начала установим Terraform на рабочую станцию HP-WST1. Для этого скачиваем последний релиз под Windows платформы AMD64 (на момент написания статьи 1.11.4) либо с официального сайта (из России через VPN), либо с зеркала Яндекс. Распаковываем zip-архив, в подходящую папку. Я распакую в "C:\Program Files\DevOps Tools" и, для удобства, добавлю указанную папку в системный path (Свойства компьютера -> Дополнительные параметры системы -> Дополнительно -> Переменные среды -> Системный path).

Добавляем путь "C:\Program Files\DevOps Tools" к path.
Добавляем путь "C:\Program Files\DevOps Tools" к path.

Если вы работаете под учётной записью без прав локального администратора, то можно использовать переменные среды пользователя. Запустим оболочку cmd или powershell и выполним команду "terraform". Должны отобразиться возможные команды.

Поиск в Google и в Terraform Registry довольно быстро привёл меня к Terraform-провайдеру для работы с Hyper-V за авторством пользователя с ником taliesins. Исходные коды, руководство по сборке указанного провайдера и инструкция по настройке сервера опубликованы в GitHub. В Terraform Registry также размещена документация по использованию провайдера.

Так как HashiCorp заблокировала доступ к своим ресурсам для пользователей из России - то необходим создать конфигурационный файл для того чтобы провайдер был скачан с зеркала компании Яндекс. В Windows файл должен называться terraform.rc и располагаться в каталоге %appdata% текущего пользователя. В остальных системах файл должен называться .terraform.rc и располагаться в корне домашнего каталога текущего пользователя.

provider_installation {
  network_mirror {
    url = "https://terraform-mirror.yandexcloud.net/"
    include = ["registry.terraform.io/*/*"]
  }
  direct {
    exclude = ["registry.terraform.io/*/*"]
  }
}

Если вы используете VPN или находитесь не в России, то этот файл не обязателен.

Теперь создадим каталог, в котором будем описывать конфигурацию виртуальной сети, к которой, впоследствии будем подключать виртуальные машины. У меня это будет каталог "D:\Terraform\Hyper-V-Network\". Terraform по-умолчанию считает все файлы *.tf размещенные в одном каталоге частью одной конфигурации и конфигурацию можно описать как в одном целом файле, так и частями в нескольких. Для начала создадим файл, в котором укажем какой провайдер будет использован в конфигурации и какие версии Terraform и провайдера необходимы.

providers.tf

terraform {
  required_version = ">= 1.2.9"
  required_providers {
    hyperv = {
      version = "1.2.1"
      source  = "registry.terraform.io/taliesins/hyperv"
    }
  }
}

Проверим работоспособность созданной конфигурации. Для этого откроем командную строку, перейдем в ней в каталог D:\Terraform\Hyper-V-Network\ (cd "D:\Terraform\Hyper-V-Network\") и выполним команду terraform init. Если всё сделано правильно то будет выведен экран с сообщением об успешной инициализации.

Окно успешной инициализации провайдера Terraform.
Окно успешной инициализации провайдера Terraform.

В рабочем каталоге должен появится файл .terraform.lock.hcl содержащий контрольную сумму провайдера и каталог .terraform внутри которого будет находится исполняемый файл провайдера.

На этом моменте оставим рабочую станцию и перейдём к настройке сервера.

4) Включение PowerShell Remoting и настройка WinRM для HTTPS

Залогинимся на сервер HP-SRV1 под встроенной учётной записью "Администратор" и запустим Windows PowerShell. Для начала создадим локальную учётную запись, под которой, в дальнейшем, к серверу будет подключаться Terraform и сразу же добавим её в локальную группу "Администраторы".

New-LocalUser -Name "ServiceAccount" -Password (Read-Host -Prompt "Enter password for new account" -AsSecureString) -Description "Remote Configuration Account" -PasswordNeverExpires | Add-LocalGroupMember -SID "S-1-5-32-544"

После ввода этой команды система будет ожидать от вас ввода пароля для создаваемой учётной записи. Введите пароль и нажмите "Enter". Логин и пароль от этой учётной записи потребуется позже. Добавление в группу администраторов производится по SID-группы, который не зависит от локализации операционной системы.

Вы также можете создать новую учётную запись через графический интерфейс. Для этого откройте "Пуск -> Средства Администрирования -> Локальные пользователи и группы -> Пользователи".

Примечание: Если вы решите выбрать другое имя для создаваемой учётной записи, то настоятельно рекомендую создавать имя на английском языке, чтобы не было проблем с кодировками при использованием Terraform в операционных системах отличных от Windows.

Создана учётная запись ServiceAccount с правами локального администратора.
Создана учётная запись ServiceAccount с правами локального администратора.

Внимание! После создания учётной записи необходимо полностью отключить UAC, иначе при попытке удалённо залогиниться на сервер через командлет Enter-PSSession, с любой учётной записью кроме встроенного Администратора, вы будете получать ошибку: "Отказано в доступе". При этом отключение UAC через "Панель управления" не даст необходимого эффекта. Отключать UAC необходимо через редактор групповых политик.

Для этого необходимо выполнить команду gpedit.msc, в открывшейся консоли открыть "Конфигурация компьютера -> Конфигурация Windows -> Параметры безопасности -> Локальные политики -> Параметры безопасности -> Контроль учётных записей: все администраторы работают в режиме одобрения администратором" и переключить параметр в значение "Отключено". После чего перезагрузить сервер.

Теперь проверим какой тип сетевого подключения выбран для сетевого адаптера и при необходимости, поменяем его на "Private" (тип "Domain" нам недоступен, т.к. сервер не входит в домен). Средствами PowerShell это делается следующим образом:

Get-NetConnectionProfile
Set-NetConnectionProfile -InterfaceAlias "Ethernet" -NetworkCategory "Private"
Get-NetConnectionProfile
Меняем тип сетевого подключения на "Private" через PowerShell.
Меняем тип сетевого подключения на "Private" через PowerShell.

Также можно поменять тип сетевого подключения через графический интерфейс. Для этого открываем "Пуск -> Параметры -> Сеть и Интернет -> Ethernet -> Сеть" и переключаем сетевой профиль с "Общедоступные" на "Частные".

Меняем тип сетевого подключения на "Частные" через графический интерфейс.
Меняем тип сетевого подключения на "Частные" через графический интерфейс.

Включаем удаленное выполнение командлетов PowerShell. Служба WinRM при этом будет включена автоматически с настройками по-умолчанию. Также будут включены правила для брандмауэра Windows, разрешающее подключение к WinRM по порту 5985(HTTP) во всех типах сетей.

Enable-PSRemoting -SkipNetworkProfileCheck -Force

Примечание: Т.к. мы указали ключ -SkipNetworkProfileCheck, то разрешающие правила будут созданы правила сразу для всех типов сетей.

Теперь можно посмотреть настройки WinRM выполнив команду:

winrm get winrm/config

Для просмотра автоматически созданных прослушивателей WinRM выполните команду:

Get-ChildItem wsman:\localhost\Listener
HTTP-прослушиватель WinRM созданный автоматически.
HTTP-прослушиватель WinRM созданный автоматически.

В выводе команды видно что сейчас для WinRM создан HTTP-прослушиватель.

Внимание! Дальнейшие шаги необходимо выполнять в одном и том-же окне PowerShell, не закрывая это окно до окончания настройки, т.к. в ходе выпуска сертификата и создания HTTPS-прослушивателя будут использоваться сессионные переменные.

Сперва создадим самоподписанный сертификат с именем субъекта (CN) равным имени нашего сервера, и содержащим альтернативные имена (SAN) равные имени и IP-адресу нашего сервера.

$ComputerName = $env:COMPUTERNAME
$ComputerIP=(Get-NetAdapter| Get-NetIPAddress).IPv4Address
$ExtentionString = ("2.5.29.17={text}DNS=" + $ComputerName + ($ComputerIP -join "&IPAddress="))
$ComputerCert = New-SelfSignedCertificate -Subject $ComputerName -TextExtension @($ExtentionString) -NotAfter (Get-date).AddYears(3) -CertStoreLocation Cert:\LocalMachine\My
certlm

Ключ -TextExtension использовался для того чтобы был создан сертификат содержащий имя сервера и IP-адреса в качестве дополнительных имен субъекта (SAN), что будет давать возможность без ошибок подключаться к серверу как по имени, так и по IP-адресу. Срок действия сертификата 3 года. Если вы хотите создать сертификат с другим сроком действия, то поменяйте в третьей команде значение в параметре -NotAfter.

Примечание: Если у вас возникают ошибки при выполнении команд - попробуйте копировать команды построчно. Замечено что при копированнии команд с Habr-а в PowerShell нарушается порядок строк.

В открывшейся консоли "Сертификаты - Локальный компьютер" откроем "Личное -> Сертификаты" и убедимся в том что самоподписанный сертификат был создан.

Самоподписанный сертификат в личном хранилище локального компьютера.
Самоподписанный сертификат в личном хранилище локального компьютера.

Удалим существующие прослушиватели HTTP и HTTPS и создадим один новый HTTPS-прослушиватель, к которому привяжем свежевыпущенный сертификат.

Get-ChildItem wsman:\localhost\Listener\ | Where-Object -Property Keys -like 'Transport=HTTP*' | Remove-Item -Recurse
New-Item -Path WSMan:\localhost\Listener\ -Transport HTTPS -Address * -CertificateThumbPrint $ComputerCert.Thumbprint -Force
Создан HTTPS-прослушиватель.
Создан HTTPS-прослушиватель.

Создадим разрешающее правило для брандмауэра Windows и перезапустим службу WinRM.

New-NetFirewallRule -Displayname 'WinRM - Powershell remoting HTTPS-In' -Name 'WinRM - Powershell remoting HTTPS-In' -Profile Any -LocalPort 5986 -Protocol TCP
Restart-Service WinRM

Посмотреть отпечаток сертификата, к которому привязан HTTPS-прослушиватель WinRM можно при помощи команды:

WinRM enumerate winrm/config/listener

Так-же можно проверить, что конфигурация WinRM запрещает незашифрованные подключения как серверу, так и клиенту:

dir WSMan:\localhost\Service | Where-Object Name -eq AllowUnencrypted
dir WSMan:\localhost\Client | Where-Object Name -eq AllowUnencrypted

При необходимости запретите незашифрованные подключения:

winrm set winrm/config/service '@{AllowUnencrypted="false"}'
winrm set winrm/config/client '@{AllowUnencrypted="false"}'

Экспортируем самоподписанный сертификат в файл:

Export-Certificate -Cert $ComputerCert -FilePath "D:\Certificates\$ComputerName.cer"

На этом конфигурирование сервера HP-SRV1 завершено. Возвращаемся к рабочей станции.

5) Проверка подключения к WinRM через HTTPS

Скопируем самоподписанный сертификат сервера HP-SRV1 из каталога "D:\Certificates\" на рабочую станцию HP-WST1, в такую же папку "D:\Certificates\". Затем импортируем сертификат в "Доверенные корневые центры сертификации" локального компьютера и убедимся что импорт сертификата произведён:

Import-Certificate -FilePath "D:\Certificates\HP-SRV1.cer" -CertStoreLocation Cert:\LocalMachine\root\
certlm
Самоподписанный сертификат импортирован в локальное хранилище рабочей станции
Самоподписанный сертификат импортирован в локальное хранилище рабочей станции

Теперь запустим Windows PowerShell и проверим возможность создания PowerShell-сессии на удалённом сервере HP-SRV1:

Enter-PSSession -ComputerName "HP-SRV1" -UseSSL -Credential "ServiceAccount"

Будет запрошен пароль, после чего должна быть установлена PowerShell сессия с удалённым сервером HP-SRV1.

Также можно проверить подключение к серверу по IP-адресу:

Enter-PSSession -ComputerName 192.168.1.15 -UseSSL -Credential "ServiceAccount"

Благодаря тому что мы использовали ключ -TextExtention при выпуске сертификата, то подключение по IP-адресу тоже должно проходить успешно и не требовать отключения проверки CN при помощи параметров.

Для справки: Подключение без проверки CN выполняется следующим образом:

Enter-PSSession -ComputerName 192.168.1.15 -UseSSL -Credential "ServiceAccount" -SessionOption (New-PSSessionOption -SkipCNCheck)

После того как подключение установлено, можно выполнить команду hostname для того чтобы убедиться что команды выполняются на удалённом сервере.

Проверка выполнения команд в сессии PowerShell на удалённом компьютере
Проверка выполнения команд в сессии PowerShell на удалённом компьютере

Если всё прошло успешно, то можно ввести команду exit для завершения сессии, закрыть окно PowerShell и приступить к созданию конфигурации Terraform.

6) Создание виртуального коммутатора при помощи Terraform

Откроем каталог, который мы подготовили для хранения конфигурации сети Hyper-V (у меня это D:\Terraform\Hyper-V-Network) и создадим в нём файл, в котором мы определим переменные для дальнейшего использования в конфигурации и зададим типы и значения по-умолчанию для этих переменных.

variables.tf

variable "hyperv_username"{
  description = "User account of Hyper-V server"
  type = string
  default = "ServiceAccount"
}

variable "hyperv_password"{
  description = "User password of Hyper-V server"
  type = string
  sensitive   = true
}

variable "hyperv_host"{
  description = "Hostname or address of Hyper-V server"
  type = string
  default = "HP-SRV1"
}

Теперь откроем ранее созданный файл providers.tf и добавим в него следующий блок:

# Hyper-V provider configuration
provider "hyperv" {
  user            = (var.hyperv_username)
  password        = (var.hyperv_password)
  host            = (var.hyperv_host)
  port            = 5986
  https           = true
  insecure        = false
  use_ntlm        = true
  tls_server_name = ""
  cacert_path     = ""
  cert_path       = ""
  key_path        = ""
  script_path     = "C:/Temp/terraform_%RAND%.cmd"
  timeout         = "30s"
}

В этом блоке мы задаём параметры Hyper-V провайдера. Как видно из конфигурации, имя пользователя, пароль и имя хоста будут взяты из ранее созданного файла содержащего переменные. При этом, при применении конфигурации Terraform будет запрашивать только ввод пароля, т.к. для имени пользователя и имени хоста заданы значения по-умолчанию.

Теперь создадим файл, в котором опишем создание виртуального коммутатора, подключённого к внешнему Ethernet-адаптеру.

external_switch.tf

# Create external virtual switch
resource "hyperv_network_switch" "external_network_switch" {
  notes                                   = ""
  name                                    = "External Switch"
  switch_type                             = "External"
  net_adapter_names                       = ["Ethernet"]
}

В этой конфигурации указано что Terraform должен создать ресурс типа "hyperv_network_switch" с именем ресурса "external_network_switch". Ресурс будет представлять собой виртуальный коммутатор с именем "External Switch", типа "External", подключённый к физическому адаптеру с названием "Ethernet". Данный набор свойств позволяет создать виртуальный коммутатор, аналогичный тому, который создаётся через графический интерфейс с параметрами по-умолчанию.

Теперь откроем powershell, перейдём в каталог "D:\Terraform\Hyper-V-Network" и выполним команду terraform plan. Будет запрошен пароль от учётной записи ServiceAccount и выведен полный список свойств создаваемого коммутатора. При необходимости, вы можете скопировать какие-то из выведенных свойств в конфигурацию и изменить их значения. Описания значений есть в документации.

Список планируемых параметров виртуального коммутатора.
Список планируемых параметров виртуального коммутатора.

Примечание: символом "+" обозначается ресурс который будет создан, символом "-" обозначается ресурс который будет удалён, "-/+" - удалён и создан с новыми параметрами, "~" - изменён без удаления.

Откроем командную строку, перейдём в каталог "D:\Terraform\Hyper-V-Network" и выполним команду terraform apply. Вновь будет запрошен пароль от учётной записи ServiceAccount, отображён список вносимых изменений и выведен запрос подтверждения. Вводим "yes" и ждём некоторое время.

Виртуальный коммутатор был успешно создан.
Виртуальный коммутатор был успешно создан.

Перейдём к сервере HP-SRV1, откроем Диспетчер Hyper-V -> Диспетчер виртуальных коммутаторов и убедимся что описанный виртуальный коммутатор был создан.

Внешний виртуальный коммутатор созданный на сервере HP-SRV1
Внешний виртуальный коммутатор созданный на сервере HP-SRV1

Теперь можно переходить к созданию виртуального компьютера.

7) Создание шаблонного компьютера при помощи Terraform

Для начала скопируем в каталог D:\ISO\ установочного диска с операционной системой Windows Server 2022 (en-us_windows_server_2022_updated_jan_2025_x64_dvd_7b59ccdd.iso).

Примечание: Возможно вам для этого понадобится включить общий доступ к файлам и принтерам и проверить что в брэндмауэре-Windows включены правила разрешающие соединение по протоколу SMB (445/TCP), а затем либо открыть сетевой доступ к каталогу на сервере, либо обратиться к скрытому админскому ресурсу D$. Я же просто воспользовался копированием через буфер обмена RDP.

Создадим на рабочей станции каталог D:\Terraform\Template-Windows-Server-2022\ и создадим в нём файлы конфигурации:

providers.tf
terraform {
  required_version = ">= 1.2.9"
  required_providers {
    hyperv = {
      version = "1.2.1"
      source  = "registry.terraform.io/taliesins/hyperv"
    }
  }
}

# Hyper-V provider configuration
provider "hyperv" {
  user            = (var.hyperv_username)
  password        = (var.hyperv_password)
  host            = (var.hyperv_host)
  port            = 5986
  https           = true
  insecure        = false
  use_ntlm        = true
  tls_server_name = ""
  cacert_path     = ""
  cert_path       = ""
  key_path        = ""
  script_path     = "C:/Temp/terraform_%RAND%.cmd"
  timeout         = "30s"
}
variables.tf
variable "hyperv_username"{
  description = "User account of Hyper-V server"
  type = string
  default = "ServiceAccount"
}

variable "hyperv_password"{
  description = "User password of Hyper-V server"
  type = string
  sensitive   = true
}

variable "hyperv_host"{
  description = "Hostname or address of Hyper-V server"
  type = string
  default = "HP-SRV1"
}

variable "hyperv_default_vm_path"{
  description = "Default path for virtual machines"
  type = string
  default = "D:\\Hyper-V"
}

variable "vm_name"{
  description = "Virtual machine name"
  type = string
  default = "TMPL_EN_WS2022"
}

variable "iso_path" {
  description = "Path to iso file"
  type = string
  default = "D:\\ISO\\en-us_windows_server_2022_updated_jan_2025_x64_dvd_7b59ccdd.iso"
}
virtual_machines.tf
# Create a virtual machine
resource "hyperv_machine_instance" "template_vm" {
  name                                                = var.vm_name
  path                                                = var.hyperv_default_vm_path
  smart_paging_file_path                              = "${var.hyperv_default_vm_path}\\${var.vm_name}"
  snapshot_file_location                              = "${var.hyperv_default_vm_path}\\${var.vm_name}"
  generation                                          = 1
# static_memory                                       = false
  dynamic_memory                                      = true
  memory_startup_bytes                                = 4294967296 # 4Gb
  processor_count                                     = 2  
  state                                               = "Off"

  # Add a network adaptor
  network_adaptors {
    name                                              = "Network Adapter"
    switch_name                                       = "External Switch"
    iov_weight                                        = 0
    allow_teaming                                     = "Off"
  }

  # Add a DVD drive
  dvd_drives {
    path                                              = var.iso_path
    controller_number                                 = "1"
    controller_location                               = "0"
	resource_pool_name                                = "Primordial"
  }

  # Add a hard disk drive
  hard_disk_drives {
    controller_type                                   = "Ide"
    controller_number                                 = "0"
    controller_location                               = "0"
    path                                              = hyperv_vhd.template_vm_vhd.path
    support_persistent_reservations                   = false
  }

  vm_processor {
    compatibility_for_migration_enabled               = false
    compatibility_for_older_operating_systems_enabled = false
    enable_host_resource_protection                   = false
    expose_virtualization_extensions                  = false
    hw_thread_count_per_core                          = 0
    maximum                                           = 100
    maximum_count_per_numa_node                       = 4
    maximum_count_per_numa_socket                     = 1
    relative_weight                                   = 100
    reserve                                           = 0
  }

}

# Create a hard disk drive
resource "hyperv_vhd" "template_vm_vhd" {
  path                                                = "${var.hyperv_default_vm_path}\\${var.vm_name}\\Virtual Hard Disks\\${var.vm_name}.vhdx"
  vhd_type                                            = "Dynamic"
  size                                                = 42949672960 # 40GB
}

В приведённом файле описана виртуальная машина первого поколения с 2 CPU, 4GB оперативной памяти с включенным динамическим выделением, жестким диском подключённым к интерфейсу IDE размером 40GB, сетевым адаптером подключённым к ранее созданному коммутатору с именем External Switch и максимально приближенная по остальным параметрам к виртуальной машине создаваемой через графический интерфейс.

Двойные слеши в путях используются потому что одинарный слеш воспринимается как экранирующий символ.

Обратите внимание на то, что часть значений в файле virtual_machines.tf указана в явном виде, а часть берётся из значений переменных и формируется при помощи обращения к свойствам ресурсов. Для обращения к переменным указывается префикс var затем ставится точка и указывается имя переменной например var.vm_name. Для объединения нескольких значений всё выражение берётся в кавычки, перед каждой переменной ставится знак $, а переменная берётся в фигурные скобки. Например "${var.hyperv_default_vm_path}\\${var.vm_name}". Помимо вызова переменных, также можно обращаться к свойствам ресурсов, последовательно указав тип ресурса, его имя и вызываемое свойство. Например hyperv_vhd.template_vm_vhd.path.

Теперь запускаем Powershell, перейдём в каталог "D:\Terraform\Template-Windows-Server-2022" и выполним команду terraform init. Будет произведена инициализация провайдера. Выполним команду terraform plan. Будет запрошен пароль от учётной записи ServiceAccount и выведен полный список свойств создаваемого компьютера. С подробным описанием возможных свойств можно ознакомиться в документации. После чего выполняем команду terraform apply, еще раз вводим пароль от учётной записи ServiceAccount и ожидаем создания виртуальной машины на сервере HP-SRV1.

После создания виртуальной машины, можно переключиться на сервер HP-SRV1, произвести включение виртуальной машины и произвести установку операционной системы.

Созданная и запущенная виртуальная машина.
Созданная и запущенная виртуальная машина.

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

C:\Windows\System32\Sysprep\Sysprep /generalize /oobe /shutdown

И дождёмся завершения работы операционной системы. Шаблон готов.

8) Проблемы с клонированием дисков из шаблона

До текущего момента все действия проходили без особых сложностей, но попытки развернуть сразу несколько компьютеров из одного шаблона полностью компенсируют нехватку проблем.

В документации по Terraform-провайдеру указано что можно использовать несколько способов для создания виртуального жесткого диска из ранее приготовленного шаблона. Перечислю некоторые из них и опишу проблемы которые у меня возникли. Для демонстрации проблем я буду приводить упрощенные конфигурации.

Проблемы с клонированием диска при помощи параметра source_vm

main.tf
terraform {
  required_version = ">= 1.2.9"
  required_providers {
    hyperv = {
      version = "1.2.1"
      source  = "registry.terraform.io/taliesins/hyperv"
    }
  }
}

# Hyper-V provider configuration
provider "hyperv" {
  user            = (var.hyperv_username)
  password        = (var.hyperv_password)
  host            = (var.hyperv_host)
  port            = 5986
  https           = true
  insecure        = false
  use_ntlm        = true
  tls_server_name = ""
  cacert_path     = ""
  cert_path       = ""
  key_path        = ""
  script_path     = "C:/Temp/terraform_%RAND%.cmd"
  timeout         = "30s"
}

# Create a hard disk drive
resource "hyperv_vhd" "this" {
  for_each = var.vm_names

  path = "${var.hyperv_default_vm_path}\\${each.value}\\Virtual Hard Disks\\${each.value}.vhdx"
  source_vm = var.source_vm
}

variable "hyperv_username"{
  description = "User account of Hyper-V server."
  type = string
  default = "ServiceAccount"
}

variable "hyperv_password"{
  description = "User password of Hyper-V server."
  type = string
  sensitive   = true
}

variable "hyperv_host"{
  description = "Hostname or address of Hyper-V server."
  type = string
  default = "HP-SRV1"
}

variable "hyperv_default_vm_path"{
  description = "Default path for virtual machines."
  type = string
  default = "D:\\Hyper-V"
}

variable "source_vm" {
  type        = string
  default     = "TMPL_EN_WS2022"
}

variable "vm_names" {
  description = "Names of new virtual machines."
  default = {
    "vm1"       = "FDWS-1",
    "vm2"       = "FDWS-2",
    "vm3"       = "FDWS-3"
  }
}

Terraform, по-умолчанию, пытается создавать ресурсы параллельно. Если указать в конфигурации что нам требуется несколько экземпляров ресурса, например при помощи счётчика count или цикла for_each то Terraform будет пытаться создавать до 10-и ресурсов одновременно. Соответственно, если мы укажем в коде, что нам необходимо сделать 3 виртуальных жестких диска из одной шаблонной виртуальной машины (параметр source_vm), то Terraform попытается выполнить экспорт одновременно трёх экземпляров одной и той-же виртуальной машины.

Используемый Terraform-провайдер выполняет необходимые действия путём выполнения powershell-скриптов на удалённом компьютере. При использовании параметра source_vm происходит попытка экспорта существующей виртуальной машины в новое местоположение, при котором файлы исходной виртуальной машины блокируются. В результате, успешно клонируется диск только для одной виртуальной машины, а остальные завершаются с ошибкой.

Ошибки при создании 2-ух дисков из 3-ех при использовании source_vm
Ошибки при создании 2-ух дисков из 3-ех при использовании source_vm

Изменить поведение Terraform можно использовав параметр -parallelism=n, где n - количество одновременно создаваемых ресурсов. Однако и здесь меня ждала неудача. При выполнении команды terraform apply -parallelism=1 3 виртуальных диска из 3-ех запрошенных успешно создаются примерно 1 раз из 10-и попыток. В остальных случаях создаётся 2 диска из 3-ех, а создание третьего завершается с ошибкой.

Ошибки при создании 1-го диска из 3-ех при использовании source_vm
Ошибки при создании 1-го диска из 3-ех при использовании source_vm

Конечно, можно запустить команду terraform apply -parallelism=1 еще раз и тогда недостающий диск будет досоздан, но это не тот результат которого бы хотелось.

Проблемы с клонированием диска при помощи параметра source

main.tf
terraform {
  required_version = ">= 1.2.9"
  required_providers {
    hyperv = {
      version = "1.2.1"
      source  = "registry.terraform.io/taliesins/hyperv"
    }
  }
}

# Hyper-V provider configuration
provider "hyperv" {
  user            = (var.hyperv_username)
  password        = (var.hyperv_password)
  host            = (var.hyperv_host)
  port            = 5986
  https           = true
  insecure        = false
  use_ntlm        = true
  tls_server_name = ""
  cacert_path     = ""
  cert_path       = ""
  key_path        = ""
  script_path     = "C:/Temp/terraform_%RAND%.cmd"
  timeout         = "30s"
}

# Create a hard disk drive
resource "hyperv_vhd" "this" {
  for_each = var.vm_names

  path = "${var.hyperv_default_vm_path}\\${each.value}\\Virtual Hard Disks\\${each.value}.vhdx"
  source = var.source_vhd
}

variable "hyperv_username"{
  description = "User account of Hyper-V server."
  type = string
  default = "ServiceAccount"
}

variable "hyperv_password"{
  description = "User password of Hyper-V server."
  type = string
  sensitive   = true
}

variable "hyperv_host"{
  description = "Hostname or address of Hyper-V server."
  type = string
  default = "HP-SRV1"
}

variable "hyperv_default_vm_path"{
  description = "Default path for virtual machines."
  type = string
  default = "D:\\Hyper-V"
}

variable "source_vhd" {
  type        = string
  default     = "D:\\Hyper-V\\TMPL_EN_WS2022\\Virtual Hard Disks\\TMPL_EN_WS2022.vhdx"
}

variable "vm_names" {
  description = "Names of new virtual machines."
  default = {
    "vm1"       = "FDWS-1",
    "vm2"       = "FDWS-2",
    "vm3"       = "FDWS-3"
  }
}

В целом, поведение и проблемы при использовании параметра source, аналогичны проблемам с параметром source_vm. Исключение составляют только выводимые ошибки. Если при использовании параметра source_vm появляются ошибки что невозможно произвести затребованную операцию в текущем состоянии виртуальной машины, то при использовании параметра source, ошибки гласят что операция не может быть произведена, потому что объект используется.

Ошибки при создании 2-ух дисков из 3-ех при использовании source
Ошибки при создании 2-ух дисков из 3-ех при использовании source

При использовании команды terraform apply -parallelism=1 результаты так же далеки от желаемых.

Примечание: Субъективно мне показалось что вариант с source работает даже хуже чем source_vm, потому что регулярно бывали случаи что при использовании параметра source попытка создания всех дисков оканчивалась ошибками.

В какой-то момент я предположил, что может влиять то, что шаблонная виртуальная машина зарегистрирована в Hyper-V Manager. Скопировав шаблонную машину в отдельный каталог и поменяв путь в переменной source_vm

variable "source_vm" {
  type        = string
  default     = "D:\\Hyper-V\\TMPL_EN_WS2022\\Virtual Hard Disks\\TMPL_EN_WS2022.vhdx"
}

Я снова выполнил команду terraform apply -parallelism=1, однако результат лучше не стал. Разве что, в очередной раз, поменялась ошибка.

Ошибка при создании 1-го дисков из 3-ех при использовании source
Ошибка при создании 1-го дисков из 3-ех при использовании source

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

Многократно обратившись к документации, я заметил что автор провайдера отдельным пунктом указал то что для ресурса hyperv_vhd существует такой параметр как timeouts, в котором можно задавать тайминги для различных операций с ресурсом.

К слову, официальная документация Terraform предусматривает этот параметр для ресурсов, но указывает что всё остается на усмотрение автора провайдера. Предположив что проблема возникает из-за того что Terraform по-умолчанию отводит на создание одного ресурса 60 секунд и по-истечении этого срока переходит к созданию следующего ресурса я указал этот параметр при клонировании жестких дисков

# Create a hard disk drive
resource "hyperv_vhd" "this" {
  for_each                                            = var.vm_names
  
  path                                                = "${var.hyperv_default_vm_path}\\${each.value}\\Virtual_Hard_Disks\\${each.value}.vhdx"
  source_vm                                           = var.source_vm 
  timeouts {
    create                                            = "6000s"
	read                                              = "6000s"
	update                                            = "6000s"
  }
}

Однако результаты лучше не стали. Клонирование диска стабильно завершалось с ошибками. Не помогали ни использование count вместо for_each, ни source вместо source_vm. Изменение таймаутов также не помогло. У меня сложилось впечатление что провайдер попросту не обрабатывает значения таймаутов.

9) Поиск решения

В результате экспериментов стало очевидно что с Terraform-провайдером имеются некоторые проблемы. Притом, судя по всему, проблемы возникают с таймингами отведёнными на выполнение операций и при определении того закончилась ли предыдущая операция с виртуальным диском. Для проверки этой гипотезы я решил сделать еще одну конфигурацию, в которой будут принудительно заданы паузы между созданием дисков.

9.1) Конфигурация с жестко заданными последовательностями и паузами

Создадим каталог D:\Terraform\Deploy-Windows-Server-2022-Hard-Code, а в нем файлы следующие файлы конфигурации:

providers.tf
terraform {
  required_version = ">= 1.2.9"
  required_providers {
    hyperv = {
      version = "1.2.1"
      source  = "registry.terraform.io/taliesins/hyperv"
    }
	time = {
      source = "hashicorp/time"
      version = "0.13.1"
    }
  }
}

# Hyper-V provider configuration
provider "hyperv" {
  user            = (var.hyperv_username)
  password        = (var.hyperv_password)
  host            = (var.hyperv_host)
  port            = 5986
  https           = true
  insecure        = false
  use_ntlm        = true
  tls_server_name = ""
  cacert_path     = ""
  cert_path       = ""
  key_path        = ""
  script_path     = "C:/Temp/terraform_%RAND%.cmd"
  timeout         = "30s"
}
variables.tf
variable "hyperv_username"{
  description = "User account of Hyper-V server."
  type = string
  default = "ServiceAccount"
}

variable "hyperv_password"{
  description = "User password of Hyper-V server."
  type = string
  sensitive   = true
}

variable "hyperv_host"{
  description = "Hostname or address of Hyper-V server."
  type = string
  default = "HP-SRV1"
}

variable "hyperv_default_vm_path"{
  description = "Default path for virtual machines."
  type = string
  default = "D:\\Hyper-V"
}

variable "vm_count"{
  description = "Virtual machines count."
  type = number
  default = 3
}

variable "vm_name_prefix" {
  description = "Name prefix for virtual machines."
  type        = string
  default     = "FDWS"
}

variable "source_vm" {
  type        = string
  default     = "TMPL_EN_WS2022"
}

variable "default_timeout" {
  type        = string
  default     = "30s"
}
virtual_machines.tf
# Create a hard disk drive
resource "hyperv_vhd" "vm1" {
  path                                                = "${var.hyperv_default_vm_path}\\${var.vm_name_prefix}-1\\Virtual Hard Disks\\${var.vm_name_prefix}-1.vhdx"
  source_vm                                           = var.source_vm
}

resource "time_sleep" "wait_after_creation_vm_1" {
  create_duration                                     = var.default_timeout
  depends_on                                          = [hyperv_vhd.vm1]
}


# Create a hard disk drive
resource "hyperv_vhd" "vm2" {
  path                                                = "${var.hyperv_default_vm_path}\\${var.vm_name_prefix}-2\\Virtual Hard Disks\\${var.vm_name_prefix}-2.vhdx"
  source_vm                                           = var.source_vm
  depends_on                                          = [time_sleep.wait_after_creation_vm_1]
}

resource "time_sleep" "wait_after_creation_vm_2" {
  create_duration                                     = var.default_timeout
  depends_on                                          = [hyperv_vhd.vm2]
}

# Create a hard disk drive
resource "hyperv_vhd" "vm3" {
  path                                                = "${var.hyperv_default_vm_path}\\${var.vm_name_prefix}-3\\Virtual Hard Disks\\${var.vm_name_prefix}-3.vhdx"
  source_vm                                           = var.source_vm
  depends_on                                          = [time_sleep.wait_after_creation_vm_2]
}

resource "time_sleep" "wait_after_creation_vm_3" {
  create_duration                                     = var.default_timeout
  depends_on                                          = [hyperv_vhd.vm2]
}

# Create a virtual machines
resource "hyperv_machine_instance" "this" {
  count                                               = var.vm_count

  name                                                = "${var.vm_name_prefix}-${count.index+1}"
  path                                                = "${var.hyperv_default_vm_path}"
  smart_paging_file_path                              = "${var.hyperv_default_vm_path}\\${var.vm_name_prefix}-${count.index+1}"
  snapshot_file_location                              = "${var.hyperv_default_vm_path}\\${var.vm_name_prefix}-${count.index+1}"
  generation                                          = 1
# static_memory                                       = false
  dynamic_memory                                      = true
  memory_startup_bytes                                = 4294967296 # 4Gb
  processor_count                                     = 2  
  state                                               = "Off"

  # Add a network adaptor
  network_adaptors {
    name                                              = "Network Adapter"
    switch_name                                       = "External Switch"
    iov_weight                                        = 0
    allow_teaming                                     = "Off"
  }

  # Add a DVD drive
  dvd_drives {
    path                                              = ""
    controller_number                                 = "1"
    controller_location                               = "0"
#   resource_pool_name                                = "Primordial"
  }

  # Add a hard disk drive
  hard_disk_drives {
    controller_type                                   = "Ide"
    controller_number                                 = "0"
    controller_location                               = "0"
    path                                              = "${var.hyperv_default_vm_path}\\${var.vm_name_prefix}-${count.index+1}\\Virtual Hard Disks\\${var.vm_name_prefix}-${count.index+1}.vhdx"
    support_persistent_reservations                   = false
  }

  vm_processor {
    compatibility_for_migration_enabled               = false
    compatibility_for_older_operating_systems_enabled = false
    enable_host_resource_protection                   = false
    expose_virtualization_extensions                  = false
    hw_thread_count_per_core                          = 0
    maximum                                           = 100
    maximum_count_per_numa_node                       = 4
    maximum_count_per_numa_socket                     = 1
    relative_weight                                   = 100
    reserve                                           = 0
  }
  depends_on                                          = [time_sleep.wait_after_creation_vm_3]
}

В приведённой конфигурации, помимо провайдера taliesins/hyperv еще используется стандартный провайдер hashicorp/time, который используется для создания пауз между созданием ресурсов. В явном видео описаны количество и имена виртуальных жестких дисков и при помощи управляющей структуры depends_on жестко указана последовательность создания трёх виртуальных жестких дисков.

1) Сперва из шаблона создаётся первый жесткий диск;

2) Выдерживается пауза 30 секунд;

3) Создаётся второй жесткий диск;

4) Выдерживается пауза 30 секунд;

5) Создаётся третий жесткий диск;

6) Выдерживается пауза 30 секунд;

7) При помощи счётчика count одновременно создаётся три одинаковых виртуальных машины, с именами генерируемыми по шаблону, к которым подключаются ранее созданные жёсткие диски.

Открываем командную строку, переходим каталог D:\Terraform\Deploy-Windows-Server-2022-Hard-Code, выполняем команды terraform init и terraform apply и, после нескольких минут ожидания, наконец-то получаем развернутые виртуальные машины с жестками дисками склонированными из ранее подготовленного шаблона.

Виртуальные машины созданные при помощи Terraform
Виртуальные машины созданные при помощи Terraform

Так как в конфигурации жестко указана последовательность создания ресурсов, то использование параметра -parallelism=1 не требуется.

Основной недостаток приведённой конфигурации - сложность масштабирования. Т.к. каждый создаваемый диск и паузы между созданием придётся описывать вручную и вручную же описывать зависимости между действиями.

Таким образом было подтверждено то что проблема с клонированием дисков связана с таймингами, а если быть точным, то с тем что стендовый компьютер не успевал выполнять процедуры клонирования дисков за то время которое Terraform (или провайдер) считал достаточным. Штатных способов для того чтобы изменить это поведение я не нашёл и стал искать способы, которые позволили бы либо увеличить время отведённое на процедуру клонирования диска, либо выполнять процедуру клонирования иными способами, в обход terraform-провайдера.

9.2) Конфигурация с задержками реализованными при помощи local-exec провиженера

Создадим каталог D:\Terraform\Deploy-Windows-Server-2022-Local-Exec, а в нем файлы следующие файлы конфигурации:

providers.tf
terraform {
  required_version = ">= 1.2.9"
  required_providers {
    hyperv = {
      version = "1.2.1"
      source  = "registry.terraform.io/taliesins/hyperv"
    }
	time = {
      source = "hashicorp/time"
      version = "0.13.1"
    }
  }
}

# Hyper-V provider configuration
provider "hyperv" {
  user            = (var.hyperv_username)
  password        = (var.hyperv_password)
  host            = (var.hyperv_host)
  port            = 5986
  https           = true
  insecure        = false
  use_ntlm        = true
  tls_server_name = ""
  cacert_path     = ""
  cert_path       = ""
  key_path        = ""
  script_path     = "C:/Temp/terraform_%RAND%.cmd"
  timeout         = "30s"
}
variables.tf
variable "hyperv_username"{
  description = "User account of Hyper-V server."
  type = string
  default = "ServiceAccount"
}

variable "hyperv_password"{
  description = "User password of Hyper-V server."
  type = string
  sensitive   = true
}

variable "hyperv_host"{
  description = "Hostname or address of Hyper-V server."
  type = string
  default = "HP-SRV1"
}

variable "hyperv_default_vm_path"{
  description = "Default path for virtual machines."
  type = string
  default = "D:\\Hyper-V"
}

variable "default_timeout" {
  type        = string
  default     = "60s"
}

variable "source_vm" {
  type        = string
  default     = "TMPL_EN_WS2022"
}

variable "vm_names" {
  description = "Names of new virtual machines."
  default = {
    "vm1"       = "FDWS-1",
    "vm2"       = "FDWS-2",
    "vm3"       = "FDWS-3",
	"vm4"       = "FDWS-4",
    "vm5"       = "FDWS-5"
  }
}

virtual_machines.tf
# Create a hard disk drive
resource "hyperv_vhd" "this" {
  for_each                                           = var.vm_names
  
  path                                               = "${var.hyperv_default_vm_path}\\${each.value}\\Virtual Hard Disks\\${each.value}.vhdx"
  source_vm                                          = var.source_vm
  provisioner "local-exec" {
    interpreter = ["powershell","-command"]
    command     = "start-sleep -seconds 600"
  }
}

resource "time_sleep" "wait_after_create_disk" {
  create_duration                                     = var.default_timeout
  depends_on                                          = [hyperv_vhd.this]
}

# Create a virtual machines
resource "hyperv_machine_instance" "this" {
  for_each                                            = var.vm_names

  name                                                = "${each.value}"
  path                                                = "${var.hyperv_default_vm_path}"
  smart_paging_file_path                              = "${var.hyperv_default_vm_path}\\${each.value}"
  snapshot_file_location                              = "${var.hyperv_default_vm_path}\\${each.value}"
  generation                                          = 1
# static_memory                                       = false
  dynamic_memory                                      = true
  memory_startup_bytes                                = 4294967296 # 4Gb
  processor_count                                     = 2  
  state                                               = "Off"

  # Add a network adaptor
  network_adaptors {
    name                                              = "Network Adapter"
    switch_name                                       = "External Switch"
    iov_weight                                        = 0
    allow_teaming                                     = "Off"
  }

  # Add a DVD drive
  dvd_drives {
    path                                              = ""
    controller_number                                 = "1"
    controller_location                               = "0"
#   resource_pool_name                                = "Primordial"
  }

  # Add a hard disk drive
  hard_disk_drives {
    controller_type                                   = "Ide"
    controller_number                                 = "0"
    controller_location                               = "0"
    path                                              = "${var.hyperv_default_vm_path}\\${each.value}\\Virtual Hard Disks\\${each.value}.vhdx"
    support_persistent_reservations                   = false
  }

  vm_processor {
    compatibility_for_migration_enabled               = false
    compatibility_for_older_operating_systems_enabled = false
    enable_host_resource_protection                   = false
    expose_virtualization_extensions                  = false
    hw_thread_count_per_core                          = 0
    maximum                                           = 100
    maximum_count_per_numa_node                       = 4
    maximum_count_per_numa_socket                     = 1
    relative_weight                                   = 100
    reserve                                           = 0
  }
  depends_on                                          = [time_sleep.wait_after_create_disk]
}

Применять данную конфигурацию следует командой terraform apply -parallelism=1

Данная конфигурация создаст 5 виртуальных машин и склонирует для них жесткие диски из шаблонной машины при помощи параметра source_vm предусмотренного в провайдере. Основное отличие приведённой конфигурации заключается в том, что при создании ресурса типа hyperv_vhd на компьютере где выполняется terraform при помощи провиженера local-exec вызывается командлет powershell который дополнительно отсчитывает 600 секунд при создании каждого диска. Что даёт время powershell-сценариям, выполняемым terraform-провайдером на сервере успеть завершить процедуры клонирования диска из шаблонной виртуальной машины.

Успешное развертывание 5-и виртуальных машин при использовании local-exec провиженера
Успешное развертывание 5-и виртуальных машин при использовании local-exec провиженера

Данная конфигурация уже не имеет проблем с масштабированием, однако расход времени может оказаться излишним. Для оптимизации временных затрат можно адаптировать задержку при создании диска под вашу инфраструктуру.

9.3) Конфигурация с копированием жестких дисков при помощи remote-exec провиженера

Создадим каталог D:\Terraform\Deploy-Windows-Server-2022-Remote-Exec, а в нем файлы следующие файлы конфигурации:

providers.tf
terraform {
  required_version = ">= 1.2.9"
  required_providers {
    hyperv = {
      version = "1.2.1"
      source  = "registry.terraform.io/taliesins/hyperv"
    }
	time = {
      source = "hashicorp/time"
      version = "0.13.1"
    }
  }
}

# Hyper-V provider configuration
provider "hyperv" {
  user            = (var.hyperv_username)
  password        = (var.hyperv_password)
  host            = (var.hyperv_host)
  port            = 5986
  https           = true
  insecure        = false
  use_ntlm        = true
  tls_server_name = ""
  cacert_path     = ""
  cert_path       = ""
  key_path        = ""
  script_path     = "C:/Temp/terraform_%RAND%.cmd"
  timeout         = "30s"
}
variables.tf
variable "hyperv_username"{
  description = "User account of Hyper-V server."
  type = string
  default = "ServiceAccount"
}

variable "hyperv_password"{
  description = "User password of Hyper-V server."
  type = string
  sensitive   = true
}

variable "hyperv_host"{
  description = "Hostname or address of Hyper-V server."
  type = string
  default = "HP-SRV1"
}

variable "hyperv_default_vm_path"{
  description = "Default path for virtual machines."
  type = string
  default = "D:\\Hyper-V"
}

variable "default_timeout" {
  type        = string
  default     = "60s"
}

variable "source_vhd_disk" {
  type        = string
  default     = "D:\\Hyper-V\\TMPL_EN_WS2022\\Virtual Hard Disks\\TMPL_EN_WS2022.vhdx"
}

variable "vm_names" {
  description = "Names of new virtual machines."
  default = {
    "vm1"       = "FDWS-1",
    "vm2"       = "FDWS-2",
    "vm3"       = "FDWS-3",
	"vm4"       = "FDWS-4",
    "vm5"       = "FDWS-5"
  }
}

virtual_machines.tf
# Create a blank hard disk drives
resource "hyperv_vhd" "this" {
  for_each                                           = var.vm_names
  
  path                                               = "${var.hyperv_default_vm_path}\\${each.value}\\Virtual Hard Disks\\${each.value}.vhdx"
  vhd_type                                           = "Dynamic"
  size                                               = 42949672960 # 40GB
}

# Replace blank hard disk drives
resource "terraform_data" "replace_blank_vhd" {
  for_each                                            = var.vm_names
  
  connection {
    type                                              = "winrm"
    user                                              = var.hyperv_username
    password                                          = var.hyperv_password
    host                                              = var.hyperv_host
    port                                              = 5986
    https                                             = true
    use_ntlm                                          = true
    timeout                                           = "240m"
  }
  provisioner "remote-exec" {
    inline = [
      "copy /Y \"${var.source_vhd_disk}\" \"${var.hyperv_default_vm_path}\\${each.value}\\Virtual Hard Disks\\${each.value}.vhdx\""
    ]
  }
  depends_on                                          = [hyperv_vhd.this]
}

resource "time_sleep" "wait_after_replace_blank_disk" {
  create_duration                                     = var.default_timeout
  depends_on                                          = [terraform_data.replace_blank_vhd]
}

# Create a virtual machines
resource "hyperv_machine_instance" "this" {
  for_each                                            = var.vm_names

  name                                                = "${each.value}"
  path                                                = "${var.hyperv_default_vm_path}"
  smart_paging_file_path                              = "${var.hyperv_default_vm_path}\\${each.value}"
  snapshot_file_location                              = "${var.hyperv_default_vm_path}\\${each.value}"
  generation                                          = 1
# static_memory                                       = false
  dynamic_memory                                      = true
  memory_startup_bytes                                = 4294967296 # 4Gb
  processor_count                                     = 2  
  state                                               = "Off"

  # Add a network adaptor
  network_adaptors {
    name                                              = "Network Adapter"
    switch_name                                       = "External Switch"
    iov_weight                                        = 0
    allow_teaming                                     = "Off"
  }

  # Add a DVD drive
  dvd_drives {
    path                                              = ""
    controller_number                                 = "1"
    controller_location                               = "0"
#   resource_pool_name                                = "Primordial"
  }

  # Add a hard disk drive
  hard_disk_drives {
    controller_type                                   = "Ide"
    controller_number                                 = "0"
    controller_location                               = "0"
    path                                              = "${var.hyperv_default_vm_path}\\${each.value}\\Virtual Hard Disks\\${each.value}.vhdx"
    support_persistent_reservations                   = false
  }

  vm_processor {
    compatibility_for_migration_enabled               = false
    compatibility_for_older_operating_systems_enabled = false
    enable_host_resource_protection                   = false
    expose_virtualization_extensions                  = false
    hw_thread_count_per_core                          = 0
    maximum                                           = 100
    maximum_count_per_numa_node                       = 4
    maximum_count_per_numa_socket                     = 1
    relative_weight                                   = 100
    reserve                                           = 0
  }
  depends_on                                          = [time_sleep.wait_after_replace_blank_disk]
}

При применении данной конфигурации производятся следующие действия:

1) Для каждой виртуальной машины из списка создаётся пустой жесткий диск, размером 40 Gb (такого же размера как на шаблонной виртуальной машине);

2) После создания жестких дисков, для каждой виртуальной машины в ресурсе типа terraform_data создаётся провиженер типа remote-exec, который подключается к удалённому серверу и копирует из шаблонной машины vhdx-файл, заменяя им ранее созданный пустой жесткий диск;

3) После окончания копирования файлов жестких дисков выдерживается небольшая пауза;

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

Успешное развертывание 5-и виртуальных машин при использовании remote-exec провиженера
Успешное развертывание 5-и виртуальных машин при использовании remote-exec провиженера

Описанный механизм развертывания не имеет проблем с масштабированием, не выдерживает избыточных пауз между созданием жестких дисков и не требует обязательного использования ключа -parallelism=1, при применении конфигурации. Использовать или нет этот ключ и сколькими потоками ограничить создание ресурсов - остаётся на ваше усмотрение.

Так как исходный диск был создан такого-же размера, как и в шаблонной машине, то при повторном применении конфигурации terraform считает что никаких изменений в инфраструктуре не произошло. Более того, если после создания виртуальных машин вы увеличите в конфигурации размер жестких дисков, при этом оставив ресурс terraform_data.replace_blank_vhd неизменным, то terraform успешно увеличит жесткие диски на ранее созданных виртуальных машинах и сохранит информацию о новом состоянии виртуальных машин.

Внимание! Не забывайте очищать каталог C:\Users\ServiceAccount\AppData\Local\Temp так как в нём остаются powershell-скрипты и файлы вывода создаваемые terraform-провайдером!

10) Заключение и полезные ссылки

Как видите, при желании, можно организовать взаимодействие между Terraform и сервером Hyper-V, хотя использованный мною terraform-провайдер нельзя назвать идельным и для его использования потребовалось придумать несколько обходных путей и проявить некоторую настойчивость.

Напоследок оставлю ссылки, на материалы, которые мне показались полезными:

Теги:
Хабы:
+2
Комментарии1

Публикации

Работа

Ближайшие события