Azure Functions — Blue/Green Deployment with Slots

Staging Slot Deploy, Swap, Smoke Test Pattern


Introduction

Blue-green deployment is a release strategy that eliminates downtime by maintaining two identical production environments. One (blue) serves the current production traffic while the new version (green) is deployed to a staging environment for testing. Once verified, traffic is swapped to the green environment.

Azure Functions Deployment Slots provide this capability out of the box, allowing you to:

  • Test changes in a production-equivalent environment
  • Warm up the new version before going live
  • Instant rollback if issues are detected
  • Zero-downtime deployments

Architecture Overview

Before Deployment

                    ┌─────────────────────────────────┐
                    │         Load Balancer           │
                    │            (Traffic)            │
                    └──────────────┬──────────────────┘
                                   │
                    ┌──────────────▼──────────────────┐
                    │      PRODUCTION SLOT (Blue)     │
                    │      Function App v1.0          │
                    │      Traffic: 100%              │
                    └─────────────────────────────────┘

                    ┌─────────────────────────────────┐
                    │      STAGING SLOT (Green)       │
                    │      Function App v2.0          │
                    │      Traffic: 0%                │
                    └─────────────────────────────────┘

After Successful Swap

                    ┌─────────────────────────────────┐
                    │         Load Balancer           │
                    │            (Traffic)            │
                    └──────────────┬──────────────────┘
                                   │
                    ┌──────────────▼──────────────────┐
                    │      PRODUCTION SLOT (Blue)     │◀── Now v2.0
                    │      Function App v2.0          │
                    │      Traffic: 100%              │
                    └─────────────────────────────────┘

                    ┌─────────────────────────────────┐
                    │      STAGING SLOT (Green)       │◀── Now v1.0
                    │      Function App v1.0          │
                    │      Traffic: 0%                │
                    └─────────────────────────────────┘

Setting Up Deployment Slots

Create a Staging Slot

# Using Azure CLI
az functionapp deployment slot create \
  --name my-function-app \
  --resource-group my-rg \
  --slot staging

Configure Slot with Different Settings

# Set staging-specific app settings
az functionapp config appsettings set \
  --name my-function-app \
  --resource-group my-rg \
  --slot staging \
  --settings \
    "AzureWebJobsStorage=staging-storage-connection" \
    "WEBSITE_NODE_DEFAULT_VERSION=~18" \
    "APPINSIGHTS_INSTRUMENTATIONKEY=staging-instrumentation-key" \
    "ENVIRONMENT=Staging"

Using ARM Template

{
  "type": "Microsoft.Web/sites/slots",
  "name": "[concat(parameters('functionAppName'), '/staging')]",
  "apiVersion": "2021-02-01",
  "location": "[parameters('location')]",
  "dependsOn": [
    "[resourceId('Microsoft.Web/sites', parameters('functionAppName'))]"
  ],
  "properties": {
    "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]"
  }
}

Deployment Strategies

Strategy 1: Direct Swap (Instant)

# Deploy to staging
az functionapp deployment source config-zip \
  --name my-function-app \
  --resource-group my-rg \
  --slot staging \
  --src-url "https://storage.blob.core.windows.net/container/v2.0.zip"

# Swap to production
az functionapp deployment slot swap \
  --name my-function-app \
  --resource-group my-rg \
  --slot staging \
  --target-slot production

Strategy 2: Staged Swap with Traffic

# Swap with 10% traffic to new version first
az functionapp deployment slot swap \
  --name my-function-app \
  --resource-group my-rg \
  --slot staging \
  --target-slot production \
  --action-type SwapWithPreview \
  --preview-percentage 10

Strategy 3: Manual Routing

# Route percentage of traffic to staging
az functionapp deployment slot auto-swap \
  --name my-function-app \
  --resource-group my-rg \
  --slot staging \
  --target-slot production

GitHub Actions for Slot Deployment

Workflow Configuration

name: Deploy to Staging

on:
  push:
    branches:
      - develop

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      
      - name: Setup .NET
        uses: actions/setup-dotnet@v3
        with:
          dotnet-version: '8.0.x'
      
      - name: Restore dependencies
        run: dotnet restore
      
      - name: Build
        run: dotnet build --configuration Release
      
      - name: Publish
        run: dotnet publish --configuration Release --output ./publish
      
      - name: Deploy to Staging Slot
        uses: Azure/functions-action@v1
        with:
          app-name: my-function-app
          slot-name: staging
          package: ./publish
          publish-profile: ${{ secrets.STAGING_PUBLISH_PROFILE }}
          inline-arguments: |
            --name my-function-app \
            --resource-group my-rg \
            --slot staging

Complete CI/CD Pipeline

name: Production Deployment

on:
  push:
    branches:
      - main
    paths:
      - 'src/**'

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build and Test
        run: |
          dotnet restore
          dotnet build
          dotnet test --verbosity minimal
  
  deploy-staging:
    needs: build-and-test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Deploy to Staging
        uses: Azure/functions-action@v1
        with:
          app-name: ${{ secrets.APP_NAME }}
          slot-name: staging
          package: ./publish
          publish-profile: ${{ secrets.STAGING_PUBLISH_PROFILE }}
      
      - name: Wait for Staging
        run: sleep 30
      
  smoke-test:
    needs: deploy-staging
    runs-on: ubuntu-latest
    steps:
      - name: Run Smoke Tests
        run: |
          # Test staging endpoint
          STAGING_URL="https://my-function-app-staging.azurewebsites.net/api/health"
          curl -f "$STAGING_URL" || exit 1
      
  swap-to-production:
    needs: smoke-test
    runs-on: ubuntu-latest
    steps:
      - name: Swap Slots
        uses: Azure/functions-action@v1
        with:
          app-name: ${{ secrets.APP_NAME }}
          slot-name: staging
          target-slot: production
          action: swap

Implementing Warm-Up Endpoints

Health Check Endpoint

[FunctionName("HealthCheck")]
public static IActionResult HealthCheck(
    [HttpTrigger] HttpTriggerOptions options,
    ILogger log)
{
    log.LogInformation("Health check called");
    
    // Check dependencies
    var healthy = true;
    var checks = new Dictionary<string, object>();
    
    try
    {
        // Check database
        // using var conn = new SqlConnection(connectionString);
        // await conn.ExecuteScalarAsync("SELECT 1");
        checks["database"] = "healthy";
    }
    catch (Exception ex)
    {
        checks["database"] = $"unhealthy: {ex.Message}";
        healthy = false;
    }
    
    try
    {
        // Check Redis
        // var conn = await _redis.GetConnectionAsync();
        checks["cache"] = "healthy";
    }
    catch (Exception ex)
    {
        checks["cache"] = $"degraded: {ex.Message}";
    }
    
    return healthy
        ? new OkObjectResult(new { status = "healthy", checks })
        : new ObjectResult(new { status = "unhealthy", checks })
        { StatusCode = 503 };
}

Warm-Up Endpoint for Slot

[FunctionName("WarmUp")]
public static async Task WarmUp(
    [HttpTrigger] HttpTriggerOptions options,
    ILogger log)
{
    log.LogInformation("Warm-up triggered - initializing function");
    
    // Initialize any lazy-loaded resources
    // This forces the first invocation to happen during swap
    
    // Pre-load commonly used data
    await _cache.WarmUpAsync();
    
    // Verify connections
    await _serviceBusClient.GetPropertiesAsync();
    await _blobClient.GetPropertiesAsync();
    
    log.LogInformation("Warm-up complete");
    
    return new OkObjectResult(new { status = "ready" });
}

Configure Auto-Warmup

{
  "functionAppConfiguration": {
    "autoSwapSlotName": "production",
    "warmupPath": "/api/warmup"
  }
}

Smoke Testing After Swap

Automated Smoke Tests

[FunctionName("PostDeploymentTests")]
public static async Task PostDeploymentTests(
    [TimerTrigger("0 0 * * * *")] TimerInfo timer,
    ILogger log)
{
    var baseUrl = "https://my-function-app.azurewebsites.net";
    var testResults = new List<TestResult>();
    
    // Test 1: Health endpoint
    var healthResult = await RunTestAsync(async () =>
    {
        var client = new HttpClient();
        var response = await client.GetAsync($"{baseUrl}/api/health");
        return response.IsSuccessStatusCode;
    }, "Health Check");
    testResults.Add(healthResult);
    
    // Test 2: Process a test message
    var processResult = await RunTestAsync(async () =>
    {
        var client = new HttpClient();
        var content = new { testData = "smoke-test" };
        var response = await client.PostAsJsonAsync($"{baseUrl}/api/process", content);
        return response.IsSuccessStatusCode;
    }, "Message Processing");
    testResults.Add(processResult);
    
    // Test 3: Verify data in storage
    var storageResult = await RunTestAsync(async () =>
    {
        var blobClient = _blobClient.GetBlobClient("test-result.txt");
        return await blobClient.ExistsAsync();
    }, "Storage Verification");
    testResults.Add(storageResult);
    
    // Alert if any test failed
    var failedTests = testResults.Where(t => !t.Passed).ToList();
    if (failedTests.Any())
    {
        log.LogError("Smoke tests failed: {FailedTests}", 
            string.Join(", ", failedTests.Select(t => t.Name)));
        
        // Send alert
        await SendAlertAsync(failedTests);
    }
    
    log.LogInformation("Smoke tests completed. Passed: {Passed}/{Total}",
        testResults.Count(t => t.Passed), testResults.Count);
}

Rollback Strategy

Automatic Rollback on Failure

name: Deploy with Auto-Rollback

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Deploy to Staging
        uses: Azure/functions-action@v1
        with:
          app-name: my-function-app
          slot-name: staging
          package: ./publish
          publish-profile: ${{ secrets.STAGING_PUBLISH_PROFILE }}
      
      - name: Verify Deployment
        run: |
          # Run quick validation
          curl -f "https://my-function-app-staging.azurewebsites.net/api/health" || exit 1
      
      - name: Swap to Production
        uses: Azure/functions-action@v1
        with:
          app-name: my-function-app
          slot-name: staging
          action: swap
      
      - name: Monitor for Errors
        run: |
          # Wait 5 minutes and check for errors
          sleep 300
          
          # Check Application Insights
          az monitor app-insights query \
            --app my-function-app \
            --query "requests | where timestamp > ago(5m) | where success == false" \
            --analytics \
            | tee /tmp/errors.txt
          
          if [ -s /tmp/errors.txt ]; then
            echo "Errors detected - initiating rollback"
            exit 1
          fi
      
      - name: Rollback on Failure
        if: failure()
        uses: Azure/functions-action@v1
        with:
          app-name: my-function-app
          slot-name: production
          action: swap

Manual Rollback

# If you need to rollback immediately:
# Swap back to previous version
az functionapp deployment slot swap \
  --name my-function-app \
  --resource-group my-rg \
  --slot production \
  --target-slot staging

# Alternative: Deploy previous version to staging, then swap
az functionapp deployment source config-zip \
  --name my-function-app \
  --resource-group my-rg \
  --slot staging \
  --src-url "https://storage.blob.core.windows.net/container/v1.0.zip"

Slot-Specific App Settings

Settings That Stick with Slot

# These will stay with the slot (don't swap)
az functionapp config appsettings set \
  --name my-function-app \
  --slot staging \
  --settings \
    "AzureWebJobsStorage=staging-connection" \
    "APPINSIGHTS_INSTRUMENTATIONKEY=staging-key" \
    "ENVIRONMENT=Staging" \
    --slot-settings "AzureWebJobsStorage,APPINSIGHTS_INSTRUMENTATIONKEY,ENVIRONMENT"

Settings That Swap

# These will swap between slots
az functionapp config appsettings set \
  --name my-function-app \
  --slot staging \
  --settings \
    "FeatureFlag=NewFeature" \
    "LogoUrl=https://staging.logo.png"

Best Practices Summary

PracticeDescription
Use separate storageStaging should have its own storage account
Configure slot-specific settingsEnvironment-specific configs stay with slot
Implement warm-up endpointPre-warm function before traffic hits
Run smoke testsVerify functionality after swap
Enable auto-swapConfigure for automatic swap in production
Monitor closelyWatch Application Insights after deployment
Have rollback planKnow how to revert quickly

Troubleshooting

IssueSolution
Swap failsCheck app settings compatibility
Functions cold after swapUse warm-up endpoint
Tests fail on stagingEnsure staging has correct config
Cannot connect to resourcesVerify VNet/Private Endpoint

Azure Integration Hub - Advanced Level