DEV Community

Edoardo Sanna
Edoardo Sanna

Posted on • Updated on

Deploying a PowerShell module with Chocolatey

Table of contents

Introduction

During my journey towards automation, I had the chance to start using Chocolatey as the Windows package manager. Microsoft just started a new open-source project called winget, although it seems a bit in its early days to be already integrated into a production-like environment.

The deployment and configuration procedure of our .NET applications suite requires a lot of manual steps: we're talking about a complex N-tier architecture made of several IIS-based web applications, Windows services and WPF apps, etc. Any runbook or written guide may vary with the new releases and should be versioned into some automation instructions, hence the need to include the install scripts together with the binaries themselves.

Plus, the long-term maintenance of the applications also need the usage of operational tasks, often trivial (such as copying and pasting, repeating the same configuration for multiple instances of the same application, etc.) or just too repetitive to be performed manually without being intrinsically error-prone.

For both these reasons, we decided to implement an ad-hoc PowerShell module to fully manage our application suite. The PowerShell module should be used by the Chocolatey scripts when installing, upgrading or removing software, and by any operator wanting to perform maintenance tasks on the suite itself.

Note: this post was inspired by Patrick Huber's guide on using PowerShellGet and Chocolatey to manage and deploy PowerShell modules, although some details may vary on the parameters of PowerShellGet.

Creating a Chocolatey extension

Chocolatey extensions are a way to make your customized PowerShell modules available to the Chocolatey scripts. More specifically, Chocolatey installs or upgrades or uninstalls the applications using PowerShell scripts (called ChocolateyInstall, ChocolateyUninstall and ChocolateyBeforeModify - here is a table of various conditions where these scripts are called) that may be extended with additional custom-made public and private functions.

In the following paragraphs we'll assume you already have such a PowerShell module. There are multiple helpful guides on the web on how to build a PowerShell module, although I strongly recommend the Rambling Cookie Monster's post about a proper PowerShell module structure.

Creating an extension is as simple as following the create instructions on the Chocolatey website: you simply need to put all your module's files and folders, except for the root level (which is usually called with the module's name), into a new folder called extension.

This is what your Chocolatey package folder would look like:

your-ps-module
├── extension
│   ├── functions
│   │   ├── public
|   |   |   └── your-public-function.ps1
|   |   └── private
|   |       └── your-private-function.ps1
│   ├── your-ps-module.psd1
|   ├── your-ps-module.psm1
|   ├── ChocolateyInstall.ps1
|   ├── ChocolateyBeforeModify.ps1
|   └── ChocolateyUninstall.ps1
└── your-ps-module.nuspec
Enter fullscreen mode Exit fullscreen mode

Where you may recognize:

  • the .psm1 module script and the .psd1 module manifest file, containing respectively any instructions to be followed during the module import procedure and the module specifications. Although it's beyond the scope of this article, a typical PowerShell module script would look like:
# Module requirements
#Requires -Version 5.1
#Requires -RunAsAdministrator

# Export public functions
$PublicFunctionsFiles = [System.IO.Path]::Combine($PSScriptRoot, "Functions", "Public", "*.ps1")
Get-ChildItem -Path $PublicFunctionsFiles -Exclude *.tests.ps1, *profile.ps1 | ForEach-Object {
    try {
        . $_.FullName
        Write-Verbose "Exporting public function $($_.FullName)"
    }
    catch {
        Write-Warning "$($_.Exception.Message)"
    }
}

# Export private functions
$PrivateFunctionsFiles = [System.IO.Path]::Combine($PSScriptRoot, "Functions", "Private", "*.ps1")
Get-ChildItem -Path $PrivateFunctionsFiles -Exclude *.tests.ps1, *profile.ps1 | ForEach-Object {
    try {
        . $_.FullName
        Write-Verbose "Exporting private function $($_.FullName)"
    }
    catch {
        Write-Warning "$($_.Exception.Message)"
    }
}
Enter fullscreen mode Exit fullscreen mode
  • the functions\ folder, hosting multiple public and private functions, exported in the shell session by the main .psm1 module script - as described in the previous point.
  • the Chocolatey*.ps1 scripts, with the install, upgrade and uninstall instructions when using Chocolatey
  • the .nuspec file, containing the Chocolatey package's specifications such as title, description, author, version, dependencies, etc. Remember that some items in the spec file are mandatory and needs to be filled: you may find a quick guide here. Below my example (notice the <files> section):
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2015/06/nuspec.xsd">
  <metadata>
    <id>your-ps-module.extension</id>
    <version>0.0.1</version>
    <title>your-ps-module</title>
    <authors>You, my friend</authors>
    <copyright>If any!</copyright>
    <summary>Your marvelous PowerShell module</summary>
    <description>A longer description of your marvelous PowerShell module</description>
    <dependencies>
      <dependency id="any-other-dependency" version="the.package.version"/>
    </dependencies>
  </metadata>
  <files>
    <!-- this section controls what actually gets packaged into the Chocolatey package -->
    <file src="extension\**" target="extension" />
  </files>
</package>
Enter fullscreen mode Exit fullscreen mode

Installing the extension

So, if you have a PowerShell module already, you can quickly pack it into a Chocolatey extension, then put it in your package source and install it with Chocolatey like any other package by using:

choco install your-ps-module --yes --source your-source
Enter fullscreen mode Exit fullscreen mode

Where the --yes options skips any question prompted to the user during the installation, and --source overrides the default source (i.e. the Chocolatey Community Repository) - unless you want to publish in it.
The details of the choco install command are available here.

What's the purpose of installing an extension? By definition, the extension extends the functions that are natively available in Chocolatey, by making available to the Chocolatey CLI the functions in your new module.

As described in the Chocolatey documentation,

Extensions allow you to package up PowerShell functions that you may reuse across packages as a package that other packages can use and depend on. This allows you to use those same functions as if they were part of Chocolatey itself. Chocolatey loads these PowerShell modules up as part of the regular module import load that it does for built-in PowerShell modules.

This means that now you can use your custom functions in any ChocolateyInstall.ps1, ChocolateyBeforeModify.ps1 or ChocolateyUninstall.ps1 script, for any package depending on this extension! 😄

Making the module available to the current PS session

So far, the PowerShell module we developed will only be available as a Chocolatey extension.

Our scenario is a bit different - the requirement is that the custom functions should be available not only to the Chocolatey scripts, but also to the current user opening a PowerShell session and starting any administration task on the applications... basically anything from retrieving data about the applications suite architecture (such as a list of running Windows services, database instance information, or the value of specific parameters in the configuration) to performing actual operational tasks (running a database backup, executing a query, changing a set of configuration values, upgrading applications, etc.).

Some of our public and private functions include Get-SuiteWindowsService, Set-WebApplicationPool, Invoke-SqlQuery and similar stuff, used in the daily administration of the software application suite: an example of a similar module is available here.

What if we want to distribute it to any admin/operator user who would like to take advantage of the custom PowerShell functions?

As described in Microsoft Docs' official documentation about installing PowerShell modules, you just need to add the module in all the paths specified by the $Env:PSModulePath environment variable.

Warning: not all these paths are directly usable, and sometimes they shouldn't be available for installing new modules. In the following example we'll only use the Program Files location ($Env:ProgramFiles\WindowsPowershell\Modules\), to make the module available to all user accounts on the computer.

From the ChocolateyInstall.ps1 script's point of view, the extension folder is reachable by using:

$extensionDir = Split-Path $MyInvocation.MyCommand.Definition
Enter fullscreen mode Exit fullscreen mode

Therefore, we'll copy all the files in the module root folder (excluding for the Chocolatey scripts) with:

# ModuleName
$moduleName = 'your-ps-module'

# Source and destination variables
$destination = "$Env:ProgramFiles\WindowsPowerShell\Modules\$moduleName\"
$ExcludeFiles = $(Get-item "$extensionDir\*Chocolatey*.ps1").Name
$source = "$extensionDir\*"

# Copy-Item results differ depending on if destination exists or not, therefore we're creating it if non-existent
if (-not (Test-Path $destination)) { mkdir $destination | Out-Null }
Get-item $source | 
    Where-Object {$_.Name -notin $ExcludeFiles} | 
    Foreach-Object { Copy-Item $_ -Destination $destination -Force -Recurse }
Enter fullscreen mode Exit fullscreen mode

Then, we'll need to import the module itself in the open shell session, so that we'll be able to immediately use it:

# Import module in current shell session
Import-Module your-ps-module
Enter fullscreen mode Exit fullscreen mode

And finally, we want to make it available to any following session. This can be done by adding the same instructions to the current user's PowerShell $profile file, i.e. the settings that the shell should import at each startup. The following script creates the profile file if necessary and adds the import command:

# Create $profile file if it doesn't exist
if (!(Test-Path $profile)) { New-Item $profile -ItemType File}

# Add automatic Import-module to the $profile
if (!(Get-Content -Path $profile -Filter "your-ps-module")) {
    Write-Verbose "Adding automatic Import-Module in PowerShell profile file."
    Add-Content -Value "Import-Module your-ps-module" -Path $Profile
}
Enter fullscreen mode Exit fullscreen mode

The other users of the computer may run the same script, or add the Import-module your-ps-module manually in their $profile file, in order to have it available when opening any new PowerShell session.

Using the PS module

...in Chocolatey

To recall your custom functions during the install, upgrade or uninstall procedure of other Chocolatey packages, you may just add the extension among your package dependencies. Open the spec file of the package whose ChocolateyInstall.ps1 is going to use those function, and paste in the <dependencies> section:

    <dependency id="your-ps-module.extension" version="0.0.1"/>   <!-- Replace '0.0.1' with your actual module version-->
Enter fullscreen mode Exit fullscreen mode

Installing our package will return the following output:

PS C:\> $source = "Path/to/source"
PS C:\> choco install your-package --source $source --yes
Chocolatey v0.10.16-beta
2 validations performed. 1 success(es), 1 warning(s), and 0 error(s).

Validation Warnings:
 - A pending system reboot request has been detected, however, this is
   being ignored due to the current Chocolatey configuration.  If you
   want to halt when this occurs, then either set the global feature
   using:
     choco feature enable -name=exitOnRebootDetected
   or pass the option --exit-when-reboot-detected.

Installing the following packages:
your-package
By installing you accept licenses for the packages.

your-ps-module.extension v0.0.1
your-ps-module.extension package files install completed. Performing other installation steps.
Installing module your-ps-module to C:\Program Files\WindowsPowerShell\Modules ...
The PowerShell module your-ps-module was successfully installed!
 Installed/updated your-ps-module extensions.
 The install of your-ps-module.extension was successful.
  Software installed to 'C:\ProgramData\chocolatey\extensions\your-ps-module'

your-package v0.0.1
your-package package files install completed. Performing other installation steps.
...
Enter fullscreen mode Exit fullscreen mode

As you may see, according to Chocolatey's logic, the dependencies (i.e. the your-ps-module extension) are installed before proceeding with the installation of the Chocolatey package.

...in a shell session

Once your package is installed, whenever you wish to call your custom functions when operating on your applications suite just invoke them on any shell session (we automatically imported the your-ps-module by using the $profile file: otherwise, just type Import-module your-ps-module).

For instance, you may want to know the list of all the functions included in the module and available to your current session:

Get-Command -Module your-ps-module
Enter fullscreen mode Exit fullscreen mode

Or, since all the public and private functions were exported, you may just call them explicitely:

your-public-function
Enter fullscreen mode Exit fullscreen mode

Conclusion

Since your-ps-module is now deployable as a Chocolatey package, we're able to take advantage of Chocolatey's package management logic, including versioning, dependencies management, repeatable and automatic install/upgrade/uninstall procedures, and so on.

The installed PowerShell module will also be available to any user willing to administrate the software application suite via PowerShell functions, which can be stored in an internal code repository and versioned, thus simplifying the long-term maintenance and operability.

Discussion (0)