Custom Provider for AWS SSM using Microsoft.Configuration.ConfigurationBuilders
Asked Answered
H

1

5

I seem to be stuck at developing a custom Key/Value pair provider for Amazon's System Manager Parameter Store (SSM) using NETFramework 4.7.1 that utilizes Microsoft.Configuration.ConfigurationBuilders.

The implementation:

using System;
using System.Collections.Generic;
using Amazon.SimpleSystemsManagement;
using Amazon.SimpleSystemsManagement.Model;
using Microsoft.Configuration.ConfigurationBuilders;
using System.Linq;
using System.Diagnostics;
using System.Collections.Specialized;
using Amazon.Runtime;
using Amazon.Runtime.CredentialManagement;
using System.Configuration;
using System.Threading.Tasks;

namespace AXS.Configurations
{
    public class ParameterStoreConfigBuilder : KeyValueConfigBuilder
    {
        public const string envTag = "Environment";
        public const string appNameTag = "AppName";

        private IAmazonSimpleSystemsManagement client;
        /// <summary>
        /// Gets or sets an environment (dev|qa|staging|production)
        /// </summary>
        public string Environment { get; set; }
        /// <summary>
        /// Gets or sets a AppName 
        /// </summary>
        public string AppName { get; set; }

        public ParameterStoreConfigBuilder(IAmazonSimpleSystemsManagement client,
            string appName,
            string environment)
        {
            this.client = client;
            Environment = environment.ToLower();
            AppName = appName;
        }
        public ParameterStoreConfigBuilder()
        {
            client = new AmazonSimpleSystemsManagementClient();
        }

        public override string Description => "Parameter Store";
        public override string Name => "SSM";

        protected override void LazyInitialize(string name, NameValueCollection config)
        {
            Optional = false;
            base.LazyInitialize(name, config);
            string env = UpdateConfigSettingWithAppSettings(envTag);
            if (string.IsNullOrWhiteSpace(env))
                throw new ArgumentException($"environment must be specified with the '{envTag}' attribute.");
            Environment = env;
            string appName = UpdateConfigSettingWithAppSettings(appNameTag);
            if (string.IsNullOrWhiteSpace(appName))
                throw new ArgumentException($"appName must be specified with the '{appNameTag}' attribute.");
            AppName = appName;
            client = new AmazonSimpleSystemsManagementClient("","", Amazon.RegionEndpoint.USWest2);

        }
        public override ICollection<KeyValuePair<string, string>> GetAllValues(string prefix)
        {
            Trace.TraceInformation($"return values prefix {prefix}");
            if (client == null)
                return null;
            var parameters = new List<Parameter>();
            string nextToken = null;
            do
            {
                var response = client.GetParametersByPath(new GetParametersByPathRequest { Path = prefix, Recursive = true, WithDecryption = true, NextToken = nextToken });
                nextToken = response.NextToken;
                parameters.AddRange(response.Parameters);
            } while (!string.IsNullOrEmpty(nextToken));
            return parameters.Select(p => new
            {
                Key = p.Name,
                p.Value
            }).ToDictionary(parameter => parameter.Key, parameter => parameter.Value, StringComparer.OrdinalIgnoreCase);
        }

        public override string GetValue(string key)
        {
            return Task.Run(async () => { return await GetValueAsync(key); }).Result;
        }
        private async Task<string> GetValueAsync(string key)
        {
            var name = $"/{Environment}/{AppName}/{key.Replace(':', '/')}";
            Trace.WriteLine($"get value async:{name}");
            if (client == null)
                return null;

            try
            {
                Trace.TraceInformation($"fetch key {name}");
                var request = new GetParameterRequest
                {
                    Name = name,
                    WithDecryption = true
                };
                var response = await client.GetParameterAsync(request);
                var parameter = response.Parameter;
                var value = parameter.Type == ParameterType.SecureString ? "*****" : parameter.Value;
                Trace.TraceInformation($"fetched name={name} value={value}");
                return value;
            }
            catch (Exception e) when (Optional && ((e.InnerException is System.Net.Http.HttpRequestException) || (e.InnerException is UnauthorizedAccessException))) { }

            return null;
        }
    }
}

The problem seems to be that AWS SSM client never gets created. If I change the code and try to instantiate in the constructor I get a stack overflow exception due to recursion.

Any ideas on how to force to get AmazonSimpleSystemsManagementClient created?

The code uses guidance from https://github.com/aspnet/MicrosoftConfigurationBuilders

The App.Config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, 
             System.Configuration, Version=4.0.0.0, Culture=neutral, 
             PublicKeyToken=b03f5f7f11d50a3a" 
             restartOnExternalChanges="false" 
             requirePermission="true" />
  </configSections>
  <configBuilders>
    <builders>
      <add name="ParameterStore" Environment="development" AppName="myAppNameforParmStore" type="AXS.Configurations.ParameterStoreConfigBuilder, AXS.Configurations" />
      <add name="Env" prefix="appsettings_" stripPrefix="true" type="Microsoft.Configuration.ConfigurationBuilders.EnvironmentConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Environment, Version=2.0.0.0, Culture=neutral" />
    </builders>
  </configBuilders>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1" />
  </startup>
  <appSettings configBuilders="Env,ParameterStore">
    <add key="Url" value="URL Value for from paramter Store" />
    <add key="Secret" value="Some Secret value decrypted" />
  </appSettings>
</configuration>

Thanks

Hoye answered 24/9, 2019 at 16:51 Comment(3)
Hi Haroon - I decided today to write one of these for myself, and first searched online to see if anyone already wrote one, and found your question above. Assuming I get one working in the next few days, I'll come back and post the code, unless you already resolved your issue.Treasury
@Treasury - The underlying problem is the NET Framework with working with any 3rd party library that also reads from web.config such as Amazon's Runtime class to get configuration information. See the following: github.com/aspnet/MicrosoftConfigurationBuilders/issues/78Hoye
Hi Haroon - I actually worked around the recursive calls to the constructor by omitting the concrete constructor, and using only a static one, along with null checks in the GetValue/AllValues methods. I'll post my solution below. It works locally, but I haven't tested it on EC2 or anything yet.Treasury
T
7

UPDATE I posted an updated version of the AwsSsmConfigurationBuilder, and a sample ASP.NET Web Forms project that uses it, on my GitHub: https://github.com/Kirkaiya/AwsSsmConfigBuilderPoC/

Disclaimer: This is a proof-of-concept (POC) for a custom ConfigurationBuilder for ASP.NET 4.7.1 or higher (running on .NET Framework obviously). It's a POC, so it doesn't do anything besides allow you store Configuration AppSettings in AWS Parameter Store (a feature of Simple Systems Manager). So, clearly, don't use this in production without productizing and testing it!

Prerequisites:

  • Your project must target .NET Framework 4.7.1 or higher
  • Include NuGet package Microsoft.Configuration.ConfigurationBuilders.Base
  • Have parameters in AWS SSM Parameter Store that have the same name (not counting the prefix) as parameters in your web.config file, and vice-versa.

Notes In order to avoid recursively calling a concrete constructor or Initialize, I used a static constructor to instantiate the AmazonSimpleSystemsManagementClient, which is held in a static member.

Web.Config additions Note: change the assembly/class-name of your builder to match yours, etc.

<configSections>
  <section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />
</configSections>

<configBuilders>
  <builders>
    <add name="ParameterStore" ssmPrefix="/padnugapp/ApiKeys" type="Microsoft.Configuration.ConfigurationBuilders.AwsSsmConfigBuilder, AspNetWebFormsSample" />
  </builders>
</configBuilders>

<appSettings configBuilders="ParameterStore">
  <add key="TestKey" value="TestKey Value from web.config" /> 
  <add key="TwitterKey" value="TwitterKey value from web.config" />
</appSettings>

And the AwsSsmConfigBuilder.cs file:

namespace Microsoft.Configuration.ConfigurationBuilders
{
    public class AwsSsmConfigBuilder : KeyValueConfigBuilder
    {
        private string BaseParameterPath = "/padnugapp/ApiKeys";
        private static IAmazonSimpleSystemsManagement _client;

        static AwsSsmConfigBuilder()
        {
            _client = new AmazonSimpleSystemsManagementClient();
        }

        public override void Initialize(string name, NameValueCollection config)
        {
            base.Initialize(name, config);

            if (config["ssmPrefix"] == null)
                return;

            BaseParameterPath = config["ssmPrefix"];
        }

        public override ICollection<KeyValuePair<string, string>> GetAllValues(string prefix)
        {
            if (_client == null)
                return null;

            var request = new GetParametersByPathRequest
            {
                Path = $"{BaseParameterPath}/{prefix}",
                WithDecryption = true,
            };

            var response = _client.GetParametersByPathAsync(request).Result;

            var result = response.Parameters.ToDictionary(param => param.Name, param => param.Value, StringComparer.OrdinalIgnoreCase);

            return result;
        }

        public override string GetValue(string key)
        {
            if (_client == null)
                return null;

            var request = new GetParameterRequest
            {
                Name = $"{BaseParameterPath}/{key}",
                WithDecryption = true, 
            };

            var response = _client.GetParameterAsync(request).Result;

            return response.Parameter.Value;
        }
    }
}

The code I put into a web-forms (.aspx) page that renders the two appSettings items in HTML:

TestKey = 
    <%=(System.Configuration.ConfigurationManager.AppSettings["TestKey"]) %>
    <br />
    TwitterKey = 
    <%=(System.Configuration.ConfigurationManager.AppSettings["TwitterKey"]) %>

I can't stress enough that this is just for a demo I'm doing, and not tested in any way, shape or form except on my laptop ;-)

Treasury answered 26/12, 2019 at 20:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.