You can write your entire cloud infrastructure using C# and .NET with Pulumi, treating your cloud resources like any other object in your program.

Let’s see it in action. Imagine you want to deploy a simple AWS S3 bucket.

using Pulumi;
using Pulumi.Aws.S3;

public class MyStack : Stack
{
    public MyStack()
    {
        // Create an AWS resource (Storage Bucket)
        var bucket = new Bucket("my-csharp-bucket");

        // Export the name of the bucket
        this.BucketName = bucket.Id;
    }

    [Output]
    public Output<string> BucketName { get; set; }
}

When you run pulumi up, Pulumi’s engine interprets this C# code. It talks to the AWS API, creates an S3 bucket with the name my-csharp-bucket, and then outputs the bucket’s ID. The Bucket class is Pulumi’s representation of an AWS S3 bucket, defined in the Pulumi.Aws.S3 namespace. This isn’t just a wrapper; it’s a first-class object in your C# code.

Pulumi solves the problem of managing infrastructure in a way that feels natural to developers. Instead of learning declarative languages like Terraform’s HCL or CloudFormation, you use familiar programming languages. This means you get the full power of your chosen language: loops, conditionals, classes, functions, and your existing IDE’s refactoring and debugging tools. You can even pull in NuGet packages for shared logic or external data.

Internally, Pulumi works by executing your C# code. This code defines a tree of resources. When you run pulumi up, Pulumi serializes this resource tree into a desired state. It then compares this desired state with the current state of your cloud environment (obtained by querying cloud provider APIs). Based on the differences, Pulumi plans and executes the necessary create, update, or delete operations to reach the desired state. The Bucket constructor call isn’t just a declaration; it’s an instruction to Pulumi to manage a resource. The Id property on the bucket variable is a special Output<string> that Pulumi knows how to resolve after the bucket has been created.

The magic of Output<T> is key here. When you reference an output property of one resource as an input to another, Pulumi automatically understands the dependency and ensures resources are created in the correct order. For instance, if you wanted to create a bucket and then upload a file to it, the file upload resource would reference the BucketName output, and Pulumi would wait for the bucket to exist before attempting the upload. This avoids the need for manual ordering or complex explicit dependency declarations in many cases.

You can also leverage C# features like dependency injection to manage configurations or complex resource relationships. For example, if you have a shared networking component, you can inject its outputs into multiple resource definitions.

Consider how you handle secrets. Pulumi’s Config class allows you to read configuration values, and it has built-in support for marking values as secret. When you run pulumi config set --secret myApiKey my-super-secret-value, this value is encrypted and stored securely. Your C# code can then read it using Config.Get("myApiKey"), and Pulumi ensures it’s decrypted only when needed and masked in the output.

The next step is managing state and understanding how Pulumi tracks your infrastructure’s lifecycle.

Want structured learning?

Take the full Pulumi course →