AgentSkillsCN

azure-keyvault-to-secrets

将Azure Key Vault SDK迁移至AWS Secrets Manager。将Azure.Security.KeyVault.Secrets转换为AWSSDK.SecretsManager,同时保留现有扩展方法的签名。支持密钥的检索、存储、版本控制与配置模式。

SKILL.md
--- frontmatter
name: azure-keyvault-to-secrets
description: |
  Migrate Azure Key Vault SDK to AWS Secrets Manager. Converts Azure.Security.KeyVault.Secrets
  to AWSSDK.SecretsManager while preserving existing extension method signatures. Handles
  secret retrieval, storage, versioning, and configuration patterns.
disposition: contextual
filePatterns:
  - "**/*.cs"
  - "**/*.csproj"
  - "**/appsettings*.json"
  - "**/Program.cs"
  - "**/Startup.cs"
version: 1.0.0

Azure Key Vault → AWS Secrets Manager Migration

Overview

Migrate projects from Azure.Security.KeyVault.Secrets to AWSSDK.SecretsManager while maintaining identical public API contracts.

Package Migration

Azure Packages (Remove)

xml
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.x.x" />
<PackageReference Include="Azure.Identity" Version="1.x.x" />

AWS Packages (Add)

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

Configuration Migration

Azure Configuration (appsettings.json)

json
{
  "KeyVault": {
    "VaultUri": "https://my-vault.vault.azure.net/",
    "TenantId": "...",
    "ClientId": "...",
    "ClientSecret": "..."
  }
}

AWS Configuration (appsettings.json)

json
{
  "AWS": {
    "Region": "us-east-1",
    "Profile": "default"
  },
  "SecretsManager": {
    "SecretPrefix": "myapp/"
  }
}

Environment Variables:

  • AWS_REGION - AWS region (e.g., us-east-1)
  • AWS_ACCESS_KEY_ID - AWS access key (local dev only)
  • AWS_SECRET_ACCESS_KEY - AWS secret key (local dev only)
  • Use IAM roles/instance profiles in production

Service Registration

Azure (Before)

csharp
services.AddSingleton(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    var vaultUri = new Uri(config["KeyVault:VaultUri"]!);
    var credential = new DefaultAzureCredential();
    return new SecretClient(vaultUri, credential);
});

AWS (After)

csharp
services.AddDefaultAWSOptions(configuration.GetAWSOptions());
services.AddAWSService<IAmazonSecretsManager>();

// Or with explicit configuration
services.AddSingleton<IAmazonSecretsManager>(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    var awsConfig = new AmazonSecretsManagerConfig
    {
        RegionEndpoint = RegionEndpoint.GetBySystemName(config["AWS:Region"])
    };
    return new AmazonSecretsManagerClient(awsConfig);
});

Extension Method Preservation

CRITICAL: Existing extension method signatures MUST remain unchanged.

Example Extension Methods (Keep Signatures)

csharp
// Original interface that consuming apps depend on
public interface ISecretService
{
    Task<string?> GetSecretAsync(string secretName, CancellationToken cancellationToken = default);
    Task SetSecretAsync(string secretName, string secretValue, CancellationToken cancellationToken = default);
    Task<bool> DeleteSecretAsync(string secretName, CancellationToken cancellationToken = default);
}

Azure Implementation (Before)

csharp
public class AzureSecretService : ISecretService
{
    private readonly SecretClient _client;
    private readonly ILogger<AzureSecretService> _logger;

    public AzureSecretService(SecretClient client, ILogger<AzureSecretService> logger)
    {
        _client = client;
        _logger = logger;
    }

    public async Task<string?> GetSecretAsync(string secretName, CancellationToken cancellationToken = default)
    {
        try
        {
            KeyVaultSecret secret = await _client.GetSecretAsync(secretName, cancellationToken: cancellationToken);
            return secret.Value;
        }
        catch (RequestFailedException ex) when (ex.Status == 404)
        {
            _logger.LogWarning("Secret {SecretName} not found", secretName);
            return null;
        }
    }

    public async Task SetSecretAsync(string secretName, string secretValue, CancellationToken cancellationToken = default)
    {
        await _client.SetSecretAsync(secretName, secretValue, cancellationToken);
        _logger.LogInformation("Secret {SecretName} updated", secretName);
    }

    public async Task<bool> DeleteSecretAsync(string secretName, CancellationToken cancellationToken = default)
    {
        try
        {
            await _client.StartDeleteSecretAsync(secretName, cancellationToken);
            return true;
        }
        catch (RequestFailedException ex) when (ex.Status == 404)
        {
            return false;
        }
    }
}

AWS Implementation (After)

csharp
public class AwsSecretService : ISecretService
{
    private readonly IAmazonSecretsManager _client;
    private readonly ILogger<AwsSecretService> _logger;
    private readonly string _secretPrefix;

    public AwsSecretService(
        IAmazonSecretsManager client, 
        IConfiguration configuration,
        ILogger<AwsSecretService> logger)
    {
        _client = client;
        _logger = logger;
        _secretPrefix = configuration["SecretsManager:SecretPrefix"] ?? string.Empty;
    }

    public async Task<string?> GetSecretAsync(string secretName, CancellationToken cancellationToken = default)
    {
        try
        {
            var request = new GetSecretValueRequest
            {
                SecretId = $"{_secretPrefix}{secretName}"
            };
            
            var response = await _client.GetSecretValueAsync(request, cancellationToken);
            return response.SecretString;
        }
        catch (ResourceNotFoundException)
        {
            _logger.LogWarning("Secret {SecretName} not found", secretName);
            return null;
        }
    }

    public async Task SetSecretAsync(string secretName, string secretValue, CancellationToken cancellationToken = default)
    {
        try
        {
            // Try to update existing secret
            var updateRequest = new UpdateSecretRequest
            {
                SecretId = $"{_secretPrefix}{secretName}",
                SecretString = secretValue
            };
            
            await _client.UpdateSecretAsync(updateRequest, cancellationToken);
            _logger.LogInformation("Secret {SecretName} updated", secretName);
        }
        catch (ResourceNotFoundException)
        {
            // Create new secret if it doesn't exist
            var createRequest = new CreateSecretRequest
            {
                Name = $"{_secretPrefix}{secretName}",
                SecretString = secretValue
            };
            
            await _client.CreateSecretAsync(createRequest, cancellationToken);
            _logger.LogInformation("Secret {SecretName} created", secretName);
        }
    }

    public async Task<bool> DeleteSecretAsync(string secretName, CancellationToken cancellationToken = default)
    {
        try
        {
            var request = new DeleteSecretRequest
            {
                SecretId = $"{_secretPrefix}{secretName}",
                ForceDeleteWithoutRecovery = false, // Allow 30-day recovery window
                RecoveryWindowInDays = 30
            };
            
            await _client.DeleteSecretAsync(request, cancellationToken);
            return true;
        }
        catch (ResourceNotFoundException)
        {
            return false;
        }
    }
}

IAM Requirements

Minimum IAM Policy (attach to EC2/ECS role or IAM user):

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "arn:aws:secretsmanager:*:*:secret:myapp/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:CreateSecret",
        "secretsmanager:UpdateSecret"
      ],
      "Resource": "arn:aws:secretsmanager:*:*:secret:myapp/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:DeleteSecret"
      ],
      "Resource": "arn:aws:secretsmanager:*:*:secret:myapp/*"
    }
  ]
}

Testing with Localstack

docker-compose.yml for local development:

yaml
version: '3.8'
services:
  localstack:
    image: localstack/localstack:latest
    ports:
      - "4566:4566"
    environment:
      - SERVICES=secretsmanager
      - DEBUG=1
    volumes:
      - ./localstack-data:/var/lib/localstack

Test configuration (appsettings.Development.json):

json
{
  "AWS": {
    "Region": "us-east-1",
    "ServiceURL": "http://localhost:4566"
  }
}

Migration Checklist

  • Remove Azure Key Vault NuGet packages
  • Add AWS Secrets Manager NuGet packages
  • Update service registration in Program.cs/Startup.cs
  • Convert SecretClient to IAmazonSecretsManager
  • Update configuration (appsettings.json)
  • Implement secret prefix for multi-tenant scenarios
  • Add IAM policies to AWS infrastructure (Terraform/CloudFormation)
  • Update integration tests to use Localstack
  • Verify extension method signatures unchanged
  • Update documentation and runbooks
  • Test secret rotation if applicable

Common Pitfalls

⚠️ Secret naming: AWS Secrets Manager allows / in names for hierarchical structure
⚠️ Deletion: AWS has 30-day recovery window by default (vs Azure's immediate delete)
⚠️ Versioning: AWS automatic versioning uses staging labels (AWSCURRENT, AWSPREVIOUS)
⚠️ Cost: AWS charges per secret per month + API calls (different from Azure pricing)
⚠️ Secret size: AWS max 65,536 bytes (64 KB) vs Azure's 25 KB limit

Success Criteria

✅ All Azure Key Vault code replaced with AWS Secrets Manager
✅ Extension method signatures unchanged
✅ Configuration migrated to AWS patterns
✅ IAM policies documented and applied
✅ Localstack tests passing
✅ No breaking changes to consuming applications