Pulumi — Infrastructure as Code with C# on .NET 10: Manage Cloud Like Writing Software

Posted on: 4/21/2026 8:21:27 AM

Why Infrastructure as Code Matters

In the cloud-native era, managing infrastructure manually through a web console is a serious anti-pattern. A single misclick on the AWS Console can wipe out your production database. An untracked security group change can open the door for attackers. Infrastructure as Code (IaC) solves this definitively: every infrastructure change is version-controlled, reviewed, and reproducible.

However, most .NET developers still have to learn HCL (HashiCorp Configuration Language) for Terraform or Bicep DSL for Azure — specialized languages far removed from their familiar C# ecosystem. Pulumi changes the game: you write infrastructure in plain C#, using Visual Studio/Rider with IntelliSense, NuGet packages, xUnit tests — everything you already know.

150+ Supported Cloud Providers
76% IaC Market Share (Terraform)
45% Pulumi YoY Growth
$2.1B IaC Market Size 2026

What Is Pulumi? Architecture Overview

Pulumi is an Infrastructure as Code platform that lets you define, deploy, and manage cloud infrastructure using familiar programming languages — TypeScript, Python, Go, Java, and especially C# / .NET. Unlike Terraform (which uses HCL) or Bicep (Azure-only DSL), Pulumi leverages the full power of general-purpose programming languages.

graph TB
    subgraph Developer["Developer Workspace"]
        A["C# Program
.NET 10 Project"] --> B["Pulumi SDK
NuGet Packages"] end subgraph Engine["Pulumi Engine"] B --> C["Language Host
dotnet runtime"] C --> D["Deployment Engine
Resource Graph"] D --> E["State Backend
Pulumi Cloud / S3 / Azure Blob"] end subgraph Providers["Cloud Providers"] D --> F["AWS Provider"] D --> G["Azure Native"] D --> H["Kubernetes"] D --> I["Cloudflare / GCP / ..."] end style A fill:#e94560,stroke:#fff,color:#fff style D fill:#2c3e50,stroke:#fff,color:#fff style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style F fill:#ff9800,stroke:#fff,color:#fff style G fill:#0078d4,stroke:#fff,color:#fff style H fill:#326ce5,stroke:#fff,color:#fff style I fill:#f48120,stroke:#fff,color:#fff
Pulumi architecture overview — from C# code to cloud resources

Key Insight

Pulumi does not generate Terraform or ARM templates under the hood. It has its own engine to manage the resource graph, state, and communicate directly with cloud provider APIs. This means you have full control, unconstrained by an intermediate DSL.

Pulumi vs Terraform vs Bicep: Detailed Comparison

This is the question every DevOps team faces when choosing an IaC tool. Each tool has its own strengths, and the choice depends on your team's context.

Criteria Pulumi Terraform Bicep
Language C#, TypeScript, Python, Go, Java HCL (proprietary DSL) Bicep DSL (Azure-only)
Multi-cloud ✅ AWS, Azure, GCP, K8s, 150+ providers ✅ 4800+ providers ❌ Azure only
Testing xUnit, NUnit, MSTest — unit + integration Terratest (Go), terraform test What-if preview, limited
IDE Support Full IntelliSense, refactoring, debugging HCL extension (basic) VS Code extension (good for Azure)
State Management Pulumi Cloud (managed), S3, Azure Blob, local Terraform Cloud, S3, local Azure Resource Manager (implicit)
Reusability NuGet packages, OOP, interfaces Modules (HCL) Modules (Bicep)
Learning Curve (.NET dev) ⭐ Low — familiar C# ⭐⭐⭐ Must learn HCL ⭐⭐ Must learn Bicep syntax
AI Assistant Pulumi Neo (enterprise agent) Terraform AI (beta) Copilot for Azure
Open Source ✅ Apache 2.0 ⚠️ BSL (OpenTofu fork = true OSS) ✅ MIT

When to Choose Pulumi?

.NET/C# teams that want to manage multi-cloud infrastructure without learning a new language. Especially powerful when you need complex logic (loops, conditions, async), serious testing, and code reuse via NuGet packages. If your team has already invested in Terraform and the infrastructure is stable, migration may not be worth it — Pulumi supports importing from Terraform state, but switching costs still exist.

Getting Started with Pulumi + .NET 10

Installation and Project Initialization

The Pulumi CLI works cross-platform on Windows, macOS, and Linux. After installation, you initialize a C# project with a single command:

# Install Pulumi CLI
curl -fsSL https://get.pulumi.com | sh

# Or on Windows via Chocolatey
choco install pulumi

# Initialize a C# project for Azure
pulumi new azure-csharp --name my-infra --stack dev

# Generated directory structure:
# my-infra/
# ├── Pulumi.yaml          # Project metadata
# ├── Pulumi.dev.yaml      # Stack config (dev)
# ├── Program.cs            # Entry point
# └── my-infra.csproj       # .NET project file

Real-World Example: Deploy a Web App to Azure

Here's a complete example — creating a Resource Group, App Service Plan, and Web App on Azure, all in pure C#:

Program.cs — Azure Web App Infrastructure
using Pulumi;
using Pulumi.AzureNative.Resources;
using Pulumi.AzureNative.Web;
using Pulumi.AzureNative.Web.Inputs;

return await Deployment.RunAsync(() =>
{
    var resourceGroup = new ResourceGroup("rg-production", new()
    {
        ResourceGroupName = "rg-myapp-production",
        Location = "southeastasia"
    });

    var appServicePlan = new AppServicePlan("plan-production", new()
    {
        ResourceGroupName = resourceGroup.Name,
        Kind = "Linux",
        Reserved = true,
        Sku = new SkuDescriptionArgs
        {
            Name = "P1v3",
            Tier = "PremiumV3"
        }
    });

    var webApp = new WebApp("app-production", new()
    {
        ResourceGroupName = resourceGroup.Name,
        ServerFarmId = appServicePlan.Id,
        SiteConfig = new SiteConfigArgs
        {
            LinuxFxVersion = "DOTNETCORE|10.0",
            AlwaysOn = true,
            MinTlsVersion = "1.2",
            HttpsOnly = true,
            AppSettings = new[]
            {
                new NameValuePairArgs
                {
                    Name = "ASPNETCORE_ENVIRONMENT",
                    Value = "Production"
                }
            }
        },
        HttpsOnly = true
    });

    return new Dictionary<string, object?>
    {
        ["endpoint"] = webApp.DefaultHostName
            .Apply(h => $"https://{h}"),
        ["resourceGroup"] = resourceGroup.Name
    };
});

Notice the Difference?

This is pure C# — var, new(), Dictionary, lambda expressions. No new syntax to learn. IntelliSense in Visual Studio/Rider will auto-complete every property, letting you explore the API without constantly reading documentation.

Example: AWS S3 + CloudFront CDN

Pulumi is not limited to Azure. Here's an example creating a static website hosted on AWS with an S3 bucket and CloudFront distribution:

Program.cs — AWS Static Site with CDN
using Pulumi;
using Pulumi.Aws.S3;
using Pulumi.Aws.CloudFront;
using Pulumi.Aws.CloudFront.Inputs;

return await Deployment.RunAsync(() =>
{
    var bucket = new BucketV2("site-bucket", new()
    {
        BucketPrefix = "mysite-"
    });

    var bucketWebsite = new BucketWebsiteConfigurationV2(
        "site-config", new()
    {
        Bucket = bucket.Id,
        IndexDocument = new BucketWebsiteConfigurationV2IndexDocumentArgs
        {
            Suffix = "index.html"
        },
        ErrorDocument = new BucketWebsiteConfigurationV2ErrorDocumentArgs
        {
            Key = "404.html"
        }
    });

    var oac = new OriginAccessControl("site-oac", new()
    {
        OriginAccessControlOriginType = "s3",
        SigningBehavior = "always",
        SigningProtocol = "sigv4"
    });

    var cdn = new Distribution("site-cdn", new()
    {
        Enabled = true,
        DefaultRootObject = "index.html",
        Origins = new DistributionOriginArgs[]
        {
            new()
            {
                OriginId = "s3Origin",
                DomainName = bucket.BucketRegionalDomainName,
                OriginAccessControlId = oac.Id
            }
        },
        DefaultCacheBehavior = new DistributionDefaultCacheBehaviorArgs
        {
            TargetOriginId = "s3Origin",
            ViewerProtocolPolicy = "redirect-to-https",
            AllowedMethods = new[] { "GET", "HEAD" },
            CachedMethods = new[] { "GET", "HEAD" },
            Compress = true,
            CachePolicyId = "658327ea-f89d-4fab-a63d-7e88639e58f6"
        },
        ViewerCertificate = new DistributionViewerCertificateArgs
        {
            CloudfrontDefaultCertificate = true
        },
        Restrictions = new DistributionRestrictionsArgs
        {
            GeoRestriction = new DistributionRestrictionsGeoRestrictionArgs
            {
                RestrictionType = "none"
            }
        }
    });

    return new Dictionary<string, object?>
    {
        ["cdnUrl"] = cdn.DomainName.Apply(d => $"https://{d}"),
        ["bucketName"] = bucket.Id
    };
});

Advanced Production Patterns

Component Resources — Reuse Like NuGet Packages

Pulumi's real power lies in creating abstractions with OOP. You can package a set of resources into a Component Resource, then publish it to NuGet for your entire team:

SecureWebApp.cs — Custom Component Resource
using Pulumi;
using Pulumi.AzureNative.Web;
using Pulumi.AzureNative.Web.Inputs;

public class SecureWebAppArgs : ResourceArgs
{
    [Input("resourceGroupName", required: true)]
    public Input<string> ResourceGroupName { get; set; } = null!;

    [Input("planId", required: true)]
    public Input<string> PlanId { get; set; } = null!;

    [Input("dotnetVersion")]
    public Input<string>? DotnetVersion { get; set; }

    [Input("customDomain")]
    public Input<string>? CustomDomain { get; set; }
}

public class SecureWebApp : ComponentResource
{
    [Output("endpoint")]
    public Output<string> Endpoint { get; private set; } = null!;

    [Output("principalId")]
    public Output<string> PrincipalId { get; private set; } = null!;

    public SecureWebApp(string name, SecureWebAppArgs args,
        ComponentResourceOptions? options = null)
        : base("custom:azure:SecureWebApp", name, options)
    {
        var dotnetVer = args.DotnetVersion ?? "DOTNETCORE|10.0";

        var app = new WebApp($"{name}-app", new()
        {
            ResourceGroupName = args.ResourceGroupName,
            ServerFarmId = args.PlanId,
            HttpsOnly = true,
            Identity = new ManagedServiceIdentityArgs
            {
                Type = Pulumi.AzureNative.Web.ManagedServiceIdentityType
                    .SystemAssigned
            },
            SiteConfig = new SiteConfigArgs
            {
                LinuxFxVersion = dotnetVer,
                AlwaysOn = true,
                MinTlsVersion = "1.2",
                FtpsState = "Disabled",
                Http20Enabled = true
            }
        }, new() { Parent = this });

        Endpoint = app.DefaultHostName
            .Apply(h => $"https://{h}");
        PrincipalId = app.Identity
            .Apply(i => i?.PrincipalId ?? "");

        RegisterOutputs();
    }
}

Using this component is just like using a regular class:

var api = new SecureWebApp("api-service", new()
{
    ResourceGroupName = rg.Name,
    PlanId = plan.Id,
    DotnetVersion = "DOTNETCORE|10.0"
});

var frontend = new SecureWebApp("frontend", new()
{
    ResourceGroupName = rg.Name,
    PlanId = plan.Id,
    DotnetVersion = "NODE|22-lts"
});

Stack References — Multi-Stack Architecture

In production, you typically split infrastructure into multiple stacks: networking, compute, database, monitoring. Pulumi Stack References allow stacks to reference each other's outputs:

graph LR
    subgraph Network["Stack: Networking"]
        A["VNet / VPC
Subnets
NSG / Security Groups"] end subgraph Data["Stack: Database"] B["SQL Server
Connection String
Firewall Rules"] end subgraph Compute["Stack: Compute"] C["App Service
Container Apps
Functions"] end subgraph Monitor["Stack: Monitoring"] D["Application Insights
Log Analytics
Alerts"] end A -->|"vnetId, subnetIds"| C A -->|"subnetId"| B B -->|"connectionString"| C C -->|"appId"| D style A fill:#2c3e50,stroke:#fff,color:#fff style B fill:#e94560,stroke:#fff,color:#fff style C fill:#4CAF50,stroke:#fff,color:#fff style D fill:#ff9800,stroke:#fff,color:#fff
Multi-stack architecture — each stack manages one layer, cross-referencing via Stack References
Compute Stack — referencing Network and Database stacks
var networkStack = new StackReference("org/networking/production");
var dbStack = new StackReference("org/database/production");

var vnetId = networkStack.RequireOutput("vnetId")
    .Apply(v => v.ToString()!);
var subnetId = networkStack.RequireOutput("appSubnetId")
    .Apply(v => v.ToString()!);
var connString = dbStack.RequireOutput("connectionString")
    .Apply(v => v.ToString()!);

var app = new WebApp("api", new()
{
    ResourceGroupName = rg.Name,
    ServerFarmId = plan.Id,
    VirtualNetworkSubnetId = subnetId,
    SiteConfig = new SiteConfigArgs
    {
        ConnectionStrings = new[]
        {
            new ConnStringInfoArgs
            {
                Name = "DefaultConnection",
                ConnectionString = connString,
                Type = ConnectionStringType.SQLAzure
            }
        }
    }
});

Testing Infrastructure with xUnit

One of Pulumi's biggest advantages over Terraform: you can write unit tests for infrastructure using your familiar testing framework. Pulumi provides a mocking framework to test logic without actually provisioning resources:

InfraTests.cs — Unit testing infrastructure logic
using Pulumi;
using Pulumi.Testing;

public class InfraTests
{
    [Fact]
    public async Task WebApp_Must_Use_Https()
    {
        var resources = await Testing.RunAsync<MyStack>();

        var webApps = resources
            .OfType<Pulumi.AzureNative.Web.WebApp>();

        foreach (var app in webApps)
        {
            var httpsOnly = await app.HttpsOnly
                .GetValueAsync(whenUnknown: false);
            Assert.True(httpsOnly,
                $"WebApp {app.GetResourceName()} must enforce HTTPS");
        }
    }

    [Fact]
    public async Task All_Storage_Must_Be_Encrypted()
    {
        var resources = await Testing.RunAsync<MyStack>();

        var accounts = resources
            .OfType<Pulumi.AzureNative.Storage.StorageAccount>();

        Assert.All(accounts, async account =>
        {
            var encryption = await account.Encryption
                .GetValueAsync(whenUnknown: null);
            Assert.NotNull(encryption);
        });
    }
}

Policy as Code

Beyond unit tests, Pulumi also has CrossGuard — a Policy as Code system that runs before every deployment. You write policies in C# to enforce standards: "every S3 bucket must enable encryption," "no public subnets allowed," "the 'environment' tag is mandatory." Policies run on Pulumi Cloud and block deployments on violation.

Pulumi Neo — AI Agent for Cloud Engineering

In early 2026, Pulumi launched Pulumi Neo — a specialized AI agent for infrastructure management, succeeding and surpassing the earlier Pulumi Copilot. Unlike generic AI coding assistants, Neo deeply understands infrastructure dependencies and governance policies.

What Can Pulumi Neo Do?

  • Generate infrastructure code — "Create an AKS cluster with 3 node pools, autoscaling, and Azure CNI" → Neo generates complete C# code
  • Debug deployment failures — analyzes errors and suggests fixes based on stack context
  • Cost optimization — scans running resources, suggests right-sizing and reserved instances
  • Drift detection — detects when cloud resources are changed outside of Pulumi
  • Migration assistant — automatically converts Terraform HCL to Pulumi C#

Pulumi Agent Skills for AI Coding Assistants

Since January 2026, Pulumi has released Agent Skills — integrating with Claude Code, GitHub Copilot, Cursor, VS Code, and other AI coding tools. Skills provide AI assistants with deep infrastructure pattern knowledge, producing more accurate Pulumi code compared to vanilla AI.

# Install Pulumi Agent Skills for Claude Code
pulumi plugin install skills

# In Claude Code, you can ask:
# "Create a Pulumi stack for a 3-tier architecture on Azure
#  with App Service, SQL Database, and Redis Cache"

Kubernetes with Pulumi — Beyond YAML

A particularly powerful use case for Pulumi is managing Kubernetes. Instead of writing hundreds of lines of YAML, you use C# with full type safety:

Kubernetes Deployment in C#
using Pulumi.Kubernetes.Apps.V1;
using Pulumi.Kubernetes.Core.V1;
using Pulumi.Kubernetes.Types.Inputs.Apps.V1;
using Pulumi.Kubernetes.Types.Inputs.Core.V1;
using Pulumi.Kubernetes.Types.Inputs.Meta.V1;

var appLabels = new InputMap<string> { { "app", "my-api" } };

var deployment = new Deployment("api-deployment", new DeploymentArgs
{
    Spec = new DeploymentSpecArgs
    {
        Replicas = 3,
        Selector = new LabelSelectorArgs
        {
            MatchLabels = appLabels
        },
        Template = new PodTemplateSpecArgs
        {
            Metadata = new ObjectMetaArgs { Labels = appLabels },
            Spec = new PodSpecArgs
            {
                Containers = new ContainerArgs[]
                {
                    new()
                    {
                        Name = "api",
                        Image = "myregistry.azurecr.io/api:latest",
                        Ports = new ContainerPortArgs[]
                        {
                            new() { ContainerPortValue = 8080 }
                        },
                        Resources = new ResourceRequirementsArgs
                        {
                            Requests =
                            {
                                { "cpu", "250m" },
                                { "memory", "256Mi" }
                            },
                            Limits =
                            {
                                { "cpu", "500m" },
                                { "memory", "512Mi" }
                            }
                        }
                    }
                }
            }
        }
    }
});

var service = new Service("api-service", new ServiceArgs
{
    Spec = new ServiceSpecArgs
    {
        Selector = appLabels,
        Ports = new ServicePortArgs[]
        {
            new() { Port = 80, TargetPort = 8080 }
        },
        Type = "LoadBalancer"
    }
});

CI/CD Integration

Pulumi integrates seamlessly into CI/CD pipelines. Here's a typical GitHub Actions workflow:

graph LR
    A["git push"] --> B["PR Created"]
    B --> C["pulumi preview
Show diff"] C --> D["Code Review
+ Infra Review"] D --> E["Merge to main"] E --> F["pulumi up
Apply changes"] F --> G["Stack Outputs
Update DNS/Config"] style A fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style C fill:#ff9800,stroke:#fff,color:#fff style F fill:#4CAF50,stroke:#fff,color:#fff style G fill:#2c3e50,stroke:#fff,color:#fff
Pulumi in CI/CD — preview on PR, deploy on merge
.github/workflows/infra.yml
name: Infrastructure
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  id-token: write    # OIDC auth
  contents: read

jobs:
  preview:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'
      - uses: pulumi/actions@v6
        with:
          command: preview
          stack-name: org/dev
          comment-on-pr: true
        env:
          PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
          ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

  deploy:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'
      - uses: pulumi/actions@v6
        with:
          command: up
          stack-name: org/production
        env:
          PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
          ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

Secrets Management and Security

Pulumi has built-in secrets management — every secret is encrypted in the state file. You never store plaintext passwords in code or config:

# Set a secret for the stack
pulumi config set --secret dbPassword "S3cur3P@ss!"

# In C# code, read the secret
var cfg = new Config();
var dbPassword = cfg.RequireSecret("dbPassword");
// Output<string> — always masked in logs

Pulumi supports multiple encryption providers: Pulumi Cloud (default), AWS KMS, Azure Key Vault, Google Cloud KMS, or passphrase-based encryption for self-hosted state.

Security Note

When using Pulumi with CI/CD, always use OIDC authentication (OpenID Connect) instead of static credentials. GitHub Actions, GitLab CI, and Azure DevOps all support OIDC — no need to store long-lived access keys as secrets. Pulumi ESC (Environments, Secrets, and Configuration) helps centralize secrets management across stacks.

Migrating from Terraform to Pulumi

If your team already has a Terraform codebase, Pulumi provides migration tools:

# Convert Terraform HCL to Pulumi C#
pulumi convert --from terraform --language csharp

# Import existing Terraform state
pulumi import --from terraform ./terraform.tfstate

# Pulumi also has a Terraform Bridge — wrap any
# Terraform provider as a Pulumi provider

Migration typically happens in phases: start using Pulumi for new resources, then gradually import existing resources from Terraform state. No big-bang migration needed.

Automation API — IaC Inside Application Code

This is a unique feature only Pulumi offers: the Automation API lets you embed the Pulumi engine inside your application code. Use cases include multi-tenant SaaS that automatically provisions infrastructure per tenant, internal developer platforms, or self-service portals.

Automation API — Provision a stack from application code
using Pulumi.Automation;

// Create or select a stack
var stack = await LocalWorkspace.CreateOrSelectStackAsync(
    new InlineProgramArgs("org", "tenant-infra", "tenant-123",
        PulumiFn.Create(() =>
        {
            var rg = new ResourceGroup($"rg-tenant-123");
            var db = new Database($"db-tenant-123", new()
            {
                ResourceGroupName = rg.Name,
                // ... tenant-specific config
            });
            return new Dictionary<string, object?>
            {
                ["connectionString"] = db.ConnectionString
            };
        })));

// Preview first
var preview = await stack.PreviewAsync();
Console.WriteLine($"Changes: {preview.ChangeSummary}");

// Deploy
var result = await stack.UpAsync();
var connStr = result.Outputs["connectionString"].Value;
Console.WriteLine($"Tenant DB: {connStr}");

Pulumi 2026 Roadmap

January 2026
Pulumi Agent Skills — integrate AI coding assistants (Claude Code, Copilot, Cursor) with deep infrastructure knowledge
February 2026
Pulumi Neo GA — AI agent managing the full lifecycle: provision, govern, and optimize infrastructure at enterprise scale
Q1 2026
Scheduled Deployments — automate operations (night scale-down, refresh, destroy dev stacks at end of day) to optimize costs
Q2 2026
Pulumi ESC v2 — Environments, Secrets, and Configuration upgrade with dynamic credentials, OIDC everywhere, and HashiCorp Vault integration

Conclusion

Pulumi brings a paradigm shift for .NET developers: infrastructure IS software. Instead of learning a new DSL, you leverage your entire C# knowledge — from OOP and generics to testing and the NuGet ecosystem — to manage cloud infrastructure. With the arrival of Pulumi Neo and Agent Skills, the line between "writing application code" and "writing infrastructure code" continues to blur.

If you're a .NET developer starting your IaC journey, Pulumi is the most natural choice. If your team already uses Terraform successfully, consider using Pulumi for new projects rather than a big-bang migration. In either case, the "infrastructure as software" mindset will elevate how you design and operate cloud systems.

Quick Start

Visit pulumi.com/docs/iac/get-started to create your first project in 10 minutes. Pulumi has an unlimited free tier for individual developers, including state management on Pulumi Cloud and 1 stack per project.

References