Azure Infrastructure — App Service, SQL, Redis and Storage

Azure infrastructure for the classified website spans several services that work together: the App Service hosts the ASP.NET Core API, Azure SQL holds the data, Redis caches view counts and session data, Blob Storage stores listing photos, CDN delivers photos globally, and Azure SignalR Service enables real-time notifications across multiple API instances. Provisioning with Bicep (Azure’s Infrastructure as Code language) means the entire environment can be recreated in minutes from a single command — essential for disaster recovery and creating staging environments.

Azure Resource Provisioning

// ── infra/main.bicep — classified website infrastructure ──────────────────
// Deploy with: az deployment group create
//              --resource-group classifiedapp-rg
//              --template-file infra/main.bicep
//              --parameters @infra/params.prod.json

// Resource group parameters
param location     string = resourceGroup().location
param environment  string = 'production'
param appName      string = 'classifiedapp'

// ── Azure SQL Database ────────────────────────────────────────────────────
resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = {
  name: '${appName}-sql-${environment}'
  location: location
  properties: {
    administratorLogin: 'sqladmin'
    // Password from Key Vault reference — never in Bicep!
    administratorLoginPassword: '@Microsoft.KeyVault(VaultName=kv-classified;SecretName=sql-password)'
    version: '12.0'
    minimalTlsVersion: '1.2'
    publicNetworkAccess: 'Enabled'  // or 'Disabled' for VNet-only
  }
}

resource sqlDb 'Microsoft.Sql/servers/databases@2022-05-01-preview' = {
  parent: sqlServer
  name: '${appName}-db'
  location: location
  sku: { name: 'GP_Gen5_2', tier: 'GeneralPurpose', capacity: 2 }
  properties: {
    collation: 'SQL_Latin1_General_CP1_CI_AS'
    maxSizeBytes: 32 * 1024 * 1024 * 1024  // 32 GB
    zoneRedundant: false  // true for high-availability (extra cost)
    backupStorageRedundancy: 'Local'
  }
}

// ── Azure App Service ─────────────────────────────────────────────────────
resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = {
  name: '${appName}-plan'
  location: location
  sku: { name: 'B2', tier: 'Basic', capacity: 2 }
  kind: 'linux'
  properties: { reserved: true }  // Linux plan
}

resource apiApp 'Microsoft.Web/sites@2022-03-01' = {
  name: '${appName}-api'
  location: location
  identity: { type: 'SystemAssigned' }  // Managed Identity
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      linuxFxVersion: 'DOTNETCORE|8.0'
      alwaysOn:      true
      http20Enabled: true
      appSettings: [
        { name: 'ASPNETCORE_ENVIRONMENT', value: 'Production' }
        { name: 'Azure__SignalRConnectionString',
          value: '@Microsoft.KeyVault(...)' }
        { name: 'Stripe__SecretKey',
          value: '@Microsoft.KeyVault(...)' }
      ]
    }
    httpsOnly: true
  }
}

// ── Auto-scaling ──────────────────────────────────────────────────────────
resource autoScale 'Microsoft.Insights/autoscalesettings@2022-10-01' = {
  name: '${appName}-autoscale'
  location: location
  properties: {
    targetResourceUri: appServicePlan.id
    enabled: true
    profiles: [{
      name: 'Default'
      capacity: { minimum: '2', maximum: '5', default: '2' }
      rules: [
        {
          // Scale out when CPU > 70% for 5 minutes
          metricTrigger: {
            metricName: 'CpuPercentage'
            threshold:  70
            operator:   'GreaterThan'
            timeWindow: 'PT5M'
          }
          scaleAction: { direction: 'Increase', value: '1', cooldown: 'PT10M' }
        }
        {
          // Scale in when CPU < 30% for 10 minutes
          metricTrigger: {
            metricName: 'CpuPercentage'
            threshold:  30
            operator:   'LessThan'
            timeWindow: 'PT10M'
          }
          scaleAction: { direction: 'Decrease', value: '1', cooldown: 'PT10M' }
        }
      ]
    }]
  }
}
Note: Managed Identity (identity: { type: 'SystemAssigned' }) eliminates stored credentials entirely for Azure service-to-service authentication. The App Service authenticates to Azure SQL using its identity, not a username/password. Grant access with: CREATE USER [classifiedapp-api] FROM EXTERNAL PROVIDER; ALTER ROLE db_datareader ADD MEMBER [classifiedapp-api]; ALTER ROLE db_datawriter ADD MEMBER [classifiedapp-api]; — the minimum necessary permissions. No connection string with passwords means no passwords to rotate, leak, or expire.
Tip: Deployment slots (production + staging) enable zero-downtime deployments with instant rollback. Deploy new code to the staging slot, run smoke tests against staging, then swap staging ↔ production (a DNS-level swap — takes seconds, no downtime). If the new deployment has issues, swap back to the previous production code (which is now in the staging slot) in seconds. This is the gold standard for production deployments — no maintenance windows, no risk windows.
Warning: Never store database passwords, API keys, or connection strings in Bicep files or GitHub Actions YAML — even in the repository's secrets. Use Azure Key Vault references (@Microsoft.KeyVault(VaultName=...; SecretName=...)) in App Service configuration to read secrets directly from Key Vault at runtime. The App Service's Managed Identity must have Key Vault Secrets User role on the Key Vault. This way, secrets never pass through GitHub — they are fetched directly by Azure at deployment time.

Infrastructure at a Glance

Service SKU Purpose Monthly Cost (approx)
Azure App Service B2 (2 instances) ASP.NET Core API ~$70
Azure SQL GP_Gen5_2 Classified data ~$185
Azure Redis C1 Standard View counts, sessions ~$55
Azure SignalR Standard S1 Real-time notifications ~$50
Blob + CDN LRS + Standard Listing photos ~$20
Static Web Apps Standard Angular frontend ~$9
Total ~$389/mo

Common Mistakes

Mistake 1 — Connection strings with passwords in App Service config (credential exposure)

❌ Wrong — Server=...;Password=MyPassword123 in App Service environment variable; password visible in Azure Portal.

✅ Correct — Managed Identity for SQL/Storage; Key Vault references for 3rd-party secrets (Stripe, SendGrid).

Mistake 2 — Single App Service instance with no autoscaling (single point of failure)

❌ Wrong — one instance; any restart (deployment, crash) causes downtime.

✅ Correct — minimum 2 instances always running; autoscale to 5 on high load; deployments use slot swap (zero downtime).

🧠 Test Yourself

The classified website runs on 3 App Service instances. Azure SignalR Service is configured. A user connects to Instance 1. A contact request arrives for their listing (handled by Instance 2). Does the seller receive the SignalR notification?