Back to Blog
6 min read

Multi-Tenant Management Patterns in Azure

Introduction

Managing multiple Azure tenants presents unique challenges around consistency, efficiency, and governance. Whether you’re an enterprise with subsidiaries, a Managed Service Provider (MSP), or an ISV serving multiple customers, understanding multi-tenant patterns is essential. This post explores architectural patterns and implementation strategies for effective multi-tenant management.

Multi-Tenant Architecture Patterns

Pattern 1: Shared Management Tenant

┌─────────────────────────────────────────────────────────────────┐
│                    Management Tenant                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐             │
│  │ Automation  │  │  Monitoring │  │  Security   │             │
│  │  Account    │  │  Workspace  │  │   Center    │             │
│  └─────────────┘  └─────────────┘  └─────────────┘             │
│                          │                                       │
│              Azure Lighthouse Delegations                        │
└──────────────────────────┼───────────────────────────────────────┘

        ┌──────────────────┼──────────────────┐
        │                  │                  │
   ┌────▼────┐       ┌────▼────┐       ┌────▼────┐
   │Tenant A │       │Tenant B │       │Tenant C │
   │Customer │       │Customer │       │Customer │
   └─────────┘       └─────────┘       └─────────┘

Pattern 2: Hub-and-Spoke Tenant Model

public class HubSpokeManager
{
    private readonly ArmClient _armClient;
    private readonly ILogger<HubSpokeManager> _logger;

    public HubSpokeManager(ILogger<HubSpokeManager> logger)
    {
        _armClient = new ArmClient(new DefaultAzureCredential());
        _logger = logger;
    }

    public async Task<HubTenantInfo> GetHubTenantAsync()
    {
        // Hub tenant contains shared services
        var hubSubscription = await _armClient.GetDefaultSubscriptionAsync();

        return new HubTenantInfo
        {
            TenantId = hubSubscription.Data.TenantId,
            SubscriptionId = hubSubscription.Data.SubscriptionId,
            Services = await GetSharedServicesAsync(hubSubscription)
        };
    }

    public async IAsyncEnumerable<SpokeTenantInfo> GetSpokesAsync()
    {
        // Iterate through all delegated subscriptions (spokes)
        await foreach (var subscription in _armClient.GetSubscriptions())
        {
            var hub = await GetHubTenantAsync();
            if (subscription.Data.TenantId != hub.TenantId)
            {
                yield return new SpokeTenantInfo
                {
                    TenantId = subscription.Data.TenantId,
                    SubscriptionId = subscription.Data.SubscriptionId,
                    SubscriptionName = subscription.Data.DisplayName
                };
            }
        }
    }

    private async Task<SharedServices> GetSharedServicesAsync(
        SubscriptionResource subscription)
    {
        // Query shared services in hub
        return new SharedServices
        {
            LogAnalyticsWorkspace = await GetLogAnalyticsAsync(subscription),
            AutomationAccount = await GetAutomationAccountAsync(subscription),
            KeyVault = await GetKeyVaultAsync(subscription)
        };
    }
}

Centralized Logging Architecture

Log Analytics Workspace Setup

// hub-logging.bicep
param location string = 'australiaeast'
param retentionDays int = 90

resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-06-01' = {
  name: 'log-hub-central'
  location: location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: retentionDays
    features: {
      enableLogAccessUsingOnlyResourcePermissions: true
    }
  }
}

// Data Collection Rule for cross-tenant logs
resource dataCollectionRule 'Microsoft.Insights/dataCollectionRules@2021-04-01' = {
  name: 'dcr-cross-tenant'
  location: location
  properties: {
    destinations: {
      logAnalytics: [
        {
          workspaceResourceId: logAnalytics.id
          name: 'centralWorkspace'
        }
      ]
    }
    dataFlows: [
      {
        streams: [
          'Microsoft-SecurityEvent'
          'Microsoft-Syslog'
          'Microsoft-Perf'
        ]
        destinations: [
          'centralWorkspace'
        ]
      }
    ]
  }
}

output workspaceId string = logAnalytics.id
output workspaceCustomerId string = logAnalytics.properties.customerId

Cross-Tenant Log Forwarding

# Deploy diagnostic settings across all tenants
function Set-CrossTenantDiagnostics {
    param(
        [string]$CentralWorkspaceId,
        [string[]]$ResourceTypes = @(
            'Microsoft.Compute/virtualMachines',
            'Microsoft.Web/sites',
            'Microsoft.Sql/servers'
        )
    )

    $subscriptions = Get-AzSubscription

    foreach ($sub in $subscriptions) {
        Set-AzContext -SubscriptionId $sub.Id

        foreach ($type in $ResourceTypes) {
            $resources = Get-AzResource -ResourceType $type

            foreach ($resource in $resources) {
                $diagName = "diag-central-$(Get-Random)"

                Set-AzDiagnosticSetting `
                    -ResourceId $resource.ResourceId `
                    -Name $diagName `
                    -WorkspaceId $CentralWorkspaceId `
                    -Enabled $true `
                    -Category @("AllMetrics")

                Write-Output "Enabled diagnostics for: $($resource.Name)"
            }
        }
    }
}

Centralized Security Management

Microsoft Defender for Cloud Configuration

public class CrossTenantSecurityManager
{
    private readonly ArmClient _armClient;

    public CrossTenantSecurityManager()
    {
        _armClient = new ArmClient(new DefaultAzureCredential());
    }

    public async Task<SecurityOverview> GetSecurityOverviewAsync()
    {
        var overview = new SecurityOverview();

        await foreach (var subscription in _armClient.GetSubscriptions())
        {
            var securityCenter = subscription.GetSecurityCenterSubscriptionResource();

            // Get secure score
            var secureScore = await GetSecureScoreAsync(subscription);

            // Get security alerts
            var alerts = await GetSecurityAlertsAsync(subscription);

            // Get recommendations
            var recommendations = await GetRecommendationsAsync(subscription);

            overview.TenantScores.Add(new TenantSecurityScore
            {
                SubscriptionId = subscription.Data.SubscriptionId,
                SubscriptionName = subscription.Data.DisplayName,
                SecureScore = secureScore,
                AlertCount = alerts.Count,
                RecommendationCount = recommendations.Count
            });
        }

        return overview;
    }

    public async Task ApplySecurityBaselineAsync(
        string subscriptionId,
        SecurityBaseline baseline)
    {
        var subscription = await _armClient.GetSubscriptions()
            .GetAsync(subscriptionId);

        // Enable security features based on baseline
        foreach (var feature in baseline.RequiredFeatures)
        {
            await EnableSecurityFeatureAsync(subscription.Value, feature);
        }

        // Apply security policies
        foreach (var policy in baseline.Policies)
        {
            await ApplySecurityPolicyAsync(subscription.Value, policy);
        }
    }
}

Unified Cost Management

Cross-Tenant Cost Analysis

public class CrossTenantCostManager
{
    public async Task<CostSummary> GetCostSummaryAsync(
        DateTime startDate,
        DateTime endDate)
    {
        var summary = new CostSummary
        {
            StartDate = startDate,
            EndDate = endDate,
            TenantCosts = new List<TenantCost>()
        };

        var armClient = new ArmClient(new DefaultAzureCredential());

        await foreach (var subscription in armClient.GetSubscriptions())
        {
            var costData = await GetSubscriptionCostAsync(
                subscription,
                startDate,
                endDate);

            summary.TenantCosts.Add(new TenantCost
            {
                SubscriptionId = subscription.Data.SubscriptionId,
                SubscriptionName = subscription.Data.DisplayName,
                TotalCost = costData.TotalCost,
                Currency = costData.Currency,
                CostByService = costData.ServiceCosts,
                CostByResourceGroup = costData.ResourceGroupCosts
            });
        }

        summary.TotalCost = summary.TenantCosts.Sum(t => t.TotalCost);

        return summary;
    }

    private async Task<SubscriptionCostData> GetSubscriptionCostAsync(
        SubscriptionResource subscription,
        DateTime startDate,
        DateTime endDate)
    {
        // Use Cost Management API
        var costManagement = new CostManagementClient(
            new DefaultAzureCredential());

        var query = new QueryDefinition
        {
            Type = ExportType.ActualCost,
            Timeframe = TimeframeType.Custom,
            TimePeriod = new QueryTimePeriod
            {
                From = startDate,
                To = endDate
            },
            Dataset = new QueryDataset
            {
                Granularity = GranularityType.Daily,
                Aggregation = new Dictionary<string, QueryAggregation>
                {
                    ["totalCost"] = new QueryAggregation
                    {
                        Name = "Cost",
                        Function = FunctionType.Sum
                    }
                },
                Grouping = new[]
                {
                    new QueryGrouping
                    {
                        Type = QueryColumnType.Dimension,
                        Name = "ServiceName"
                    }
                }
            }
        };

        var result = await costManagement.QueryUsageAsync(
            $"/subscriptions/{subscription.Data.SubscriptionId}",
            query);

        return ParseCostResult(result);
    }
}

Automated Governance Enforcement

Policy Compliance Checker

public class GovernanceEnforcer
{
    private readonly ILogger _logger;
    private readonly ArmClient _armClient;

    public GovernanceEnforcer(ILogger<GovernanceEnforcer> logger)
    {
        _logger = logger;
        _armClient = new ArmClient(new DefaultAzureCredential());
    }

    public async Task<ComplianceReport> CheckComplianceAsync()
    {
        var report = new ComplianceReport();

        await foreach (var subscription in _armClient.GetSubscriptions())
        {
            var tenantCompliance = new TenantCompliance
            {
                SubscriptionId = subscription.Data.SubscriptionId,
                SubscriptionName = subscription.Data.DisplayName
            };

            // Check tag compliance
            tenantCompliance.TagCompliance = await CheckTagComplianceAsync(subscription);

            // Check network compliance
            tenantCompliance.NetworkCompliance = await CheckNetworkComplianceAsync(subscription);

            // Check security compliance
            tenantCompliance.SecurityCompliance = await CheckSecurityComplianceAsync(subscription);

            report.TenantComplianceResults.Add(tenantCompliance);
        }

        return report;
    }

    private async Task<TagComplianceResult> CheckTagComplianceAsync(
        SubscriptionResource subscription)
    {
        var requiredTags = new[] { "Environment", "CostCenter", "Owner" };
        var nonCompliantResources = new List<string>();

        await foreach (var rg in subscription.GetResourceGroups())
        {
            await foreach (var resource in rg.GetGenericResourcesAsync())
            {
                var missingTags = requiredTags
                    .Where(t => !resource.Data.Tags?.ContainsKey(t) ?? true)
                    .ToList();

                if (missingTags.Any())
                {
                    nonCompliantResources.Add(
                        $"{resource.Data.Name} (missing: {string.Join(", ", missingTags)})");
                }
            }
        }

        return new TagComplianceResult
        {
            IsCompliant = nonCompliantResources.Count == 0,
            NonCompliantResources = nonCompliantResources
        };
    }
}

Deployment Orchestration

Multi-Tenant Deployment Pipeline

# azure-pipelines-multi-tenant.yml
trigger:
  - main

parameters:
  - name: tenants
    type: object
    default:
      - name: 'CustomerA'
        subscriptionId: 'sub-a-id'
        environment: 'production'
      - name: 'CustomerB'
        subscriptionId: 'sub-b-id'
        environment: 'production'
      - name: 'CustomerC'
        subscriptionId: 'sub-c-id'
        environment: 'staging'

stages:
  - stage: Build
    jobs:
      - job: BuildArtifacts
        steps:
          - task: DotNetCoreCLI@2
            inputs:
              command: 'publish'
              arguments: '-c Release -o $(Build.ArtifactStagingDirectory)'
          - publish: $(Build.ArtifactStagingDirectory)
            artifact: drop

  - ${{ each tenant in parameters.tenants }}:
    - stage: Deploy_${{ tenant.name }}
      dependsOn: Build
      jobs:
        - deployment: Deploy
          environment: ${{ tenant.environment }}
          strategy:
            runOnce:
              deploy:
                steps:
                  - download: current
                    artifact: drop

                  - task: AzureCLI@2
                    displayName: 'Deploy to ${{ tenant.name }}'
                    inputs:
                      azureSubscription: 'Lighthouse-Connection'
                      scriptType: 'bash'
                      scriptLocation: 'inlineScript'
                      inlineScript: |
                        # Set context to customer subscription
                        az account set --subscription ${{ tenant.subscriptionId }}

                        # Deploy resources
                        az deployment group create \
                          --resource-group rg-app-${{ tenant.environment }} \
                          --template-file $(Pipeline.Workspace)/drop/template.json \
                          --parameters environment=${{ tenant.environment }}

Conclusion

Multi-tenant management in Azure requires thoughtful architecture that balances operational efficiency with tenant isolation. Azure Lighthouse provides the foundation, while centralized logging, security, and governance tools enable consistent management. By implementing these patterns, you can scale operations across hundreds of tenants while maintaining security and compliance.

References

Michael John Peña

Michael John Peña

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