Skip to content

Infrastructure Model (Azure) in ConnectSoft Templates

Purpose & Overview

Infrastructure Model in ConnectSoft Templates refers to the Infrastructure as Code (IaC) project that defines, provisions, and manages Azure cloud infrastructure resources using Pulumi. This project provides a programmatic, version-controlled, and repeatable way to define the infrastructure required to run applications in Azure cloud environments.

The Infrastructure Model enables:

  • Infrastructure as Code: Infrastructure defined in code, versioned with application code
  • Cloud Resource Provisioning: Automated creation and management of Azure resources (App Service, databases, storage, etc.)
  • Environment Consistency: Same infrastructure definitions across dev, staging, and production
  • Reproducibility: Recreate infrastructure from code at any time
  • Stack Management: Isolated infrastructure stacks for different environments
  • Dependency Management: Infrastructure dependencies managed programmatically
  • Configuration Management: Infrastructure configuration externalized and parameterized

Infrastructure Model Philosophy

Infrastructure should be treated as code—defined, versioned, tested, and deployed just like application code. The Infrastructure Model project provides a single source of truth for cloud infrastructure, enabling teams to manage infrastructure changes with the same rigor as application changes.

Architecture Overview

Infrastructure Model Project Structure

InfrastructureModel/
├── Program.cs                      # Pulumi program entry point
├── AzureInfrastructureStack.cs     # Azure infrastructure stack definition
├── Pulumi.yaml                     # Pulumi project configuration
├── Pulumi.dev.yaml                 # Development stack configuration
└── *.csproj                        # Project file with Pulumi dependencies

Infrastructure Stack Architecture

Infrastructure Stack
├── Resource Group
├── Application Infrastructure
│   ├── App Service Plan (Linux)
│   ├── App Service (API)
│   ├── App Service (Worker)
│   ├── Container Apps Environment
│   └── Container App (Orleans Silo)
├── Database Infrastructure
│   ├── Azure SQL Server
│   ├── Azure SQL Database
│   ├── Cosmos DB Account (MongoDB API)
│   ├── Cosmos DB Database
│   └── Cosmos DB Container
├── Cache Infrastructure
│   └── Azure Cache for Redis
├── Messaging Infrastructure
│   ├── Service Bus Namespace
│   └── Service Bus Queue
├── Security Infrastructure
│   ├── Azure Key Vault
│   ├── Managed Identities (API, Worker, Orleans)
│   └── RBAC Role Assignments
├── Monitoring Infrastructure
│   ├── Application Insights
│   └── Log Analytics Workspace
└── Storage Infrastructure
    └── Storage Account

Pulumi Stack Model

Pulumi Organization
├── Stack: dev
│   └── InfrastructureModel (dev environment)
├── Stack: staging
│   └── InfrastructureModel (staging environment)
└── Stack: prod
    └── InfrastructureModel (production environment)

Project Structure

InfrastructureModel Project

Project File (ConnectSoft.MicroserviceTemplate.InfrastructureModel.csproj):

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>

  <ItemGroup Label="Pulumi Libraries">
    <PackageReference Include="Pulumi" />
    <PackageReference Include="Pulumi.AzureNative" />
  </ItemGroup>
</Project>

Dependencies: - Pulumi: Core Pulumi SDK for .NET - Pulumi.AzureNative: Azure Native provider for Azure resources

Program.cs

Entry Point:

namespace ConnectSoft.MicroserviceTemplate.InfrastructureModel
{
    using System.Threading.Tasks;

    /// <summary>
    /// Pulumi program to run infrastructure code for ConnectSoft.MicroserviceTemplate microservice.
    /// </summary>
    public static class Program
    {
        /// <summary>
        /// ConnectSoft.MicroserviceTemplate micro service infrastructure as a code Pulumi program.
        /// </summary>
        /// <returns>Returns 0 on success.</returns>
        public static Task<int> Main()
        {
            return Pulumi.Deployment.RunAsync<AzureInfrastructureStack>();
        }
    }
}

Purpose: - Entry point for Pulumi program execution - Initializes and runs the AzureInfrastructureStack - Returns exit code (0 for success, non-zero for failure)

AzureInfrastructureStack.cs

Stack Definition:

The AzureInfrastructureStack class creates a complete Azure infrastructure stack with all required resources for the microservice template. It uses direct resource creation (not StackReference) for self-contained infrastructure provisioning.

Key Components: - Stack Class: Inherits from Pulumi.Stack to define infrastructure resources - Config: Reads configuration from Pulumi stack config files - Direct Resource Creation: Creates all resources directly in the stack (Resource Group, App Service Plan, etc.) - Managed Identities: System-assigned managed identities for all app services - RBAC: Role-based access control for Key Vault access - Secrets Management: All connection strings stored in Key Vault - Environment-Aware: Configurable SKUs and settings based on environment (dev/prod)

Resources Created:

  1. Resource Group: Container for all resources
  2. Log Analytics Workspace: Centralized logging
  3. Application Insights: Application performance monitoring
  4. Storage Account: General-purpose storage
  5. Azure SQL Server & Database: Relational database
  6. Cosmos DB: MongoDB API database with database and container
  7. Redis Cache: Distributed caching
  8. Service Bus: Messaging namespace and queue
  9. Key Vault: Secrets management with RBAC
  10. App Service Plan: Linux-based hosting plan
  11. App Service (API): API application with managed identity
  12. App Service (Worker): Background worker with managed identity
  13. Container Apps Environment: Serverless container hosting
  14. Container App (Orleans): Orleans Silo actor model runtime

Pulumi.yaml

Project Configuration:

name: ConnectSoft.MicroserviceTemplate.InfrastructureModel
description: Infrastructure as a code for ConnectSoft.MicroserviceTemplate
runtime: dotnet
config:
  pulumi:tags:
    value:
      pulumi:template: azure-csharp

Configuration: - name: Project name (must match stack name pattern) - description: Project description - runtime: Pulumi runtime (dotnet for .NET) - config: Project-level configuration

Stack Configuration Files

Pulumi.dev.yaml (Development Stack):

config:
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:resourceGroupName: "rg-connectsoft-microservicetemplate-dev"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:location: "East US"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:environment: "dev"

  # SQL Database configuration
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:sqlDatabaseServerName: "sql-connectsoft-microservicetemplate-dev"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:sqlServerAdminLogin: "sqladmin"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:sqlServerAdminPassword:
    secure: AAABAgAAAQAAAAA... # Set using: pulumi config set --secret sqlServerAdminPassword "YourPassword123!"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:sqlDatabaseName: "MicroserviceTemplateDb"

  # Cosmos DB configuration
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:cosmosDbAccountName: "cosmos-connectsoft-microservicetemplate-dev"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:cosmosDbDatabaseName: "MicroserviceTemplateDb"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:cosmosDbContainerName: "Items"

  # Redis Cache configuration
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:redisCacheName: "redis-connectsoft-microservicetemplate-dev"

  # App Service configuration
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:appServicePlanName: "asp-connectsoft-microservicetemplate-dev"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:apiAppName: "app-connectsoft-microservicetemplate-api-dev"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:workerAppName: "app-connectsoft-microservicetemplate-worker-dev"

  # Container Apps configuration
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:containerAppEnvironmentName: "cae-connectsoft-microservicetemplate-dev"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:orleansContainerAppName: "ca-connectsoft-microservicetemplate-orleans-dev"

  # Service Bus configuration
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:serviceBusNamespaceName: "sb-connectsoft-microservicetemplate-dev"

  # Key Vault configuration
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:keyVaultName: "kv-connectsoft-microservicetemplate-dev"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:tenantId: "00000000-0000-0000-0000-000000000000" # Replace with your Azure AD Tenant ID

  # Application Insights configuration
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:appInsightsName: "appi-connectsoft-microservicetemplate-dev"

  # Log Analytics configuration
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:logAnalyticsWorkspaceName: "law-connectsoft-microservicetemplate-dev"

  # Storage Account configuration
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:storageAccountName: "stconnectsoftmicroservicetemplatedev" # Must be globally unique

Pulumi.prod.yaml (Production Stack):

Similar structure with production-specific values and Premium SKUs.

Environment-Specific Configuration

The stack supports environment-aware configuration:

  • Development: Basic/Standard SKUs, shorter retention periods, auto-pause for SQL
  • Production: Premium SKUs, longer retention, high availability, no auto-pause

Example Environment Differences:

Resource Development Production
App Service Plan Basic B1 Premium P1V2
SQL Database Basic (auto-pause) GeneralPurpose (no auto-pause)
Redis Cache Basic C0 Premium P1
Service Bus Standard Premium
Key Vault Standard Premium
Log Analytics Retention 30 days 365 days

Resource Architecture

Direct Resource Creation Pattern

The current implementation uses direct resource creation rather than StackReferences. This approach:

  • Self-Contained: All resources created in a single stack
  • Simpler: No dependency on external stacks
  • Complete: Full control over all infrastructure components
  • Portable: Easy to deploy to new environments

Managed Identities and RBAC

All application services use system-assigned managed identities for secure access to Azure resources:

  • API App: Managed identity with Key Vault Secrets User role
  • Worker App: Managed identity with Key Vault Secrets User role
  • Orleans Container App: Managed identity with Key Vault Secrets User role

RBAC Role Assignment:

// Get subscription ID from resource group
var subscriptionId = resourceGroup.Id.Apply(id => id.Split('/')[2]);

// Key Vault Secrets User role definition ID (built-in role)
var keyVaultSecretsUserRoleId = subscriptionId.Apply(subId => 
    $"/subscriptions/{subId}/providers/Microsoft.Authorization/roleDefinitions/4633458b-17de-408a-b874-0445c86b69e6");

// Grant API App access to Key Vault
var apiAppKeyVaultAccess = new AzureNative.Authorization.RoleAssignment(
    name: "apiAppKeyVaultAccess",
    args: new AzureNative.Authorization.RoleAssignmentArgs
    {
        Scope = keyVault.Id,
        RoleDefinitionId = keyVaultSecretsUserRoleId,
        PrincipalId = apiApp.Identity.Apply(i => i.PrincipalId),
        PrincipalType = "ServicePrincipal",
    });

Secrets Management

All connection strings and secrets are stored in Azure Key Vault:

  • SQL Connection String
  • Cosmos DB Connection String
  • Redis Connection String
  • Service Bus Connection String
  • Storage Account Connection String

Applications access secrets using managed identities, eliminating the need for connection strings in app settings.

Azure Resource Definitions

Azure App Services

App Service Plan (Linux-based):

var appServicePlan = new AzureNative.Web.AppServicePlan(
    name: "appServicePlan",
    args: new AzureNative.Web.AppServicePlanArgs
    {
        Name = appServicePlanName,
        ResourceGroupName = resourceGroup.Name,
        Location = location,
        Kind = "Linux",
        Reserved = true, // Required for Linux plans
        Sku = new AzureNative.Web.Inputs.SkuDescriptionArgs
        {
            Name = environment == "prod" ? "P1V2" : "B1",
            Tier = environment == "prod" ? "PremiumV2" : "Basic",
            Capacity = 1,
        },
    });

API App Service (with Managed Identity):

var apiApp = new AzureNative.Web.WebApp(
    name: "apiApp",
    args: new AzureNative.Web.WebAppArgs
    {
        Name = apiAppName,
        ResourceGroupName = resourceGroup.Name,
        Location = location,
        ServerFarmId = appServicePlan.Id,
        Kind = "app,linux",
        SiteConfig = new AzureNative.Web.Inputs.SiteConfigArgs
        {
            LinuxFxVersion = "DOTNETCORE|9.0",
            AlwaysOn = true,
            Http20Enabled = true,
            AppSettings = new[]
            {
                new AzureNative.Web.Inputs.NameValuePairArgs
                {
                    Name = "ASPNETCORE_ENVIRONMENT",
                    Value = environment == "prod" ? "Production" : "Development",
                },
                new AzureNative.Web.Inputs.NameValuePairArgs
                {
                    Name = "APPINSIGHTS_INSTRUMENTATIONKEY",
                    Value = applicationInsights.InstrumentationKey,
                },
                new AzureNative.Web.Inputs.NameValuePairArgs
                {
                    Name = "KeyVault__VaultUri",
                    Value = keyVault.Properties.Apply(p => p.VaultUri),
                },
            },
        },
        HttpsOnly = true,
        Identity = new AzureNative.Web.Inputs.ManagedServiceIdentityArgs
        {
            Type = "SystemAssigned",
        },
    });

Worker App Service: Similar configuration to API App, optimized for background processing.

Application Insights

Application Insights Definition:

var appInsights = new AzureNative.Insights.Component("appInsights", new()
{
    ResourceGroupName = resourceGroupName,
    Location = location,
    ApplicationType = "web",
    Kind = "web",
    RequestSource = "rest"
});

// Link to Web App
var webAppSettings = new AzureNative.Web.WebAppApplicationSettings("webAppSettings", new()
{
    ResourceGroupName = resourceGroupName,
    Name = webApp.Name,
    Properties = new Dictionary<string, string>
    {
        ["APPINSIGHTS_INSTRUMENTATIONKEY"] = appInsights.InstrumentationKey
    }
});

Azure SQL Database

SQL Database Definition:

var sqlServer = new AzureNative.Sql.Server("sqlServer", new()
{
    ResourceGroupName = resourceGroupName,
    Location = location,
    AdministratorLogin = config.RequireSecret("sqlAdminLogin"),
    AdministratorLoginPassword = config.RequireSecret("sqlAdminPassword"),
    Version = "12.0"
});

var sqlDatabase = new AzureNative.Sql.Database("sqlDatabase", new()
{
    ResourceGroupName = resourceGroupName,
    ServerName = sqlServer.Name,
    Location = location,
    Sku = new AzureNative.Sql.Inputs.SkuArgs
    {
        Name = "Basic",
        Tier = "Basic"
    }
});

// Export connection string
this.SqlConnectionString = Output.Tuple(sqlServer.Name, sqlDatabase.Name)
    .Apply(t => 
        $"Server=tcp:{t.Item1}.database.windows.net,1433;Initial Catalog={t.Item2};" +
        "Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;");

Azure Cosmos DB (MongoDB API)

Cosmos DB Account:

var cosmosDbAccount = new AzureNative.DocumentDB.DatabaseAccount(
    name: "cosmosDbAccount",
    args: new AzureNative.DocumentDB.DatabaseAccountArgs
    {
        ResourceGroupName = resourceGroup.Name,
        AccountName = cosmosDbAccountName,
        Location = location,
        Kind = "MongoDB",
        ConsistencyPolicy = new AzureNative.DocumentDB.Inputs.ConsistencyPolicyArgs
        {
            DefaultConsistencyLevel = "Session",
        },
        Locations = new[]
        {
            new AzureNative.DocumentDB.Inputs.LocationArgs
            {
                LocationName = location,
                FailoverPriority = 0,
                IsZoneRedundant = false,
            },
        },
        DatabaseAccountOfferType = "Standard",
    });

Cosmos DB Database and Container:

var cosmosDbDatabase = new AzureNative.DocumentDB.MongoDBResourceMongoDBDatabase(
    name: "cosmosDbDatabase",
    args: new AzureNative.DocumentDB.MongoDBResourceMongoDBDatabaseArgs
    {
        ResourceGroupName = resourceGroup.Name,
        AccountName = cosmosDbAccount.Name,
        DatabaseName = cosmosDbDatabaseName,
        Resource = new AzureNative.DocumentDB.Inputs.MongoDBDatabaseResourceArgs
        {
            Id = cosmosDbDatabaseName,
        },
    });

var cosmosDbContainer = new AzureNative.DocumentDB.MongoDBResourceMongoDBCollection(
    name: "cosmosDbContainer",
    args: new AzureNative.DocumentDB.MongoDBResourceMongoDBCollectionArgs
    {
        ResourceGroupName = resourceGroup.Name,
        AccountName = cosmosDbAccount.Name,
        DatabaseName = cosmosDbDatabase.Name,
        CollectionName = cosmosDbContainerName,
        Resource = new AzureNative.DocumentDB.Inputs.MongoDBCollectionResourceArgs
        {
            Id = cosmosDbContainerName,
        },
        Options = new AzureNative.DocumentDB.Inputs.CreateUpdateOptionsArgs
        {
            Throughput = 400,
        },
    });

Azure Service Bus

Service Bus Definition:

var serviceBusNamespace = new AzureNative.ServiceBus.Namespace("serviceBusNamespace", new()
{
    ResourceGroupName = resourceGroupName,
    Location = location,
    Sku = new AzureNative.ServiceBus.Inputs.SBSkuArgs
    {
        Name = "Standard",
        Tier = "Standard"
    }
});

var serviceBusQueue = new AzureNative.ServiceBus.Queue("serviceBusQueue", new()
{
    ResourceGroupName = resourceGroupName,
    NamespaceName = serviceBusNamespace.Name,
    MaxDeliveryCount = 10,
    LockDuration = "PT5M",
    DefaultMessageTimeToLive = "P1D"
});

Azure Key Vault

Key Vault with RBAC:

var keyVault = new AzureNative.KeyVault.Vault(
    name: "keyVault",
    args: new AzureNative.KeyVault.VaultArgs
    {
        ResourceGroupName = resourceGroup.Name,
        VaultName = keyVaultName,
        Location = location,
        Properties = new AzureNative.KeyVault.Inputs.VaultPropertiesArgs
        {
            TenantId = tenantId,
            Sku = new AzureNative.KeyVault.Inputs.SkuArgs
            {
                Family = "A",
                Name = environment == "prod" ? "premium" : "standard",
            },
            EnabledForDeployment = true,
            EnabledForTemplateDeployment = true,
            EnabledForDiskEncryption = false,
            EnableRbacAuthorization = true, // Use RBAC instead of access policies
            PublicNetworkAccess = "Enabled",
        },
    });

Storing Secrets in Key Vault:

var sqlConnectionStringSecret = new AzureNative.KeyVault.Secret(
    name: "sqlConnectionStringSecret",
    args: new AzureNative.KeyVault.SecretArgs
    {
        ResourceGroupName = resourceGroup.Name,
        VaultName = keyVault.Name,
        SecretName = "SqlConnectionString",
        Properties = new AzureNative.KeyVault.Inputs.SecretPropertiesArgs
        {
            Value = sqlConnectionString,
        },
    });

Azure Container Apps

Container Apps Environment:

var containerAppEnvironment = new AzureNative.App.ManagedEnvironment(
    name: "containerAppEnvironment",
    args: new AzureNative.App.ManagedEnvironmentArgs
    {
        ResourceGroupName = resourceGroup.Name,
        Location = location,
        EnvironmentName = containerAppEnvironmentName,
        AppLogsConfiguration = new AzureNative.App.Inputs.AppLogsConfigurationArgs
        {
            Destination = "log-analytics",
            LogAnalyticsConfiguration = new AzureNative.App.Inputs.LogAnalyticsConfigurationArgs
            {
                CustomerId = logAnalyticsWorkspace.CustomerId,
                SharedKey = logAnalyticsWorkspace.PrimarySharedKey,
            },
        },
    });

Orleans Container App:

var orleansContainerApp = new AzureNative.App.ContainerApp(
    name: "orleansContainerApp",
    args: new AzureNative.App.ContainerAppArgs
    {
        ResourceGroupName = resourceGroup.Name,
        Location = location,
        ContainerAppName = orleansContainerAppName,
        ManagedEnvironmentId = containerAppEnvironment.Id,
        Configuration = new AzureNative.App.Inputs.ConfigurationArgs
        {
            Ingress = new AzureNative.App.Inputs.IngressArgs
            {
                External = true,
                TargetPort = 30000, // Orleans Silo default port
                Transport = "tcp",
            },
        },
        Template = new AzureNative.App.Inputs.TemplateArgs
        {
            Containers = new[]
            {
                new AzureNative.App.Inputs.ContainerArgs
                {
                    Name = "orleans-silo",
                    Image = "your-registry/orleans-silo:latest",
                    Resources = new AzureNative.App.Inputs.ContainerResourcesArgs
                    {
                        Cpu = 0.5,
                        Memory = "1.0Gi",
                    },
                },
            },
            Scale = new AzureNative.App.Inputs.ScaleArgs
            {
                MinReplicas = 1,
                MaxReplicas = 3,
            },
        },
        Identity = new AzureNative.App.Inputs.ManagedServiceIdentityArgs
        {
            Type = "SystemAssigned",
        },
    });

Azure Storage Account

Storage Account Definition:

var storageAccount = new AzureNative.Storage.StorageAccount("storageAccount", new()
{
    ResourceGroupName = resourceGroupName,
    Location = location,
    Kind = "StorageV2",
    Sku = new AzureNative.Storage.Inputs.SkuArgs
    {
        Name = "Standard_LRS"
    },
    AccessTier = "Hot",
    SupportsHttpsTrafficOnly = true
});

Azure Redis Cache

Redis Cache Definition:

var redisCache = new AzureNative.Cache.Redis("redisCache", new()
{
    ResourceGroupName = resourceGroupName,
    Location = location,
    Sku = new AzureNative.Cache.Inputs.SkuArgs
    {
        Name = "Basic",
        Family = "C",
        Capacity = 0  // C0 (250MB)
    },
    EnableNonSslPort = false,
    MinimumTlsVersion = "1.2"
});

Configuration Management

Stack Configuration

Reading Configuration:

var config = new Config();

// Read string value
var environment = config.Get("environment") ?? "Production";

// Read required value (throws if missing)
var infraStackReference = config.Require("infraStackReference");

// Read secret value (encrypted in state)
var connectionString = config.RequireSecret("connectionString");

// Read boolean value
var enableHttps = config.GetBoolean("enableHttps") ?? true;

// Read integer value
var port = config.GetInt32("port") ?? 5000;

Configuration Files

Pulumi.{stack}.yaml:

config:
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:environment: "dev"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:appName: "myWebApp-dev"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:infraStackReference: "org/shared-infra/dev"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:sqlAdminLogin:
    secure: AAABAgAAAQAAAAA...
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:sqlAdminPassword:
    secure: AAABAgAAAQAAAAA...

Setting Configuration via CLI:

# Set string value
pulumi config set environment dev

# Set secret value (encrypted)
pulumi config set --secret sqlAdminPassword "MyPassword123!"

# Set boolean value
pulumi config set enableHttps true

# Set from environment variable
pulumi config set sqlAdminPassword --secret $(echo $SQL_PASSWORD)

Environment-Specific Configuration

Development (Pulumi.dev.yaml):

config:
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:environment: "Development"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:appName: "myWebApp-dev"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:sku: "B1"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:sqlSku: "Basic"

Production (Pulumi.prod.yaml):

config:
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:environment: "Production"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:appName: "myWebApp-prod"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:sku: "P1V2"
  ConnectSoft.MicroserviceTemplate.InfrastructureModel:sqlSku: "S2"

Stack Outputs

Defining Outputs

Stack Outputs:

The stack exports all important resource names, URIs, and connection details:

public class AzureInfrastructureStack : Stack
{
    // Resource identifiers
    [Output] public Output<string> ResourceGroupName { get; set; }
    [Output] public Output<string> Location { get; set; }

    // Database outputs
    [Output] public Output<string> SqlServerName { get; set; }
    [Output] public Output<string> SqlDatabaseName { get; set; }
    [Output] public Output<string> CosmosDbAccountName { get; set; }
    [Output] public Output<string> CosmosDbDatabaseName { get; set; }
    [Output] public Output<string> CosmosDbContainerName { get; set; }

    // Cache outputs
    [Output] public Output<string> RedisCacheName { get; set; }

    // Messaging outputs
    [Output] public Output<string> ServiceBusNamespaceName { get; set; }
    [Output] public Output<string> ServiceBusQueueName { get; set; }

    // Security outputs
    [Output] public Output<string> KeyVaultUri { get; set; }

    // Application outputs
    [Output] public Output<string> AppServicePlanId { get; set; }
    [Output] public Output<string> ApiAppName { get; set; }
    [Output] public Output<string> ApiAppUrl { get; set; }
    [Output] public Output<string> WorkerAppName { get; set; }
    [Output] public Output<string> WorkerAppUrl { get; set; }
    [Output] public Output<string> ContainerAppEnvironmentName { get; set; }
    [Output] public Output<string> OrleansContainerAppName { get; set; }

    // Monitoring outputs
    [Output] public Output<string> ApplicationInsightsInstrumentationKey { get; set; }
    [Output] public Output<string> ApplicationInsightsConnectionString { get; set; }
    [Output] public Output<string> LogAnalyticsWorkspaceName { get; set; }

    // Storage outputs
    [Output] public Output<string> StorageAccountName { get; set; }
}

Using Outputs

Reading Stack Outputs:

# Get all outputs
pulumi stack output

# Get specific output
pulumi stack output WebAppUrl

# Use in scripts
WEB_APP_URL=$(pulumi stack output WebAppUrl)
echo "Web App URL: $WEB_APP_URL"

Referencing from Other Stacks:

// In another stack
var appStackReference = new StackReference("org/app-infra/dev");
var webAppUrl = appStackReference.GetOutput("WebAppUrl").Apply(url => url.ToString());

Deployment

Local Deployment

Prerequisites:

# Install Pulumi CLI
# Windows: choco install pulumi
# macOS: brew install pulumi
# Linux: curl -fsSL https://get.pulumi.com | sh

# Login to Pulumi Cloud (or use self-hosted)
pulumi login

# Configure Azure credentials
az login

Deploy Stack:

# Select stack
pulumi stack select dev

# Preview changes (dry-run)
pulumi preview

# Deploy infrastructure
pulumi up

# Destroy infrastructure
pulumi destroy

CI/CD Integration

Azure DevOps Pipeline:

# azure-pipelines-infrastructure-model.yml
trigger:
  branches:
    include:
    - master
  paths:
    include:
    - ConnectSoft.MicroserviceTemplate.InfrastructureModel
    - azure-pipelines-infrastructure-model.yml

steps:
- task: UseDotNet@2
  displayName: 'Install .NET Core SDK'
  inputs:
    version: '9.x'

- task: PulumiCLI@1
  displayName: 'Install Pulumi'
  inputs:
    versionSpec: 'latest'

- task: PulumiCLI@1
  displayName: 'Pulumi Preview'
  inputs:
    command: 'preview'
    stack: 'dev'
    cwd: 'ConnectSoft.MicroserviceTemplate.InfrastructureModel'

- task: PulumiCLI@1
  displayName: 'Pulumi Up'
  inputs:
    command: 'up'
    stack: 'dev'
    cwd: 'ConnectSoft.MicroserviceTemplate.InfrastructureModel'
    args: '--yes'
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))

GitHub Actions

GitHub Actions Workflow:

name: Infrastructure Deployment

on:
  push:
    branches: [master]
    paths:
      - 'ConnectSoft.MicroserviceTemplate.InfrastructureModel/**'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - uses: actions/setup-dotnet@v3
      with:
        dotnet-version: '9.x'

    - uses: pulumi/actions@v3
      with:
        pulumi-version: latest

    - name: Configure Azure Credentials
      uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}

    - name: Deploy Infrastructure
      run: |
        cd ConnectSoft.MicroserviceTemplate.InfrastructureModel
        pulumi stack select dev
        pulumi up --yes

State Management

Pulumi State

State Storage Options:

  1. Pulumi Cloud (default):
  2. Managed state storage
  3. Automatic backups
  4. Team collaboration
  5. Stack history

  6. Self-Hosted State Backend:

  7. Azure Blob Storage
  8. AWS S3
  9. Google Cloud Storage
  10. Local file system (development only)

Configure State Backend:

# Azure Blob Storage
pulumi login --cloud-url azblob://pulumi-state

# AWS S3
pulumi login --cloud-url s3://pulumi-state-bucket

# Local file system (development only)
pulumi login --local

State File Structure

State Contains: - Resource definitions and current state - Stack outputs - Configuration values (encrypted secrets) - Resource dependencies - Stack history

State Backups: - Pulumi Cloud: Automatic backups - Self-hosted: Manual backup required - State file should never be committed to source control

Best Practices

Do's

  1. Version Control Infrastructure Code

    # ✅ GOOD - Infrastructure code in Git
    git add InfrastructureModel/
    git commit -m "Add Azure App Service infrastructure"
    

  2. Use Stack References for Shared Resources

    // ✅ GOOD - Reference shared infrastructure
    var infraStackReference = new StackReference(config.Require("infraStackReference"));
    var resourceGroupName = infraStackReference.GetOutput("resourceGroupName");
    

  3. Use Secrets for Sensitive Values

    // ✅ GOOD - Encrypted secrets
    var password = config.RequireSecret("sqlAdminPassword");
    

  4. Export Important Outputs

    // ✅ GOOD - Export outputs for use in other stacks/scripts
    [Output]
    public Output<string> WebAppUrl { get; set; }
    

  5. Use Environment-Specific Configuration

    # ✅ GOOD - Separate config files per environment
    # Pulumi.dev.yaml
    # Pulumi.staging.yaml
    # Pulumi.prod.yaml
    

  6. Preview Before Deploying

    # ✅ GOOD - Always preview changes
    pulumi preview
    pulumi up
    

Don'ts

  1. Don't Commit Secrets

    # ❌ BAD - Secrets in plain text
    sqlAdminPassword: "MyPassword123!"
    
    # ✅ GOOD - Use secret configuration
    pulumi config set --secret sqlAdminPassword "MyPassword123!"
    

  2. Don't Hardcode Values

    // ❌ BAD - Hardcoded values
    var location = "eastus";
    
    // ✅ GOOD - Configuration-driven
    var location = config.Get("location") ?? "eastus";
    

  3. Don't Hardcode Resource Names

    // ❌ BAD - Hardcoded resource names
    var appName = "myWebApp";
    
    // ✅ GOOD - Configuration-driven names
    var appName = config.Require("apiAppName");
    

  4. Don't Skip State Management

    # ❌ BAD - No state management
    # State stored locally, not backed up
    
    # ✅ GOOD - Use Pulumi Cloud or self-hosted backend
    pulumi login
    

  5. Don't Deploy Without Testing

    # ❌ BAD - Deploy without preview
    pulumi up --yes
    
    # ✅ GOOD - Preview first
    pulumi preview
    pulumi up --yes
    

Testing

Unit Testing

Pulumi infrastructure code can be unit tested using Pulumi's mock-based testing approach. This allows you to validate resource creation, configuration, tags, and outputs without requiring actual cloud deployments.

Overview:

The unit tests use Pulumi's IMocks interface to provide deterministic outputs for resources. Tests run in-memory without making actual cloud API calls, making them fast and reliable.

Test Structure:

Tests are located in ConnectSoft.MicroserviceTemplate.UnitTests/InfrastructureModel/:

  • Mocks/PulumiMocks.cs - Mock implementation that provides deterministic outputs for Azure and AWS resources
  • InfrastructureTestHelpers.cs - Helper methods for running stacks and retrieving resources
  • AzureInfrastructureStackTests.cs - Unit tests for Azure infrastructure stack
  • AwsInfrastructureStackTests.cs - Unit tests for AWS infrastructure stack

Running Tests:

# Run all infrastructure tests
dotnet test --filter "FullyQualifiedName~InfrastructureModel"

# Run Azure stack tests only
dotnet test --filter "FullyQualifiedName~AzureInfrastructureStack"

# Run AWS stack tests only
dotnet test --filter "FullyQualifiedName~AwsInfrastructureStack"

Example Test:

[TestMethod]
public async Task AzureInfrastructureStackShouldCreateResourceGroup()
{
    // Arrange
    var config = new Dictionary<string, object>
    {
        ["ConnectSoft.MicroserviceTemplate.InfrastructureModel:resourceGroupName"] = "rg-test",
        ["ConnectSoft.MicroserviceTemplate.InfrastructureModel:location"] = "East US",
        // ... other required config values
    };

    // Act
    var resources = await InfrastructureTestHelpers.TestStackAsync<AzureInfrastructureStack>(config);
    var resourceGroup = InfrastructureTestHelpers.GetResources<AzureNative.Resources.ResourceGroup>(resources).FirstOrDefault();

    // Assert
    Assert.IsNotNull(resourceGroup, "Resource Group should be created");
}

What the Tests Validate:

  • Resource Creation: Verifies that all expected resources are created (Resource Groups, App Services, Databases, etc.)
  • Configuration: Validates that configuration values are read correctly and used appropriately
  • Tags: Ensures common tags (environment, project, stack) are applied to resources
  • Resource Properties: Checks that resources are configured with correct properties based on environment (e.g., SKU selection for dev vs prod)
  • Stack Outputs: Verifies that stack outputs are exported correctly

Adding New Tests:

  1. Add a test method to AzureInfrastructureStackTests.cs or AwsInfrastructureStackTests.cs
  2. Use GetDefaultConfig() helper method to get required configuration values
  3. Call InfrastructureTestHelpers.TestStackAsync<T>() to run the stack
  4. Use InfrastructureTestHelpers.GetResources<T>() to retrieve resources
  5. Assert on resource existence, properties, or stack outputs

Test Patterns:

// Test resource creation
var resources = await InfrastructureTestHelpers.TestStackAsync<AzureInfrastructureStack>(config);
var resource = InfrastructureTestHelpers.GetResources<ResourceType>(resources).FirstOrDefault();
Assert.IsNotNull(resource);

// Test stack outputs
var stack = InfrastructureTestHelpers.GetStack<AzureInfrastructureStack>(resources);
var output = await InfrastructureTestHelpers.GetStackOutputAsync<string>(stack, "OutputName");
Assert.IsNotNull(output);

// Test configuration usage
config["key"] = "value";
var resources = await InfrastructureTestHelpers.TestStackAsync<AzureInfrastructureStack>(config);
// Assert resources use the configuration value

Integration Testing

Test Stack Deployment:

For integration testing, deploy to a test environment:

# Create test stack
pulumi stack init test

# Configure test environment
pulumi config set resourceGroupName rg-test
pulumi config set location "East US"
# ... set other required config values

# Deploy test stack
pulumi up --yes

# Verify resources
az webapp list --query "[?name=='app-api-test']"

# Clean up
pulumi destroy --yes
pulumi stack rm test --yes

Troubleshooting

Issue: Stack Reference Not Found

Symptoms: StackReference fails with "stack not found" error.

Solutions: 1. Verify stack reference format: org/project/stack 2. Ensure referenced stack exists: pulumi stack ls 3. Check stack permissions (if using Pulumi Cloud) 4. Verify stack reference in configuration

Issue: Resource Creation Fails

Symptoms: pulumi up fails with resource creation errors.

Solutions: 1. Check Azure credentials: az account show 2. Verify resource quotas and limits 3. Check resource name availability (Azure requires unique names) 4. Review Azure service health status 5. Check resource provider registration: az provider list

Issue: State Locked

Symptoms: "stack is currently locked" error.

Solutions: 1. Wait for previous operation to complete 2. Force unlock (if safe): pulumi cancel 3. Check for stuck operations in Pulumi Cloud 4. Verify no other processes are using the stack

Issue: Configuration Not Found

Symptoms: Config.Require() throws exception.

Solutions: 1. Check stack configuration: pulumi config 2. Set missing configuration: pulumi config set key value 3. Verify configuration key name matches exactly 4. Check stack context: pulumi stack

Integration with Application

Passing Infrastructure Outputs to Application

Option 1: Environment Variables

// In infrastructure stack
this.WebAppUrl = webApp.DefaultHostName;

// In deployment pipeline
WEB_APP_URL=$(pulumi stack output WebAppUrl)
az webapp config appsettings set --name myWebApp --settings WEB_APP_URL=$WEB_APP_URL

Option 2: Azure App Configuration

var appConfig = new AzureNative.AppConfiguration.ConfigurationStore("appConfig", new()
{
    ResourceGroupName = resourceGroupName,
    Location = location
});

// Store outputs in App Configuration
var appConfigKey = new AzureNative.AppConfiguration.KeyValue("webAppUrl", new()
{
    ResourceGroupName = resourceGroupName,
    ConfigStoreName = appConfig.Name,
    Key = "WebApp:Url",
    Value = webApp.DefaultHostName.Apply(url => url.ToString())
});

Option 3: Key Vault

var connectionStringSecret = new AzureNative.KeyVault.Secret("connectionStringSecret", new()
{
    ResourceGroupName = resourceGroupName,
    VaultName = keyVault.Name,
    Properties = new AzureNative.KeyVault.Inputs.SecretPropertiesArgs
    {
        Value = sqlConnectionString
    }
});

Quick Start Guide

Prerequisites

  1. Install Pulumi CLI:

    # Windows
    choco install pulumi
    
    # macOS
    brew install pulumi
    
    # Linux
    curl -fsSL https://get.pulumi.com | sh
    

  2. Install .NET 9.0 SDK

  3. Azure CLI (for authentication):

    az login
    az account set --subscription "your-subscription-id"
    

  4. Pulumi Account: Sign up at pulumi.com or configure self-hosted backend

Initial Setup

  1. Navigate to Infrastructure Project:

    cd ConnectSoft.MicroserviceTemplate.InfrastructureModel
    

  2. Login to Pulumi:

    pulumi login
    

  3. Select or Create Stack:

    pulumi stack select dev
    # Or create new stack
    pulumi stack init dev
    

  4. Configure Required Settings:

    # Set Azure AD Tenant ID
    pulumi config set tenantId "your-tenant-id"
    
    # Set SQL Server admin password (encrypted)
    pulumi config set --secret sqlServerAdminPassword "YourSecurePassword123!"
    
    # Verify configuration
    pulumi config
    

  5. Preview Infrastructure:

    pulumi preview
    

  6. Deploy Infrastructure:

    pulumi up
    

  7. View Stack Outputs:

    pulumi stack output
    pulumi stack output ApiAppUrl
    

Complete Resource List

The infrastructure stack creates the following Azure resources:

Compute Resources: - 1 App Service Plan (Linux) - 1 App Service (API) - 1 App Service (Worker) - 1 Container Apps Environment - 1 Container App (Orleans Silo)

Database Resources: - 1 Azure SQL Server - 1 Azure SQL Database - 1 Cosmos DB Account (MongoDB API) - 1 Cosmos DB Database - 1 Cosmos DB Container

Cache & Messaging: - 1 Azure Cache for Redis - 1 Service Bus Namespace - 1 Service Bus Queue

Security & Secrets: - 1 Azure Key Vault - 3 Managed Identities (API, Worker, Orleans) - 3 RBAC Role Assignments

Monitoring & Storage: - 1 Application Insights - 1 Log Analytics Workspace - 1 Storage Account

Total: ~20 Azure resources per environment

Summary

Infrastructure Model in the ConnectSoft ConnectSoft Templates provides:

  • Infrastructure as Code: Version-controlled infrastructure definitions
  • Pulumi Integration: Modern IaC tool with .NET support
  • Stack Management: Isolated infrastructure per environment
  • Direct Resource Creation: Self-contained, portable infrastructure stacks
  • Azure Native Provider: Full Azure resource support
  • Configuration Management: Environment-specific configurations
  • Managed Identities: Secure, passwordless authentication
  • RBAC: Role-based access control for Key Vault
  • Secrets Management: All secrets stored in Key Vault
  • Output Management: Exported values for integration
  • CI/CD Integration: Automated infrastructure deployment
  • State Management: Reliable state storage and backup
  • Testing Support: Unit and integration testing capabilities
  • Complete Stack: All required resources for microservice template

By following these patterns, teams can:

  • Manage Infrastructure as Code: Version, test, and deploy infrastructure like application code
  • Ensure Consistency: Same infrastructure definitions across environments
  • Enable Collaboration: Multiple team members can work on infrastructure safely
  • Automate Deployments: Infrastructure changes deployed through CI/CD pipelines
  • Maintain Traceability: Complete history of infrastructure changes
  • Reduce Errors: Infrastructure validated before deployment
  • Enable Rollbacks: Revert infrastructure changes when needed

The Infrastructure Model ensures that cloud infrastructure is managed with the same rigor, automation, and version control as application code, enabling reliable, repeatable, and scalable infrastructure deployments.