For .NET developers, Pulumi offers a familiar experience using C# to define infrastructure. You get the full power of .NET—LINQ, async/await, dependency injection, and the rich ecosystem—applied to infrastructure as code. This guide shows you how to manage DanubeData infrastructure with idiomatic C#.
Why Pulumi with .NET?
C# and .NET are excellent for infrastructure as code:
- Strong static typing catches errors at compile time
- IntelliSense and code completion in Visual Studio/VS Code
- Use async/await for complex provisioning workflows
- Familiar patterns: dependency injection, interfaces, generics
- Integration with existing .NET tooling and CI/CD
Getting Started
Prerequisites
- .NET 8.0 SDK or later
- Pulumi CLI
- Visual Studio 2022, VS Code, or JetBrains Rider
Install Pulumi CLI
# macOS
brew install pulumi
# Linux
curl -fsSL https://get.pulumi.com | sh
# Windows
choco install pulumi
# or
winget install pulumi
Create a New Project
# Create a new directory
mkdir my-infrastructure && cd my-infrastructure
# Initialize a new C# project
pulumi new csharp
# Add the DanubeData provider package
dotnet add package DanubeData.Pulumi
Configure Authentication
# Set your DanubeData API token
pulumi config set danubedata:apiToken your-api-token --secret
Project Structure
A well-organized Pulumi .NET project:
my-infrastructure/
├── Pulumi.yaml # Project configuration
├── Pulumi.dev.yaml # Development stack config
├── Pulumi.prod.yaml # Production stack config
├── MyInfrastructure.csproj # C# project file
├── Program.cs # Main entry point
└── Resources/
├── VpsResources.cs # VPS resources
├── DatabaseResources.cs # Database resources
├── CacheResources.cs # Cache resources
└── Components/
└── WebAppComponent.cs # Reusable components
Creating Your First VPS
// Program.cs
using Pulumi;
using DanubeData = DanubeData.Pulumi;
return await Deployment.RunAsync(() =>
{
// Create an SSH key for authentication
var sshKey = new DanubeData.SshKey("deploy-key", new()
{
Name = "deploy-key",
PublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample... deploy@example.com"
});
// Create a VPS instance
var webServer = new DanubeData.Vps("web-server", new()
{
Name = "web-server",
Image = "ubuntu-24.04",
Datacenter = "fsn1",
ResourceProfile = "small_shared", // 2 vCPU, 4GB RAM
AuthMethod = "ssh_key",
SshKeyId = sshKey.Id,
EnableIpv6 = true,
EnableBackups = true
});
// Export the VPS public IP
return new Dictionary<string, object?>
{
["webServerIp"] = webServer.PublicIp,
["webServerIpv6"] = webServer.Ipv6Address
};
});
Complete Web Application Stack
Here's a complete example using C# best practices:
// Program.cs
using Pulumi;
using DanubeData = DanubeData.Pulumi;
using System.Collections.Generic;
return await Deployment.RunAsync(() =>
{
var config = new Config();
var environment = Deployment.Instance.StackName;
var appName = config.Require("appName");
var adminIp = config.Require("adminIp");
// Get profiles based on environment
var profiles = GetResourceProfiles(environment);
// SSH Key
var sshKey = new DanubeData.SshKey("deploy-key", new()
{
Name = $"{appName}-{environment}-deploy",
PublicKey = config.RequireSecret("sshPublicKey")
});
// PostgreSQL Database
var database = new DanubeData.Database("app-db", new()
{
Name = $"{appName}-{environment}-db",
Engine = "postgresql",
EngineVersion = "16",
ResourceProfile = profiles.Database,
DatabaseName = appName.Replace("-", "_"),
Datacenter = "fsn1",
EnableSsl = true
});
// Redis Cache
var cache = new DanubeData.Cache("app-cache", new()
{
Name = $"{appName}-{environment}-cache",
CacheProvider = "redis",
ResourceProfile = profiles.Cache,
Datacenter = "fsn1"
});
// Storage Bucket
var bucket = new DanubeData.StorageBucket("app-storage", new()
{
Name = $"{appName}-{environment}-assets",
Region = "fsn1",
VersioningEnabled = true
});
var storageKeys = new DanubeData.StorageAccessKey("app-storage-key", new()
{
Name = $"{appName}-{environment}-key"
});
// Application VPS
var appServer = new DanubeData.Vps("app-server", new()
{
Name = $"{appName}-{environment}-app",
Image = "ubuntu-24.04",
Datacenter = "fsn1",
ResourceProfile = profiles.Vps,
AuthMethod = "ssh_key",
SshKeyId = sshKey.Id,
EnableIpv6 = true,
EnableBackups = environment == "prod"
});
// Firewall Rules
var webFirewall = new DanubeData.Firewall("web-firewall", new()
{
Name = $"{appName}-{environment}-web-fw",
Rules = new[]
{
new DanubeData.Inputs.FirewallRuleArgs
{
Direction = "inbound",
Protocol = "tcp",
Ports = "80",
SourceIps = new[] { "0.0.0.0/0", "::/0" },
Description = "Allow HTTP"
},
new DanubeData.Inputs.FirewallRuleArgs
{
Direction = "inbound",
Protocol = "tcp",
Ports = "443",
SourceIps = new[] { "0.0.0.0/0", "::/0" },
Description = "Allow HTTPS"
},
new DanubeData.Inputs.FirewallRuleArgs
{
Direction = "inbound",
Protocol = "tcp",
Ports = "22",
SourceIps = new[] { adminIp },
Description = "SSH from admin only"
}
}
});
// Database Firewall
var dbFirewall = new DanubeData.Firewall("db-firewall", new()
{
Name = $"{appName}-{environment}-db-fw",
Rules = new[]
{
new DanubeData.Inputs.FirewallRuleArgs
{
Direction = "inbound",
Protocol = "tcp",
Ports = "5432",
SourceIps = new InputList<string>
{
appServer.PublicIp.Apply(ip => $"{ip}/32")
},
Description = "PostgreSQL from app server"
}
}
});
// Exports
return new Dictionary<string, object?>
{
["appServerIp"] = appServer.PublicIp,
["appServerIpv6"] = appServer.Ipv6Address,
["databaseEndpoint"] = database.Endpoint,
["databaseName"] = database.DatabaseName,
["cacheEndpoint"] = cache.Endpoint,
["storageEndpoint"] = bucket.EndpointUrl,
["storageAccessKey"] = storageKeys.AccessKey,
["storageSecretKey"] = storageKeys.SecretKey
};
});
static ResourceProfiles GetResourceProfiles(string environment) => environment switch
{
"prod" => new("medium_dedicated", "medium", "medium"),
"staging" => new("small_shared", "small", "small"),
_ => new("nano_shared", "small", "micro")
};
record ResourceProfiles(string Vps, string Database, string Cache);
Using Component Resources
Create reusable C# classes for complex infrastructure:
// Resources/Components/WebAppComponent.cs
using Pulumi;
using DanubeData = DanubeData.Pulumi;
using System.Collections.Generic;
namespace MyInfrastructure.Resources.Components;
public class WebAppArgs
{
public required string Name { get; init; }
public required string Environment { get; init; }
public required Input<string> SshPublicKey { get; init; }
public string VpsProfile { get; init; } = "small_shared";
public string DbProfile { get; init; } = "small";
public string CacheProfile { get; init; } = "micro";
public bool EnableBackups { get; init; } = false;
}
public class WebApp : ComponentResource
{
public Output<string> VpsIp { get; private set; } = null!;
public Output<string> DatabaseEndpoint { get; private set; } = null!;
public Output<string> CacheEndpoint { get; private set; } = null!;
public Output<string> StorageEndpoint { get; private set; } = null!;
public DanubeData.Vps Vps { get; private set; } = null!;
public DanubeData.Database Database { get; private set; } = null!;
public DanubeData.Cache Cache { get; private set; } = null!;
public DanubeData.StorageBucket Bucket { get; private set; } = null!;
public WebApp(string name, WebAppArgs args, ComponentResourceOptions? options = null)
: base("custom:resource:WebApp", name, options)
{
var childOpts = new CustomResourceOptions { Parent = this };
// SSH Key
var sshKey = new DanubeData.SshKey($"{name}-ssh", new()
{
Name = $"{args.Name}-{args.Environment}",
PublicKey = args.SshPublicKey
}, childOpts);
// Database
Database = new DanubeData.Database($"{name}-db", new()
{
Name = $"{args.Name}-{args.Environment}-db",
Engine = "postgresql",
EngineVersion = "16",
ResourceProfile = args.DbProfile,
DatabaseName = args.Name.Replace("-", "_"),
Datacenter = "fsn1"
}, childOpts);
// Cache
Cache = new DanubeData.Cache($"{name}-cache", new()
{
Name = $"{args.Name}-{args.Environment}-cache",
CacheProvider = "redis",
ResourceProfile = args.CacheProfile,
Datacenter = "fsn1"
}, childOpts);
// Storage
Bucket = new DanubeData.StorageBucket($"{name}-storage", new()
{
Name = $"{args.Name}-{args.Environment}-assets",
Region = "fsn1"
}, childOpts);
// VPS
Vps = new DanubeData.Vps($"{name}-vps", new()
{
Name = $"{args.Name}-{args.Environment}",
Image = "ubuntu-24.04",
Datacenter = "fsn1",
ResourceProfile = args.VpsProfile,
AuthMethod = "ssh_key",
SshKeyId = sshKey.Id,
EnableBackups = args.EnableBackups
}, childOpts);
// Set outputs
VpsIp = Vps.PublicIp;
DatabaseEndpoint = Database.Endpoint;
CacheEndpoint = Cache.Endpoint;
StorageEndpoint = Bucket.EndpointUrl;
RegisterOutputs(new Dictionary<string, object?>
{
["vpsIp"] = VpsIp,
["databaseEndpoint"] = DatabaseEndpoint,
["cacheEndpoint"] = CacheEndpoint,
["storageEndpoint"] = StorageEndpoint
});
}
}
// Program.cs
using Pulumi;
using MyInfrastructure.Resources.Components;
return await Deployment.RunAsync(() =>
{
var app = new WebApp("myapp", new()
{
Name = "myapp",
Environment = "prod",
SshPublicKey = "ssh-ed25519 AAAA...",
VpsProfile = "medium_shared",
DbProfile = "medium",
EnableBackups = true
});
return new Dictionary<string, object?>
{
["appIp"] = app.VpsIp,
["dbEndpoint"] = app.DatabaseEndpoint
};
});
Creating Multiple Resources with LINQ
// Program.cs
using Pulumi;
using DanubeData = DanubeData.Pulumi;
using System.Linq;
return await Deployment.RunAsync(() =>
{
var servers = new[]
{
new { Name = "web-1", Profile = "small_shared" },
new { Name = "web-2", Profile = "small_shared" },
new { Name = "api-1", Profile = "medium_shared" },
new { Name = "worker-1", Profile = "medium_dedicated" }
};
// Create SSH key once
var sshKey = new DanubeData.SshKey("shared-key", new()
{
Name = "shared-deploy-key",
PublicKey = "ssh-ed25519 AAAA..."
});
// Create all servers using LINQ
var vpsInstances = servers.Select(server =>
new DanubeData.Vps(server.Name, new()
{
Name = server.Name,
Image = "ubuntu-24.04",
Datacenter = "fsn1",
ResourceProfile = server.Profile,
AuthMethod = "ssh_key",
SshKeyId = sshKey.Id
})
).ToArray();
// Export all IPs
return servers.Zip(vpsInstances).ToDictionary(
pair => $"{pair.First.Name}Ip",
pair => (object?)pair.Second.PublicIp
);
});
Serverless Containers
// Resources/ServerlessResources.cs
using Pulumi;
using DanubeData = DanubeData.Pulumi;
namespace MyInfrastructure.Resources;
public static class ServerlessResources
{
public static (DanubeData.Serverless Api, DanubeData.Serverless Web) Create(
Output<string> databaseEndpoint,
Output<string> cacheEndpoint)
{
// Deploy from Docker image
var apiService = new DanubeData.Serverless("api", new()
{
Name = "api-service",
DeploymentType = "image",
ImageUrl = "ghcr.io/myorg/api:latest",
Port = 8080,
MinInstances = 0, // Scale to zero
MaxInstances = 10,
Memory = 512,
Cpu = 0.5,
EnvVars = new InputMap<string>
{
["NODE_ENV"] = "production",
["DATABASE_URL"] = databaseEndpoint,
["REDIS_URL"] = cacheEndpoint
}
});
// Deploy from Git repository
var webApp = new DanubeData.Serverless("web", new()
{
Name = "web-app",
DeploymentType = "git",
GitUrl = "https://github.com/myorg/web-app.git",
GitBranch = "main",
Port = 3000,
MinInstances = 1,
MaxInstances = 5,
BuildCommand = "npm run build",
StartCommand = "npm start"
});
return (apiService, webApp);
}
}
Using Data Sources
// DataSources.cs
using Pulumi;
using DanubeData = DanubeData.Pulumi;
namespace MyInfrastructure;
public static class DataSources
{
public static async Task<IEnumerable<string>> GetAvailableImagesAsync()
{
var images = await DanubeData.GetVpsImages.InvokeAsync();
return images.Images.Select(i => i.Slug);
}
public static Output<IEnumerable<DanubeData.Outputs.GetVpssInstanceResult>> GetExistingVps()
{
return DanubeData.GetVpss.Invoke().Apply(result => result.Instances);
}
public static Output<IEnumerable<DanubeData.Outputs.GetDatabasesInstanceResult>> GetExistingDatabases()
{
return DanubeData.GetDatabases.Invoke().Apply(result => result.Instances);
}
}
Dependency Injection Pattern
// Infrastructure/IInfrastructureFactory.cs
using Pulumi;
namespace MyInfrastructure.Infrastructure;
public interface IInfrastructureFactory
{
DanubeData.Vps CreateVps(string name, VpsOptions options);
DanubeData.Database CreateDatabase(string name, DatabaseOptions options);
DanubeData.Cache CreateCache(string name, CacheOptions options);
}
public record VpsOptions(
string Image,
string Profile,
Input<string> SshKeyId,
bool EnableBackups = false
);
public record DatabaseOptions(
string Engine,
string Profile,
string DatabaseName
);
public record CacheOptions(
string Provider,
string Profile
);
public class InfrastructureFactory : IInfrastructureFactory
{
private readonly string _environment;
private readonly string _datacenter;
private readonly ComponentResource? _parent;
public InfrastructureFactory(string environment, string datacenter = "fsn1",
ComponentResource? parent = null)
{
_environment = environment;
_datacenter = datacenter;
_parent = parent;
}
public DanubeData.Vps CreateVps(string name, VpsOptions options)
{
return new DanubeData.Vps(name, new()
{
Name = $"{name}-{_environment}",
Image = options.Image,
Datacenter = _datacenter,
ResourceProfile = options.Profile,
AuthMethod = "ssh_key",
SshKeyId = options.SshKeyId,
EnableBackups = options.EnableBackups
}, new CustomResourceOptions { Parent = _parent });
}
public DanubeData.Database CreateDatabase(string name, DatabaseOptions options)
{
return new DanubeData.Database(name, new()
{
Name = $"{name}-{_environment}",
Engine = options.Engine,
ResourceProfile = options.Profile,
DatabaseName = options.DatabaseName,
Datacenter = _datacenter
}, new CustomResourceOptions { Parent = _parent });
}
public DanubeData.Cache CreateCache(string name, CacheOptions options)
{
return new DanubeData.Cache(name, new()
{
Name = $"{name}-{_environment}",
CacheProvider = options.Provider,
ResourceProfile = options.Profile,
Datacenter = _datacenter
}, new CustomResourceOptions { Parent = _parent });
}
}
Async/Await Patterns
// AsyncInfrastructure.cs
using Pulumi;
using DanubeData = DanubeData.Pulumi;
namespace MyInfrastructure;
public static class AsyncInfrastructure
{
public static async Task<Dictionary<string, object?>> CreateAsync()
{
// Get available images first
var images = await DanubeData.GetVpsImages.InvokeAsync();
var latestUbuntu = images.Images
.Where(i => i.Slug.StartsWith("ubuntu"))
.OrderByDescending(i => i.Slug)
.FirstOrDefault()?.Slug ?? "ubuntu-24.04";
// Create SSH key
var sshKey = new DanubeData.SshKey("key", new()
{
Name = "async-key",
PublicKey = "ssh-ed25519 AAAA..."
});
// Create VPS with latest Ubuntu
var vps = new DanubeData.Vps("server", new()
{
Name = "async-server",
Image = latestUbuntu,
Datacenter = "fsn1",
ResourceProfile = "small_shared",
AuthMethod = "ssh_key",
SshKeyId = sshKey.Id
});
return new Dictionary<string, object?>
{
["image"] = latestUbuntu,
["ip"] = vps.PublicIp
};
}
}
CI/CD with GitHub Actions
# .github/workflows/pulumi.yml
name: Pulumi Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
DANUBEDATA_API_TOKEN: ${{ secrets.DANUBEDATA_API_TOKEN }}
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: "8.0.x"
- run: dotnet restore
- uses: pulumi/actions@v5
with:
command: preview
stack-name: dev
deploy:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- run: dotnet restore
- uses: pulumi/actions@v5
with:
command: up
stack-name: prod
Testing Infrastructure Code
// Tests/InfrastructureTests.cs
using Pulumi;
using Pulumi.Testing;
using DanubeData = DanubeData.Pulumi;
using Xunit;
using System.Collections.Immutable;
namespace MyInfrastructure.Tests;
public class InfrastructureTests
{
[Fact]
public async Task VpsHasCorrectImage()
{
var mocks = new TestMocks();
var resources = await Testing.RunAsync<MyStack>(mocks);
var vps = resources.OfType<DanubeData.Vps>().FirstOrDefault();
Assert.NotNull(vps);
var image = await vps.Image.GetValueAsync();
Assert.Equal("ubuntu-24.04", image);
}
}
public class TestMocks : IMocks
{
public Task<(string? id, object state)> NewResourceAsync(MockResourceArgs args)
{
return Task.FromResult<(string?, object)>((args.Name + "_id", args.Inputs));
}
public Task<object> CallAsync(MockCallArgs args)
{
return Task.FromResult<object>(args.Args);
}
}
Project File
<!-- MyInfrastructure.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Pulumi" Version="3.*" />
<PackageReference Include="DanubeData.Pulumi" Version="1.*" />
</ItemGroup>
</Project>
Useful Commands
# Build the project
dotnet build
# Preview changes
pulumi preview
# Apply changes
pulumi up
# View outputs
pulumi stack output
# Destroy resources
pulumi destroy
# Import existing resource
pulumi import danubedata:index:Vps my-vps existing-vps-id
Cost Estimation
| Resource | Profile | Monthly Cost |
|---|---|---|
| VPS (small_shared) | 2 vCPU, 4GB RAM | 8.99 |
| PostgreSQL (small) | 1 vCPU, 2GB RAM | 19.99 |
| Redis (micro) | 256MB RAM | 4.99 |
| Storage Bucket | 1TB included | 3.99 |
| Total | 37.96/month |
Get Started
Ready to manage your infrastructure with C#?
Create your free DanubeData account
Then install the Pulumi provider:
dotnet add package DanubeData.Pulumi
Full documentation and examples are available in our GitHub repository.