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:
- Resource Group: Container for all resources
- Log Analytics Workspace: Centralized logging
- Application Insights: Application performance monitoring
- Storage Account: General-purpose storage
- Azure SQL Server & Database: Relational database
- Cosmos DB: MongoDB API database with database and container
- Redis Cache: Distributed caching
- Service Bus: Messaging namespace and queue
- Key Vault: Secrets management with RBAC
- App Service Plan: Linux-based hosting plan
- App Service (API): API application with managed identity
- App Service (Worker): Background worker with managed identity
- Container Apps Environment: Serverless container hosting
- 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:
- Pulumi Cloud (default):
- Managed state storage
- Automatic backups
- Team collaboration
-
Stack history
-
Self-Hosted State Backend:
- Azure Blob Storage
- AWS S3
- Google Cloud Storage
- 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¶
-
Version Control Infrastructure Code
-
Use Stack References for Shared Resources
-
Use Secrets for Sensitive Values
-
Export Important Outputs
-
Use Environment-Specific Configuration
-
Preview Before Deploying
Don'ts¶
-
Don't Commit Secrets
-
Don't Hardcode Values
-
Don't Hardcode Resource Names
-
Don't Skip State Management
-
Don't Deploy Without Testing
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 resourcesInfrastructureTestHelpers.cs- Helper methods for running stacks and retrieving resourcesAzureInfrastructureStackTests.cs- Unit tests for Azure infrastructure stackAwsInfrastructureStackTests.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:
- Add a test method to
AzureInfrastructureStackTests.csorAwsInfrastructureStackTests.cs - Use
GetDefaultConfig()helper method to get required configuration values - Call
InfrastructureTestHelpers.TestStackAsync<T>()to run the stack - Use
InfrastructureTestHelpers.GetResources<T>()to retrieve resources - 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¶
-
Install Pulumi CLI:
-
Install .NET 9.0 SDK
-
Azure CLI (for authentication):
-
Pulumi Account: Sign up at pulumi.com or configure self-hosted backend
Initial Setup¶
-
Navigate to Infrastructure Project:
-
Login to Pulumi:
-
Select or Create Stack:
-
Configure Required Settings:
-
Preview Infrastructure:
-
Deploy Infrastructure:
-
View Stack Outputs:
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.