Pulumi — Infrastructure as Code with C# on .NET 10: Manage Cloud Like Writing Software
Posted on: 4/21/2026 8:21:27 AM
Table of contents
- Why Infrastructure as Code Matters
- What Is Pulumi? Architecture Overview
- Pulumi vs Terraform vs Bicep: Detailed Comparison
- Getting Started with Pulumi + .NET 10
- Advanced Production Patterns
- Pulumi Neo — AI Agent for Cloud Engineering
- Kubernetes with Pulumi — Beyond YAML
- CI/CD Integration
- Secrets Management and Security
- Migrating from Terraform to Pulumi
- Automation API — IaC Inside Application Code
- Pulumi 2026 Roadmap
- Conclusion
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.
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
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#:
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:
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:
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
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:
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:
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
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.
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
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
Vue 3.6 Vapor Mode — Goodbye Virtual DOM, Solid.js-level Performance
Idempotency Pattern — Designing Duplicate-Proof APIs for Distributed Systems
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.