Back to Blog
7 min read

Pulumi for Azure Infrastructure as Code

Introduction

Pulumi enables infrastructure as code using familiar programming languages like C#, TypeScript, Python, and Go. For .NET developers working with Azure, Pulumi offers a compelling alternative to Terraform, allowing you to leverage your existing skills and IDE tooling. This guide demonstrates Azure infrastructure management with Pulumi and C#.

Getting Started with Pulumi

Installation

# Install Pulumi CLI
brew install pulumi

# Or on Windows
choco install pulumi

# Login to Pulumi
pulumi login

# Create new Azure C# project
pulumi new azure-csharp

Project Structure

MyAzureInfra/
├── Pulumi.yaml           # Project configuration
├── Pulumi.dev.yaml       # Dev stack configuration
├── Pulumi.prod.yaml      # Prod stack configuration
├── Program.cs            # Main entry point
├── MyStack.cs            # Infrastructure definition
└── MyAzureInfra.csproj   # C# project file

Basic Pulumi Configuration

Pulumi.yaml

name: my-azure-infra
runtime: dotnet
description: Azure infrastructure for my application

Program.cs

using System.Threading.Tasks;
using Pulumi;

class Program
{
    static Task<int> Main() => Deployment.RunAsync<MyStack>();
}

Defining Azure Resources

Basic Resource Group and App Service

using Pulumi;
using Pulumi.AzureNative.Resources;
using Pulumi.AzureNative.Web;
using Pulumi.AzureNative.Web.Inputs;

class MyStack : Stack
{
    public MyStack()
    {
        var config = new Config();
        var environment = config.Require("environment");
        var location = config.Get("location") ?? "AustraliaEast";

        // Resource Group
        var resourceGroup = new ResourceGroup("rg", new ResourceGroupArgs
        {
            ResourceGroupName = $"rg-myapp-{environment}",
            Location = location,
            Tags =
            {
                { "Environment", environment },
                { "ManagedBy", "Pulumi" }
            }
        });

        // App Service Plan
        var appServicePlan = new AppServicePlan("asp", new AppServicePlanArgs
        {
            Name = $"asp-myapp-{environment}",
            ResourceGroupName = resourceGroup.Name,
            Location = resourceGroup.Location,
            Kind = "Linux",
            Reserved = true,
            Sku = new SkuDescriptionArgs
            {
                Name = environment == "prod" ? "S1" : "B1",
                Tier = environment == "prod" ? "Standard" : "Basic"
            }
        });

        // App Service
        var appService = new WebApp("app", new WebAppArgs
        {
            Name = $"app-myapp-{environment}",
            ResourceGroupName = resourceGroup.Name,
            Location = resourceGroup.Location,
            ServerFarmId = appServicePlan.Id,
            HttpsOnly = true,
            SiteConfig = new SiteConfigArgs
            {
                LinuxFxVersion = "DOTNETCORE|6.0",
                AlwaysOn = environment == "prod",
                Http20Enabled = true,
                MinTlsVersion = "1.2"
            },
            Identity = new ManagedServiceIdentityArgs
            {
                Type = Pulumi.AzureNative.Web.ManagedServiceIdentityType.SystemAssigned
            }
        });

        // Outputs
        this.ResourceGroupName = resourceGroup.Name;
        this.AppServiceUrl = appService.DefaultHostName.Apply(h => $"https://{h}");
        this.AppServicePrincipalId = appService.Identity.Apply(i => i?.PrincipalId ?? "");
    }

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

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

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

SQL Database Infrastructure

using Pulumi;
using Pulumi.AzureNative.Sql;
using Pulumi.AzureNative.Sql.Inputs;

public class SqlInfrastructure
{
    public Server SqlServer { get; }
    public Database Database { get; }

    public SqlInfrastructure(
        string name,
        Input<string> resourceGroupName,
        Input<string> location,
        string environment,
        Config config)
    {
        var adminUsername = config.RequireSecret("sqlAdminUsername");
        var adminPassword = config.RequireSecret("sqlAdminPassword");

        // SQL Server
        SqlServer = new Server($"sql-{name}", new ServerArgs
        {
            ServerName = $"sql-{name}-{environment}",
            ResourceGroupName = resourceGroupName,
            Location = location,
            AdministratorLogin = adminUsername,
            AdministratorLoginPassword = adminPassword,
            Version = "12.0",
            MinimalTlsVersion = "1.2"
        });

        // Database
        Database = new Database($"sqldb-{name}", new DatabaseArgs
        {
            DatabaseName = $"sqldb-{name}-{environment}",
            ServerName = SqlServer.Name,
            ResourceGroupName = resourceGroupName,
            Location = location,
            Sku = new SkuArgs
            {
                Name = environment == "prod" ? "S1" : "Basic",
                Tier = environment == "prod" ? "Standard" : "Basic"
            },
            MaxSizeBytes = environment == "prod" ? 53687091200 : 2147483648
        });

        // Firewall rule for Azure services
        var firewallRule = new FirewallRule("allow-azure", new FirewallRuleArgs
        {
            FirewallRuleName = "AllowAzureServices",
            ServerName = SqlServer.Name,
            ResourceGroupName = resourceGroupName,
            StartIpAddress = "0.0.0.0",
            EndIpAddress = "0.0.0.0"
        });
    }
}

Key Vault with Secrets

using Pulumi;
using Pulumi.AzureNative.KeyVault;
using Pulumi.AzureNative.KeyVault.Inputs;
using Pulumi.AzureNative.Authorization;

public class KeyVaultInfrastructure
{
    public Vault KeyVault { get; }

    public KeyVaultInfrastructure(
        string name,
        Input<string> resourceGroupName,
        Input<string> location,
        string tenantId,
        string environment,
        Output<string?> appServicePrincipalId)
    {
        KeyVault = new Vault($"kv-{name}", new VaultArgs
        {
            VaultName = $"kv-{name}-{environment}",
            ResourceGroupName = resourceGroupName,
            Location = location,
            Properties = new VaultPropertiesArgs
            {
                TenantId = tenantId,
                Sku = new SkuArgs
                {
                    Family = SkuFamily.A,
                    Name = Pulumi.AzureNative.KeyVault.SkuName.Standard
                },
                EnableSoftDelete = true,
                SoftDeleteRetentionInDays = 7,
                EnablePurgeProtection = environment == "prod",
                AccessPolicies = new[]
                {
                    // App Service access
                    new AccessPolicyEntryArgs
                    {
                        TenantId = tenantId,
                        ObjectId = appServicePrincipalId!,
                        Permissions = new PermissionsArgs
                        {
                            Secrets = new[]
                            {
                                SecretPermissions.Get,
                                SecretPermissions.List
                            }
                        }
                    }
                }
            }
        });
    }

    public Secret AddSecret(
        string name,
        Input<string> value,
        Input<string> resourceGroupName)
    {
        return new Secret(name, new SecretArgs
        {
            SecretName = name,
            VaultName = KeyVault.Name,
            ResourceGroupName = resourceGroupName,
            Properties = new SecretPropertiesArgs
            {
                Value = value
            }
        });
    }
}

Component Resources

using Pulumi;
using Pulumi.AzureNative.Resources;
using Pulumi.AzureNative.Web;

public class WebAppComponentArgs
{
    public Input<string> ResourceGroupName { get; set; } = null!;
    public Input<string> Location { get; set; } = null!;
    public string Environment { get; set; } = null!;
    public Dictionary<string, Input<string>> AppSettings { get; set; } = new();
}

public class WebAppComponent : ComponentResource
{
    public Output<string> Url { get; private set; }
    public Output<string?> PrincipalId { get; private set; }

    public WebAppComponent(
        string name,
        WebAppComponentArgs args,
        ComponentResourceOptions? options = null)
        : base("mycompany:azure:WebAppComponent", name, options)
    {
        var plan = new AppServicePlan($"{name}-plan", new AppServicePlanArgs
        {
            Name = $"asp-{name}-{args.Environment}",
            ResourceGroupName = args.ResourceGroupName,
            Location = args.Location,
            Kind = "Linux",
            Reserved = true,
            Sku = new Pulumi.AzureNative.Web.Inputs.SkuDescriptionArgs
            {
                Name = "B1",
                Tier = "Basic"
            }
        }, new CustomResourceOptions { Parent = this });

        var appSettings = args.AppSettings
            .Select(kvp => new NameValuePairArgs
            {
                Name = kvp.Key,
                Value = kvp.Value
            })
            .ToList();

        var app = new WebApp($"{name}-app", new WebAppArgs
        {
            Name = $"app-{name}-{args.Environment}",
            ResourceGroupName = args.ResourceGroupName,
            Location = args.Location,
            ServerFarmId = plan.Id,
            HttpsOnly = true,
            SiteConfig = new SiteConfigArgs
            {
                LinuxFxVersion = "DOTNETCORE|6.0",
                AppSettings = appSettings
            },
            Identity = new ManagedServiceIdentityArgs
            {
                Type = ManagedServiceIdentityType.SystemAssigned
            }
        }, new CustomResourceOptions { Parent = this });

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

        RegisterOutputs(new Dictionary<string, object?>
        {
            { "url", Url },
            { "principalId", PrincipalId }
        });
    }
}

Using Components in Stack

using Pulumi;
using Pulumi.AzureNative.Resources;

class MyStack : Stack
{
    public MyStack()
    {
        var config = new Config();
        var environment = config.Require("environment");

        var resourceGroup = new ResourceGroup("rg", new ResourceGroupArgs
        {
            ResourceGroupName = $"rg-myapp-{environment}",
            Location = "AustraliaEast"
        });

        // Use component for API
        var apiComponent = new WebAppComponent("api", new WebAppComponentArgs
        {
            ResourceGroupName = resourceGroup.Name,
            Location = resourceGroup.Location,
            Environment = environment,
            AppSettings = new Dictionary<string, Input<string>>
            {
                { "ASPNETCORE_ENVIRONMENT", "Production" }
            }
        });

        // Use component for Web
        var webComponent = new WebAppComponent("web", new WebAppComponentArgs
        {
            ResourceGroupName = resourceGroup.Name,
            Location = resourceGroup.Location,
            Environment = environment,
            AppSettings = new Dictionary<string, Input<string>>
            {
                { "ApiUrl", apiComponent.Url }
            }
        });

        this.ApiUrl = apiComponent.Url;
        this.WebUrl = webComponent.Url;
    }

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

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

Stack References

// Networking stack (deployed separately)
class NetworkingStack : Stack
{
    [Output]
    public Output<string> VNetId { get; set; }

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

// Application stack referencing networking
class ApplicationStack : Stack
{
    public ApplicationStack()
    {
        var config = new Config();
        var environment = config.Require("environment");

        // Reference another stack
        var networkingStack = new StackReference($"myorg/networking/{environment}");
        var subnetId = networkingStack.GetOutput("SubnetId").Apply(id => id.ToString());

        // Use the subnet from networking stack
        var appService = new WebApp("app", new WebAppArgs
        {
            // ... other config
            VirtualNetworkSubnetId = subnetId
        });
    }
}

Deployment Commands

# Preview changes
pulumi preview

# Deploy to dev
pulumi up --stack dev

# Deploy to prod
pulumi up --stack prod

# View outputs
pulumi stack output

# Destroy resources
pulumi destroy

Conclusion

Pulumi brings the power of general-purpose programming languages to infrastructure as code. For .NET developers, using C# for Azure infrastructure means familiar tooling, strong typing, refactoring support, and the ability to create reusable components. The learning curve is minimal for developers already comfortable with Azure and C#.

References

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.