Pulumi and Typescript

Pulumi and Typescript.

Introduction

I have been working a few months with an interesting customer and using Pulumi with Typescript. You can read the first part of this journey in my previous blog post Pulumi with Typescript - First Impressions. This new blog post focuses on the Pulumi Input / Output business.

Pulumi Input / Output

The Pulumi Input / Output documentation says about Pulumi Outputs:

“All resource properties on the instance object itself are outputs. Outputs are values of type Output<T> , which behave very much like promises. This is necessary because outputs are not fully known until the infrastructure resource has actually completed provisioning, which happens asynchronously. Outputs are also how Pulumi tracks dependencies between resources.”

This makes sense. When you run infrastructure code the resources get created in the cloud provider when they get created. Typescript (Javascript) with its asynchronous programming style plays with this provisioning mechanism really well: it’s a bit like a Javascript Promise - we know the computed value is there, but not necessarily right now.

The Hard Stuff

Most of the time programming Pulumi with Typescript is quite natural even for a not-so-seasoned Typescript programmer like me. But occasionally the Pulumi Input / Output business gets a bit hairy.

Example. I had to do some refactoring. Previously a certain namespace was created in the client Pulumi stack. But then I had to make a change and create the namespace in another stack and use it in the client side.

Previously:

serviceA.createSomeResource("entityA", {
  namespace: "service-a,
...

After refactoring:

export const config = new pulumi.Config();
export const infra = new pulumi.StackReference(config.require("stack"));
...
const commonNamespace = infra.requireOutput("commonNamespace") as pulumi.Output<kubernetes.core.v1.Namespace>;
...
serviceA.createSomeResource("entityA", {
  namespace: commonNamespace.apply(v => v.metadata.name),

As you can see, we need to use apply to compute a new pulumi.Output from an existing pulumi.Output (i.e. to get the v.metadata.name). This is easy.

But then came the hard part. I needed to create some other resource B and pass it to the serviceA.createSomeResource later on. The resource B creation was in the company’s general purpose library and it required a few parameters, one of them being the namespace. So, the library accepts string as the parameter type for the namespace, but I didn’t have a string any more - it’s pulumi.Output<string> after the refactoring. Damn.

Now came the hard part. I had to do quite a lot of refactoring in the library side since it now needed to accept pulumi.Output<string>, so

instead of string:

export type ResourceBParams = {
  namespace: string;
...

to:

export type ResourceBParams = {
  namespace: pulumi.Input<string>;
...

… but creating the ResourceB required to create other stuff first. Quickly the story started to cascade…

… like change this:

export type SomeSecrets = Record<string, pulumi.Output<string>[]>;

to:

export type SomeSecrets = pulumi.Output<Record<string, pulumi.Output<string>[]>>;

… and this:

  const someSecrets = createSomeSecrets({
    namespaces: {
      [params.namespace]: [secretHelper]
    },
...

to:

  const someSecrets = createSomeSecrets({
    namespaces: pulumi.output(params.namespace).apply((namespace) => {
      return { [namespace]: [secretHelper] };
    }),

… and changing the types for the function parameters and return values of SomeSecret and SecretHelper, and so on.

… and now the return value from the library to the client was also cascaded to pulumi.Output … and I had to make some changes in the client side as well.

Conclusions

Pulumi is a great IaC tool and Typescript provides excellent developer experience with VSCode with type hints when you create your infrastructure code. Pulumi Inputs / Outputs provide excellent way to refer infrastructure resources that might get created in later time. But working with Pulumi Inputs / Outputs can get a bit hairy at times.

The writer is working at Metosin using Clojure in cloud projects. If you are interested to start a cloud or Clojure project in Finland or you are interested getting cloud or Clojure training in Finland you can contact me by sending an email to my Metosin email address or contact me via LinkedIn.

Kari Marttila