Card image cap

Externalizing Configurations to AWS SSM Parameter Store and using in ASP.NET Core API

AWS ASP.NET Core  • Posted 2 months ago

So far we have spoken in length about the importance of using strongly typed classes for binding configuration sections and how the Options pattern offered by AspNetCore helps us in binding these configuration sections to their respective strongly typed classes and offers an injectable solution by means of its service collection.

In a parallel universe, not always we can maintain all our configurations inside our application / API project. configurations such as SMTP Mail, Connection Strings and JWT settings can change with the deployed environments and might need to be maintained in a place out of the project code. While singular settings such as Connection String can be maintained in the environmental variables which obviously depend on the deployment slots, storing complex sections such as JWT or SMTP which have multiple keys under them into environmental variables may not be an elegant solution.

For deploying applications / APIs in the world of AWS, we solve this by using another service from AWS called as "Parameters Store". In this article, let's talk in detail about what this "Parameter Store" offers and how we can store values in Parameter Store and access them in our AspNetCore API.

What is Parameter Store?

Parameter Store is a part of the larger AWS Systems Manager, which is yet another offering from the AWS Cloud suite of services. The official documentation explains the "Systems Manager" as:

"Systems Manager provides a unified user interface so you can view operational data from multiple AWS services and allows you to automate operational tasks across your AWS resources."

While this is something which solves a greater cause, what we're really interested in is a service offered as a part of the AWS Systems Manager. The Parameter Store is explained in the documentation as:

"AWS Systems Manager provides a centralized store to manage your configuration data, whether plain-text data such as database strings or secrets such as passwords. Parameters can be tagged and organized into hierarchies, helping you manage parameters more easily."

So basically it acts as an externalized configuration store, which can be used to store and manage our application related settings in a "hierarchical" fashion. Access to resources can be secured by means of the IAM policies and can be used in other AWS services such as AWS Lambda, EC2 and others. To explain how this works, let's look at how we add settings into Parameter Store.

Adding Keys to Parameter Store

To access parameter store, open the AWS Console and search for Systems Manager in the services or the search bar. In the Systems Manager, on the left menu bar click on "Parameter Store" which opens up a list of key settings with a "key" and a "value" along with other properties.

To create, click on "Add Parameter" on the top right which opens up a new page, which looks like below:

data/Admin/2020/7/paramstore-key-string.png

There is the standard "Name" which doubles as the unique parameter "Key", an optional "Description" to explain about the setting being created, the tier, Type being one from the three options provided - "String", "StringList" or the security focused "SecureString" and finally the "Value" which holds the value for this "Key".

When we select the type as a "SecureString" the value shall be encrypted with a key powered by Amazon KMS service. In our case, we've stored our Smtp password setting as a "SecureString".

data/Admin/2020/7/paramstore-key-securestring.png

The convention used in giving a "Key" is where we indicate the hierarchy. For example, let's add the Smtp section generally stored inside the appsettings.json into the Parameter Store. The section looks like below in the appsettings.

"Smtp": {
    "Port": 36,
    "Server": "abc.com",
    "Username": "admin",
    "Password": "abcd@1234",
    "From": "admin",
    "IsSsl": true
}

To add these settings into Parameter Store, their respective "Key" names would be like this:

/myparams/smtp/port
/myparams/smtp/server
/myparams/smtp/username
/myparams/smtp/password
/myparams/smtp/from
/myparams/smtp/isssl

Similarly for our Oidc settings we used before,

"Oidc": {
    "Google": {
      "ClientId": "27454jshd64dufmgngterh",
      "ClientSecret": "39jfytittthsd83jgygtttktt7tyy8kthgh8"
    },
    "Facebook": {
      "ClientId": "o375j6593jdgdb254en62rd",
      "ClientSecret": "vhfyrfjhrrfdsow0273485djdgw722pr955ht"
    },
    "Okta": {
      "ClientId": "a1b2c3d4e5f6g7h8i9j03fg",
      "ClientSecret": "hfgfwornrtftfjnsfre693i34543u2gdjfbff"
    }
}

It'd be:

/myparams/google/clientid
/myparams/google/clientsecret
/myparams/facebook/clientid
/myparams/facebook/clientsecret
/myparams/okta/clientid
/myparams/okta/clientsecret

"The setting names reflect the hierarchy."

Imagine the Systems Manager as a large and global appsettings.json which contains all sorts of values across the services. To add our own set of configurations pertained to our own application, we group them into a "section" called "myparams" and this "myparams" represents our own appsettings.json contents. From there, its all self-explanatory: similar to how we access a value under a section in the form "smtp:port", we just use "/" in place of ":". And this nesting can go as deep as possible - although 3 levels are recommended even for our ease of maintenence.

data/Admin/2020/7/paramstore-key-suggestions.png

Accessing Parameter Store inside ASP.NET Core:

Coming to the most interesting part of this article, let's see how we can bind these settings inside our ASP.NET Core API. For better understanding, let's create a new dotnetcore API project called "ParamStoreApp".

> dotnet new webapi --name ParamStoreApp

Once created, let's add a class SmtpOptions which resembles the Smtp settings in the Parameter Store. We're going to use this class to bind the values from the Parameter Store and access them in our application.

public class SmtpOptions
{
    public const string SectionName = "Smtp";
    public int Port { get; set; }
    public string From { get; set; }
    public string Server { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public bool IsSsl { get; set; }
}

Observe that we have created the accessors with their actual expected datatypes - Port as an integer and IsSsl as a boolean, while the settings in the Parameter Store are all basically "Strings".

To access the Parameter Store in DotnetCore applications, AWS provides a library using which we can add the ParameterStore as a Configuration Provider to the IConfiguration and access the settings directly as we do from the IConfiguration service!

> dotnet add package Amazon.Extensions.Configuration.SystemsManager --version 1.2.0

Once added, inside our Main method where the Host is built, link the SystemsManager as an AppConfiguration provider.

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .ConfigureAppConfiguration((builder) =>
        {
        builder.AddSystemsManager("/myparams", new AWSOptions
        {
            Region = RegionEndpoint.USWest2
        });
    });

Keep in mind that the string parameter "/myparams" represents the section of the settings we are interested in to use from the ParameterStore - which is the "prefix" added to all settings we've moved from our appsettings into the ParameterStore followed by their hierarchy. The Region is a mandatory parameter to be passed - which represents the region where these settings are available.

This completes the link between the ParameterStore and our API, and we're as good as simply using the settings from the IConfiguration as we do naturally.

Let's test this by configuring our SmtpOptions class to the "Smtp" section from the ParameterStore inside the Startup.

// SmtpOptions.SectionName = "Smtp"
// configured inside the model class itself
services.Configure<SmtpOptions>(
    Configuration.GetSection(SmtpOptions.SectionName));

Now this class is available to access via the Options pattern we're pretty much aware of. Let's create a simple Controller which returns this SmtpOptions object when requested.

[Route("api/[controller]")]
public class ParamsController : ControllerBase
{
    private readonly IOptions<SmtpOptions> _smtpOptions;
        
    public ParamsController(
        IOptions<SmtpOptions> smtpOptions
    )
    {
        _smtpOptions = smtpOptions;
    }

    [HttpGet, Route("smtp")]
    public SmtpOptions GetSmtpOptions()
    {
        return _smtpOptions.Value;
    }

When we run this setup and hit on the API /api/params/smtp, we get the following response:

data/Admin/2020/7/params-store-output.png

Similarly, to access the Oidc settings, we can make use of an OidcOptions class to represent the model and then configure them with "named" options from the IOptionsSnaphot.

public class OidcProviders
{
    public const string Google = "Google";
    public const string Facebook = "Facebook";
    public const string Okta = "Okta";
}

public class OidcOptions
{
    public const string SectionName = "Oidc";
    public string ClientId { get; set; }
    public string ClientSecret { get; set; }
}
services.Configure<OidcOptions>(
    OidcProviders.Google,
    Configuration.GetSection($"{OidcOptions.SectionName}:{OidcProviders.Google}"));

services.Configure<OidcOptions>(
    OidcProviders.Facebook,
    Configuration.GetSection($"{OidcOptions.SectionName}:{OidcProviders.Facebook}"));

services.Configure<OidcOptions>(
    OidcProviders.Okta,
    Configuration.GetSection($"{OidcOptions.SectionName}:{OidcProviders.Okta}"));
public class ParamsController : ControllerBase
{
    private readonly IOptions<SmtpOptions> _smtpOptions;
    private readonly IOptionsSnapshot<OidcOptions> _oidcOptions;
        
    public ParamsController(
        IOptions<SmtpOptions> smtpOptions, 
        IOptionsSnapshot<OidcOptions> oidcOptions
    ){
        _smtpOptions = smtpOptions;
        _oidcOptions = oidcOptions;
        _allowedHostsOptions = configOptions;
    }

    [HttpGet, Route("smtp")]
    public SmtpOptions GetSmtpOptions()
    {
        return _smtpOptions.Value;
    }

    [HttpGet, Route("oidc/{id}")]
    public OidcOptions GetOidcOptions(string id)
    {
        return _oidcOptions.Get(id);
    }
}

To access the respective configurations we access the API as /api/params/oidc/google or /api/params/oidc/facebook or /api/params/oidc/okta.

ParameterStore and AutoReload:

One of the interesting features of IOptionsSnaphot is that the settings can be reloaded even after the application has started on the runtime. However, using ParameterStore seems to only partially embrace this feature, by means of a polling mechanism (I assume). By default, the settings aren't reloaded but we can configure the provider to reload the settings for a preconfigured time as below:

// settings are picked and reloaded
// for every 10 minutes
// this reload reflects when using 
// IOptionsSnaphot or IOptionsMonitor
// services for accessing the config

builder.AddSystemsManager(
    path: "/myparams", 
    awsOptions: new AWSOptions
    {
        Region = RegionEndpoint.USWest2
    }, 
    optional: false, 
    reloadAfter: TimeSpan.FromMinutes(10));

This isn't mentioned anywhere (as far as I've searched) but seems like this reloadAfter configures the application to read and reload the configuration settings after every 10 minutes (since we configured above) and the changed settings are visible when using IOptionsSnaphot or IOptionsMonitor services.

Accessing StringLists:

We know that StringList is another Type we can choose while creating a Parameter inside the ParameterStore. The input would be a comma seperated set of values like below:

"abc.com","123.com","localhost","accessed.com"

However, when we try to configure this to a List in our API it doesn't implicitly bind to the type and seems like we still would need to bind it to a "string" and process it by ourselves. Phew!

In this way, we can make use of the prowess of AWS ParameterStore to externalize our application settings and then easily access it in our application using the AddSystemsManager() extension provided by AWS library.

The source code of the example used in this article is available under: https://github.com/referbruv/aws-parameterstore-aspnetcore-sample-app

What is the difference between Response.Redirect() and Server.Transfer() ?
How do you handle errors Globally in ASP.NET Core?
How do you design a strongly-typed class for a configuration?
How can you bind a configuration section to an object?
When to use IOptionsMonitor?

aws parameter store .net core

We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept