Cloud-init – Part 5 – Running Containers

In this last part of this series (for now) we will show how to use containers to run your PowerShell/PowerCLI scripts on the deployed instances. And although technically not a ‘real‘ cloud-init post, I consider it related to Part 1Part 2 and Part3 in this series. 


In Post 4 of the cloud-init series, we ran our PowerShell/PowerCLI scripts natively in the Guest OS of the instance.

That might have an impact in some situations. When the first script installs for example the VMware PowerCLI modules, these modules will stay installed in the Guest OS. Consider this as a kind of PowerShell tattooing.

Another task that might be somewhat of a challenge is testing your PowerShell scripts against multiple environments, different module versions, different OS versions… Again, when we make changes to the Guest OS, these changes might be hard to remove.

Besides deploying a new ‘cattle’ station for each possible combination, we can also run our scripts in Docker containers. Once the script is done, we throw away the container instance. And we are back to a pristine OS.

And as it turns out, containers also offer some new features that are harder, or even impossible, to implement when running locally in the Guest OS.

Since we will be using several docker commands in the following sections, it will be useful to have the Docker Reference documentation nearby.

Install Docker


The Ubuntu OVAs we have been installing in the previous posts in this series, do not contain the docker package. We can easily include that in our YAML file under the runcmd section.


Line 33-36: Install the docker package, start it and make sure it runs after a reboot.

Line 33: If there should already be a docker package installed, this line removes that package. If no docker package is installed, the line does nothing.


The Photon OVA we use comes with docker installed. No need for any additional commands in the YAML file.

Image microsoft/powershell

In the following sections we will run a number of scripts in the microsoft/powershell container on Ubuntu and Photon. Only when there is a difference between both platforms, will the text mention the platforms.

A Simple Run

Let’s try running a simple PowerShell script in a container on the station.


Line 3: The docker command we sent to the station. The details of the different parts in this line are explained in the following table.

run --rmRun a command in a container and clean up when the container exits
microsoft/powershell:latestThe name of the Image. In this case, the latest Microsoft provided stable PowerShell image
pwsh -CThe command and argument we send to the container. In this case we start PowerShell and use the Command parameter
whoami;Get-Process;pwdA series of commands, separated by semi-columns, that will be executed by ‘pwsh’

The output will be something like this.

Notice how docker will search for and install the image (since it isn’t present). That is the output in the yellow box. The actual output of the commands we sent to pwsh, are in the green box.

The second time we run the same script, we will only see the actual output of the commands. And that’s because the image is already present.


Time to start diving into PowerCLI. For that we need to install the VMware PowerCLI modules of course.


Line 3-7: We use a here-string for the script we want to execute under pwsh in the container.

Line 9-11: And another here-string for the docker command.

Line 10: The script is running on a Windows box, hence the <CR><LF> to <LF>. The second manipulation is to convert the array of lines to a single line, where the lines are separated with a semi-column. Note that the actual conversion does not happen here.

Line 20: The script uses the ExpandString method to perform the text manipulations we described in the comments on Line 10. Also note that we don’t have to escape the dollar-signs in the $script part, since the variable substitution will not go that deep.

The result (don’t mind the funny characters for now).


When we use all the defaults in the container, we seem to end up with a simple screen that has the default 80 x 25 dimensions. For better results, we can introduce bigger screens inside the container.


Line 4: The reset -w command is a fix for an issue that Docker seems to have when a pseudo-terminal (the –tty parameter) is used. With the reset -w command, the terminal will reread the content of setupterm and adjust the lines and columns accordingly.

Line 10: The –tty parameter allocates a pseudo-terminal to the container.

Line 11: The -e parameter is used to set a number of environment variables for the container. In this case, specify that the container shall use an xterm with dimensions 25 x 135.

The changed screen is visible in the output (compare it with the width of the previous output).


With the method we just showed, we will have to incur the overhead of installing the VMware PowerCLI modules on each run, because we throw away the container after we used it.

But there is a solution for this, by using docker volumes. They are kept inside the guest OS of the instance and can be attached to each container we run.


Line 5: We have some ‘Inception’ going on here, hence the ‘`’ sequence before the dollar sign. In line 50 we substitute the variables in the bash script itself. That way we can provide the script as an inline file in the bash script. But since bash also interprets that back-tick, we have to escape it. The back-tick itself is to avoid variable substitution that is done in line 55.

Line 24: We create a docker volume named PCLIModules. If that volume already exists, the bash script will just continue.

Line 25-27: Instead of having the PowerShell/PowerCLI script in the Command parameter of the pwsh command, we use an inline file. This effectively creates a .ps1 file in the home directory of the user in the guest OS.

Line 30: The Volume parameter on the docker run command maps the docker volume PCLIModules to the folder /root/.local/share/powershell/Modules in the container. With an Install-Module, the modules will be installed in this /root/.local/share/powershell/Modules , and thus be stored on the docker volume PCLIModules. This volume can be reused on subsequent runs of the container, eliminating the need to install the VMware PowerCLI modules on each run.

Line 31: On the docker run command we map the user’s home directory (~) in the guest OS to a folder, named Scripts in the container.

Line 34: The pwsh command that runs in the container uses the File parameter to run the .ps1 file created in lines 25-27.

Line 35: The .ps1 file that was created before calling the docker run command, is now removed.

Line 55: The second substitution round. In this case we substitute primarily all non-escaped (back-tick) dollar sign in the PowerShell script (at this point in the script present as an inline file in the bash script).

The result when we run the script.

Notice how the ScriptText property contains the bash script as it is presented to the Guest OS of the instance. In there the dollar signs are escaped (with a back-slash) to avoid substitution by bash.

ANSI codes and Existing Scripts

In this section, we will tackle two subjects.

  • Eliminate those ‘funny’ characters in the output
  • How to run my existing script, similar to what we did in Part 4 – Running Scripts.

As it turns out, these ‘funny characters’ are ANSI escape codes. In the container, we are using a pseudo-terminal, and as a result, the OS in the container sends those ANSI escape codes to position the cursor, change the text colour, but also those ‘…’ (three dots) when PowerShell has to cut short the value of a property.

For now, the only way if found to eliminate those ANSI escape codes, is to find and replace them.

For using your existing scripts in this setup, we adopt the same strategy as we did in Part 4 – Running Scripts.

Our existing script, in Sample-PCLI-Script3.ps1, looks like this.

And the script to run all this looks like this


Line 3, 20, 21: We split the code into three parts. A fixed prefix, the code of our existing script and a suffix.

Line 27-31: We use the same concept of an inline file as we did in the previous section. The only difference is that we now have three parts instead of one.

Line 64-67: The ANSI escape sequences are removed from the output. The RegEx expression does not cover all possible ANSI escape sequences, but it did the job for all the ones I encountered. On the other hand, the RegEx expression can easily be expanded.

The result, which is an HTML file, looks like this.

The part where we filter away the ANSI code sequence is also used to translate some well-known characters. Remember the funny characters in the module list?

With the first -replace operator in the previous code, we can change this back to the character we are used to. The hex sequence 0xE2, 0x80, 0xA6 that get back is, in fact, the code for the horizontal ellipsis, the 3 horizontal dots PowerShell uses to indicate a cut off value.

There are most probably many other characters or character combinations that do not translate well, but at least we have a method to correct this.

InFile & OutFile

Using inline files and the stdout stream for input and results works in most cases, but it might make things needlessly complex in others. To avoid that, we can make use of the InFile and OutFile parameters on the Invoke-VMScriptPlus function. The parameters allows us to pass one or more files along with the script, and retrieve one or more files back from the environment where the script runs.

If we use InFile and OutFile together with containers, there is some Inception going on. The following schematic might make this a bit more visible.

As you can see, the InFile files are stored in the guest OS of the instance. And then through the Volume parameter on the docker command, passed along into the container.

The same goes for the OutFile files but in the reverse sense.

We are going to execute an existing script. Note how to make a small change in the path of the resulting file. We have to use the path to the Scripts folder, which will be mapped through the Volume parameter.

And this is the script that will run that script in a container.


Line 2: The name of our script we want to execute. Note that we had to change the Path of the resulting file. This to allow the mapping through the volume parameter on the docker run command.

Line 3: A filename for the script we are going to run in the container. Ultimately this could be replaced by a randomly generated filename.

Line 5-27: The full script we are sending to the container. It is comprised of the Prefix, our original script and the Suffix. The Prefix and Suffix are the same as we have shown in previous sections. The only difference is that all three parts now come together in one here-string.

Line 23: We substitute this line with our original script later on with the call to ExpandString.

Line 29-38: The actual bash script, with the docker commands, that we are running inside the Guest OS of the instance.

Line 49: We expand the $script variable and save the resulting text to a file.

Line 54: Another call to ExpandString. This time to substitute the variables in the $docker variable.

Line 56: The script file we send along to the Guest OS.

Line 57: The output file we retrieve from the Guest OS.

The script’s output looks like this. Notice that it only contains the result from the Write-Host line in our original script.

And the result, report.html, is displayed in a web browser through the Invoke-Item cmdlet.

Image vmware/powerclicore

Shortly after the new tdnf package for PowerShell was made available, a new version of the vmware/powerclicore docker image was created.

The difference with the microsoft/powershell image is the underlying OS, Photon v3R2 instead of Ubuntu 18.04.

Let’s start with a simple script, similar to the one we used earlier.

With the microsoft/powershell image this produces the following. We see the result of all three commands.

On the vmware/powerclicore image, the result is a bit different. The whoami output is there, but the output from the other two commands is missing.

As it turns out, there is an issue when passing commands to pwsh via the Command parameter. When using the File parameter everything seems to work well.

One difference to note, compared with the micorosft/powershell image, the vmware/powerclicore:latest image, and the Photon OS in that image, have minimal terminal types present. As a result, you will need to set the TERM environment variable to linux instead of xterm.

A typical call would look something like this.


Note that there is also an image for the latest PS v7 Preview available. If you want to test/develop your scripts in this PS v7 Preview, this is a simple and easy method to do it.

All previous methods discussed in this post work against this Preview.



One Comment

    Tayfun Deger

    Thanks for sharing:)

Leave a Reply

Your email address will not be published. Required fields are marked *


This site uses Akismet to reduce spam. Learn how your comment data is processed.