Challenge 27: Feature flags
Exam skills mapped
- Implement feature flags by using Azure App Configuration Feature Manager
Scenario
Contoso Ltd wants to decouple their deployment cadence from feature releases. Currently, a "big bang" release every two weeks bundles multiple features together, making it difficult to isolate which feature causes issues. The product team wants the ability to:
- Deploy code to production with features hidden behind flags
- Enable features for internal users first (ring 0)
- Gradually roll out to beta testers by percentage
- Instantly disable a feature without redeploying if issues are detected
- Run A/B tests comparing the new checkout flow against the existing one
Environment details:
- .NET 8 Web API application
- Azure App Configuration:
appconfig-contoso-prod - Resource group:
rg-contoso-config - Region: East US
Task 1: Create Azure App Configuration resource
Provision the App Configuration store
RESOURCE_GROUP="rg-contoso-config"
LOCATION="eastus"
CONFIG_STORE="appconfig-contoso-prod"
# Create resource group
az group create --name $RESOURCE_GROUP --location $LOCATION
# Create App Configuration store (Standard tier for feature flags)
az appconfig create \
--name $CONFIG_STORE \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--sku Standard
# Enable managed identity for the App Configuration store
az appconfig identity assign \
--name $CONFIG_STORE \
--resource-group $RESOURCE_GROUP
Configure RBAC for applications to read feature flags
# Get the App Configuration resource ID
CONFIG_ID=$(az appconfig show \
--name $CONFIG_STORE \
--resource-group $RESOURCE_GROUP \
--query id -o tsv)
# Get the web app managed identity principal ID
APP_PRINCIPAL_ID=$(az webapp identity show \
--name app-contoso-web \
--resource-group rg-contoso-webapp-prod \
--query principalId -o tsv)
# Assign App Configuration Data Reader role
az role assignment create \
--assignee $APP_PRINCIPAL_ID \
--role "App Configuration Data Reader" \
--scope $CONFIG_ID
Task 2: Configure feature flags (boolean, targeting, time window)
Create basic boolean feature flags
# Simple on/off feature flag
az appconfig feature set \
--name $CONFIG_STORE \
--feature NewCheckoutFlow \
--label production \
--description "New streamlined checkout experience" \
--yes
# Disable the flag initially (safe deployment)
az appconfig feature disable \
--name $CONFIG_STORE \
--feature NewCheckoutFlow \
--label production \
--yes
# Create a time-window feature flag
az appconfig feature set \
--name $CONFIG_STORE \
--feature MaintenanceMode \
--label production \
--description "Enable maintenance mode banner" \
--yes
# Add a time window filter
az appconfig feature filter add \
--name $CONFIG_STORE \
--feature MaintenanceMode \
--label production \
--filter-name "Microsoft.TimeWindow" \
--filter-parameters Start="2025-03-01T00:00:00Z" End="2025-03-01T06:00:00Z"
Create a targeting feature flag (percentage rollout)
# Create the feature flag for gradual rollout
az appconfig feature set \
--name $CONFIG_STORE \
--feature NewPaymentProcessor \
--label production \
--description "New payment processing engine with improved performance" \
--yes
# Add targeting filter for percentage-based rollout
az appconfig feature filter add \
--name $CONFIG_STORE \
--feature NewPaymentProcessor \
--label production \
--filter-name "Microsoft.Targeting" \
--filter-parameters \
Audience.DefaultRolloutPercentage=0 \
Audience.Groups.0.Name="InternalTeam" \
Audience.Groups.0.RolloutPercentage=100 \
Audience.Groups.1.Name="BetaTesters" \
Audience.Groups.1.RolloutPercentage=50 \
Audience.Users.0="admin@contoso.com" \
Audience.Users.1="productmanager@contoso.com"
List and manage feature flags
# List all feature flags
az appconfig feature list \
--name $CONFIG_STORE \
--label production \
--query "[].{name:name, state:state, description:description}" \
--output table
# Enable a feature flag
az appconfig feature enable \
--name $CONFIG_STORE \
--feature NewCheckoutFlow \
--label production \
--yes
# Disable a feature flag (instant kill switch)
az appconfig feature disable \
--name $CONFIG_STORE \
--feature NewCheckoutFlow \
--label production \
--yes
Task 3: Implement feature flags in a .NET app
Install required NuGet packages
dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore
dotnet add package Microsoft.FeatureManagement.AspNetCore
Configure the application to use App Configuration
In Program.cs:
using Microsoft.FeatureManagement;
using Azure.Identity;
var builder = WebApplication.CreateBuilder(args);
// Connect to Azure App Configuration
var appConfigEndpoint = builder.Configuration["AppConfig:Endpoint"];
builder.Configuration.AddAzureAppConfiguration(options =>
{
options.Connect(new Uri(appConfigEndpoint), new DefaultAzureCredential())
.UseFeatureFlags(featureFlagOptions =>
{
featureFlagOptions.Label = "production";
featureFlagOptions.CacheExpirationInterval = TimeSpan.FromSeconds(30);
});
});
// Add feature management services
builder.Services.AddFeatureManagement()
.AddFeatureFilter<TargetingFilter>();
// Add Azure App Configuration middleware for dynamic refresh
builder.Services.AddAzureAppConfiguration();
// Register targeting context accessor
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<ITargetingContextAccessor, HttpContextTargetingContextAccessor>();
builder.Services.AddControllers();
var app = builder.Build();
// Use Azure App Configuration middleware
app.UseAzureAppConfiguration();
app.MapControllers();
app.Run();
Configure appsettings.json
{
"AppConfig": {
"Endpoint": "https://appconfig-contoso-prod.azconfig.io"
}
}
Use feature flags in controllers
using Microsoft.AspNetCore.Mvc;
using Microsoft.FeatureManagement;
using Microsoft.FeatureManagement.Mvc;
[ApiController]
[Route("api/[controller]")]
public class CheckoutController : ControllerBase
{
private readonly IFeatureManager _featureManager;
private readonly ILogger<CheckoutController> _logger;
public CheckoutController(IFeatureManager featureManager, ILogger<CheckoutController> logger)
{
_featureManager = featureManager;
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> ProcessCheckout([FromBody] CheckoutRequest request)
{
if (await _featureManager.IsEnabledAsync("NewCheckoutFlow"))
{
_logger.LogInformation("Processing with new checkout flow");
return await ProcessNewCheckout(request);
}
_logger.LogInformation("Processing with legacy checkout flow");
return await ProcessLegacyCheckout(request);
}
// Gate an entire endpoint behind a feature flag
[FeatureGate("NewPaymentProcessor")]
[HttpPost("v2/payment")]
public async Task<IActionResult> ProcessPaymentV2([FromBody] PaymentRequest request)
{
return Ok(new { processor = "v2", transactionId = Guid.NewGuid() });
}
private async Task<IActionResult> ProcessNewCheckout(CheckoutRequest request)
{
// New checkout logic
return Ok(new { flow = "new", orderId = Guid.NewGuid() });
}
private async Task<IActionResult> ProcessLegacyCheckout(CheckoutRequest request)
{
// Legacy checkout logic
return Ok(new { flow = "legacy", orderId = Guid.NewGuid() });
}
}
Task 4: Implement targeting filters (user groups, percentages)
Custom targeting context accessor
using Microsoft.FeatureManagement.FeatureFilters;
public class HttpContextTargetingContextAccessor : ITargetingContextAccessor
{
private readonly IHttpContextAccessor _httpContextAccessor;
public HttpContextTargetingContextAccessor(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public ValueTask<TargetingContext> GetContextAsync()
{
var httpContext = _httpContextAccessor.HttpContext;
var user = httpContext?.User;
var targetingContext = new TargetingContext
{
UserId = user?.Identity?.Name ?? "anonymous",
Groups = GetUserGroups(user)
};
return new ValueTask<TargetingContext>(targetingContext);
}
private IEnumerable<string> GetUserGroups(System.Security.Claims.ClaimsPrincipal? user)
{
if (user == null) return Enumerable.Empty<string>();
var groups = new List<string>();
// Add internal team group based on email domain
var email = user.FindFirst("email")?.Value ?? "";
if (email.EndsWith("@contoso.com"))
groups.Add("InternalTeam");
// Add beta testers group based on claim
if (user.HasClaim("beta_tester", "true"))
groups.Add("BetaTesters");
// Add enterprise users group
if (user.HasClaim("tier", "enterprise"))
groups.Add("EnterpriseUsers");
return groups;
}
}
Targeting with percentage rollout per group
[HttpGet("features/status")]
public async Task<IActionResult> GetFeatureStatus()
{
var features = new Dictionary<string, bool>
{
["NewCheckoutFlow"] = await _featureManager.IsEnabledAsync("NewCheckoutFlow"),
["NewPaymentProcessor"] = await _featureManager.IsEnabledAsync("NewPaymentProcessor"),
["MaintenanceMode"] = await _featureManager.IsEnabledAsync("MaintenanceMode")
};
return Ok(features);
}
Task 5: Integrate feature flags with CI/CD pipeline
GitHub Actions workflow that toggles feature flag after deployment verification
Create .github/workflows/deploy-with-feature-flag.yml:
name: Deploy and enable feature flag
on:
push:
branches: [main]
paths:
- 'src/WebApp/**'
env:
CONFIG_STORE: appconfig-contoso-prod
RESOURCE_GROUP: rg-contoso-config
APP_NAME: app-contoso-web
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build and publish
run: |
dotnet publish src/WebApp/WebApp.csproj \
--configuration Release \
--output ./publish
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy to App Service
uses: azure/webapps-deploy@v3
with:
app-name: ${{ env.APP_NAME }}
slot-name: staging
package: ./publish
- name: Validate deployment
run: |
STAGING_URL="https://${{ env.APP_NAME }}-staging.azurewebsites.net"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$STAGING_URL/health")
if [ "$STATUS" != "200" ]; then
echo "Deployment validation failed"
exit 1
fi
- name: Swap to production
run: |
az webapp deployment slot swap \
--name ${{ env.APP_NAME }} \
--resource-group rg-contoso-webapp-prod \
--slot staging \
--target-slot production
- name: Enable feature flag for internal team
run: |
az appconfig feature enable \
--name ${{ env.CONFIG_STORE }} \
--feature NewCheckoutFlow \
--label production \
--yes
echo "Feature 'NewCheckoutFlow' enabled for internal team"
Azure Pipelines feature flag toggle
- task: AzureCLI@2
displayName: 'Toggle feature flag based on deployment success'
inputs:
azureSubscription: 'contoso-production-connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
FEATURE_NAME="NewCheckoutFlow"
CONFIG_STORE="appconfig-contoso-prod"
LABEL="production"
HEALTH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://app-contoso-web.azurewebsites.net/health")
if [ "$HEALTH_STATUS" == "200" ]; then
az appconfig feature enable \
--name $CONFIG_STORE \
--feature $FEATURE_NAME \
--label $LABEL \
--yes
echo "Feature flag '$FEATURE_NAME' enabled"
else
az appconfig feature disable \
--name $CONFIG_STORE \
--feature $FEATURE_NAME \
--label $LABEL \
--yes
echo "Deployment unhealthy - feature flag '$FEATURE_NAME' disabled"
fi
Task 6: GitHub Actions workflow for feature flag management
Create .github/workflows/feature-flag-management.yml:
name: Feature flag management
on:
workflow_dispatch:
inputs:
feature_name:
description: 'Feature flag name'
required: true
type: string
action:
description: 'Action to perform'
required: true
type: choice
options:
- enable
- disable
- rollout-10
- rollout-50
- rollout-100
label:
description: 'Label (environment)'
required: true
default: 'production'
type: string
env:
CONFIG_STORE: appconfig-contoso-prod
jobs:
manage-feature-flag:
runs-on: ubuntu-latest
environment: production
steps:
- name: Login to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Perform feature flag action
run: |
FEATURE="${{ inputs.feature_name }}"
ACTION="${{ inputs.action }}"
LABEL="${{ inputs.label }}"
case $ACTION in
enable)
az appconfig feature enable \
--name ${{ env.CONFIG_STORE }} \
--feature "$FEATURE" \
--label "$LABEL" \
--yes
echo "Feature '$FEATURE' enabled"
;;
disable)
az appconfig feature disable \
--name ${{ env.CONFIG_STORE }} \
--feature "$FEATURE" \
--label "$LABEL" \
--yes
echo "Feature '$FEATURE' disabled (kill switch activated)"
;;
rollout-10)
az appconfig feature filter update \
--name ${{ env.CONFIG_STORE }} \
--feature "$FEATURE" \
--label "$LABEL" \
--filter-name "Microsoft.Targeting" \
--filter-parameters Audience.DefaultRolloutPercentage=10 \
--yes
echo "Feature '$FEATURE' rolling out to 10%"
;;
rollout-50)
az appconfig feature filter update \
--name ${{ env.CONFIG_STORE }} \
--feature "$FEATURE" \
--label "$LABEL" \
--filter-name "Microsoft.Targeting" \
--filter-parameters Audience.DefaultRolloutPercentage=50 \
--yes
echo "Feature '$FEATURE' rolling out to 50%"
;;
rollout-100)
az appconfig feature filter update \
--name ${{ env.CONFIG_STORE }} \
--feature "$FEATURE" \
--label "$LABEL" \
--filter-name "Microsoft.Targeting" \
--filter-parameters Audience.DefaultRolloutPercentage=100 \
--yes
echo "Feature '$FEATURE' fully rolled out to 100%"
;;
esac
- name: Verify feature flag state
run: |
az appconfig feature show \
--name ${{ env.CONFIG_STORE }} \
--feature "${{ inputs.feature_name }}" \
--label "${{ inputs.label }}"
Task 7: A/B testing with feature flags
Configure an A/B test using targeting percentages
# Create an A/B test feature flag with 50/50 split
az appconfig feature set \
--name $CONFIG_STORE \
--feature CheckoutExperiment \
--label production \
--description "A/B test: new checkout vs existing checkout" \
--yes
az appconfig feature filter add \
--name $CONFIG_STORE \
--feature CheckoutExperiment \
--label production \
--filter-name "Microsoft.Targeting" \
--filter-parameters \
Audience.DefaultRolloutPercentage=50
az appconfig feature enable \
--name $CONFIG_STORE \
--feature CheckoutExperiment \
--label production \
--yes
Track A/B test results in Application Insights
[HttpPost("checkout")]
public async Task<IActionResult> Checkout([FromBody] CheckoutRequest request)
{
var isNewCheckout = await _featureManager.IsEnabledAsync("CheckoutExperiment");
var variant = isNewCheckout ? "new_checkout" : "legacy_checkout";
// Track which variant the user is in
_telemetryClient.TrackEvent("CheckoutStarted", new Dictionary<string, string>
{
["variant"] = variant,
["userId"] = User.Identity?.Name ?? "anonymous"
});
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
IActionResult result = isNewCheckout
? await ProcessNewCheckout(request)
: await ProcessLegacyCheckout(request);
stopwatch.Stop();
// Track completion metrics for both variants
_telemetryClient.TrackMetric("CheckoutDurationMs", stopwatch.ElapsedMilliseconds,
new Dictionary<string, string> { ["variant"] = variant });
return result;
}
Query A/B test results in Application Insights (KQL)
// Compare conversion rates between variants
customEvents
| where name == "CheckoutCompleted"
| extend variant = tostring(customDimensions["variant"])
| summarize
totalAttempts = count(),
successCount = countif(tostring(customDimensions["success"]) == "true")
by variant
| extend conversionRate = round(100.0 * successCount / totalAttempts, 2)
| project variant, totalAttempts, successCount, conversionRate
Break and fix exercises
Exercise 1: Feature flag not refreshing
Symptom: A feature flag was enabled in Azure App Configuration 10 minutes ago, but the application still shows the old behavior.
Investigate:
# Verify the flag is enabled in App Configuration
az appconfig feature show \
--name $CONFIG_STORE \
--feature NewCheckoutFlow \
--label production \
--query "{state:state, lastModified:lastModified}"
Show solution
Root cause: The CacheExpirationInterval is set too high (default may have been overridden), or the Azure App Configuration middleware (app.UseAzureAppConfiguration()) is missing from the pipeline.
Fix:
// Ensure cache expiration is set to 30 seconds
options.UseFeatureFlags(featureFlagOptions =>
{
featureFlagOptions.CacheExpirationInterval = TimeSpan.FromSeconds(30);
featureFlagOptions.Label = "production";
});
// Ensure middleware is registered in the correct order
app.UseAzureAppConfiguration(); // Must be before app.MapControllers()
app.MapControllers();
Exercise 2: Targeting filter not applying correctly
Symptom: Internal team members (with @contoso.com email) are not seeing the new feature even though the targeting filter includes them at 100%.
Show solution
Root cause: The IHttpContextAccessor is not registered in the DI container, so the HttpContext is null and the targeting context resolver returns "anonymous" with no groups.
Fix:
// In Program.cs, register IHttpContextAccessor
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton<ITargetingContextAccessor, HttpContextTargetingContextAccessor>();
Exercise 3: Feature flag label mismatch
Symptom: Feature flags work in staging but not in production. The flag is shown as enabled in the Azure portal.
Investigate:
# Check what labels exist for the feature
az appconfig feature list \
--name $CONFIG_STORE \
--feature NewCheckoutFlow \
--query "[].{label:label, state:state}"
Show solution
Root cause: The flag was enabled with label staging but the production app is configured to read label production.
Fix:
# Enable the flag with the correct label
az appconfig feature enable \
--name $CONFIG_STORE \
--feature NewCheckoutFlow \
--label production \
--yes
Knowledge check
1. Contoso wants to enable a new feature for 10% of all users, 100% of internal team members, and specific named beta testers. Which Azure App Configuration feature flag filter should they use?
2. A .NET application uses Azure App Configuration feature flags, but changes made in the portal take over 5 minutes to reflect in the application. What configuration change reduces this delay to under 30 seconds?
3. Contoso deploys new code that includes a feature behind a feature flag. The flag is disabled at deploy time. After verifying the deployment is healthy, the pipeline enables the flag. If the feature causes errors, what is the fastest remediation?
4. Which NuGet package provides the '[FeatureGate]' attribute for gating ASP.NET Core controller actions behind feature flags?
Cleanup
# Delete feature flags
az appconfig feature delete \
--name $CONFIG_STORE \
--feature NewCheckoutFlow \
--label production \
--yes
az appconfig feature delete \
--name $CONFIG_STORE \
--feature NewPaymentProcessor \
--label production \
--yes
az appconfig feature delete \
--name $CONFIG_STORE \
--feature CheckoutExperiment \
--label production \
--yes
# Delete the resource group
az group delete --name rg-contoso-config --yes --no-wait