Introduction
Automating Windows Server 2016 provisioning with PowerShell and Terraform enables fully repeatable, infrastructure-as-code server deployments. PowerShell handles operating system configuration, role installation, and application setup. Terraform manages the underlying infrastructure layer — creating virtual machines on Hyper-V, Azure, or VMware — and passes configuration data to PowerShell via user data scripts or provisioners. Together they form a complete provisioning pipeline that eliminates manual server setup and ensures every deployment is identical to the last.
Architecture Overview
The provisioning pipeline works in three stages: (1) Terraform provisions the infrastructure — VM, networking, storage, DNS records; (2) cloud-init / WinRM bootstrap establishes connectivity and runs the initial setup; (3) PowerShell DSC or scripts configure the OS, roles, and applications to the desired state. Terraform’s state file tracks what has been deployed, enabling safe re-runs, updates, and eventual destruction of the environment.
Installing Terraform
# On the automation server (Linux or Windows)
# Download Terraform
wget https://releases.hashicorp.com/terraform/1.8.1/terraform_1.8.1_linux_amd64.zip
unzip terraform_1.8.1_linux_amd64.zip -d /usr/local/bin/
terraform version
# Install required Terraform providers
mkdir /tf/ws2016-deploy && cd /tf/ws2016-deploy
cat > versions.tf <= 1.8"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
}
}
EOF
Writing the Terraform Azure VM Configuration
Create a Terraform configuration to deploy a Windows Server 2016 VM in Azure:
cat > main.tf << 'EOF'
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "ws2016" {
name = "rg-ws2016-prod"
location = "uksouth"
}
resource "azurerm_virtual_network" "main" {
name = "vnet-ws2016"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.ws2016.location
resource_group_name = azurerm_resource_group.ws2016.name
}
resource "azurerm_subnet" "servers" {
name = "subnet-servers"
resource_group_name = azurerm_resource_group.ws2016.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.1.0/24"]
}
resource "azurerm_network_interface" "vm_nic" {
name = "nic-ws2016-01"
location = azurerm_resource_group.ws2016.location
resource_group_name = azurerm_resource_group.ws2016.name
ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.servers.id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_windows_virtual_machine" "ws2016" {
name = "WS2016-01"
resource_group_name = azurerm_resource_group.ws2016.name
location = azurerm_resource_group.ws2016.location
size = "Standard_D2s_v3"
admin_username = "localadmin"
admin_password = var.admin_password
network_interface_ids = [azurerm_network_interface.vm_nic.id]
os_disk {
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
disk_size_gb = 128
}
source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2016-Datacenter"
version = "latest"
}
timezone = "GMT Standard Time"
}
EOF
Bootstrapping with PowerShell via Terraform Provisioner
cat > bootstrap.tf << 'EOF'
resource "null_resource" "bootstrap_ws2016" {
depends_on = [azurerm_windows_virtual_machine.ws2016]
connection {
type = "winrm"
host = azurerm_windows_virtual_machine.ws2016.private_ip_address
user = "localadmin"
password = var.admin_password
https = true
insecure = true
timeout = "15m"
}
provisioner "remote-exec" {
inline = [
"powershell.exe -ExecutionPolicy Bypass -Command "Install-WindowsFeature Web-Server,Web-Mgmt-Tools -IncludeAllSubFeature"",
"powershell.exe -ExecutionPolicy Bypass -Command "Set-TimeZone -Id 'GMT Standard Time'"",
"powershell.exe -ExecutionPolicy Bypass -Command "Set-ExecutionPolicy RemoteSigned -Scope LocalMachine -Force""
]
}
}
EOF
PowerShell DSC for Role Configuration
Use Desired State Configuration to declaratively define the server’s configuration:
cat > /scripts/ws2016-config.ps1 << 'EOF'
Configuration WS2016BaseConfig {
param ([string[]]$NodeName = 'localhost')
Import-DscResource -ModuleName PSDesiredStateConfiguration
Import-DscResource -ModuleName xWebAdministration
Node $NodeName {
WindowsFeature IIS {
Ensure = 'Present'
Name = 'Web-Server'
IncludeAllSubFeature = $true
}
WindowsFeature DotNet45 {
Ensure = 'Present'
Name = 'NET-Framework-45-Core'
}
Service W3SVC {
Name = 'W3SVC'
State = 'Running'
StartupType = 'Automatic'
DependsOn = '[WindowsFeature]IIS'
}
Registry DisableSMBv1 {
Ensure = 'Present'
Key = 'HKLM:SYSTEMCurrentControlSetServicesLanmanServerParameters'
ValueName = 'SMB1'
ValueData = '0'
ValueType = 'Dword'
}
}
}
WS2016BaseConfig -OutputPath 'C:DSCWS2016BaseConfig'
Start-DscConfiguration -Path 'C:DSCWS2016BaseConfig' -Wait -Verbose -Force
EOF
Deploying the Full Stack
cd /tf/ws2016-deploy
# Initialise and validate
terraform init
terraform validate
terraform fmt
# Preview the deployment
terraform plan -var="admin_password=YourP@ssw0rd!" -out=tfplan
# Apply the deployment
terraform apply tfplan
# After deployment, verify outputs
terraform output
terraform show
# Scale up — add more VMs by changing count
# count = 5 in the azurerm_windows_virtual_machine resource
terraform apply -var="admin_password=YourP@ssw0rd!" -var="vm_count=5"
Storing State Remotely and Managing Lifecycle
# Configure remote state in Azure Storage for team collaboration
cat > backend.tf << 'EOF'
terraform {
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "tfstate2016prod"
container_name = "tfstate"
key = "ws2016.tfstate"
}
}
EOF
# Destroy the environment when no longer needed
terraform destroy -var="admin_password=YourP@ssw0rd!" -auto-approve
# Import existing resources into Terraform state
terraform import azurerm_resource_group.ws2016 /subscriptions/SUB-ID/resourceGroups/rg-ws2016-prod
Summary
Automating Windows Server 2016 provisioning with PowerShell and Terraform creates a repeatable, versioned, and fully automated deployment pipeline. Terraform handles infrastructure creation across Azure, Hyper-V, or VMware with declarative configuration files tracked in version control. PowerShell DSC ensures that the OS, roles, and applications converge to a known desired state on every deployment. Together they eliminate configuration drift, reduce provisioning time from hours to minutes, and make disaster recovery as simple as running a single terraform apply command.