Chapter 9. Configuring Microservice Ecosystems

Configuration is one of the areas of architecture and implementation that are often overlooked by product teams. A lot of teams just assume that the legacy paradigms for configuring applications will work fine in the cloud. Further, it’s easy to assume that you’ll “just” inject all configuration through environment variables.

Configuration in a microservice ecosystem requires attention to a number of other factors, including:

  • Securing read and write access to configuration values
  • Ensuring that an audit trail of value changes is available
  • Resilience and reliability of the source of configuration information
  • Support for large and complex configuration information likely too burdensome to cram into a handful of environment variables
  • Determining whether your application needs to respond to live updates or real-time changes in configuration values, and if so, how to provision for that
  • Ability to support things like feature flags and complex hierarchies of settings
  • Possibly supporting the storage and retrieval of secure (encrypted) information or the encryption keys themselves

Not every team has to worry about all of these things, but this is just a hint as to the complexity of configuration management lying below the surface waiting to strike those who underestimate this problem.

This chapter will begin by talking about the mechanics of using environment variables in an application and illustrate Docker’s support for this. Next, we’ll explore a configuration server product from the Netflix OSS stack. Finally, we’ll dive deeper into etcd, an open source distributed key-value store often used for configuration management.

Using Environment Variables with Docker

It is actually fairly easy to work with environment variables and Docker. This book has harped on this point a number of times. Cloud-native applications need to be able to accept configuration through environment variables. While you might accept more robust configuration mechanisms (we’ll discuss those shortly), environment variables supplied by the platform on which you deploy should the minimal level of configuration support your applications have.

Even if you have a default set of configuration available, you should figure out which settings can be overridden by environment variables at application startup.

You can explicitly set configuration values using name-value pairs as shown in the following command:

$ sudo docker run -e SOME_VAR='foo' 
  -e PASSWORD='foo' 
  -e USER='bar' 
  -e DB_NAME='mydb' 
  -p 3000:3000 
  --name container_name microservices-aspnetcore/image:tag

Or, if you want to avoid passing explicit values on the command line, you can forward environment variables from the launching environment into the container by simply not passing values or using the equals sign, as shown here:

$ docker run -e PORT -e CLIENTSECRET -e CLIENTKEY [...]

This will take the PORT, CLIENTSECRET, and CLIENTKEY environment variables from the shell in which the command was run and pass their values into the Docker container without exposing their values on the command line, preventing a potential security vulnerability or leaking of confidential information.

If you have a large number of environment variables to pass into your container, you can give the docker command the name of a file that contains name-value pairs:

$ docker run --env-file ./myenv.file [...]

If you’re running a higher-level container orchestration tool like Kubernetes, then you will have access to more elegant ways to manage your environment variables and how they get injected into your containers. With Kubernetes, you can use a concept called ConfigMap to make external configuration values available to your containers without having to create complex launch commands or manage bloated start scripts.

A deep dive into container orchestration systems is beyond the scope of this book, but this should reinforce the idea that no matter what your ultimate deployment target is going to be, all of them should have some means of injecting environment variables so your application must know how to accept those values.

By supporting environment variable injection and sticking with Docker as your unit of immutable artifact deployment, you’re well positioned to run in any number of environments without becoming too tightly coupled to any one in particular.

Using Spring Cloud Config Server

One of the biggest difficulties surrounding configuration management for services lies not in the mechanics of injecting values into environment variables, but in the day-to-day maintenance of the values themselves.

How do we know when the ultimate source of truth for the configuration values has changed? How do we know who changed them, and how do we implement security controls to prevent these values from being changed by unauthorized personnel and keep the values hidden from those without appropriate access?

Further, if values do change, how do we go back and see what the previous values were? If you’re thinking that we could use a solution like a Git repository to manage configuration values, then you’re not alone.

The folks who built Spring Cloud Config Server (SCCS) had the same idea. Why reinvent the wheel (security, version control, auditing, etc.) when Git has already solved the problem? Instead they built a service that exposes the values contained in a Git repository through a RESTful API.

This API exposes URLs in the following format:

/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

If your application is named foo, then all of the {application} segments in the preceding template would be replaced with foo. To see the configuration values available in the development profile (environment), you would issue a GET request to the /foo/development URL.

To find out more about Spring Cloud Config Server, you can start with the documentation.

While the documentation and code are targeted at Java developers, there are plenty of other clients that can talk to SCCS, including a .NET Core client that is part of the Steeltoe project (discussed in the previous chapter).

To add client-side support for SCCS to our .NET Core application, we just need to add a reference to the Steeltoe.Extensions.Configuration.ConfigServer NuGet package.

Next, we need to configure our application so it can get the right settings from the right place. This means we need to define a Spring application name and give the URL to the configuration server in our appsettings.json file (remember every setting in this file can be overridden with environment variables):

{
  "spring": {
    "application": {
      "name": "foo"
    },
    "cloud": {
      "config": {
        "uri": "http://localhost:8888"
      }
    }
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

With this configuration set up, our Startup method looks almost exactly like it does in most of our other applications:

public Startup(IHostingEnvironment env)
{ 
   var builder = new ConfigurationBuilder()
       .SetBasePath(env.ContentRootPath)
       .AddJsonFile("appsettings.json", optional: false, 
            reloadOnChange: false)                
       .AddEnvironmentVariables()
       .AddConfigServer(env);
   
   Configuration = builder.Build();
}

The next changes required to add support for the configuration server come in the ConfigureServices method. First, we call AddConfigServer, which enables the client through dependency injection. Next, we call Configure with a generic type parameter. This allows us to capture the application’s settings as retrieved from the server in an IOptionsSnapshot, which is then available for injection into our controllers and other code:

public void ConfigureServices(IServiceCollection services)
{
    services.AddConfigServer(Configuration);

    services.AddMvc();

    services.Configure<ConfigServerData>(Configuration);
}

The class we’re using here to hold the data from the config server is modeled after the sample configuration that can be found in the Spring Cloud server sample repository:

public class ConfigServerData
{
    public string Bar { get; set; }
    public string Foo { get; set; }
    public Info Info { get; set; }

}

public class Info
{
   public string Description { get; set; }
   public string Url { get; set; }
}

We can then inject our C# class as well as the configuration server client settings if we need them:

public class MyController : Controller 
{
   private IOptionsSnapshot<ConfigServerData> 
     MyConfiguration { get; set; }

   private ConfigServerClientSettingsOptions 
     ConfigServerClientSettingsOptions { get; set; }

   public MyController(IOptionsSnapShot<ConfigServerData> opts,
                       IOptions<ConfigServerClientSettingsOptions>
                          clientOpts)
   {
      ...
   }

    ....
}

With this setup in place, and a running configuration server, the opts variable in the constructor will contain all of the relevant configuration for our application.

To run the config server, we can build and launch the code from GitHub if we want, but not all of us have a functioning Java/Maven development environment up and running (and some of us simply don’t want a Java environment). The easiest way to start a configuration server is to just launch it from a Docker image:

$ docker run -p 8888:8888 
 -e SPRING_CLOUD_CONFIG_SERVER_GIT_URI=https://github.com/spring-cloud-samples/ 
 config-repohyness/spring-cloud-config-server

This will start the server and point it at the sample GitHub repo mentioned earlier to obtain the “foo” application’s configuration properties. If the server is running properly, you should get some meaningful information from the following command:

$ curl http://localhost:8888/foo/development

With a config server Docker image running locally and the C# code illustrated in this section of the chapter, you should be able to play with exposing external configuration data to your .NET Core microservices.

Before continuing on to the next chapter, you should experiment with the Steeltoe configuration server client sample and then take stock of the options available to you for externalizing configuration.

Configuring Microservices with etcd

Not everyone wants to use the Netflix OSS stack, for a number of reasons. For one, it is noticeably Java-heavy—all of the advanced development in that stack occurs in Java first, and all of the other clients (including C#) are delayed ports of the original. Some developers are fine with this; others may not like it.

Others may also take umbrage with the size of the Spring Cloud Config Server. It is a Spring boot application but it consumes a pretty hefty chunk of memory, and if you’re running multiple instances of it to ensure resilience and to prevent any of your applications from failing to obtain configuration, you could end up consuming a lot of the underlying virtual resources just to support configuration.

There is no end to the number of alternatives to Spring Cloud Config Server, but one very popular alternative is etcd. As mentioned briefly in the previous chapter, etcd is a lightweight, distributed key-value store.

This is where you put the most critical information required to support a distributed system. etcd is a clustered product that uses the Raft consensus algorithm to communicate with peers. There are more than 500 projects on GitHub that rely on etcd. One of the most common use cases for etcd is the storage and retrieval of configuration information and feature flags.

To get started with etcd, check out the documentation. You can install a local version of it (it really is a small-footprint server) or you can run it from a Docker image.

Another option is to use a cloud-hosted version. For the sample in this chapter, I went over to compose.io and signed up for a free trial hosting of etcd (you will have to supply a credit card, but they won’t charge you if you cancel within the trial period).

To work with the key-value hierarchy in etcd that resembles a simple folder structure, you’re going to need the etcdctl command-line utility. This comes for free when you install etcd. On a Mac, you can just brew install etcd and you’ll have access to the tool. Check the documentation for Windows and Linux instructions.

The etcdctl command requires you to pass the addresses of the cluster peers as well as the username and password and other options every time you run it. To make this far less annoying, I created an alias as follows:

$ alias e='etcdctl --no-sync 
   --peers https://portal1934-21.euphoric-etcd-31.host.host.composedb.com:17174,
   https://portal2016-22.euphoric-etcd-31.host.host.composedb.com:17174 
  -u root:password'

You’ll want to change root:password to something that actually applies to your installation, regardless of whether you’re running locally or cloud-hosted.

Now that you’ve got the alias configured and you have access to a running copy of etcd, you can issue some basic commands:

mk

Creates a key and can optionally create directories if you define a deep path for the key.

set

Sets a key’s value.

rm

Removes a key.

ls

Queries for a list of subkeys below the parent. In keeping with the filesystem analogy, this works like listing the files in a directory.

update

Updates a key value.

watch

Watches a key for changes to its value.

Armed with a command-line utility, let’s issue a few commands:

$ e ls /
$ e set myapp/hello world
world
$ e set myapp/rate 12.5
12.5
$ e ls
/myapp
$ e ls /myapp
/myapp/hello
/myapp/rate
$ e get /myapp/rate
12.5

This session first examined the root and saw that there was nothing there. Then, the myapp/hello key was created with the value world and the myapp/rate key was created with the value 12.5. This implicitly created /myapp as a parent key/directory. Because of its status as a parent, it didn’t have a value.

After running these commands, I refreshed my fancy dashboard on compose.io’s website and saw the newly created keys and their values, as shown in Figure 9-1.

Figure 9-1. Compose.io’s etcd dashboard

This is great—we have a configuration server and it has data ready for us to consume—but how are we going to consume it? To do that we’re going to create a custom ASP.NET configuration provider.

Creating an etcd Configuration Provider

Throughout the book we’ve gone through a number of different ways to consume the ASP.NET configuration system. You’ve seen to how add multiple different configuration sources with the AddJsonFile and AddEnvironmentVariables methods.

Our goal now is to add an AddEtcdConfiguration method that plugs into a running etcd server and grabs values that appear as though they are a native part of the ASP.NET configuration system.

Creating a configuration source

The first thing we need to do is add a configuration source. The job of a configuration source is to create an instance of a configuration builder. Thankfully these are pretty simple interfaces and there’s already a starter ConfigurationBuilder class for us to build upon.

Here’s the new configuration source:

using System;
using Microsoft.Extensions.Configuration;

namespace ConfigClient
{
    public class EtcdConfigurationSource : IConfigurationSource
    {
        public EtcdConnectionOptions Options { get; set; }

        public EtcdConfigurationSource(
          EtcdConnectionOptions options)
        {
            this.Options = options;
        }

        public IConfigurationProvider Build(
          IConfigurationBuilder builder)
        {
            return new EtcdConfigurationProvider(this);
        }
    }
}

There is some basic amount of information that we’ll need in order to communicate with etcd. You’ll recognize this information as mostly the same values we supplied to the CLI earlier:

public class EtcdConnectionOptions
{
    public string[] Urls { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public string RootKey { get; set; }
}

Creating a configuration builder

Next we can create a configuration builder. The base class from which we’ll inherit maintains a protected dictionary called Data that stores simple key-value pairs. This is convenient for a sample, so we’ll use that now. More advanced configuration providers for etcd would probably want the flexibility of maybe splitting keys on the / character and building a hierarchy of configuration sections, so /myapp/rate would become myapp:rate (nested sections) rather than a single section named /myapp/rate:

using System;
using System.Collections.Generic;
using EtcdNet;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;

namespace ConfigClient
{
    public class EtcdConfigurationProvider : ConfigurationProvider
    {
        private EtcdConfigurationSource source;

        public EtcdConfigurationProvider(
          EtcdConfigurationSource source)
        {
            this.source = source;
        }

        public override void Load()
        {
            EtcdClientOpitions options = new EtcdClientOpitions()
            {
                Urls = source.Options.Urls,
                Username = source.Options.Username,
                Password = source.Options.Password,
                UseProxy = false,
                IgnoreCertificateError = true
            };
            EtcdClient etcdClient = new EtcdClient(options);
            try
            {
                EtcdResponse resp = 
                 etcdClient.GetNodeAsync(source.Options.RootKey,
                    recursive: true, sorted: true).Result;
                if (resp.Node.Nodes != null)
                {
                    foreach (var node in resp.Node.Nodes)
                    {
                        // child node
                        Data[node.Key] = node.Value;
                    }
                }
            }
            catch (EtcdCommonException.KeyNotFound)
            {
                // key does not 
                Console.WriteLine("key not found exception");
            }
        }
    }
}

The important part of this code is highlighted in bold. It calls GetNodeAsync and then iterates over a single level of child nodes. A production-grade library might recursively sift through an entire tree until it had fetched all values. Each key-value pair retrieved from etcd is simply added to the protected Data member.

This code uses an open source module available on NuGet called EtcdNet. At the time I wrote this book, this was the most stable and reliable of the few I could find that were compatible with .NET Core.

With a simple extension method like this:

 public static class EtcdStaticExtensions
 {
    public static IConfigurationBuilder AddEtcdConfiguration(
        this IConfigurationBuilder builder,
        EtcdConnectionOptions connectionOptions)
    {
        return builder.Add(
         new EtcdConfigurationSource(connectionOptions));
    }
 }

We can add etcd as a configuration source in our Startup class:

 public Startup(IHostingEnvironment env)
 {
      var builder = new ConfigurationBuilder()
          .SetBasePath(env.ContentRootPath)
          .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
          .AddEtcdConfiguration(new EtcdConnectionOptions
          {
              Urls = new string[] {
              "https://(host1):17174",
              "https://(host2):17174"
               },
              Username = "root",
              Password = "(hidden)",
              RootKey = "/myapp"
          })
          .AddEnvironmentVariables();
      Configuration = builder.Build();
}

For obvious reasons, I’ve snipped out the root password for the instance. Yours will vary depending on how you installed etcd or where you’re hosting it. If you end up going this route, you’ll probably want to “bootstrap” the connection information to the config server with environment variables containing the peer URLs, the username, and the password.

Using the etcd configuration values

There’s just one last thing to do, and that’s make sure that our application is aware of the values we’re getting from the configuration source. To do that, we can add a somewhat dirty hack to the “values” controller you get from the webapi scaffolding:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using EtcdNet;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;

namespace ConfigClient.Controllers
{
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        private ILogger logger;

        public ValuesController(ILogger<ValuesController> logger)
        {
            this.logger = logger;
        }

        // GET api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            List<string> values = new List<string>();
            values.Add(
              Startup.Configuration.GetSection("/myapp/hello").Value);
            values.Add(
              Startup.Configuration.GetSection("/myapp/rate").Value);

            return values;
        }

      // ... snip ...
    }
}

To keep the code listing down I snipped out the rest of the boilerplate from the values controller. With a reference to EtcdNet in the project’s .csproj file, you can dotnet restore and then dotnet run the application.

Hitting the http://localhost:3000/api/values endpoint now returns these values:

["world","12.5"]

These are the exact values that we added to our etcd server earlier in the section. With just a handful of lines of code we were able to add a rock-solid, remote configuration server as a standards-conforming ASP.NET configuration source!

Summary

There are a million different ways to solve the problem of configuring microservices, and this chapter only showed you a small sample of these. While you’re free to chose whichever you like, keep a close eye on how you’re going to maintain your configuration after your application is up and running in production. Do you need audit controls, security, revision history, and other Git-like features?

Your platform might come with its own way of helping you inject and manage configuration, but the single most important lesson to learn from this chapter is that every single application and service you build must be able to accept external configuration through environment variables, and anything more complicated than a handful of environment variables is likely going to require some kind of external, out-of-process configuration management service.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.108.175