Introduction
In 2026, PowerShell 7+ remains the go-to tool for cross-platform automation in DevOps and sysadmin roles. Creating an advanced module lets you reuse complex code, such as parallel system service management, object-oriented classes, and unit tests with Pester. Why is it essential? Modules encapsulate expert features (parallelism via ForEach-Object -Parallel, strict parameter validation, built-in logging), making your scripts scalable and maintainable in CI/CD pipelines. This tutorial walks you through building a complete 'ServiceManager' module: multi-server monitoring, secure parallel restarts, and JSON reports. By the end, you'll deploy it to PowerShell Gallery. Ideal for managing fleets of 100+ machines with zero downtime. (118 words)
Prerequisites
- PowerShell 7.4+ installed (via winget or PSResourceGet)
- Visual Studio Code with PowerShell extension
- Pester 5+ module (
Install-Module Pester -Force) - Advanced knowledge of pipelines, splatting, and error handling
- Admin rights to test system services
Initialize the Module Structure
$ModuleName = 'ServiceManager'
$ModulePath = "$PSScriptRoot/$ModuleName"
# Create the module folder
New-Item -Path $ModulePath -ItemType Directory -Force
# Generate the manifest
New-ModuleManifest -Path "$ModulePath/$ModuleName.psd1" -RootModule "$ModuleName.psm1" -Author 'Expert Dev' -Description 'Module avancé pour gestion services' -PowerShellVersion 7.0 -RequiredModules @() -FunctionsToExport @('Get-ServiceStatus', 'Start-ServiceParallel') -AliasesToExport @()
# Create the main file empty
New-Item -Path "$ModulePath/$ModuleName.psm1" -ItemType File -Force
Write-Output "Module $ModuleName initialisé dans $ModulePath"This script sets up the standard module structure: folder, PSD1 manifest with explicit exports, and main PSM1 file. The manifest defines SEO metadata for the Gallery, avoiding pitfalls like implicit exports that pollute the global namespace. Run it as admin to test.
Understanding the Manifest and Exports
The PSD1 file acts as a contract: it lists exported functions, required modules, and minimum versions. Without FunctionsToExport, your functions stay private. Add PrivateData for PSResourceGet compatibility in 2026.
Implement Basic Advanced Functions
function Get-ServiceStatus {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$ServiceName,
[ValidateSet('Running', 'Stopped', 'All')]
[string]$Status = 'All'
)
process {
foreach ($name in $ServiceName) {
$svc = Get-Service -Name $name -ErrorAction SilentlyContinue
if ($svc) {
[PSCustomObject]@{
Name = $svc.Name
Status = $svc.Status
StartType = $svc.StartType
} | Where-Object { $Status -eq 'All' -or $_.Status -eq $Status }
}
}
}
}
export-modulemember -Function Get-ServiceStatusThis advanced function supports pipelining with ValueFromPipeline, strict validation, and PSCustomObject for structured output. The process {} block enables streaming, crucial for large volumes; ErrorAction SilentlyContinue prevents crashes on missing services.
Pipeline and Parameter Validation
[ValidateSet] restricts inputs, while [CmdletBinding()] enables splatting. Always use PSCustomObject for consistent objects compatible with Export-Csv or JSON.
Add Classes for Object-Oriented Modeling
class ServiceReport {
[string]$Name
[ServiceControllerStatus]$Status
[ServiceStartMode]$StartType
[DateTime]$LastCheck
[string]$ErrorMessage
ServiceReport([string]$name, [ServiceControllerStatus]$status, [ServiceStartMode]$startType) {
$this.Name = $name
$this.Status = $status
$this.StartType = $startType
$this.LastCheck = Get-Date
}
[void] AddError([string]$msg) {
$this.ErrorMessage = $msg
}
}
function Get-ServiceStatus {
# ... code précédent ...
process {
foreach ($name in $ServiceName) {
try {
$svc = Get-Service -Name $name -ErrorAction Stop
[ServiceReport]::new($svc.Name, $svc.Status, $svc.StartType)
}
catch {
[ServiceReport]::new($name, 'Error', 'Unknown') | ForEach-Object { $_.AddError($_.Exception.Message); $_ }
}
}
}
}PowerShell 5+ classes encapsulate logic (constructors, methods). Here, ServiceReport handles errors with try/catch, making the module robust. Append to the existing PSM1; it overrides the previous function for OO support without breaking compatibility.
Benefits of Classes in PowerShell
Classes provide IntelliSense in VSCode, inheritance, and calculated properties. Ideal for modeling complex entities like aggregated reports.
Implement Parallelism with Jobs
function Start-ServiceParallel {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$ServiceName,
[int]$ThrottleLimit = 5,
[scriptblock]$InitScriptBlock = { }
)
begin {
$InitScriptBlock.Invoke()
}
process {
$ServiceName | ForEach-Object -Parallel {
$using:ThrottleLimit
try {
$svc = Get-Service -Name $_ -ErrorAction Stop
if ($svc.Status -ne 'Running') {
Start-Service -Name $_ -ErrorAction Stop
"Started $_"
}
}
catch {
"Error starting $_ : $($_.Exception.Message)"
}
} -ThrottleLimit $ThrottleLimit
}
}
export-modulemember -Function Start-ServiceParallelForEach-Object -Parallel (PS7+) runs in .NET threads, using $using: for parent variables. SupportsShouldProcess adds -WhatIf/-Confirm. ThrottleLimit prevents CPU overload; test on 10+ services.
Managing Parallelism and Throttling
In 2026, native parallelism is powerful but memory-intensive; always use $using: and limit to 10-20 threads. Add logging with Write-Verbose.
Create Unit Tests with Pester 5
$Here = Split-Path -Parent $PSCommandPath
Import-Module "$Here/../ServiceManager/ServiceManager.psm1" -Force
Describe 'Get-ServiceStatus' {
It 'Retourne un rapport pour service existant' {
$result = Get-ServiceStatus -ServiceName 'BITS'
$result.Status | Should -Be 'Running'
$result | Should -BeOfType ServiceReport
}
It 'Gère les erreurs gracefully' {
$result = Get-ServiceStatus -ServiceName 'NonExistant'
$result.ErrorMessage | Should -Not -BeNullOrEmpty
}
}
Describe 'Start-ServiceParallel' {
It 'Supporte WhatIf' {
{ Start-ServiceParallel -ServiceName 'BITS' -WhatIf } | Should -Not -Throw
}
}
Pester 5 uses modern Should assertions. Place in tests/ and run Invoke-Pester. Covers happy paths, errors, and flags. Integrate with CI using GitHub Actions.
Build and Deployment Script
$ModuleName = 'ServiceManager'
$ModulePath = "$PSScriptRoot/$ModuleName"
# Build: test and pack
Import-Module Pester -PassThru | Install-Module -Force
Invoke-Pester -Path "$ModulePath/tests" -Output Detailed
# Publish with PSResourceGet (2026 standard)
$manifestPath = "$ModulePath/$ModuleName.psd1"
Publish-Module -Path $ModulePath -NuGetApiKey $env:PSGALLERY_APIKEY -Repository PSGallery
Write-Output "Module publié ! Importez avec: Install-Module $ModuleName"This build script runs Pester tests then publishes to the Gallery with Publish-Module. Set $env:PSGALLERY_APIKEY; PSResourceGet replaces PackageManagement in 2026 for enhanced security.
Best Practices
- Always export explicitly in PSM1 to avoid pollution.
- Use
[CmdletBinding(SupportsShouldProcess)]for all mutating actions. - Implement logging:
Write-Error,Write-Verbose,Write-Debug. - Use semantic versioning in PSD1 and Git tags.
- Document with
.EXAMPLEinGet-Help.
Common Errors to Avoid
- Forgetting
export-modulemember: functions become invisible after import. - Parallelism without
ThrottleLimit: system overload and OOM. - Ignoring
ErrorAction: scripts break on minor errors. - Classes without try/catch: unhandled exceptions pollute output.
Next Steps
Dive deeper into Desired State Configuration (DSC) v3, PowerShell 7.5 threading model, and Azure/AWS integration. Check the official PowerShell docs. Explore our Learni training courses on automation for expert certification.