Azure App Configuration: desacopla la configuración de tus aplicaciones

En todas las aplicaciones que he conocido existen ciertos valores parametrizables, que suelen almacenarse en el típico archivo de configuración o incluso en una base de datos. Esta práctica está genial en aplicaciones monolíticas, y sin replicas en otras ubicaciones, pero cuando varios componentes de tu aplicación necesitan de estos valores puede resultar complicado orquestar la actualización de estos en tiempo y forma en todos los servicios afectados.

Hoy te quiero hablar de Azure App Configuration, un servicio todavía en preview, que sirve para administrar la típica configuración que pondrías en un archivo de tu sitio web, de forma centralizada. Este servicio no está indicado para guardar secretos, para ello está Azure Key Vault o el uso de las Managed Identities. Aquí lo que se pretende almacenar son valores como parámetros de paginación, colores, cadenas de texto, etcétera. El objetivo principal es claro: Azure App Configuration nos permite cambiar de manera dinámica parámetros sin la necesidad de redesplegar o reiniciar la aplicación.

Crear un servicio de Azure App Configuration

En el portal de Microsoft Azure, haz clic en Create a resource y busca por App Configuration.

Microsoft Azure – App Configuration

Selecciónalo y haz clic en el botón Create. Necesitarás elegir un nombre para el nuevo servicio, seleccionar la suscripción donde quieres que este sea desplegado, el grupo de recursos y una localización, las cuales están limitadas durante la preview.

Creación de App Configuration

Añade la configuración de tu aplicación

La configuración que podemos almacenar en este servicio tiene el formato clave-valor. El lugar donde se establecen estos valores es en la sección Configuration Explorer. Para este ejemplo he añadido un par de ellos:

  • AppConfigurationDemo:Index:HomeTitle con el valor Home.
  • AppConfigurationDemo:Index:AboutImage con este enlace.

El resultado sería el siguiente:

Azure App Configuration – Configuration Explorer – Keys

Una buena práctica es utilizar prefijos para las claves (AppConfigurationDemo como nombre de la aplicación/componente e Index como el apartado donde estoy usando la clave). Las etiquetas se utilizan cuando existen diferentes entornos/versiones de una misma aplicación (por ejemplo Europe, US, UK, etcétera).

Crear un proyecto ASP.NET Core

En el momento de escribir este artículo, hay librerías de App Configuration para ASP.NET Core y Java. Hoy vamos a probarlo desde una aplicación con ASP.NET Core sencilla.

Crea una carpeta donde alojarás tu proyecto. En mi caso la he llamado AppConfigurationDemo.

mkdir AppConfigurationDemo

Entra en el directorio que acabas de crear y lanza el siguiente comando para generar la estructura de tu proyecto:

dotnet new mvc

Para trabajar con App Configuration necesitas agregar la referencia a la librería Microsoft.Extensions.Configuration.AzureAppConfiguration

dotnet add package Microsoft.Extensions.Configuration.AzureAppConfiguration --version 1.0.0-preview-008520001

Ahora modifica el archivo Program.cs de tu aplicación, para cargar la configuración del servicio en CreateWebHostBuilder:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using Microsoft.Extensions.Logging;

namespace AppConfigurationDemo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
             .ConfigureAppConfiguration((hostingContext, config) =>
            {
                var settings = config.Build();
                config.AddAzureAppConfiguration(settings["ConnectionStrings:AppConfiguration"]);
            })
            .UseStartup<Startup>();
    }
}

La cadena de conexión ConnectionStrings:AppConfiguration puedes almacenarla en diferentes sitios. Lo recomendable cuando estás trabajando en local es utilizar User Secrets, para no tener que almacenar los secretos en el propio proyecto de ASP.NET Core y que estos sean guardados accidentalmente en el repositorio de código fuente. Para ello sólo debes añadir en el archivo .csproj un GUID para la clave UserSecretsId.

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <UserSecretsId>F04C1DBA-8183-4D1F-9F2A-D27F140D0746</UserSecretsId>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
  </PropertyGroup>


  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
    <PackageReference Include="Microsoft.Extensions.Configuration.AzureAppConfiguration" Version="1.0.0-preview-008520001" />
  </ItemGroup>

</Project>

Si estás en Mac OS puedes hacer uso de uuidgen para generar un GUID aleatorio.

uuidgen

Una vez que tienes configurado Secret Manager, ya puedes añadir la cadena de conexión que necesitamos. En el apartado Access Keys debes copiar la de tipo Read Only.

Cadena de conexión a Azure App Configuration

La forma de añadirla en el Secret Manager es la siguiente:

dotnet user-secrets set ConnectionStrings:AppConfiguration "Endpoint=https://my-configuration.azconfig.io;Id=2-l9-s0:qj/Rzd+6Z2lI/ik526yF;Secret=yhp4lIgRyDP9ax2VUxqLnbvN0BaDHwRfBrIUd/bnhTA="

Una vez que tu aplicación esté en Azure te recomiendo que utilices Managed Identities con App Service y App Configuration. Así no tendrás que preocuparte de guardar ninguna credencial en ningún sitio.

Utiliza tu configuración el tu código

Con todo configurado, ya puedes empezar a utilizar en tu código las claves que diste de alta en el apartado Configuration Explorer. Este sería un ejemplo con las que yo he dado de alta:

Ejemplo de uso de Azure App Configuration en ASP.NET Core

Nota: este ejemplo está basado en este Codepen. También he modificado el archivo Views/Shared/Layout.cshtml, para eliminar clases del HTML y el menú, además de reemplazar el contenido de www/css/site.css y www/js/site.js. En ellos no he utilizado nada relacionado con el objeto de este artículo, pero puedes recuperarlos desde GitHub.

Como puedes ver, para referenciar tus claves en el código necesitas importar Microsoft.Extensions.Configuration e inyectar IConfiguration. Configuration es un array del que puedes recuperar las claves dadas de alta en el servicio. En este ejemplo utilizo mis dos claves tanto en el apartado de estilos CSS como en el propio HTML.

Para comprobar que todo funciona correctamente, ejecuta el proyecto a través de dotnet run y comprueba que tus claves son recuperadas correctamente. Si quisieras modificar estas, con la configuración actual, deberías de cambiar su valor en Configuration Explorer y reiniciar el sitio.

Ya tienes tu configuración desacoplada de tu código 🙂

Feature Manager

Este apartado, como su propio nombre indica, sirve para la gestión de nuevas características en tu sitio. Tradicionalmente, cuando nos hemos visto en un escenario como este, donde era necesario tener nuevas funcionalidades y el poder habilitarlas/deshabilitarlas, ha sido necesaria la creación de una nueva rama de nuestro código y redirigir el tráfico a las diferentes versiones de la aplicación. Con Feature Manager lo que consigues es generar una serie de feature flags, que podrás usar para habilitar o deshabilitar características.

Para que puedas verlo con un ejemplo práctico, crea un flag llamado WelcomeSection. El mismo, por defecto, estará en estado Off.

Creación de una feature flag llamada WelcomeSection

Para poder utilizar feature flags debes añadir otra librería más a tu proyecto:

dotnet add package Microsoft.FeatureManagement.AspNetCore --version 1.0.0-preview-008560001-910

Además de actualizar el archivo Program.cs, añadiendo la llamada a UseFeatureFlags():

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using Microsoft.Extensions.Logging;

namespace AppConfigurationDemo
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
             .ConfigureAppConfiguration((hostingContext, config) =>
            {
                var settings = config.Build();
                config.AddAzureAppConfiguration(options =>
               {
                   options.Connect(settings["ConnectionStrings:AppConfiguration"]).UseFeatureFlags();
               });
            })
            .UseStartup<Startup>();
    }
}

En el archivo Startup.cs también debes incluir services.AddFeatureManagement();, en el método ConfigureServices, además de añadir la librería Microsoft.FeatureManagement.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.FeatureManagement;

namespace AppConfigurationDemo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddFeatureManagement();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

Ahora, para probar nuestra nueva característica, modifica de nuevo el archivo Index, añadiendo la referencia al helper y la etiqueta feature con el contenido de la nueva sección.

Uso de feature flags de Azure App Configuration en ASP.NET Core

Si pones a On el flag desde Feature Manager, y vuelves a arrancar tu aplicación, deberías de ver la nueva sección entre las etiquetas <feature></feature>.

Refrescar sin tener que reiniciar

Azure App Configuration también te permite actualizar los valores que ha recuperado nuestra aplicación sin necesidad de tener que reiniciar o redesplegar la misma, y es a través del método WatchAndReloadAll. A este debemos pasarle una clave que usaremos para alertar al sistema de que nuestra configuración ha cambiado. Este método se configura también en Program.cs dentro de CreateWebHostBuilder:

 public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
     WebHost.CreateDefaultBuilder(args)
      .ConfigureAppConfiguration((hostingContext, config) =>
     {
         var settings = config.Build();
         config.AddAzureAppConfiguration(options =>
        {
            options.Connect(settings["ConnectionStrings:AppConfiguration"])
            .UseFeatureFlags()
            .WatchAndReloadAll("Sentinel", TimeSpan.FromSeconds(5));
        });
     })
     .UseStartup<Startup>();

En mi ejemplo, he creado una nueva llamada Sentinel y le he añadido un valor cualquiera, ya que el sistema sólo va a tener en cuenta que el mismo ha cambiado, pero no lo que contiene. También he especificado que quiero que se compruebe si hay cambios cada 5 segundos. Si vuelves a modificar tus valores y después cambias el valor de Sentinel verás que se recargan sin necesidad de reiniciar la aplicación. De hecho, esto también sirve para las feature flags, ya que si te fijas en el apartado de Configuration Explorer verás que la que creaste también aparece ahí, con una marca especial.

Exportar e importar configuraciones

Por último, existe la posibilidad de exportar/importar la configuración generada a/desde otras fuentes, como otro App Configuration, un App Service o incluso un archivo de configuración.

El código de ejemplo lo tienes en mi GitHub.

¡Saludos!