PowerShell modules provide a clean, powerful way to package, share, and maintain automation code. Instead of scattered scripts, modules help you organize your functions, enforce consistency, and deliver reliable tools to colleagues, teams, or external users.

Why write modules?

  • πŸ“¦ Reusability: Group related functions together for easy reuse across projects.
  • βš™ Maintainability: Simplify updates, debugging, and version control.
  • 🀝 Collaboration: Provide a clear API surface for team members or customers.
  • πŸš€ Distribution: Make your code installable via PowerShell Gallery, NuGet feeds, or internal repositories.
  • πŸ”’ Security and trust: With code signing, modules help ensure only verified code is executed in controlled environments.

This guide walks through setting up a clean module structure, adding documentation, packaging it for distribution, testing it with Pester, and applying code signing to protect your work and build trust with users.


πŸš€ Process Summary Link to heading

This table summarizes the steps to create a well-structured, secure, and distributable PowerShell module.

Step Action Command/Tool
1 Design structure Manual
2 Write loader + functions PowerShell
3 Create manifest New-ModuleManifest
4 Add README.md Markdown
5 Write Pester tests Pester
6 Run tests Invoke-Pester
7 Sign code Set-AuthenticodeSignature
8 Validate Test-ModuleManifest
9 Package Save-Module, nuget pack
10 Publish Publish-Module, nuget push

MyModule/
β”‚   MyModule.psm1
β”‚   MyModule.psd1
β”‚   README.md
β”‚   MyModule.nuspec
└───_public/
└───_private/
└───tests/

πŸ“„ README.md Best Practices Link to heading

Including a README.md helps users understand your module, its purpose, installation, and usage. It is especially valuable in source repositories and private NuGet feeds.

πŸ’‘ Recommended sections:

  • Overview
  • Installation instructions
  • Usage examples
  • Function list
  • Requirements
  • License and contact info

In .nuspec files for NuGet packaging, reference your README:

<metadata>
  ...
  <readme>README.md</readme>
</metadata>
<files>
  <file src="README.md" target="" />
  ...
</files>

⚠ PowerShell Gallery currently doesn’t display the readme field, but NuGet.org and many private feeds do.


βš™ Module Loader Example Link to heading

The loader script automatically dot-sources private and public function files, and exports only the intended public functions.

$moduleRoot = Split-Path -Parent $MyInvocation.MyCommand.Path;

$exportFunctions += @();

Get-ChildItem -Path (Join-Path $moduleRoot '_private') -Filter '*.ps1' -Recurse | ForEach-Object {
    . $_.FullName;
}

Get-ChildItem -Path (Join-Path $moduleRoot '_public') -Filter '*.ps1' -Recurse | ForEach-Object {
    . $_.FullName;
}

$script:exportFunction = $exportFunctions | Select-Object -Unique;
Export-ModuleMember -Function $script:exportFunction;

🌟 Example Public Function Link to heading

Here’s a minimal example of a public function file. Each public function is defined in its own file and registered for export.

$exportFunctions += 'Get-Example';

function Get-Example {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$name
    )

    Write-Output "Hello, $name";
}

πŸ§ͺ Testing with Pester Link to heading

Writing tests ensures your module functions as expected and remains reliable as it evolves.

Example test (tests/Get-Example.Tests.ps1) Link to heading

Describe 'Get-Example' {
    It 'outputs a greeting for the provided name' {
        Get-Example -name 'Steffen' | Should -Be 'Hello, Steffen';
    }

    It 'throws an error if name is missing' {
        { Get-Example } | Should -Throw;
    }
}

Run tests Link to heading

Invoke-Pester -Path .\tests

πŸ“¦ Packaging as NuGet (nupkg) Link to heading

PowerShell modules can be packaged as NuGet packages for easy sharing via PowerShell Gallery or internal feeds.

Generate manifest Link to heading

New-ModuleManifest -Path .\MyModule\MyModule.psd1 `
    -RootModule 'MyModule.psm1' `
    -ModuleVersion '1.0.0' `
    -Author 'Steffen Hoppe' `
    -Description 'Sample PowerShell module';

Package and publish Link to heading

Save-Module -Name .\MyModule -Path .\output -Force;
Publish-Module -Path .\MyModule -NuGetApiKey 'YourAPIKey' -Repository 'PSGallery';

Or with NuGet CLI:

nuget pack MyModule.nuspec -OutputDirectory .\output;
nuget push .\output\MyModule.1.0.0.nupkg -Source 'https://your-nuget-server' -ApiKey 'yourkey';

πŸ”‘ Code Signing Link to heading

Code signing adds a layer of security to your module, proving its origin and ensuring it hasn’t been tampered with after signing.

Obtain a certificate Link to heading

New-SelfSignedCertificate `
    -Type CodeSigningCert `
    -Subject 'CN=MyModuleSigner' `
    -CertStoreLocation 'Cert:\CurrentUser\My';

Sign your files Link to heading

$cert = Get-ChildItem Cert:\CurrentUser\My | Where-Object {
    $_.Subject -eq 'CN=MyModuleSigner'
};

Set-AuthenticodeSignature -FilePath .\MyModule\MyModule.psm1 -Certificate $cert;
Set-AuthenticodeSignature -FilePath .\MyModule\MyModule.psd1 -Certificate $cert;

Get-ChildItem .\MyModule\_public\*.ps1 | ForEach-Object {
    Set-AuthenticodeSignature -FilePath $_.FullName -Certificate $cert;
};

To include a timestamp:

Set-AuthenticodeSignature -FilePath .\MyModule\MyModule.psm1 `
    -Certificate $cert `
    -TimestampServer 'http://timestamp.digicert.com';

Validate Link to heading

Test-ModuleManifest -Path .\MyModule\MyModule.psd1;
Get-AuthenticodeSignature -FilePath .\MyModule\MyModule.psm1;

Considerations and Downsides Link to heading

While code signing is important, be aware of the following:

  • You need a valid code signing certificate, typically purchased from a trusted certificate authority (CA), which introduces cost and administrative effort.
  • Self-signed certificates are not trusted by others without manual configuration, limiting their use to internal or test environments.
  • Managing certificates and private keys securely adds operational complexity.
  • The signing process adds extra steps to your build or release pipeline, requiring automation for efficiency.

πŸ’‘ Tips Link to heading

  • Automate these steps using CI pipelines or build scripts.
  • Use timestamping when signing code to extend trust beyond certificate expiration.
  • Include README, LICENSE, CHANGELOG, and tests in your repository (but not in the published package).