1. Contain Yourself - Long-Running PowerShell Scripts in Containers

This chapter focuses on using a PowerShell container as a runtime environment for long-running PowerShell scripts. It covers the basics of running a Linux-based PowerShell container, setting up the container environment, and executing the long-running scripts.

Creating a consistent runtime environment is essential for executing long-running PowerShell scripts. A container is a lightweight, standalone, executable software package that includes everything needed to run an application: code, runtime, system tools, system libraries, and settings. Utilizing a container creates a portable runtime environment for long-running scripts that can run on Windows and Linux-based hosts.

Container environments isolate long-running scripts, allowing multiple containers to run on the same host. This process isolation allows for running different versions of the script or testing different PowerShell modules and dependencies.

This chapter assumes you are already at a moderate skill level with PowerShell and have a basic knowledge of Docker containers. The below commands require a Windows 10 operating system and a working internet connection.

Getting Started

Installing Docker Desktop

To get started, install the Windows version of Docker Desktop Community Edition. For this chapter, please ensure that Docker Desktop is running in the legacy Hyper-V mode. Full command line documentation for Docker Desktop is available on the Docker website.

Downloading a Container Image

To run a container, you must first download the container image to the host machine.

From the host machine, open PowerShell and enter the following command to download the PowerShell container image from Docker Hub (no account is required):

docker pull mcr.microsoft.com/powershell

Output:

Using default tag: latest
latest: Pulling from powershell
23884877105a: Pull complete
bc38caa0f5b9: Pull complete
2910811b6c42: Pull complete
36505266dcc6: Pull complete
f43b8f1114bc: Pull complete
Digest: sha256:9a141117590e8c7cad2abf42abee5c7a21e466679525c87d7a2455ad7d37e514
Status: Downloaded newer image for mcr.microsoft.com/powershell:latest
mcr.microsoft.com/powershell:latest

If you have already pulled the PowerShell container previously, some of your notifications may have “Already exists” as their status. Enter the following command to take a look at the container images on the host machine:

docker images --format "table {{.Repository}}\t{{.ID}}"

Output:

REPOSITORY                     IMAGE ID
mcr.microsoft.com/powershell   f544cbdcb00a

Technically, it is not required to pull an image before running a container. Docker will automatically pull an image when you execute the “docker run” command. However, it is a good practice to download an image before starting it with the run command.

Running a PowerShell Container in Interactive Mode

It is now time to run a container to execute commands manually. Enter the following command in the PowerShell console:

docker run -it mcr.microsoft.com/powershell

You will see that the prompt changes, and the console displays some information.

Output:

PowerShell 7.0.2
Copyright (c) Microsoft Corporation. All rights reserved.

https://aka.ms/powershell
Type 'help' to get help.

PS />

At this console, you can run PowerShell commands as if it were running the host machine. For example, to list the running processes, execute the following command:

Get-Process | Select-Object ProcessName

Output:

ProcessName
-----------
pwsh

Notice that the output only returns a single item. Containers provide a minimal environment to execute the software they need, nothing more.

Environment Variables

Environment variables are generally system-wide settings that can be used by applications running on an operating system. These system-wide settings are critical when executing long-running scripts in standard operating systems or containers. Environment variables can be added to the system at startup or altered during runtime. List the default environment variables inside the container:

Get-ChildItem -Path Env: | Sort-Object -Property Name

Output:

Name                           Value
----                           -----
DOTNET_SYSTEM_GLOBALIZATION_I… false
HOME                           /root
HOSTNAME                       a56453c89ef3
LANG                           en_US.UTF-8
LC_ALL                         en_US.UTF-8
PATH                           /opt/microsoft/powershell/7:...
POWERSHELL_DISTRIBUTION_CHANN… PSDocker-Ubuntu-18.04
PSModuleAnalysisCachePath      /var/cache/microsoft/powersh...
PSModulePath                   /root/.local/share/powershel...
TERM                           xterm

Now exit the container. To do this, type the word “exit” in the prompt, then press the “Enter” key. The container will exit and return to the initial PowerShell prompt on the host machine.

Add an Environment Variable

To add an environment variable to a container at startup, use the “–env” parameter as a key-value pair.

docker run -it --env GolfCourse=PebbleBeach mcr.microsoft.com/powershell

Once the container starts, list the environment variables:

Get-ChildItem -Path Env: | Sort-Object -Property Name

Output:

Name                           Value
----                           -----
DOTNET_SYSTEM_GLOBALIZATION_I… false
GolfCourse                     PebbleBeach
HOME                           /root
HOSTNAME                       a56453c89ef3
LANG                           en_US.UTF-8
LC_ALL                         en_US.UTF-8
PATH                           /opt/microsoft/powershell/7:...
POWERSHELL_DISTRIBUTION_CHANN… PSDocker-Ubuntu-18.04
PSModuleAnalysisCachePath      /var/cache/microsoft/powersh...
PSModulePath                   /root/.local/share/powershel...
TERM                           xterm

List the value of the GolfCourse variable:

$env:GolfCourse

Output:

PebbleBeach

Exit the container just like before by typing “exit” and pressing the “Enter” key.

Digging Deeper into the Docker CLI

Naming a Container

Start a new container. This time, use the “–name” parameter to give it a friendly name.

docker run -it --name mypwsh mcr.microsoft.com/powershell

While the container is still running, open a second PowerShell console window. To display the running containers, execute the following command:

docker ps --format 'table {{.Image}}\t{{.Status}}\t{{.Names}}'

Output:

IMAGE                          STATUS              NAMES
mcr.microsoft.com/powershell   Up 2 seconds        mypwsh

Friendly names are easier to remember when multiple containers are running.

Obtaining the IP Address of a Running Container

Obtain the IP Address of the container by executing the following commands:

$format = '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
docker inspect --format=$format mypwsh

Output:

172.17.0.2

It is possible based on your Docker configuration that your IP Address could be different.

Return to the PowerShell console with the container prompt and exit the container. Remember, to do this, type the word “exit” in the prompt, then press the “Enter” key.

Cleaning up old containers

Sadly, Docker Desktop does not do a great job of cleaning up after itself. To view all containers run the following command:

docker ps -a --format 'table {{.Image}}\t{{.Status}}\t{{.Names}}'

To delete all old containers run the following command:

docker rm $(docker ps -a -q)

Output:

dfc7099d33cf

Setting up a Local Test Environment

For the rest of this chapter, the environment will replicate a Client-Server lab environment that includes a vCenter Simulator container and a separate PowerShell container to execute the long-running script. You can find documentation for the vCenter Simulator on GitHub. Do not modify vCenter Simulator before starting the container.

Download the vCenter Simulator container image

Run the following command to download the vCenter Simulator image, created by William Lam, from Docker Hub:

docker pull lamw/govcsim

Start the vCenter Simulator image and use “vcenter” as the container name:

docker run --rm --name vcenter -d -p 443:443 lamw/govcsim

Verify that the container is running:

docker ps -a --format 'table {{.Image}}\t{{.Status}}\t{{.Names}}\t{{.Ports}}'

Output:

IMAGE               STATUS              NAMES       PORTS
lamw/govcsim        Up 2 minutes        vcenter     0.0.0.0:443->443/tcp

Get the IP Address of the vCenter Simulator container.

$format = '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
docker inspect --format=$format vcenter

Output:

172.17.0.2

Interacting with the vCenter Simulator Container

In the same PowerShell Console window, start a new PowerShell Container:

docker run -it --name mypwsh mcr.microsoft.com/powershell

Once it starts, install the VMware.PowerCLI Module:

Install-Module -Name VMware.PowerCLI -Scope AllUsers -Force

Next, set the PowerCLI Configuration, which will allow PowerCLI to connect to a server with an invalid certificate.

$Parameters = @{
    InvalidCertificateAction='Ignore'
    Scope='AllUsers'
    ParticipateInCeip=$false
    Confirm=$false
}
Set-PowerCLIConfiguration @Parameters
Connect to the vCenter Simulator

Connect to the vCenter Simulator with PowerCLI:

Connect-VIServer -Server 172.17.0.2 -User u -Password p -Port 443

Output:

Name                           Port  User
----                           ----  ----
172.17.0.2                     443   u
Play Around with VIObjects

List the ESXi hosts:

Get-VMHost | Select-Object Name,ConnectionState,PowerState

Output:

Name      ConnectionState PowerState
----      --------------- ----------
DC0_H0          Connected  PoweredOn
DC0_C0_H0       Connected  PoweredOn
DC0_C0_H1       Connected  PoweredOn
DC0_C0_H2       Connected  PoweredOn

List the virtual machines:

Get-VM

Output:

Name                 PowerState Num CPUs MemoryGB
----                 ---------- -------- --------
DC0_H0_VM0           PoweredOn  1        0.031
DC0_H0_VM1           PoweredOn  1        0.031
DC0_C0_RP0_VM0       PoweredOn  1        0.031
DC0_C0_RP0_VM1       PoweredOn  1        0.031

Power a virtual machine off:

Get-VM -Name DC0_H0_VM0 | Stop-VM -Confirm:$false

Output:

Name                 PowerState Num CPUs MemoryGB
----                 ---------- -------- --------
DC0_H0_VM0           PoweredOff 1        0.031

Place an ESXi host in maintenance mode:

Get-VMHost DC0_C0_H1 | Set-VMHost -State Maintenance |
   Select-Object Name,ConnectionState,PowerState

Output:

Name                 ConnectionState PowerState
----                 --------------- ----------
DC0_C0_H1            Maintenance     PoweredOn

Exit the PowerShell container. To do this, type the word “exit” in the prompt, then press the “Enter” key. This will stop the PowerShell container. However, the vCenter Simulator is still running.

Stopping Non-Interactive Containers

To stop a running container when it is not in the interactive mode, use the following command:

docker kill vcenter

Output:

vcenter

Verify that the container is not running:

docker ps -a --format 'table {{.Image}}\t{{.Status}}\t{{.Names}}\t{{.Ports}}'

Output:

IMAGE               STATUS              NAMES               PORTS

Check Point

At this point, it is clear that running PowerShell in a container is very similar to executing commands on a host machine. This chapter has covered how to download, run, stop, list, and clean up old containers. The next sections will focus on building a custom PowerShell container to execute a long-running script. Before moving forward, please kill all running containers and clean them up.

Start the vCenter Simulator

For the rest of the chapter, you will interact with the vCenter Simulator container. Start the vCenter Simulator container with the following command:

docker run --rm --name vcenter -d -p 443:443 lamw/govcsim

Verify that the container is running:

docker ps -a --format 'table {{.Image}}\t{{.Status}}\t{{.Names}}\t{{.Ports}}'

Output:

IMAGE               STATUS              NAMES       PORTS
lamw/govcsim        Up 2 minutes        vcenter     0.0.0.0:443->443/tcp

Get the IP Address of the vCenter Simulator container.

$format = '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
docker inspect --format=$format vcenter

Output:

172.17.0.2

Building a Custom PowerShell Container Image

The process of building a custom container image is straightforward. It is best to create a new directory that will include any build artifacts that you require. A Dockerfile is also required. This file provides the information Docker Desktop needs to build the container image.

Create a directory

Open a new PowerShell console window and create a new directory.

New-Item -Path 'C:\psconf3' -ItemType Directory

Output:

    Directory: C:\


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----        6/17/2020  12:50 PM                psconf3

Set the console location to this new directory.

Set-Location -Path 'C:\psconf3\'

Create a Long-Running Script File

Create a new script file where you will save the long-running script code.

New-Item -Path . -Name 'myscript.ps1' -Type File

Output:

    Directory: C:\psconf3


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----        6/17/2020  12:54 PM              0 myscript.ps1

Open this file in a text editor. Included below is a long-running script that will update the status of an ESXi host to “maintenance” and then reset its status back to “connected.” Add the following code to the “myscript.ps1” file and save the file.

# My Long-Running Script

# Connect to the vCenter Simulator
$Parameters = @{
    Server = "$env:vCenter"
    User = "$env:vcUser"
    Password = "$env:vcPass"
    Port = '443'
}
# Update the PowerCLI Profile
$PowerCLIProfile = @{
    InvalidCertificateAction = 'Ignore'
    ParticipateInCeip = $false
    Scope = 'AllUsers'
    Confirm = $false
}

'Setting PowerCLI Profile Configuration'
Set-PowerCLIConfiguration @PowerCLIProfile

"Connecting to vCenter: $env:vCenter"
Connect-VIServer @Parameters
'Starting Host Maintenance Loop.'
Do {
    # Find any VMHosts in Maintenance mode
    $VMHosts = Get-VMHost | Where-Object {$_.ConnectionState -eq 'Maintenance'}

    # Remove the Host from maintenance mode
    Foreach ($VMHost in $VMHosts){
        'Setting host back to Connected:'
        Set-VMHost -VMHost $VMhost -State Connected
        $VMHost.Name
    }

    # List the Current state of all VMHosts
    Get-VMHost | Select-Object Name,ConnectionState,PowerState


    'Waiting 10 Seconds'
    Start-Sleep -Seconds 10
    # Find any VMhosts with no VM's and place them in Maintenance mode
    $VMHosts = Get-VMHost
    Foreach ($VMHost in $VMHosts) {
        $VMs = Get-VMHost -Name $VMHost | Get-VM | Measure-Object
        if ($VMs.Count -eq 0){
            'Setting VMHost to Maintenance:'
            $VMHost.Name
            Set-VMHost -VMHost $VMhost -State Maintenance
        }
    }
    # List the Current state of all VMHosts
    Get-VMHost | Select-Object Name,ConnectionState,PowerState

    'Waiting 30 Seconds.'
    Start-Sleep -Seconds 30
} while ($true)

Create a DockerFile

This Dockerfile will be used to create a custom container image.

New-Item -Path . -Name DockerFile -Type File

Output:

    Directory: C:\psconf3


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       6/17/2020   6:50 PM              0 DockerFile

Open the DockerFile in a text editor. Add the following code and save the file:

# Indicates the base image.
FROM mcr.microsoft.com/powershell

# Install the VMware.PowerCLI Module
RUN pwsh -Command "Install-Module VMware.PowerCLI -Force -Confirm:0;"

# Add the Long-Running Script to the Image in the /tmp directory
COPY myscript.ps1 /tmp

Build a Custom Image

Run the below “docker build” command to build a custom image. Notice the “-t” option, which tags the image with a friendly name.

docker build . -t myimage

Output:

Step 1/3:FROM mcr.microsoft.com/powershell
 ---> f544cbdcb00a
Step 2/3:RUN pwsh -Command "Install-Module VMware.PowerCLI -Force -Confirm:0;"
 ---> Running in 5c6f6b1d38fc
 ---> d66405ec96b4
Step 3/3 : COPY myscript.ps1 /tmp
 ---> 445fd73d4c23
Successfully built 445fd73d4c23
Successfully tagged myimage:latest

This chapter truncates the output of the build command.

After the build is complete, view container images on the host machine:

docker images --format 'table {{.Repository}}\t{{.ID}}'

Output:

REPOSITORY                     IMAGE ID
myimage                        445fd73d4c23
mcr.microsoft.com/powershell   f544cbdcb00a
lamw/govcsim                   470f3123955e

Running the Custom Container

Now run the new custom image with environment variables.

docker run -it --env vCenter=172.17.0.2 --env vcUser=u --env vcPass=p myimage

Output:

PowerShell 7.0.2
Copyright (c) Microsoft Corporation. All rights reserved.

https://aka.ms/powershell
Type 'help' to get help.

PS />

Run the following command to validate that the environment variables made it to the running container:

Get-ChildItem -Path Env:* | Sort-Object -Property Name

Output:

Name                           Value
----                           -----
DOTNET_SYSTEM_GLOBALIZATION_I… false
HOME                           /root
HOSTNAME                       a56453c89ef3
LANG                           en_US.UTF-8
LC_ALL                         en_US.UTF-8
PATH                           /opt/microsoft/powershell/7:...
POWERSHELL_DISTRIBUTION_CHANN… PSDocker-Ubuntu-18.04
PSModuleAnalysisCachePath      /var/cache/microsoft/powersh...
PSModulePath                   /root/.local/share/powershel...
TERM                           xterm
vCenter                        172.17.0.2
vcPass                         p
vcUser                         u

Run the Long-Running Script inside the Custom container

Set the location of the prompt to the “/tmp” directory:

Set-Location /tmp

Execute the “myscript.ps1” long-running script file:

./myscript.ps1

The output of this script will show a single ESXi host going from a connected state to a maintenance state and back to a connected state, over and over. The script will not end unless it encounters a fatal error. While this script may not be useful in your environment, you can understand the usefulness and simplicity of running a long-running script in a PowerShell container.

Press “Ctrl+C” to end the script. Exit the PowerShell container by typing “exit” and pressing the “Enter” key.

Removing a Custom Image

View container images on host machine:

docker images --format "table {{.Repository}}\t{{.ID}}"

Output:

REPOSITORY                     IMAGE ID
myimage                        445fd73d4c23
mcr.microsoft.com/powershell   6850488c74c6
lamw/govcsim                   470f3123955e

Enter the following command in the PowerShell prompt to delete the custom “myimage” container image:

docker rmi 445fd73d4c23 -f

Output:

Untagged: myimage:latest
Deleted: sha256:2c43c46fe9a...666198af300740d02e
Deleted: sha256:cb5c8e9d519...5c98c570ad341e4765
Deleted: sha256:eff86406b33...720360fe7817911359
Deleted: sha256:d66405ec96b...0cb372e9b7ffbf8d03
Deleted: sha256:ddf197b71b5...ce5476af8fbcff6c91

You can use the “Image ID” or the “Repository” name to remove a container image.

Create a Container to AutoRun the Long-Running Script

To have the container automatically start the long-running script, the Dockerfile needs to be modified. The default command for the PowerShell containers is to run “pwsh” rather than a script. Adding a command parameter with a script name will automatically execute the script at the startup of the container.

Overwrite the existing Dockerfile with this code.

# Indicates the base image.
FROM mcr.microsoft.com/powershell

# Install the VMware.PowerCLI Module
RUN pwsh -Command "Install-Module VMware.PowerCLI -Force -Confirm:0;"

# Add the Long-Running Script to the Image in the /tmp directory
COPY myscript.ps1 /tmp

# Start the Long-Running Script at Runtime
CMD ["pwsh", "-Command", "/tmp/myscript.ps1"]

Build the AutoRun Image

Execute the build command to create the image:

docker build . -t auto

Output:

Step 1/4:FROM mcr.microsoft.com/powershell
 ---> f544cbdcb00a
Step 2/4:RUN pwsh -Command "Install-Module VMware.PowerCLI -Force -Confirm:0;"
 ---> Running in 5c6f6b1d38fc
 ---> d66405ec96b4
Step 3/4:COPY myscript.ps1 /tmp
 ---> cb5c8e9d519c
Step 4/4:CMD ["pwsh", "-Command", "/tmp/myscript.ps1"]
 ---> Running in 39216a454f3c
Removing intermediate container 39216a454f3c
 ---> 2c43c46fe9a9
Successfully built 2c43c46fe9a9
Successfully tagged myimage:latest

This chapter truncates the output of the build command. After the build completes, run the new custom image with environment variables.

docker run -it --env vCenter=172.17.0.2 --env vcUser=u --env vcPass=p auto

You will notice that the container immediately starts to run the script. It does not open to a PowerShell prompt. Press “Ctrl+C” to exit the script and container. That will return to the host machine PowerShell prompt.

Additional Considerations

This chapter has covered the basics of running PowerShell containers with Docker Desktop on a Windows 10 operating system, concluding with executing a long-running script inside of a container. While this can prove to be a useful tool, consider your requirements before converting all scripts to run in this new environment. Keep in mind this is just another tool to add to your already complex toolbox. Consider researching more about containers, deployment methods, and management tools.

The Pro’s

Containers provide an isolated, clean environment for scripts to run. The runtime environment is portable to any host capable of running containers, meaning this solution is platform-independent. The containers can be shared within a team utilizing a shared repository, allowing all users to run identical scripts in duplicate runtime environments.

The Con’s

While the setup for containers is relatively simple, it can become overwhelming for novice users to walk into a containerized environment. Using containers potentially creates an additional layer of abstraction that is not needed. Security can play a crucial role in determining the deployment of containers, the passing of secrets, and the storage location of protected data. Logging is complex when running scripts in containers if they need to be stored long term.

Summary

Executing long-running scripts in PowerShell containers is a great way to isolate those processes in a clean and consistent runtime environment. There are many other use cases to use containers as a runtime environment, such as testing PowerShell code, multiple module versions, external service versions, building PowerShell applications, and more. Let this chapter open the door to the beginning of your container journey.