Execute Arbitrary Commands with Pulumi.Command

When you are writing Infrastructure as Code, you try very hard to stick to the providers and resources available in your chosen language, but eventually, most people will hit a point where they need to do something that they can’t do directly in their IaC language - run a script, execute an application etc. Most languages offer a feature to deal with this, Terraform has local/remote exec, and Bicep/ARM has deployment scripts.

Pulumi was a bit of an odd one out in this in not having a tool for this, but the fact that you were writing your Pulumi code in actual programming languages meant it wasn’t a huge issue; you could always write some code to do what you want as part of your deployment process, but this did have a few downsides:

  • Code was executed every time it was run, regardless of whether anything changed
  • There was no way to execute some code on a destroy operation
  • Debugging this was pretty difficult

Recently, however, Pulumi has introduced a new feature to help deal with this, Pulumi.Command. This adds a feature similar to Terraforms exec, allowing you to run local and remote commands as part of your deployment as a managed resource that gets all the benefits of being part of your Pulumi state.

Pulumi.Command is currently in preview, so caution should be used before using this in production.

The examples in the rest of this article use C#, but can be translated to any of Pulumi’s supported languages.

Executing a Local Command

Executing a local command - running the command on the same machine as your IaC code is running on just requires providing the command to be run. You can specify a command for the three operations - create, update and delete. Create is run the first time you execute the code; update runs on subsequent runs. If you don’t specify an update command, then the create command will be re-run on update. It is possible to provide all three commands or a subset (only create, delete, create and update etc.).

The example below allows us to run a PowerShell script as part of our deployment:

var command = new Command("runScript", new CommandArgs
    {
        Create = "pwsh createScript.ps1",
        Delete = "pwsh deleteScript.ps1",
        Dir  = "./scripts",
    })

This runs two scripts, one on create (and update) and one on delete. It sets the directory to run the scripts from the scripts folder.

If we need to obtain any output from the commands run, we can access the stdout or stderr streams:

return new Dictionary<string, object?>
{
    ["stdOut"] = command.Stdout
}

In this example, we are adding this as an output from the Pulumi project, but we could parse this output as appropriate in the project to use it elsewhere.

Executing a Remote Command

Rather than running your command on the same machine as your deployment code, you can run it on a remote machine. The most common reason for this is when you are deploying a VM using Pulumi and want to run a command on that machine. At present, this is only supported for Linux machines using SSH. Windows support is planned.

To run a remote command, Pulumi needs to know how to connect to the remote machine. For this, we need to create a connectionArgs object providing the hostname, port, username and either password or private key.

var connection = new Pulumi.Command.Remote.Inputs.ConnectionArgs()
{
    Host = "hostname",
    Port = 22,
    User = "username",
    Password = "password"
};

We then use this to run our remote command:

var remoteCommand = new Pulumi.Command.Remote.Command("runRemoteScripts", new Pulumi.Command.Remote.CommandArgs(
{
    Connection = connection,
    Create = "pwsh createScript.ps1",
    Delete = "pwsh deleteScript.ps1",

});

Running remote commands requires you to have connectivity via SSH from the machine you are running the deployment on to the machine you want to run the command on.

Use Cases

Now you’ve seen how it works, you can see this is a pretty flexible “escape hatch” which can allow us to run arbitrary processes or commands whilst still being part of our IaC. Some use cases include:

  • Running scripts as part of our deployment
  • Running executables to access specific functionality we want to use in our deployment that is not available in IAC
  • Undertaking additional clean-up operations on delete
  • Calling external REST APIs using Curl or similar
  • Running commands remotely on machines that you have provisioned to configure these machines further
  • Any other functionality you can’t do in IaC but that could be done at the command line