For managing your Azure environment via script, PowerShell is simply the best tool for several reasons. It gets updates and new commandlets for new Azure features very quickly, typically first. Its cross platform running on Linux, Windows, Azure’s Cloud Shell and macOS. It’s easy to learn. It supports a full programming model and it has a large community of support.

 

One of the advanced concepts of PowerShell is the ability to create a PowerShell module. PowerShell modules allow you to package and distribute commandlets easily. You can even make them globally available via the PowerShell Gallery which has an extensive library baked into PowerShell for finding new modules.

 

This article will help you in understanding how and why to create a PowerShell module.

Finding Your Why

As is the case with most new initiatives, it is important to ask why. What are your goals and potential outcomes? Do you really need it? PowerShell scripts (*.ps1) can be loaded as modules by "dot sourcing" them. If you are the only user of the commandlets or the commandlets are only useful for a specific use case, you may not need to go through the process of learning how to create modules.

 

The subsequent stages in this article describe a typical journey when creating your first PowerShell Module.
Create a Simple Script

Let’s take a scenario where you just need a script that will spin down all the virtual machines in a specified resource group. You heard PowerShell was a great language for managing Azure, so you start researching how to do this in PowerShell. Very quickly via search or tutorials you find out PowerShell has a lot of commandlets that make this task easier as follows:

 

Here is an example of a simple PowerShell script for spinning down VMs:

# Get a list of the virtual machines (VMs) in the resource group
$rg = 'YourResourceGroup-rg'
$vms = Get-AzResource -ResourceType 'Microsoft.Compute/virtualMachines' `
-ResourceGroupName $rg
# Loop through the list of VMs and shut them down
foreach ($vm in $vms) {
$vminfo = Get-AzVM -name $vm.ResourceName -Status;
Stop-AzVM -ResourceGroupName $rg -Name $vminfo.Name -confirm:$false `
-Force
}

 

Pretty cool. Very straight forward, only uses 3 commandlets – Get-AzResource, Get-AzVM and Stop-AzVM which can all be found in the Az module.

Move a VM to another region

Now let’s look at a complex scenario that comes up in Azure. A lot of companies start off moving a few non-critical systems to Azure across a couple of teams. They are successful but when they start the process of moving the company to Azure they realize they moved systems to different regions and need to consolidated the systems to one region for latency purposes.

 

Now you need to move a Virtual Machine (VM) from one region to another. It turns out this isn’t just an Azure portal task. In the Azure Portal you can move VMs across resource groups in a subscription but the VM is still in the same region. To move to another region, you need to deconstruct the VM, move the OS and Data disks then reconstruct the VM ensuring it will operate as before.

 

When you think of it – the VM is nothing but configuration meta-data, the key is the location of the disks and the host it runs on. Moving the disks to another region is a long running process so you need to organize your PowerShell into several functions and invoke those functions independently.

 

The outline of major steps is:

  1. Get a list of VMs
  2. Snapshot each VM’s disks
  3. Save each snapshot VM’s disks to a storage account
  4. Move the disks to the destination region’s storage account
  5. Create VMs from storage account disks

 

You have two choices: create a separate script for each function or create functions in one PowerShell script and use it like a module. It’s better to keep all these related steps in one script because they are all necessary to accomplish to overall task. Using a single PowerShell script with separate functions is the right way to factor this code.

 

PowerShell has a naming standard of using an approved verb following by an object. So when naming these functions, we will follow this rule. For the major steps of our script that we will name “Migrate-VMs.ps1” we create the following functions:

  1. Get-VMs - gets a list of VMs for a giving resource group
  2. Invoke-SnapshotVMs - Snapshot each VM’s disks
  3. Save-Snaps - Move Snapshot disks to a storage account
  4. Move-Snaps - Move Snapshot disks to a storage account in a different region
  5. New-VMs - Create VMs from storage account disks

 

By “dot-sourcing” the PowerShell script it will load the functions just like PowerShell modules and the functions will all execute in your current scope. For our migrate script we will enter the following:

. .\Migrate-VMs.ps1

 

The first “.” dot followed by a space is the “dot-sourcing” and the second dot is part of the file reference meaning current directory. You now have your commands working just like those wonderful PowerShell module commandlets and can call Get-VMs in your current scope. If you have also followed PowerShell comment based help, you can even use “Get-Help Get-VMs” to get descriptions and examples on the function.
Azure Custom RolesAnother advanced scenario that comes up in Azure is the creation of Custom Roles. As developers we are constantly challenged to deliver on the principle of least privilege access. To help solve this, Azure has a wealth of out-of-the-box roles, however, when you want to lock down specific tasks with a more fine grain authorization, Custom Roles in Azure comes to the rescue!

 

At the time of this article Azure doesn’t support creating and managing custom roles in the Azure portal except for a small subset of Azure Active Directory roles if you bought Azure Active Directory Premium.

 

PowerShell supports the ability to manage custom roles via the Az.Resources module and its role based commandlets. However, it takes a fair bit of knowledge to learn how to use them and if you go away from the subject for a while and come back you must relearn the subject and come up to speed again.

 

So we created a PowerShell script called “Manage-Roles.ps1” which wraps the Az.Resources making it easy to edit custom roles. To make it easy to share these functions (aka commandlets) we have wrapped them into a PowerShell module which allows easier sharing across our company for installing the commandlets. Installed modules also have the benefit of not having to “dot-source” load them every time.

 

Finally, by publishing our module to the PowerShell Gallery we allow everyone to benefit from the work we have done to make using custom roles easy.

 

The easiest way to create a PowerShell module is to:

  1. Rename Manage-Roles.ps1 to ManageRoles.psm1
  2. The use the PowerShell command “Import-Module .\ManageRoles.psm1 -verbose

 

The Import-Module command works great loading the commandlets but it loads them only for your current session. If you restart the PowerShell session, POOF the commandlets are gone and you will have to reload them every time.

 

Module naming convention

  • Note the naming convention difference between the PowerShell script using verb-hyphen-object and the PowerShell module using no hyphen. This is not a convention written in stone but it’s a good practice. When you write scripts, they typically are created to solve a specific task on an object much like PowerShell commandlets. Therefore, the naming convention verb-object makes sense. Modules do not execute a verb on an object. Modules are a container, a delivery mechanism, for a set of these commandlets that do. Therefore, the naming of PowerShell modules should not follow the commandlet naming convention.

Install-ModuleIf you enter Get-Help *-Module there is a commandlet called “Install-Module” which looks like it should permanently install the ManageRoles.psm1 file. However installing this will fail with no match. This is because Install-Module only installs NuGet package (*.nupkg) files that it finds in your list of PowerShell repositories. You need to use Publish-Module in order to generate the NuGet package. Here are some cheat sheet learnings and steps needed:

Learnings:

  • Import-Module is session based.
  • Install-Module makes it available to every session, but it only installs NuGet package files.
  • You can install for everyone on the server or just for you
    Scope AllUsers this will install the module's files for everyone to "C:\Program Files\WindowsPowerShell\Modules\<version>\*"
    Scope CurrentUser (default) this will install the module's files just for you to "C:\Users\<username>\Documents\WindowsPowerShell\Modules\<version>\*"
  • Publish-Module will help you create a NuGet package file (You may be asking yourself: why didn't they call it Package-Module? It’s because Package isn't an approved verb! Publish is).
  • $env:PSModulePath will give you the search order where Install-Module will look for NuGet packages.
  • *-PSRepository allows you to manage list of PSRepositories (default is just the PSGallery).

Steps to create a PowerShell moduleThese steps will register your local folder as a PSRepository and Package your PowerShell module so that Install-Module will find the package and install it. Be sure to replace <user> with your actual username directory.

  1. At the PowerShell prompt enter $env:PSModulePath , you are interested in is the 1st one:The $env:PSModulePath variable holds the locations where PowerShell looks for the installed modules. Example output:
    C:\Users\<user>\Documents\WindowsPowerShell\Modules;
    C:\Program Files\WindowsPowerShell\Modules;
    C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules;
    c:\Users\<user>\.vscode\extensions\ms-vscode.powershell-2019.11.0\modules
  2. CD or Set-Location to this directory
    CD C:\Users\<user>\Documents\WindowsPowerShell\Modules
  3. Add your local folder as a PowerShell Gallery:
    Register-PSRepository -Name LocalModules -SourceLocation
    C:\Users\<user>\Documents\WindowsPowerShell\Modules
    Get-PSRepository # see the result of what you registered

 

These steps will create the module directory and manifest file:

  1. Mkdir <module name>
    It's important that the directory name matches the module name (not including the extension “.psm1”).
  2. Copy the PSM1 file to the <module name> directory
  3. Create the manifest (.psd1) file, example:
    New-ModuleManifest -Path .\ManageVMs.psd1
  4. Edit the manifest psd1 file, uncomment and provide values for the following required properties:
    RootModule = '<module name>.psm1' # This points to local PSM1 module
    Description = 'Your module description should be succinct'
    FunctionsToExport = '*' # This will export all functions as commandlets
  5. If you create new releases you should come back to this file to update the version property.

 

These steps will create the actual module (NuGet package)

  1. Publish-Module -Name <module name> -Repository LocalModules -Force -Verbose
    This command creates a new file in your directory using the format:
    <module name>.<version>.nupkg
    Example: ManageRoles.1.1.0.nupkg
  2. You have created your first module and are ready to install it with the following commands.
    Install-Module -Name <module name> -Repository LocalModules -Verbose `
    -requiredversion 1.0 -Force -AllowClobber -Scope AllUsers

    Get-Module


    Note: If your new module doesn't show up – enter the command:
    Import-Module -Name <module name>

PowerShell Gallery (PSGallery)Now that you have gone through the process of creating a PowerShell module that you can import. If you wish to share this with the world so that others may reuse what you have created, you can publish to the PSGallery. The PSGallery is the default PowerShell location that users find new modules, download them and receive updates.

 

To do this you will need to sign up for a free account and get your -NuGetApiKey

  1. Navigate to https://www.powershellgallery.com/ and sign-in.
  2. In the upper right click on your sign-in name dropdown and choose ‘API Keys’.
  3. Click ‘Create’, fill in the form and save your key someplace safe so you remember it.

Now that you have an apikey you may publish to the PSGallery just like you published to your LocalModules repository. Example:
Publish-Module -Name ManageRoles -NuGetApiKey 'ab12cde3fgh45ijk67lmnopq8rs9tuvwxyzabcdefghij0' -requiredversion 1.1.0 -verbose

This article is a primer for successfully creating PowerShell modules. We encourage you to leave your feedback and comments on any areas you feel are inaccurate or alternative approaches you’ve discovered.

 

Happy PowerShelling!