Skip to main content

Pulumi Pitfalls in C# - Part 1

· 4 min read

This is the first in a series (hopefully) of posts about some of the pitfalls I've encountered whilst writing C# code to deploy with Pulumi. This first post will cover a common mistake when mixing imperative and declarative code.

Introduction

Firstly, WTF is Pulumi? It's most known offering is the ability to write declarative Infrastructure as Code (IaC) in a variety of programming languages including Typescript, Python, Java, C# (and YAML if you really want to).

I've been using Pulumi to deploy Infrastructure for a number of years now and have personally found it refreshing to work with compared to some other IaC tools.

ARM?

Whilst Pulumi opens up a whole new way of writing IaC, it does also make for a great footgun if you're not careful. I'll share some of the common mistakes I've seen or made when writing C# code for Pulumi to run.

Mixing Imperative and Declarative Code

One of the concepts to grasp when first getting started with Pulumi is that the code you are writing is declarative, and not necessarily imperative. This means that if you were to look at a Pulumi program, you may assume that it would execute the resource creation top to bottom, however this is unlikely to be the case.

What Pulumi will do is create a dependency graph of the resources you are declaring and then execute the steps in a logical order. This ensures that resource dependencies are created in the correct order.

This works really well if you stick to declaring resources using the Pulumi provider SDKs. However, we all know that sometimes we need a little bit of fettling to get our infrastructure into the expected state which might entail mixing in a bit of imperative code.

This is where the problems may occur, as you'll find quite quickly that the order of execution is not what you expect. Below is an example pulumi program demonstrating how this may happen

Example

// Create a File
File.WriteAllText("output.txt", "Hello, Pulumi!");

// Read the File using the Command Provider
var result = new Pulumi.Command("cat", new Pulumi.CommandArgs
{
Create = OperatingSystem.IsWindows() ? "type output.txt" : "cat output.txt",
});

// Delete the File
File.Delete("output.txt");

To explain what is happening here, the expected sequence when running pulumi up is the following:

  1. Create a file called output.txt
  2. Read the file using a Pulumi resource
  3. Delete the file.

However, when you run pulumi up against this code, you will find that this is not the actual order and you'll get an error similar to below:

Diagnostics:
command:local:Command (cat):
error: exit status 1: running "type output.txt":
The system cannot find the file specified.

This is because Pulumi isn't aware of the dependency on the file existing, so the declarative code will get executed at the end of the program when it is submitted to the pulumi engine.

mind blown

The simplest approach to avoid this is to try and stick to using the Pulumi providers to allow Pulumi to manage the resources lifecycle and depdenencies. Here's an example of how this example could be refactored

Refactored Example

// Create a File using the Local Provider
var file = new File("output.txt", new FileArgs
{
FileName = "output.txt",
Content = "Hello, Pulumi!"
});

// Read the File using the Command Provider
var result = new Command("cat", new CommandArgs
{
Create = OperatingSystem.IsWindows()
? Output.Format($"type {file.Filename}")
: Output.Format($"cat {file.Filename}");
});

By refactoring our code to use a Pulumi provider to create the file and then referencing an output from the file resource, a dependency is created and Pulumi will know that the file needs to be created before the command resource is executed.

There are other ways to ways to manage ordering of imperative code like using the Apply() method to only run after the resource has been created, but this comes with it's own set of pitfalls...

Summary

When writing Pulumi programs in C#, it's recommended to try and avoid interweaving some imperative code in between Pulumi resource declarations as it can lead to unexpected results. Try and stick to using the Pulumi providers where possible to hand off the responsibility.