Trabalhando com configuração do sistema

Todo sistema – digital ou não – antes de entrar em funcionamento precisa passar por uma etapa de setup; aquele momento em que todas as configurações adequadas ao ambiente em que a aplicação será executada, são definidas e aplicadas. Para quem está começando o seu primeiro projeto em C#, saber como acessar essas configurações pode ser um pequeno problema.

Por isso, nesse artigo eu abordo algumas formas de obter as configurações do arquivo appsettings.json ou de qualquer outro arquivo (ou meio) em que você deseja armazenar as suas configurações. Também abordo uma prática pouco divulgada que é o armazenamento de arrays de configuração. Vamos conferir.

Arquivo de configuração appsettings

Uma das formas mais comuns para armazenamento de configurações são os arquivos de inicialização. Primeiro eles chamavam [nome-da-aplicação].ini, com um formato todo especial:

[Grupo]
Chave=Valor

A Microsoft, além do formato .ini, já adotou outros tantos formatos. Você deve lembrar o .asa, .asax, .xml e agora chegamos ao .json. O appsettings é, hoje, o nome padrão utilizado como arquivo de configurações pelos frameworks da Microsoft. Se você está utilizando o ASP.Net Core, este é o arquivo que será procurado na pasta raiz da aplicação (ou no patch do S.O.).

Para ser mais assertivo, o ASP.Net Core, por padrão, vai buscar o arquivo com o seguinte nome:

appsettings.[environment].json

Uma configuração para cada Environment

O valor de environment é substituído pelo que foi configurado nas variáveis de ambiente DOTNET_ENVIRONMENT ou ASPNETCORE_ENVIRONMENT. Como você sabe, os ambientes de Development, Staging e Production podem (e na maior parte das vezes devem) apresentar configurações e comportamento diferentes. Com esse mecanismo, você pode ter várias versões do arquivo de configuração, uma para cada ambiente.

Vale ressaltar que as configurações utilizadas como “padrão” estão no arquivo appsettings.json, que também é utilizado como arquivo de produção. Portanto, muito cuidado: Se alguma configuração não for encontrada no arquivo específico para aquele ambiente, automaticamente serão utilizados os dados de produção.

Alterar nome do arquivo

É possível que por alguma particularidade do sistema, você opte por alterar o nome do arquivo de configurações. Para fazer esse procedimento, você precisa fazer a carga das configurações da aplicação na mão.

Se você estiver utilizando o ASP.Net Core, uma opção é utilizar o método .ConfiguraAppConfiguration de IHostBuilder

public static IHostBuilder CreateHostBuilder() =>
    Host
        .CreateDefaultBuilder()
        .ConfigureAppConfiguration((_, configuration) =>
        {
            var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
            configuration
                .AddJsonFile("config.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"config.{environment}.json", optional: true)
                .AddEnvironmentVariables();
        })
        .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());

O snippet de código acima é encontrado em qualquer Program.cs de uma aplicação ASP.Net. Também é possível fazer o mesmo procedimento dentro da classe Startup. O código é basicamente o mesmo.

Caso você não esteja utilizando o ASP.Net Core, o código é basicamente o mesmo. A diferença é que você terá que construir um Builder de configuração manualmente. Veja como fazer isso no código abaixo:

var environment == Environment
    .GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var configuration = new ConfigurationBuilder()
    .AddJsonFile("config.json", optional: false)
    .AddJsonFile($"config.{environment}.json", optional: true);
    .Build();

Dois detalhes importantes dessa implementação são: 1- Nós sempre carregamos o ambiente de desenvolvimento para compor o nome do arquivo. Se você está carregando manualmente essas configurações, esse passo é imprescindível se você quer ter uma configuração por ambiente. 2- Repare no parâmetro optional que é true para o arquivo de ambiente e false para o arquivo de produção. Esta configuração é importante porque, caso um arquivo optional: false não seja encontrado, a aplicação lançará uma exceção, reclamando da sua ausência.

Trabalhando com arrays de configuração

Vamos supor que você tem um determinado comportamento que deverá ser repetido por um determinado número de vezes. E que para cada uma dessa iterações, uma configuração diferente deverá ser utilizada. E todas essas informações devem ser configuráveis.

Intuitivamente, você pode pensar que este formato é válido:

appsettings.json
"configs":[
    {
        "name":"iteration1"
    },
    {
        "name":"iteration2"
    },
    {
        "name":"iteration3"
    }
]

Mas infelizmente não é. IConfiguration não consegue traduzir arrays desta forma. Caso você realmente deseje trabalhar com configurações como esta, o seguinte formato é válido:

"dynamicConfigurations": {
    "configs":{
        "0": {
            "name":"iteration1"
        },
        "1": {
            "name":"iteration2"
        },    
        "2": {
            "name":"iteration3"
        }
    }
}

No código, você terá as classes:

public class DynamicConfigurations
{
    public List<Iteration> Configs { get; set; }
}
public class Iteration
{
    public string Name { get; set; }
}

E para carregar os dados dessa configuração:

public IConfiguration Configuration { get; }
public void Configure()
{
    var dynamicConfigs = new DynamicConfigurations();
    Configuration.GetSection("DynamicConfigurations").Bind(dynamicConfigs);
    Console.WriteLine(dynamicConfigs.Configs[0]); 
}

Configuração a partir de Env vars

Trabalhar com arquivos é cômodo, principalmente em ambiente de desenvolvimento. Entretanto, quando falamos em arquiteturas distribuídas, é muito mais fácil fazer a gerência da configuração através de variáveis de ambiente. O próprio pipeline se encarrega de definir os valores antes de levantar uma nova instância da aplicação. Mas o que muda para a aplicação?

Adaptações necessárias

Na realidade nenhuma modificação se faz necessária na aplicação. Isto, claro, se o padrão utilizado pelo motor de configurações for seguido. E qual é esse padrão? Tomemos o seguinte arquivo como padrão:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "DynamicConfigurations": {
    "configs": {
      "0": {
        "name": "iteration1"
      },
      "1": {
        "name": "iteration2"
      },
      "2": {
        "name": "iteration3"
      }
    }
  }
}

Qual seria o path para o valor da propriedade Default? Se você pensou em Logging->LogLevel->Default, está correto. E se quiser o path para a variável Microsoft?. Logging->LogLevel->Microsoft. Percebe o padrão?

Logging->LogLevel->Default
Logging->LogLevel->Microsoft

Se nós planificarmos a estrutura aninhada dos objetos, o resultado é um nome que, facilmente, poderia ser utilizado como nome de uma variável. Assim, esse mesmo objeto poderia ter seus valores armazenado nas seguintes variáveis de ambiente

LOGGING__LOGLEVEL__DEFAULT 

Repare que para cada nível de variável estamos utilizando dois _ (underscore), que é o padrão utilizado pelos providers de configuração. E para o caso de arrays? Acredito que os caracteres “[“ e “]” não sejam permitidos na composição de variáveis de ambiente. Assim, nós temos:

DynamicConfigurations__Configs__0__Name
DynamicConfigurations__Configs__1__Name
DynamicConfigurations__Configs__2__Name

Por isso, ao definir os seus objetos de configuração, tome muito cuidado para não extrapolar o máximo de caracteres permitidos pelo sistema operacional hospedeiro.

Algumas questões de segurança

Armazenar configurações de forma segura não é simples. Dizem que dados sensíveis não deveriam ser armazenados em arquivos, já que o seu conteúdo pode ser acessado por algum bug na aplicação/infraestrutura ou pelo simples descuido de subir o arquivo para um repositório aberto.

Variáveis de ambiente também tem o seu revés. Se algum usuário mal intencionado, de alguma maneira, obtiver acesso a instância onde roda a sua aplicação, um simples comando poderia revelar qual é o valor de todas as variáveis de ambiente naquele momento. Tudo bem que esse cenário é um pouco menos provável, especialmente em uma infra bem construída. Mas não é impossível.

A opção que muitas aplicações estão adotando é a ocultação de valores sensíveis em vaults. O acesso a esses cofres digitais se dá por meio de uma série de mecanismos de segurança. AWS e Azure oferecem suas próprias soluções de vault, e no caso da Azure, tem até documentação para acesso em C#. Então a solução é o vault? Não.

O vault é um serviço externo. Ou seja, para acessá-lo, além de toda a carga transacional (criptografia, autenticação e afins) você ainda pode ter problemas de latência e indisponibilidade (embora as nuvens garantam que essas ocorrências são mínimas). O que muitos de nós fazemos é, no início da aplicação, carregar os dados em objetos e torná-los singleton para a aplicação. Ou somente guardar os valores em variáveis no objeto de conexão.

O problema dessa estratégia é que o mesmo usuário que conseguiu acesso as variáveis de ambiente, pode fazer um dump da memória do ambiente em que está rodando a sua aplicação. Com um Dump, você sabe, é possível ver o valor de todas as variáveis carregadas em memória. E se você tem a senha do seu banco armazenado em Configurations.Database.Senha, vai ser bastante fácil para o atacante. O que fazer então?

Já adianto que alterar o nome da variável, vai adiantar muito pouco. Aliás a estratégia de segurança por ocultação, para esses casos especialmente, é pouco eficaz. Você pode partir para abordagens em que os valores em memória estão sempre descriptografados, ou até mesmo diminuindo o tempo em que esses dados estão em memória. Contudo, o custo dessas estratégias pode ser caro. Processo de criptografia/descriptografia, em geral, são caros em termos de processamento. Vale a pena? Deixo a pergunta no ar para você e sua equipe de segurança da informação responderem.

Por hoje é só. Espero você no próximo post 😊

Código de exemplo disponível no meu github: https://github.com/ftathiago/blogdoft-toycode/tree/master/ConfigurationReading

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.