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.