BlogTutorialsInfrastructure as Code with Pulumi and .NET: DanubeData Complete Guide

Infrastructure as Code with Pulumi and .NET: DanubeData Complete Guide

Adrian Silaghi
Adrian Silaghi
January 30, 2026
14 min read
29 views
#pulumi #dotnet #csharp #infrastructure-as-code #iac #devops #cloud #automation

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.

Share this article

Ready to Get Started?

Deploy your infrastructure in minutes with DanubeData's managed services.