How to Build PowerShell Script Modules on Windows Server 2012 R2
PowerShell script modules are the standard packaging mechanism for distributing reusable functions, workflows, and cmdlets within an organization. A script module is simply a .psm1 file — a PowerShell script that defines functions and controls which of them are exported for use by consumers of the module. Combined with a module manifest (.psd1 file), script modules support versioning, dependency declarations, and rich metadata that enables proper governance of a growing PowerShell function library.
Windows Server 2012 R2 with PowerShell 4.0 introduced module autoloading, meaning any function in a module placed in a PSModulePath directory is automatically imported the first time you call one of its exported functions — no explicit Import-Module statement required. This makes well-structured modules extremely convenient to use. This guide covers building modules from first principles: authoring .psm1 files, creating manifests, writing Comment-Based Help, controlling exports, and organizing functions for a real-world server administration module.
Prerequisites
– Windows Server 2012 R2 with PowerShell 4.0
– A text editor (PowerShell ISE, Notepad++, or Visual Studio Code)
– Administrative credentials
– Familiarity with PowerShell functions, parameters, and pipelines
Step 1: Understand Module Types and Structure
# PowerShell 4.0 supports these module types:
# Script modules (.psm1) - Functions written in PowerShell (most common)
# Binary modules (.dll) - Compiled C# cmdlets
# Manifest modules (.psd1) - Metadata file pointing to root module
# Dynamic modules - Created in memory with New-Module
# Minimum valid module: a single .psm1 file in a correctly named folder
# Module folder structure:
# C:PowerShellModules
# └── ServerAdmin ← folder name must match module name
# ├── ServerAdmin.psm1 ← root module (required)
# ├── ServerAdmin.psd1 ← manifest (recommended)
# └── Private ← optional: private helper functions
# └── Helpers.ps1
# List all available modules and their versions
Get-Module -ListAvailable | Sort-Object Name | Select-Object Name, Version, Path
# Verify PSModulePath includes your custom modules directory
$env:PSModulePath -split ";"
Step 2: Create the Module Directory Structure
# Create the module folder (name must exactly match the .psm1 filename)
$moduleName = "ServerAdmin"
$modulePath = "C:PowerShellModules$moduleName"
New-Item -Path "$modulePathPrivate" -ItemType Directory -Force
# Verify directory structure
Get-ChildItem $modulePath -Recurse
Step 3: Author the Module File with Comment-Based Help
# Create the .psm1 file with functions, private helpers, and Comment-Based Help
$psm1Content = @'
#Requires -Version 4.0
# ServerAdmin.psm1
# Module for Windows Server 2012 R2 administration utilities
# Dot-source private helper functions
. "$PSScriptRootPrivateHelpers.ps1"
function Test-ServerHealth {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline, Position = 0)]
[string[]]$ComputerName,
[Parameter()]
[PSCredential]$Credential
)
process {
foreach ($computer in $ComputerName) {
$result = [PSCustomObject]@{
ComputerName = $computer
Ping = $false
WinRM = $false
CPUPercent = $null
FreeMemGB = $null
FreeDiskC = $null
CriticalServicesOK = $false
}
$result.Ping = Test-Connection $computer -Count 1 -Quiet -ErrorAction SilentlyContinue
if ($result.Ping) {
$wmiParams = @{ ComputerName = $computer; ErrorAction = "SilentlyContinue" }
if ($Credential) { $wmiParams.Credential = $Credential }
try {
$os = Get-WmiObject Win32_OperatingSystem @wmiParams
if ($os) {
$result.WinRM = $true
$result.FreeMemGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
}
$cpu = Get-WmiObject Win32_Processor @wmiParams
if ($cpu) {
$result.CPUPercent = ($cpu | Measure-Object -Property LoadPercentage -Average).Average
}
$disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" @wmiParams
if ($disk) {
$result.FreeDiskC = "{0:N1} GB" -f ($disk.FreeSpace / 1GB)
}
$critSvcs = @("W3SVC","WinRM","EventLog") | Where-Object {
(Get-WmiObject Win32_Service -Filter "Name='$_'" @wmiParams).State -ne "Running"
}
$result.CriticalServicesOK = $critSvcs.Count -eq 0
}
catch {
Write-Warning "WMI query failed for $computer`: $_"
}
}
$result
}
}
}
function Get-DiskSpaceSummary {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$ComputerName
)
process {
foreach ($computer in $ComputerName) {
Get-WmiObject Win32_LogicalDisk -ComputerName $computer -Filter "DriveType=3" -ErrorAction SilentlyContinue |
Select-Object `
@{N="Server"; E={$computer}},
DeviceID,
@{N="TotalGB"; E={[math]::Round($_.Size / 1GB, 1)}},
@{N="UsedGB"; E={[math]::Round(($_.Size - $_.FreeSpace) / 1GB, 1)}},
@{N="FreeGB"; E={[math]::Round($_.FreeSpace / 1GB, 1)}},
@{N="PercentFree"; E={[math]::Round($_.FreeSpace / $_.Size * 100, 1)}}
}
}
}
# Control what consumers of this module can see
Export-ModuleMember -Function Test-ServerHealth, Get-DiskSpaceSummary
'@
$psm1Content | Out-File "C:PowerShellModulesServerAdminServerAdmin.psm1" -Encoding UTF8
Step 4: Create Private Helper Functions
# Create the private helpers file (not exported — only available within the module)
$helpersContent = @'
# PrivateHelpers.ps1
# Internal helper functions - NOT exported, NOT available to module consumers
function ConvertTo-ByteString {
param([long]$Bytes)
switch ($Bytes) {
{ $_ -ge 1TB } { return "{0:N2} TB" -f ($_ / 1TB) }
{ $_ -ge 1GB } { return "{0:N2} GB" -f ($_ / 1GB) }
{ $_ -ge 1MB } { return "{0:N2} MB" -f ($_ / 1MB) }
{ $_ -ge 1KB } { return "{0:N2} KB" -f ($_ / 1KB) }
default { return "$_ Bytes" }
}
}
function Write-ModuleVerbose {
param([string]$Message)
Write-Verbose "[ServerAdmin] $Message"
}
'@
$helpersContent | Out-File "C:PowerShellModulesServerAdminPrivateHelpers.ps1" -Encoding UTF8
Step 5: Create the Module Manifest
# Create a full module manifest with metadata and version
New-ModuleManifest `
-Path "C:PowerShellModulesServerAdminServerAdmin.psd1" `
-RootModule "ServerAdmin.psm1" `
-ModuleVersion "2.0.0" `
-Guid ([guid]::NewGuid().ToString()) `
-Author "IT Operations" `
-CompanyName "YourOrganization" `
-Description "Server administration utility functions for Windows Server 2012 R2" `
-PowerShellVersion "4.0" `
-ClrVersion "4.0" `
-FunctionsToExport @("Test-ServerHealth","Get-DiskSpaceSummary") `
-VariablesToExport @() `
-AliasesToExport @() `
-CmdletsToExport @() `
-Tags @("Server","Administration","Health","Disk") `
-ReleaseNotes "v2.0.0: Added Test-ServerHealth function; improved error handling in Get-DiskSpaceSummary"
# Validate the manifest
$manifest = Test-ModuleManifest "C:PowerShellModulesServerAdminServerAdmin.psd1"
Write-Host "Module: $($manifest.Name) v$($manifest.Version)"
Write-Host "Exported functions: $($manifest.ExportedFunctions.Keys -join ', ')"
Step 6: Import and Test the Module
# Import the module explicitly (or let autoloading handle it)
Import-Module ServerAdmin -Verbose
# Check imported commands
Get-Command -Module ServerAdmin
# Test the functions
Test-ServerHealth -ComputerName $env:COMPUTERNAME | Format-List
Get-DiskSpaceSummary -ComputerName $env:COMPUTERNAME | Format-Table -AutoSize
# View help for a function
Get-Help Test-ServerHealth -Full
Get-Help Test-ServerHealth -Examples
# Reload after making changes to .psm1
Remove-Module ServerAdmin -Force
Import-Module ServerAdmin -Force
Step 7: Version a Module and Maintain Multiple Versions
# PowerShell supports side-by-side module versions via version-named subdirectories
# Structure: ModuleNameModuleName.psm1
# Create version 2.0.0 in versioned directory
$versionedPath = "C:PowerShellModulesServerAdmin2.0.0"
New-Item -Path $versionedPath -ItemType Directory -Force
# Move files to versioned directory
Copy-Item "C:PowerShellModulesServerAdminServerAdmin.psm1" "$versionedPath"
Copy-Item "C:PowerShellModulesServerAdminServerAdmin.psd1" "$versionedPath"
New-Item -Path "$versionedPathPrivate" -ItemType Directory -Force
Copy-Item "C:PowerShellModulesServerAdminPrivateHelpers.ps1" "$versionedPathPrivate"
# List available versions
Get-Module -ListAvailable ServerAdmin | Select-Object Name, Version, Path
# Import a specific version
Import-Module ServerAdmin -RequiredVersion "2.0.0"
# Import minimum version
Import-Module ServerAdmin -MinimumVersion "2.0.0"
Step 8: Deploy the Module to Multiple Servers
# Deploy module from a central share to multiple servers
$servers = @("WebServer01","WebServer02","AppServer01","AppServer02")
$moduleSource = "C:PowerShellModulesServerAdmin"
$moduleDestination = "C:WindowsSystem32WindowsPowerShellv1.0ModulesServerAdmin"
Invoke-Command -ComputerName $servers -ScriptBlock {
param($Source, $Dest)
New-Item -Path $Dest -ItemType Directory -Force | Out-Null
} -ArgumentList $moduleSource, $moduleDestination
foreach ($server in $servers) {
$destPath = "\$serverC$WindowsSystem32WindowsPowerShellv1.0ModulesServerAdmin"
Copy-Item -Path $moduleSource -Destination $destPath -Recurse -Force
Write-Host "Deployed ServerAdmin module to $server"
}
# Verify deployment
Invoke-Command -ComputerName $servers -ScriptBlock {
$m = Get-Module -ListAvailable ServerAdmin
if ($m) { "Module v$($m.Version) installed on $env:COMPUTERNAME" }
else { "Module NOT found on $env:COMPUTERNAME" }
}
Step 9: Use a Module Autoload Profile for Consistent Sessions
# Create or update the PowerShell profile to autoload modules for all users
# Profile locations (in order of precedence):
# $PSHOMEMicrosoft.PowerShell_profile.ps1 <- All users, all hosts (system)
# $PSHOMEMicrosoft.PowerShellISE_profile.ps1 <- All users, ISE
# $profile.AllUsersAllHosts <- Current host variable
# $profile.CurrentUserAllHosts <- Current user, all hosts
# Create the all-users profile for all PowerShell hosts
$profilePath = "$PSHOMEMicrosoft.PowerShell_profile.ps1"
$profileContent = @'
# Corporate PowerShell Profile — loaded for all users on all PS hosts
# Do NOT add interactive or slow operations here
# Load corporate administration modules
Import-Module ServerAdmin -ErrorAction SilentlyContinue
# Set useful defaults
$ErrorActionPreference = "Continue"
$PSDefaultParameterValues["*:Encoding"] = "UTF8"
# Set location to scripts directory if it exists
if (Test-Path C:Scripts) { Set-Location C:Scripts }
'@
# Only overwrite if no profile exists yet; otherwise append
if (-not (Test-Path $profilePath)) {
$profileContent | Out-File $profilePath -Encoding UTF8
Write-Host "Created system profile at $profilePath"
} else {
Write-Host "Profile already exists at $profilePath — review manually before adding module autoloads"
}
Step 10: Document Module Changes with a Release Notes File
# Create a CHANGELOG.md file alongside the module
$changelog = @'
# ServerAdmin Module — Change Log
## [2.0.0] - 2024-01-15
### Added
- Test-ServerHealth: Tests ping, WinRM, CPU, memory, disk, and critical service status
- Get-DiskSpaceSummary: Reports fixed-disk space across multiple servers
### Changed
- Improved error handling in all functions (ErrorAction SilentlyContinue on WMI queries)
- Added Credential parameter to Test-ServerHealth for cross-domain queries
## [1.0.0] - 2023-10-01
### Added
- Initial release with Get-DiskSpaceSummary function
'@
$changelog | Out-File "C:PowerShellModulesServerAdminCHANGELOG.md" -Encoding UTF8
# Generate simple module documentation report
$module = Get-Module -ListAvailable ServerAdmin | Select-Object -First 1
$exportedFunctions = (Import-Module ServerAdmin -PassThru).ExportedFunctions.Keys
Write-Host "=== Module Documentation Report ==="
Write-Host "Module : $($module.Name)"
Write-Host "Version: $($module.Version)"
Write-Host "Exported Functions:"
foreach ($fn in $exportedFunctions) {
$help = Get-Help $fn -ErrorAction SilentlyContinue
Write-Host " - $fn`: $($help.Synopsis)"
}
Summary
Building PowerShell script modules on Windows Server 2012 R2 transforms ad-hoc scripts into organized, versioned, and discoverable libraries that the entire IT operations team can rely on. The combination of a well-structured .psm1 file, a module manifest for versioning and metadata, Comment-Based Help for every exported function, and a Private directory for internal helpers creates a maintainable module architecture that scales from a handful of functions to hundreds. Deploying modules to the system-wide Windows PowerShell Modules directory on all servers — or to a central file share added to PSModulePath via Group Policy — ensures consistent tooling across the environment, so every administrator and every automated script has access to the same, tested, version-controlled function library.