AgentSkillsCN

azure-appconfig-to-aws-appconfig

将Azure App Configuration迁移至AWS AppConfig。将Azure.Data.AppConfiguration转换为AWS AppConfig SDK,同时完成Label → 配置文件 + 环境的映射。 保留扩展方法的契约,以最小化对消费应用的影响。

SKILL.md
--- frontmatter
name: azure-appconfig-to-aws-appconfig
description: |
  Migrate Azure App Configuration to AWS AppConfig. Converts Azure.Data.AppConfiguration
  to AWS AppConfig SDK, handling Labels → Configuration Profiles + Environments mapping.
  Preserves extension method contracts for minimal impact on consuming applications.
disposition: contextual
filePatterns:
  - "**/*.cs"
  - "**/*.csproj"
  - "**/appsettings*.json"
  - "**/Program.cs"
  - "**/Startup.cs"
version: 1.0.0

Azure App Configuration → AWS AppConfig Migration

Overview

Migrate from Azure.Data.AppConfiguration to AWS AppConfig while preserving extension method signatures. Handles Azure Labels by mapping to AWS Configuration Profiles + Environments.

Package Migration

Azure Packages (Remove)

xml
<PackageReference Include="Azure.Data.AppConfiguration" Version="1.x.x" />
<PackageReference Include="Microsoft.Extensions.Configuration.AzureAppConfiguration" Version="7.x.x" />
<PackageReference Include="Azure.Identity" Version="1.x.x" />

AWS Packages (Add)

xml
<PackageReference Include="AWSSDK.AppConfig" Version="3.7.x" />
<PackageReference Include="AWSSDK.AppConfigData" Version="3.7.x" />
<PackageReference Include="AWSSDK.Extensions.NETCore.Setup" Version="3.7.x" />

Azure Labels → AWS Architecture Mapping

Azure Concept (Labels)

csharp
// Azure uses Labels to differentiate environments
var config = await client.GetConfigurationSettingAsync("ConnectionString", label: "Development");
var config = await client.GetConfigurationSettingAsync("ConnectionString", label: "Production");

AWS Concept (Configuration Profiles + Environments)

AWS AppConfig Structure:

  • Application: Top-level container (e.g., "MyApp")
  • Environment: Deployment target (Development, Staging, Production)
  • Configuration Profile: Type of configuration (e.g., "ConnectionStrings", "FeatureFlags")
  • Deployment Strategy: How config is rolled out (Instant, Linear, Exponential)

Mapping:

  • Azure Label "Development" → AWS Environment "Development"
  • Azure Label "Production" → AWS Environment "Production"
  • Azure Key groups → AWS Configuration Profiles (optional, can use single profile)

Configuration Migration

Azure Configuration (appsettings.json)

json
{
  "AzureAppConfiguration": {
    "Endpoint": "https://myapp-config.azconfig.io",
    "Label": "Development"
  }
}

AWS Configuration (appsettings.json)

json
{
  "AWS": {
    "Region": "us-east-1",
    "Profile": "default"
  },
  "AppConfig": {
    "ApplicationId": "myapp",
    "EnvironmentId": "development",
    "ConfigurationProfileId": "main-config",
    "ClientId": "unique-client-identifier",
    "PollIntervalSeconds": 60
  }
}

Environment Variables:

  • AWS_REGION - AWS region
  • APPCONFIG_APPLICATION_ID - Application identifier
  • APPCONFIG_ENVIRONMENT_ID - Environment (dev, staging, prod)
  • APPCONFIG_PROFILE_ID - Configuration profile

Service Registration

Azure (Before)

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddAzureAppConfiguration(options =>
{
    options.Connect(builder.Configuration["AzureAppConfiguration:Endpoint"])
        .Select(KeyFilter.Any, builder.Configuration["AzureAppConfiguration:Label"])
        .ConfigureRefresh(refresh =>
        {
            refresh.Register("Sentinel", refreshAll: true)
                   .SetCacheExpiration(TimeSpan.FromSeconds(60));
        });
});

builder.Services.AddAzureAppConfiguration();

AWS (After)

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDefaultAWSOptions(builder.Configuration.GetAWSOptions());
builder.Services.AddAWSService<IAmazonAppConfigData>();

builder.Services.AddSingleton<IConfigurationService, AwsAppConfigService>();

// Optional: Background service for config refresh
builder.Services.AddHostedService<AppConfigRefreshService>();

Extension Method Preservation

CRITICAL: Maintain identical public API for configuration access.

Example Extension Methods (Keep Signatures)

csharp
public interface IConfigurationService
{
    Task<string?> GetConfigurationAsync(string key, CancellationToken cancellationToken = default);
    Task<T?> GetConfigurationAsync<T>(string key, CancellationToken cancellationToken = default) where T : class;
    Task RefreshConfigurationAsync(CancellationToken cancellationToken = default);
}

Azure Implementation (Before)

csharp
public class AzureConfigurationService : IConfigurationService
{
    private readonly ConfigurationClient _client;
    private readonly string _label;
    private readonly ILogger<AzureConfigurationService> _logger;

    public AzureConfigurationService(
        ConfigurationClient client,
        IConfiguration configuration,
        ILogger<AzureConfigurationService> logger)
    {
        _client = client;
        _label = configuration["AzureAppConfiguration:Label"] ?? string.Empty;
        _logger = logger;
    }

    public async Task<string?> GetConfigurationAsync(string key, CancellationToken cancellationToken = default)
    {
        try
        {
            var setting = await _client.GetConfigurationSettingAsync(key, _label, cancellationToken);
            return setting.Value?.Value;
        }
        catch (RequestFailedException ex) when (ex.Status == 404)
        {
            _logger.LogWarning("Configuration {Key} not found", key);
            return null;
        }
    }

    public async Task<T?> GetConfigurationAsync<T>(string key, CancellationToken cancellationToken = default) where T : class
    {
        var value = await GetConfigurationAsync(key, cancellationToken);
        if (value == null) return null;

        return JsonSerializer.Deserialize<T>(value);
    }

    public async Task RefreshConfigurationAsync(CancellationToken cancellationToken = default)
    {
        // Azure SDK handles refresh internally
        await Task.CompletedTask;
    }
}

AWS Implementation (After)

csharp
public class AwsAppConfigService : IConfigurationService
{
    private readonly IAmazonAppConfigData _client;
    private readonly ILogger<AwsAppConfigService> _logger;
    private readonly string _applicationId;
    private readonly string _environmentId;
    private readonly string _configProfileId;
    private readonly string _clientId;
    private string? _configurationToken;
    private readonly SemaphoreSlim _refreshLock = new(1, 1);

    public AwsAppConfigService(
        IAmazonAppConfigData client,
        IConfiguration configuration,
        ILogger<AwsAppConfigService> logger)
    {
        _client = client;
        _logger = logger;
        _applicationId = configuration["AppConfig:ApplicationId"]!;
        _environmentId = configuration["AppConfig:EnvironmentId"]!;
        _configProfileId = configuration["AppConfig:ConfigurationProfileId"]!;
        _clientId = configuration["AppConfig:ClientId"] ?? Guid.NewGuid().ToString();
    }

    public async Task<string?> GetConfigurationAsync(string key, CancellationToken cancellationToken = default)
    {
        var config = await GetLatestConfigurationAsync(cancellationToken);
        if (config == null) return null;

        // Parse configuration (assume JSON format)
        using var doc = JsonDocument.Parse(config);
        if (doc.RootElement.TryGetProperty(key, out var value))
        {
            return value.GetString();
        }

        _logger.LogWarning("Configuration {Key} not found", key);
        return null;
    }

    public async Task<T?> GetConfigurationAsync<T>(string key, CancellationToken cancellationToken = default) where T : class
    {
        var value = await GetConfigurationAsync(key, cancellationToken);
        if (value == null) return null;

        return JsonSerializer.Deserialize<T>(value);
    }

    public async Task RefreshConfigurationAsync(CancellationToken cancellationToken = default)
    {
        await _refreshLock.WaitAsync(cancellationToken);
        try
        {
            var request = new GetLatestConfigurationRequest
            {
                ConfigurationToken = _configurationToken
            };

            var response = await _client.GetLatestConfigurationAsync(request, cancellationToken);
            
            if (response.Configuration?.Length > 0)
            {
                _configurationToken = response.NextPollConfigurationToken;
                _logger.LogInformation("Configuration refreshed");
            }
        }
        finally
        {
            _refreshLock.Release();
        }
    }

    private async Task<string?> GetLatestConfigurationAsync(CancellationToken cancellationToken)
    {
        // Start configuration session if token is null
        if (_configurationToken == null)
        {
            var startRequest = new StartConfigurationSessionRequest
            {
                ApplicationIdentifier = _applicationId,
                EnvironmentIdentifier = _environmentId,
                ConfigurationProfileIdentifier = _configProfileId
            };

            var startResponse = await _client.StartConfigurationSessionAsync(startRequest, cancellationToken);
            _configurationToken = startResponse.InitialConfigurationToken;
        }

        var request = new GetLatestConfigurationRequest
        {
            ConfigurationToken = _configurationToken
        };

        var response = await _client.GetLatestConfigurationAsync(request, cancellationToken);
        
        if (response.Configuration?.Length > 0)
        {
            _configurationToken = response.NextPollConfigurationToken;
            using var stream = response.Configuration;
            using var reader = new StreamReader(stream);
            return await reader.ReadToEndAsync(cancellationToken);
        }

        return null;
    }
}

// Background service for periodic refresh
public class AppConfigRefreshService : BackgroundService
{
    private readonly IConfigurationService _configService;
    private readonly IConfiguration _configuration;
    private readonly ILogger<AppConfigRefreshService> _logger;

    public AppConfigRefreshService(
        IConfigurationService configService,
        IConfiguration configuration,
        ILogger<AppConfigRefreshService> logger)
    {
        _configService = configService;
        _configuration = configuration;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var intervalSeconds = _configuration.GetValue<int>("AppConfig:PollIntervalSeconds", 60);
        var interval = TimeSpan.FromSeconds(intervalSeconds);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await _configService.RefreshConfigurationAsync(stoppingToken);
                await Task.Delay(interval, stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error refreshing configuration");
                await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); // Retry after 10s
            }
        }
    }
}

AWS AppConfig Setup (Terraform Example)

hcl
# AppConfig Application
resource "aws_appconfig_application" "main" {
  name        = "myapp"
  description = "Main application configuration"
}

# Environments (map from Azure Labels)
resource "aws_appconfig_environment" "development" {
  application_id = aws_appconfig_application.main.id
  name           = "development"
}

resource "aws_appconfig_environment" "production" {
  application_id = aws_appconfig_application.main.id
  name           = "production"
}

# Configuration Profile
resource "aws_appconfig_configuration_profile" "main" {
  application_id = aws_appconfig_application.main.id
  name           = "main-config"
  location_uri   = "hosted"
  type           = "AWS.Freeform"

  validator {
    type    = "JSON_SCHEMA"
    content = jsonencode({
      "$schema" = "http://json-schema.org/draft-07/schema#"
      type      = "object"
      properties = {
        ConnectionString = { type = "string" }
        ApiKey          = { type = "string" }
      }
      required = ["ConnectionString"]
    })
  }
}

# Deployment Strategy (Instant)
resource "aws_appconfig_deployment_strategy" "instant" {
  name                           = "Instant"
  deployment_duration_in_minutes = 0
  growth_factor                  = 100
  replicate_to                   = "NONE"
}

# Hosted Configuration Version
resource "aws_appconfig_hosted_configuration_version" "development" {
  application_id           = aws_appconfig_application.main.id
  configuration_profile_id = aws_appconfig_configuration_profile.main.configuration_profile_id
  content_type             = "application/json"

  content = jsonencode({
    ConnectionString = "Server=dev-db;Database=myapp"
    ApiKey          = "dev-api-key"
  })
}

# Deploy Configuration
resource "aws_appconfig_deployment" "development" {
  application_id           = aws_appconfig_application.main.id
  environment_id           = aws_appconfig_environment.development.environment_id
  configuration_profile_id = aws_appconfig_configuration_profile.main.configuration_profile_id
  configuration_version    = aws_appconfig_hosted_configuration_version.development.version_number
  deployment_strategy_id   = aws_appconfig_deployment_strategy.instant.id
}

IAM Requirements

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "appconfig:GetConfiguration",
        "appconfig:GetLatestConfiguration",
        "appconfig:StartConfigurationSession"
      ],
      "Resource": [
        "arn:aws:appconfig:*:*:application/myapp",
        "arn:aws:appconfig:*:*:application/myapp/environment/*",
        "arn:aws:appconfig:*:*:application/myapp/configurationprofile/*"
      ]
    }
  ]
}

Testing with Localstack

⚠️ Note: Localstack Pro required for AppConfig support (free tier doesn't include AppConfig)

Alternative for local testing: Use environment variable overrides or local JSON files

Migration Checklist

  • Remove Azure App Configuration NuGet packages
  • Add AWS AppConfig NuGet packages
  • Create AWS AppConfig infrastructure (Terraform/CloudFormation)
  • Map Azure Labels to AWS Environments
  • Create Configuration Profiles for key groups
  • Update service registration
  • Implement configuration refresh logic
  • Deploy initial configuration versions
  • Add IAM policies
  • Update integration tests
  • Verify extension method signatures unchanged
  • Document deployment strategies

Common Pitfalls

⚠️ Polling required: AWS AppConfig requires explicit polling (vs Azure's push model)
⚠️ Session management: Must call StartConfigurationSession before GetLatestConfiguration
⚠️ Configuration format: AWS expects structured JSON (not flat key-value like Azure)
⚠️ Deployment required: Changes need explicit deployment (not immediate like Azure)
⚠️ Cost: AWS AppConfig charges per configuration request + hosted configuration storage

Success Criteria

✅ All Azure App Configuration code replaced with AWS AppConfig
✅ Labels successfully mapped to Environments + Profiles
✅ Extension method signatures unchanged
✅ Configuration refresh working
✅ IAM policies applied
✅ Deployment strategies configured
✅ No breaking changes to consuming applications