Infrastructure as Code for Windows Server Environments
Infrastructure as Code (IaC) treats server provisioning configuration the same way software engineers treat application code: version-controlled, peer-reviewed, and automatically tested. For Windows Server 2022 environments, combining Terraform for VM lifecycle management with PowerShell DSC for OS configuration creates a complete, repeatable provisioning pipeline. Changes to infrastructure are proposed as code diffs, reviewed before merge, and applied by automation rather than by hand — eliminating configuration drift and enabling disaster recovery by re-applying the same code to rebuilt infrastructure.
This guide covers Terraform for both on-premises Hyper-V and Azure VM provisioning, PowerShell DSC for configuration management, WinRM-based remote provisioners, Terraform modules for reusable Windows server templates, and Terragrunt for managing multiple environments with a single configuration source.
Installing Terraform and Configuring the Hyper-V Provider
Terraform is a cross-platform binary distributed by HashiCorp. Install it on your management workstation or CI/CD pipeline agent:
# On Windows management workstation — using winget
winget install --id HashiCorp.Terraform
# Verify installation
terraform version
# On Linux CI/CD agent
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
The community Hyper-V Terraform provider by taliesins enables Terraform to manage Hyper-V VMs on Windows Server 2022 hosts. Create your project directory and configure the provider:
mkdir C:IaCwindows-infra && cd C:IaCwindows-infra
Create the provider configuration file (providers.tf):
terraform {
required_providers {
hyperv = {
source = "taliesins/hyperv"
version = "~> 1.2"
}
}
required_version = ">= 1.5.0"
}
provider "hyperv" {
user = var.hyperv_user
password = var.hyperv_password
host = var.hyperv_host
port = 5986
https = true
insecure = false
use_ntlm = true
timeout = "30s"
}
The provider communicates with the Hyper-V host over WinRM (port 5986 HTTPS). Ensure WinRM is configured on the Hyper-V host to accept remote PowerShell connections:
# Run on the Hyper-V host to enable HTTPS WinRM
winrm quickconfig -transport:https
winrm set winrm/config/listener?Address=*+Transport=HTTPS '@{CertificateThumbprint="YOUR_CERT_THUMBPRINT"}'
Set-Item WSMan:localhostServiceAuthBasic -Value $true
Set-Item WSMan:localhostMaxConcurrentOperationsPerUser -Value 1500
Defining Hyper-V VMs as Terraform Resources
Create a variables.tf file to define configurable inputs:
variable "hyperv_host" {
description = "Hyper-V host FQDN or IP"
type = string
default = "hvhost01.corp.example.com"
}
variable "hyperv_user" {
description = "Hyper-V admin username"
type = string
}
variable "hyperv_password" {
description = "Hyper-V admin password"
type = string
sensitive = true
}
variable "vm_name" {
description = "Name of the Windows Server VM"
type = string
default = "WS2022-Prod-01"
}
variable "vm_cpu_count" {
type = number
default = 4
}
variable "vm_memory_gb" {
type = number
default = 8
}
variable "template_vhdx_path" {
description = "Path to sysprep'd Windows Server 2022 template VHDX on Hyper-V host"
type = string
default = "C:\Templates\WS2022-Template.vhdx"
}
Create main.tf to define the VM resource:
resource "hyperv_vhd" "os_disk" {
path = "C:\VMs\${var.vm_name}\OSDisk.vhdx"
source = var.template_vhdx_path
size = 80 * 1024 * 1024 * 1024 # 80 GB
}
resource "hyperv_machine_instance" "windows_server" {
name = var.vm_name
generation = 2
processor_count = var.vm_cpu_count
dynamic_memory = false
memory_startup_bytes = var.vm_memory_gb * 1024 * 1024 * 1024
vm_firmware {
enable_secure_boot = "On"
secure_boot_template = "MicrosoftWindows"
boot_order {
boot_type = "HardDiskDrive"
controller_number = 0
controller_location = 0
}
}
network_adaptors {
name = "LAN"
switch_name = "Production"
}
hard_disk_drives {
controller_type = "Scsi"
controller_number = 0
controller_location = 0
path = hyperv_vhd.os_disk.path
}
depends_on = [hyperv_vhd.os_disk]
}
Running Terraform to Provision the VM
With providers.tf, variables.tf, and main.tf in place, initialize and apply:
# Initialize — downloads the Hyper-V provider
terraform init
# Plan — shows what will be created without making changes
terraform plan -var="hyperv_user=Administrator" -var="hyperv_password=MyPassword"
# Apply — creates the VM
terraform apply -var="hyperv_user=Administrator" -var="hyperv_password=MyPassword" -auto-approve
Sensitive variables should never be passed on the command line in production. Use a terraform.tfvars file (excluded from version control via .gitignore) or environment variables:
# terraform.tfvars (do NOT commit to git)
hyperv_user = "Administrator"
hyperv_password = "YourSecurePassword"
vm_name = "WebServer01"
vm_cpu_count = 8
vm_memory_gb = 16
# Alternatively use environment variables (TF_VAR_ prefix)
$env:TF_VAR_hyperv_user = "Administrator"
$env:TF_VAR_hyperv_password = "YourSecurePassword"
terraform apply
Combining Terraform with PowerShell DSC for Configuration
Terraform provisions the VM; PowerShell Desired State Configuration (DSC) configures its software state. DSC defines the server’s configuration declaratively — what Windows features should be installed, what services should run, what registry keys should exist — and enforces that state on every run.
Create a DSC configuration script for a Windows web server:
# WebServerConfig.ps1
Configuration WebServerConfig {
param (
[string]$ComputerName = "localhost"
)
Import-DscResource -ModuleName PSDesiredStateConfiguration
Import-DscResource -ModuleName xWebAdministration
Node $ComputerName {
WindowsFeature IIS {
Name = "Web-Server"
Ensure = "Present"
}
WindowsFeature ASP_NET45 {
Name = "Web-Asp-Net45"
Ensure = "Present"
DependsOn = "[WindowsFeature]IIS"
}
Service W3SVC {
Name = "W3SVC"
State = "Running"
StartupType = "Automatic"
DependsOn = "[WindowsFeature]IIS"
}
Registry DisableNetBIOS {
Key = "HKLM:SYSTEMCurrentControlSetServicesNetBTParameters"
ValueName = "NetbiosOptions"
ValueData = "2"
ValueType = "Dword"
Ensure = "Present"
}
}
}
# Compile to MOF
WebServerConfig -ComputerName "WebServer01" -OutputPath "C:DSCWebServerConfig"
Apply the DSC configuration to the provisioned VM using a Terraform remote-exec provisioner (via WinRM):
resource "null_resource" "configure_vm" {
triggers = {
vm_id = hyperv_machine_instance.windows_server.id
}
connection {
type = "winrm"
host = var.vm_ip_address
user = "Administrator"
password = var.vm_admin_password
port = 5986
https = true
insecure = true
timeout = "10m"
}
provisioner "file" {
source = "DSC/WebServerConfig.ps1"
destination = "C:\Temp\WebServerConfig.ps1"
}
provisioner "remote-exec" {
inline = [
"powershell -ExecutionPolicy Bypass -File C:\Temp\WebServerConfig.ps1",
"powershell Start-DscConfiguration -Path C:\DSC\WebServerConfig -Wait -Verbose -Force"
]
}
depends_on = [hyperv_machine_instance.windows_server]
}
Provisioning Azure Windows VMs with the azurerm Provider
For Azure-hosted Windows Server 2022 VMs, use the official HashiCorp azurerm provider. The configuration pattern is similar but uses Azure-specific resource types:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.100"
}
}
}
provider "azurerm" {
features {}
subscription_id = var.azure_subscription_id
}
resource "azurerm_resource_group" "rg" {
name = "rg-windows-servers-prod"
location = "East US"
}
resource "azurerm_virtual_network" "vnet" {
name = "vnet-prod"
address_space = ["10.10.0.0/16"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_subnet" "subnet" {
name = "snet-servers"
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.10.1.0/24"]
}
resource "azurerm_network_interface" "nic" {
name = "nic-webserver01"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.subnet.id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_windows_virtual_machine" "webserver" {
name = "webserver01"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
size = "Standard_D4s_v5"
admin_username = "winadmin"
admin_password = var.vm_admin_password
network_interface_ids = [azurerm_network_interface.nic.id]
os_disk {
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
disk_size_gb = 128
}
source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2022-Datacenter"
version = "latest"
}
winrm_listener {
protocol = "Http"
}
tags = {
environment = "production"
managed_by = "terraform"
team = "infrastructure"
}
}
Terraform Modules for Reusable Windows Server Templates
Terraform modules allow you to package a complete Windows server configuration as a reusable unit. Create a module structure for a standard Windows web server:
modules/
windows-webserver/
main.tf # VM + NIC + disk resources
variables.tf # Inputs: name, size, location, subnet_id
outputs.tf # Outputs: vm_id, private_ip
README.md
Reference the module from your environment-specific root configuration:
# environments/prod/main.tf
module "webserver_01" {
source = "../../modules/windows-webserver"
vm_name = "webserver01"
vm_size = "Standard_D4s_v5"
location = "East US"
resource_group_name = azurerm_resource_group.rg.name
subnet_id = azurerm_subnet.subnet.id
admin_password = var.vm_admin_password
os_disk_size_gb = 128
environment_tag = "production"
}
module "webserver_02" {
source = "../../modules/windows-webserver"
vm_name = "webserver02"
vm_size = "Standard_D4s_v5"
location = "East US"
resource_group_name = azurerm_resource_group.rg.name
subnet_id = azurerm_subnet.subnet.id
admin_password = var.vm_admin_password
os_disk_size_gb = 128
environment_tag = "production"
}
Terraform State Management for Windows Infrastructure
Terraform state files track the current state of provisioned infrastructure. For team environments, state must be stored remotely — never on a local workstation — so that multiple engineers and CI/CD pipelines see the same state. Use Azure Blob Storage as a remote backend for Azure-hosted Windows infrastructure:
# Create the storage account and container for state (run once manually or via bootstrap script)
az group create --name rg-terraform-state --location eastus
az storage account create --name tfstatewininfra --resource-group rg-terraform-state --sku Standard_LRS --min-tls-version TLS1_2
az storage container create --name tfstate --account-name tfstatewininfra
# Configure remote backend in terraform block (backend.tf)
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "tfstatewininfra"
container_name = "tfstate"
key = "prod/windows-servers.tfstate"
}
}
For S3-compatible storage with on-premises deployments (MinIO, NetApp StorageGRID), use the S3 backend:
terraform {
backend "s3" {
bucket = "terraform-state"
key = "prod/hyperv-vms.tfstate"
region = "us-east-1"
endpoint = "https://minio.corp.example.com"
access_key = var.minio_access_key
secret_key = var.minio_secret_key
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
force_path_style = true
}
}
Terragrunt for Multi-Environment Windows Deployments
Terragrunt is a thin wrapper around Terraform that adds DRY (Don’t Repeat Yourself) support for multi-environment deployments. Instead of maintaining separate copies of your Terraform configuration for dev, staging, and prod — each with slightly different variable values — Terragrunt inherits from a single parent configuration and overrides only what differs per environment.
Install Terragrunt:
winget install --id gruntwork-io.terragrunt
# or on Linux:
# wget https://github.com/gruntwork-io/terragrunt/releases/latest/download/terragrunt_linux_amd64
# chmod +x terragrunt_linux_amd64 && mv terragrunt_linux_amd64 /usr/local/bin/terragrunt
Structure your repository for multi-environment management:
infra/
terragrunt.hcl # Root config: remote state, common variables
prod/
terragrunt.hcl # Prod overrides: prod subscription, larger VM sizes
webservers/
terragrunt.hcl # Points to module, sets prod-specific inputs
staging/
terragrunt.hcl # Staging overrides: staging subscription, smaller VMs
webservers/
terragrunt.hcl
modules/
windows-webserver/ # Shared module used by all environments
Root terragrunt.hcl configures the remote backend with environment-specific state paths using path_relative_to_include():
# infra/terragrunt.hcl
remote_state {
backend = "azurerm"
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
config = {
resource_group_name = "rg-terraform-state"
storage_account_name = "tfstatewininfra"
container_name = "tfstate"
key = "${path_relative_to_include()}/terraform.tfstate"
}
}
inputs = {
azure_location = "East US"
common_tags = {
managed_by = "terragrunt"
repo = "github.com/corp/windows-infra"
}
}
Deploy all environments in parallel with Terragrunt’s run-all command:
# Plan all environments
cd infra
terragrunt run-all plan
# Apply all environments simultaneously (parallel execution)
terragrunt run-all apply --terragrunt-non-interactive
# Apply only a specific environment
cd infra/prod/webservers
terragrunt apply
Summary
Automating Windows Server 2022 provisioning with Terraform and PowerShell DSC creates a reproducible, auditable, and scalable infrastructure pipeline. Terraform manages VM lifecycle on Hyper-V or Azure, PowerShell DSC ensures consistent OS configuration, WinRM enables remote-exec provisioners without SSH, Terraform modules promote reuse across server roles, remote state backends prevent team conflicts, and Terragrunt eliminates configuration duplication across dev/staging/prod environments. Together these tools shift Windows infrastructure management from manual console work to version-controlled code that can be reviewed, tested, and applied consistently by any team member or CI/CD pipeline.