Home β€Ί .NET on AWS β€Ί Configuration with Parameter Store and Secrets Manager

Configuration with Parameter Store and Secrets Manager

Managing configuration and secrets in .NET on AWS: SSM Parameter Store, Secrets Manager, the IConfiguration integration, and CDK setup.

The Configuration Problem on AWS

Every .NET app needs configuration. Database connection strings, API keys, feature flags, service URLs. On AWS, you have two services for this:

  • SSM Parameter Store: Free (standard tier), hierarchical key-value store. Good for non-sensitive configuration and simple secrets.
  • Secrets Manager: $0.40/secret/month, automatic rotation, cross-account sharing. Built for credentials that need lifecycle management.

Most apps use both: Parameter Store for general configuration, Secrets Manager for database passwords and API keys that need rotation.

Parameter Store

Reading parameters directly

using Amazon.SimpleSystemsManagement;
using Amazon.SimpleSystemsManagement.Model;

var ssmClient = new AmazonSimpleSystemsManagementClient();

// Single parameter
var response = await ssmClient.GetParameterAsync(new GetParameterRequest
{
    Name = "/myapp/prod/feature-flags/dark-mode",
    WithDecryption = true, // for SecureString parameters
});
var value = response.Parameter.Value;

// Multiple parameters by path (hierarchical)
var paramsResponse = await ssmClient.GetParametersByPathAsync(new GetParametersByPathRequest
{
    Path = "/myapp/prod/",
    Recursive = true,
    WithDecryption = true,
});

foreach (var param in paramsResponse.Parameters)
{
    Console.WriteLine($"{param.Name} = {param.Value}");
}

Parameter naming convention

Use a hierarchy that maps to environment and service:

/myapp/prod/database/connection-string
/myapp/prod/cache/endpoint
/myapp/prod/feature-flags/dark-mode
/myapp/staging/database/connection-string

This lets you load all config for an environment with a single GetParametersByPath call.

Secrets Manager

Reading secrets

using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;

var secretsClient = new AmazonSecretsManagerClient();

var response = await secretsClient.GetSecretValueAsync(new GetSecretValueRequest
{
    SecretId = "myapp/prod/database-credentials",
});

// Secrets are often stored as JSON
var credentials = JsonSerializer.Deserialize<DatabaseCredentials>(
    response.SecretString, AppJsonContext.Default.DatabaseCredentials);

var connectionString = $"Host={credentials.Host};Database={credentials.Database};Username={credentials.Username};Password={credentials.Password}";
public record DatabaseCredentials
{
    public required string Host { get; init; }
    public required string Database { get; init; }
    public required string Username { get; init; }
    public required string Password { get; init; }
}

IConfiguration Integration

The cleanest approach for ASP.NET Core / hosted services is integrating with the standard IConfiguration pipeline. AWS provides NuGet packages for this:

Setup

<PackageReference Include="Amazon.Extensions.Configuration.SystemsManager" Version="6.*" />
<PackageReference Include="AWSSDK.Extensions.NETCore.Setup" Version="3.*" />
var builder = WebApplication.CreateBuilder(args);

// Load from Parameter Store
builder.Configuration.AddSystemsManager(config =>
{
    config.Path = $"/myapp/{builder.Environment.EnvironmentName}";
    config.ReloadAfter = TimeSpan.FromMinutes(5); // auto-refresh
    config.Optional = false;
});

// Now access like any other configuration
var featureFlag = builder.Configuration["feature-flags:dark-mode"];

This maps the parameter hierarchy to IConfiguration keys:

  • /myapp/prod/database/host β†’ Configuration["database:host"]
  • /myapp/prod/feature-flags/dark-mode β†’ Configuration["feature-flags:dark-mode"]

Secrets Manager with IConfiguration

<PackageReference Include="Kralizek.Extensions.Configuration.AWSSecretsManager" Version="2.*" />
builder.Configuration.AddSecretsManager(config =>
{
    config.SecretFilter = entry => entry.Name.StartsWith("myapp/prod/");
    config.PollingInterval = TimeSpan.FromMinutes(10);
});

Lambda Configuration Pattern

In Lambda, avoid loading configuration on every invocation. Load once during cold start:

public class Function
{
    private static readonly AppConfig _config;

    static Function()
    {
        var ssmClient = new AmazonSimpleSystemsManagementClient();
        
        var response = ssmClient.GetParametersByPathAsync(new GetParametersByPathRequest
        {
            Path = $"/myapp/{Environment.GetEnvironmentVariable("ENVIRONMENT")}/",
            Recursive = true,
            WithDecryption = true,
        }).Result;
        
        _config = new AppConfig
        {
            DatabaseEndpoint = response.Parameters.First(p => p.Name.EndsWith("/database/endpoint")).Value,
            CacheEndpoint = response.Parameters.First(p => p.Name.EndsWith("/cache/endpoint")).Value,
            FeatureFlags = response.Parameters
                .Where(p => p.Name.Contains("/feature-flags/"))
                .ToDictionary(
                    p => p.Name.Split('/').Last(),
                    p => bool.Parse(p.Value)),
        };
    }

    public async Task<APIGatewayProxyResponse> Handler(
        APIGatewayProxyRequest request, ILambdaContext context)
    {
        // Use _config β€” loaded once, reused across warm invocations
    }
}

Lambda environment variables vs Parameter Store

Approach Cold start impact Change requires Best for
Environment variables None (already loaded) Redeploy Lambda Static config (table names, region)
Parameter Store +50-100ms on cold start No redeploy Config that changes (feature flags, URLs)
Secrets Manager +50-150ms on cold start No redeploy Credentials that rotate

Recommendation: Use environment variables for things that change with deployments (resource names, stage). Use Parameter Store for things that change independently (feature flags, dynamic config). Use Secrets Manager for credentials.

CDK Setup (C#)

using Amazon.CDK;
using Amazon.CDK.AWS.SSM;
using Amazon.CDK.AWS.SecretsManager;

// Parameter Store values
new StringParameter(this, "DatabaseEndpoint", new StringParameterProps
{
    ParameterName = "/myapp/prod/database/endpoint",
    StringValue = cluster.ClusterEndpoint.Hostname,
    Description = "Aurora cluster endpoint",
    Tier = ParameterTier.STANDARD,
});

new StringParameter(this, "CacheEndpoint", new StringParameterProps
{
    ParameterName = "/myapp/prod/cache/endpoint",
    StringValue = cache.AttrEndpointAddress,
    Description = "ElastiCache endpoint",
});

// Secret (auto-generated password)
var dbSecret = new Secret(this, "DatabaseCredentials", new SecretProps
{
    SecretName = "myapp/prod/database-credentials",
    Description = "Aurora database credentials",
    GenerateSecretString = new SecretStringGenerator
    {
        SecretStringTemplate = JsonSerializer.Serialize(new { username = "app_user" }),
        GenerateStringKey = "password",
        ExcludePunctuation = true,
        PasswordLength = 32,
    },
});

// Grant Lambda access to read parameters
lambdaFunction.AddToRolePolicy(new PolicyStatement(new PolicyStatementProps
{
    Actions = new[] { "ssm:GetParameter", "ssm:GetParametersByPath" },
    Resources = new[] { $"arn:aws:ssm:{Aws.REGION}:{Aws.ACCOUNT_ID}:parameter/myapp/prod/*" },
}));

// Grant Lambda access to read secrets
dbSecret.GrantRead(lambdaFunction);

Caching and Performance

Parameter Store and Secrets Manager both have API rate limits and add latency. Cache values appropriately:

public class CachedConfigProvider
{
    private readonly IAmazonSimpleSystemsManagement _ssm;
    private Dictionary<string, string> _cache = new();
    private DateTime _lastRefresh = DateTime.MinValue;
    private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5);

    public async Task<string> GetValueAsync(string key)
    {
        if (DateTime.UtcNow - _lastRefresh > _cacheDuration)
        {
            await RefreshCacheAsync();
        }
        
        return _cache.GetValueOrDefault(key) 
            ?? throw new KeyNotFoundException($"Config key not found: {key}");
    }

    private async Task RefreshCacheAsync()
    {
        var response = await _ssm.GetParametersByPathAsync(new GetParametersByPathRequest
        {
            Path = "/myapp/prod/",
            Recursive = true,
            WithDecryption = true,
        });
        
        _cache = response.Parameters.ToDictionary(
            p => p.Name.Replace("/myapp/prod/", ""),
            p => p.Value);
        _lastRefresh = DateTime.UtcNow;
    }
}

Tips

  • Don't bake secrets into environment variables in CDK. Use dynamic references or grant the function access to read from Secrets Manager/Parameter Store at runtime.
  • Use SecureString type in Parameter Store for anything sensitive. It encrypts with KMS at no extra cost.
  • Rate limits matter. Parameter Store: 40 TPS standard, 1000 TPS advanced. Secrets Manager: 10,000 TPS. Don't call them on every request. Cache.
  • Use the IConfiguration integration for long-running services (ECS). For Lambda, the static constructor pattern with direct SDK calls is more predictable.
  • Secrets Manager supports automatic rotation. For RDS credentials, enable it. Secrets Manager rotates the password and updates the secret automatically.
  • Parameter Store is free (standard tier, up to 10,000 params). Secrets Manager costs $0.40/secret/month. Don't pay for Secrets Manager for non-sensitive config.

Further Reading

Looking for hands-on help? View my .NET on AWS services β†’

Managing secrets across environments?

Drop me a message β€” I typically respond within one business day.