Configuration in Distributed Applications
Configuration can prove challenging for distributed applications because they are comprised of multiple services and dependencies. Running live services requires maintaining configuration settings across different environments and updating them in production deployments. It is also important to have the ability to quickly and easily roll back in case of an unintended or unexpected result.
During the development of CSF, we addressed this need by building an overlay on top of existing configuration capabilities to ease the friction between the traditional .NET configuration experience and the requirements of building a distributed cloud application.
The key principles were:
No concrete dependency on a specific configuration provider. Provide a layer of indirection between the application code and the configuration store (web.config, the role environment settings, etc.). This enables the store to be abstracted and changed over time without affecting the source application.
Type management and conversion is the responsibility of the configuration framework. Applications work with strong types (such as doubles and DateTime values); embedding parsing logic everywhere a configuration value is accessed leads to brittle and less maintainable code.
Default values are natively supported when doing a configuration lookup. Missing configuration settings with well-known or broadly applicable default values should be handled in the configuration framework to streamline application code.
Humans are an error source – make configuration simple to edit. Prefer key/value pairs and dictionaries to complicated hierarchical structures.
Let’s start with a review of the core .NET and Windows Azure configuration sources and APIs.
System.Configuration
The configuration APIs in System.Configuration are the baseline configuration APIs, part of .NET since 1.0. They enable a rich experience, but they are often very difficult to create and edit, especially in the case of working with typed schemas and chaining configuration files. Configuration values are stored in the app.config or web.config file associated with the application (or application host in the case of web applications).
Figure 1
While the System.Configuration API enables the creation of highly schematized configuration sections, these are difficult to develop and debug. For simplicity, we assume that our baseline for configuration are key/value settings and configuration strings (for database connections).
There are a number of challenges with directly leveraging the web.config and app.config files via System.Configuration as part of a Windows Azure cloud application:
AppSettings (in System.Configuration) is hard-linked to the web.config and app.config files.
In a PaaS environment, all of the files deployed as part of the application package are immutable. This means that you are unable to update configuration settings without redeploying the entire application – not a practicable or desirable result.
For these reasons, app.config and web.config are best used for immutable settings (those that are tied to an application version and only changed during a build update).
Windows Azure Role Environment
The Windows Azure PaaS API layer (RoleEnvironment) provides a way to describe configuration across a cloud service. This should be the primary method of describing configuration settings that support modifications in production deployments. This data can be modified either in Visual Studio:
Figure 2
Or by editing both the ServiceDefinition.csdef (lists the “configured” keys) and the ServiceConfiguration.[Environment].cscfg (contains the values for each deployment or environment).
Managing Type Conversions
Both of the configuration APIs mentioned provide a string Get(string key) method. To streamline parsing these string values into typed objects, we created a set of wrapper APIs for converting values.
A baseline implementation could leverage Convert.ChangeType, but this doesn’t handle several common types like TimeSpan, Guid, DateTimeOffset and enums. Instead, we handle type conversions with the following ConvertValue<T> method:
/// <summary>
/// Attempt to coerce a value to the appropriate type and log the error as appropriate
/// </summary>
public static T ConvertValue<T>(string key, object obj)
{
try
{
T val = default(T);
if (typeof(T) == typeof(string))
val = (T)obj;
else if (typeof(T) == typeof(TimeSpan))
{
val = (T)(object)TimeSpan.Parse(obj.ToString());
}
else if (typeof(T) == typeof(Guid))
{
val = (T)(object)Guid.Parse(obj.ToString());
}
else if (typeof(T) == typeof(DateTimeOffset))
{
val = (T)(object)DateTimeOffset.Parse(obj.ToString(), null,
System.Globalization.DateTimeStyles.AssumeUniversal);
}
else if (typeof(T).IsEnum)
{
val = (T)Enum.Parse(typeof(T), obj.ToString());
}
else
{
val = (T)Convert.ChangeType(obj, typeof(T));
}
return val;
}
catch (Exception ex0)
{
System.Diagnostics.Trace.WriteLine(String.Format("Could not convert key {0} for value {1} to type {2}: {3}",
key, obj, typeof(T).Name, ex0.ToString()));
throw;
}
}
This method checks the desired type, works through the available parsers, and applies the appropriate transform.
Configuration Overlay
Now that we can uplevel raw string configuration, let’s look at providing an API for using both RoleEnvironment and web.config/app.config settings in our applications. The following screenshot shows the members of a helper class, ConfigurationHelper.
Figure 3
In the ConfigurationHelper class, we have three methods for retrieving data:
GetAppConfigValue: gets the configuration value from System.Configuration.
GetAzureConfigValue: gets the configuration value from RoleEnvironment.
GetConfigValue: tries to get the configuration value from Windows Azure, otherwise get it from System.Configuration.
Each of them has an override for a TryGet method, and an optional default parameter (i.e. if no valid configuration setting found, return the default). The implementation of each of these is straightforward. For example, the following code shows the implementation of the TryGetAzureConfigValue method:
/// <summary>
/// If the Azure role environment is available, attempt to get the configuration
/// value from the role configuration as the given type
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="val"></param>
/// <returns></returns>
public static bool TryGetAzureConfigValue<T>(string key, out T val)
{
val = default(T);
try
{
if (!RoleEnvironment.IsAvailable)
return false;
object strValue = RoleEnvironment.GetConfigurationSettingValue(key);
val = ConvertValue<T>(key, strValue);
return true;
}
catch (RoleEnvironmentException)
{
// When the trace destination is being looked up from configuration settings for the
// azure trace listener _trace may yet be invalid
System.Diagnostics.Trace.WriteLine(String.Format(
"The requested key {0} does not exist in Azure role configuration", key));
return false;
}
catch (Exception)
{
return false;
}
}
With this approach, applications have a streamlined way of looking up configuration values:
Figure 4
Conclusion
When building out configuration management in your cloud applications, ensure that you leverage an appropriate abstraction layer to save yourself pain and complexity as your application goes live and evolves. The configuration wrappers in CSF provide a solid baseline for this type of abstraction layer that can be used to create more manageable Windows Azure applications.