Learnixo

Azure for Developers · Lesson 2 of 6

Azure App Service — Deploying ASP.NET Core APIs

Why App Service for Clinical .NET Applications

Azure App Service:
  → PaaS: Microsoft manages the OS, runtime, patching
  → Native .NET 8/9 support — deploy a zip, get a running app
  → Deployment slots: Blue/Green deployments with swap
  → Built-in scaling: manual or automatic
  → Managed Identity: no credentials in connection strings
  → HTTPS termination and custom domains built-in

When App Service is the right choice:
  → 1-50 developer team
  → No need for container orchestration (K8s)
  → Modular monolith or single API
  → Team does not have AKS operational experience

When to move beyond App Service:
  → Need per-module independent scaling (consider AKS)
  → Need Windows AND Linux containers (consider AKS)
  → Deploy more than 10 separate services (App Service becomes expensive)

Basic Deployment

YAML
# GitHub Actions: build and deploy to Azure App Service
name: Deploy to Azure App Service

on:
  push:
    branches: [main]

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

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.x'

      - name: Build and publish
        run: |
          dotnet publish src/Host/SystemForge.Api/SystemForge.Api.csproj \
            --configuration Release \
            --output ./publish

      - name: Deploy to Azure App Service
        uses: azure/webapps-deploy@v3
        with:
          app-name: 'clinical-platform-prod'
          publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
          package: './publish'

Configuration — App Settings and Connection Strings

C#
// In Azure Portal or Bicep: set App Settings
// These override appsettings.json at runtime
// Never put secrets in appsettings.json — use Key Vault references

// appsettings.json (checked into source control — no secrets here)
{
  "Logging": {
    "LogLevel": { "Default": "Information" }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "Clinical": "Server=localhost;Database=ClinicalDev;Trusted_Connection=True;"
    // Overridden in Azure by App Service connection string settings
  }
}

// In Azure App Service → Configuration → Connection strings:
//   Name:  Clinical
//   Value: Server=clinical-sql.database.windows.net;Database=Clinical;Authentication=Active Directory Managed Identity
//   Type:  SQLAzure

// In Program.cs — read configuration normally:
builder.Services.AddDbContext<ClinicalDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Clinical")));
// App Service injects the connection string as an environment variable at runtime

Managed Identity — No Credentials in Code

C#
// Instead of: "Server=...;User Id=sa;Password=secret123"
// Use Managed Identity — App Service authenticates to SQL with its Azure AD identity

// Azure SQL: add App Service identity as a database user
// Run in Azure SQL Database:
// CREATE USER [clinical-platform-prod] FROM EXTERNAL PROVIDER;
// ALTER ROLE db_datareader ADD MEMBER [clinical-platform-prod];
// ALTER ROLE db_datawriter ADD MEMBER [clinical-platform-prod];

// Connection string (no credentials):
// "Server=clinical-sql.database.windows.net;Database=Clinical;Authentication=Active Directory Managed Identity"

// Program.cs — no changes needed for basic Managed Identity with SQL Azure
builder.Services.AddDbContext<ClinicalDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Clinical")));
// EF Core + Npgsql/SqlClient pick up Managed Identity automatically from the connection string

// Accessing Key Vault with Managed Identity:
builder.Configuration.AddAzureKeyVault(
    new Uri($"https://clinical-kv.vault.azure.net/"),
    new DefaultAzureCredential()); // uses Managed Identity in production, dev credentials locally
// Secrets in Key Vault flow into IConfiguration — no code changes needed

Deployment Slots (Blue/Green)

App Service deployment slots:
  → Production slot: https://clinical-platform.azurewebsites.net
  → Staging slot:    https://clinical-platform-staging.azurewebsites.net

Deployment flow:
  1. Deploy new version to staging slot (no downtime on production)
  2. Run smoke tests against staging
  3. Swap staging → production (near-zero downtime)
  4. If issues: swap back (rollback in seconds)

GitHub Actions with staging slot:
  - name: Deploy to staging slot
    uses: azure/webapps-deploy@v3
    with:
      app-name: 'clinical-platform'
      slot-name: 'staging'
      publish-profile: ${{ secrets.AZURE_STAGING_PUBLISH_PROFILE }}
      package: './publish'

  - name: Swap staging to production
    uses: azure/cli@v2
    with:
      inlineScript: |
        az webapp deployment slot swap \
          --resource-group clinical-rg \
          --name clinical-platform \
          --slot staging \
          --target-slot production

Scaling Configuration

BICEP
// Bicep: auto-scale App Service based on CPU
resource autoscale 'Microsoft.Insights/autoscalesettings@2022-10-01' = {
  name: 'clinical-platform-autoscale'
  location: location
  properties: {
    enabled: true
    targetResourceUri: appServicePlan.id
    profiles: [
      {
        name: 'Default'
        capacity: { minimum: '2', maximum: '10', default: '2' }
        rules: [
          {
            // Scale out: CPU above 70% for 5 minutes
            metricTrigger: {
              metricName: 'CpuPercentage'
              timeGrain: 'PT1M'
              statistic: 'Average'
              timeWindow: 'PT5M'
              operator: 'GreaterThan'
              threshold: 70
            }
            scaleAction: {
              direction: 'Increase'
              type: 'ChangeCount'
              value: '1'
              cooldown: 'PT5M'
            }
          }
        ]
      }
    ]
  }
}

Production issue I've seen: A clinical platform stored the SQL connection string (including username and password) directly in appsettings.Production.json, which was checked into the repository. Six months later, a developer accidentally pushed this file to a public fork for a demo. The credentials were exposed for 4 hours before the breach was detected. Rotation required emergency downtime across 3 environments. Managed Identity would have meant no credentials to expose — the connection string contains only the server address, no secrets. The migration to Managed Identity took 2 hours. The breach incident took 3 days to fully remediate.


Key Takeaway

App Service is ideal for clinical .NET applications: managed runtime, built-in HTTPS, deployment slots for Blue/Green deployment, and Managed Identity for credential-free Azure resource access. Never store credentials in configuration files or source control — use Managed Identity for SQL, Redis, and Service Bus, and Key Vault for all secrets. Use deployment slots to validate new versions in staging before swapping to production with near-zero downtime.