ASP.NET Core 2.1 Web API - Load App Configuration from appsettings.json, Dockerfile environment variables, Azure Key Vault Secrets and Kubernetes ConfigMaps/Secrets

This article is second part of the series on Deploying Angular, ASP.NET Core and SQL Server on Linux to Azure Kubernetes Service (AKS) cluster. The first part, describes steps needed to deploy these components to AKS. App configuration in ASP.NET Core is based on key-value pairs established by configuration providers. Configuration providers read configuration data into key-value pairs from a variety of configuration sources. In this article I am going to share multiple ways to load App configuration in Core Web API

  • Hosting Environment specific appsettings.json
  • Dockerfile Environment Variables
  • Kubernetes
    • Container Environment variables with data from ConfigMap/Secret
    • Populate Volume (Config file) with data stored in a ConfigMap/Secret
  • Azure Key Vault Secrets

The tools used to develop these components are Visual Studio for Mac/VS Code/VS 2017, AKS Dashboard, Docker for Desktop and kubectl. The formatting of code snippets in this article may get distorted (especially yaml), thus please refer to GitHub repository for complete source code for this article.

I have extended sample solution of first part of the series by adding new files i.e. ConfigController, Kubernetes_ConfigMap.yaml and Kubernetes_Deployment_V2.yaml

Hosting environment specific appsettings.json

Hosting environment specific versions of appsettings.json is one of the ways to define App configuration in core. In ConfigureAppConfiguration method, based on Hosting environment, App Config will be loaded as displayed below

config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)                        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: false, reloadOnChange: true)

I have defined a few configuration settings in appsettings.json {    "AppConfiguration": {        "DatabaseConnectionString": "Server=mssql-sample-service,1433;Database=UsersDB;User Id=sa;Password=P@ssword1$;",        "IsVaultEnabled": false,        "Vault":  "AZURE_KEY_VAULT_DNS_NAME",        "ClientId": "APPLICATION_ID",        "ClientSecret": "APPLICATION_KEY",        "DatabaseConnectionStringFromAppsettings": "DatabaseConnectionStringFromAppsettingsValue"    }}

The Production environment file i.e. appsettings.Production.json also defines DatabaseConnectionStringFromAppsettings setting, thus during Production Environment build, configuration from this file will get applied based on precedence.

{  "AppConfiguration": {    "DatabaseConnectionStringFromAppsettings": "Prod:DatabaseConnectionStringFromAppsettingsValue"  }}

Dockerfile Environment Variable

Environment variables can be defined in Dockerfile and args can be passed during building Docker image. In ConfigureAppConfiguration method, config.AddEnvironmentVariables() loads environment variables. In DockerFile displayed below, argument ARG dbConnectionString is defined along with environment variable ENV DatabaseConnectionString. This ARG value can then be specified when building Docker image and the command is docker build --build-arg dbConnectionString=DatabaseConnectionStringFromDockerEnvVariable -t testappwebapi . FROM microsoft/dotnet:2.1-aspnetcore-runtime AS baseWORKDIR /appEXPOSE 80ARG dbConnectionStringENV DatabaseConnectionStringFromDockerEnvVariable $dbConnectionStringFROM microsoft/dotnet:2.1-sdk AS buildWORKDIR /srcCOPY ["SampleWebApp/SampleWebApp.csproj", "."]RUN dotnet restore "SampleWebApp.csproj"COPY . .RUN dotnet build "SampleWebApp.csproj" -c Release -o /appFROM build AS publishRUN dotnet publish "SampleWebApp.csproj" -c Release -o /appFROM base AS finalWORKDIR /appCOPY --from=publish /app .ENTRYPOINT ["dotnet", "SampleWebApp.dll"]

ConfigMaps and Secrets in Kubernetes

ConfigMaps and Secret allows you to decouple configuration artifacts from image content to keep containerized applications portable. For this article, I will show how to use ConfigMap to define app configuration.

Container Environment variables with data from ConfigMap/Secret

The ConfigMap resource displayed below defines DatabaseConnectionStringFromKubernetesEnvVariable setting.

apiVersion: v1kind: ConfigMapmetadata:  name: samplewebapp-configmap-1  namespace: defaultdata:  DatabaseConnectionStringFromKubernetesEnvVariable: DatabaseConnectionStringFromKubernetesEnvVariableValue

Container environment variables can be set from Secret using env[].valueFrom.secretKeyRef and from ConfigMap using env[].valueFrom.configMapKeyRef. Container environment variable defined in deployment resource(Kubernetes_Deployment_V2.yaml) will be set from ConfigMap and the partial yaml snippet is

env:- name: DatabaseConnectionStringFromKubernetesEnvVariable  valueFrom:    configMapKeyRef:       name: samplewebapp-configmap-1       key: DatabaseConnectionStringFromKubernetesEnvVariable

Populate Volume (Config file) with data stored in a ConfigMap/Secret

This is similar to previous step however in this case, data stored in ConfigMap will be mounted to a json file. core web api will load App Configuration from this json file. The ConfigMap resource displayed below defines data for AppConfig.json: apiVersion: v1kind: ConfigMapmetadata:  name: samplewebapp-configmap-2  namespace: defaultdata:  AppConfig.json: |-    {        "AppConfiguration": {            " DatabaseConnectionStringFromKubernetesMountedFile": "DatabaseConnectionStringFromKubernetesMountedFileValue"        }    } AppConfig.json file will be mounted from ConfigMap in deployment resource(Kubernetes_Deployment_V2.yaml). The partial yaml snippets where AppConfig.json is mounted to path mountPath: /app/AppConfig.json from samplewebapp-configmap-2 ConfigMap is

volumeMounts:- name: samplewebapp-configmap-2  mountPath: /app/AppConfig.json  subPath: AppConfig.json volumes:- name: samplewebapp-configmap-2  configMap:    name: samplewebapp-configmap-2

In ConfigureAppConfiguration method, App configuration is loaded from AppConfig.jsonconfig.AddJsonFile("/app/AppConfig.json", optional: false, reloadOnChange: true

Azure Key Vault Secrets

Azure Key Vault can be used to securely store and tightly control access to tokens, passwords, certificates, API keys, and other secrets. Centralizing storage of application secrets in Azure Key Vault allows you to control their distribution. Key Vault greatly reduces the chances that secrets may be accidentally leaked. I will provide the steps needed to load App Configuration from Azure Key Vault secrets.

Create Azure AD Application

For Service-to-Azure-Service authentication, this approach involves creating an Azure AD application and associated credential, and using that credential to get a token. This approach does has short comings i.e. application credentials need to be specified in code and credentials expiry which Managed Service Identity fixes. Azure Kubernetes Service(AKS) is not currently natively integrated with Azure Key Vault however, the Azure Key Vault FlexVolume for Kubernetes project enables direct integration from Kubernetes pods to KeyVault secrets.

Navigate to Azure AD > App Registrations > New Application Registration and select Application Type as Web app/API and specify Name and Sign-on URL

After Azure AD Application is created, navigate to the resource and take note of Application ID which is needed to be specified for ClientID in Core Web API appsettings.json.

Open Settings and create a new Key. Take note of the value (you can only copy after saving) which is needed to be specified for ClientSecret in Core Web API appsettings.json.

Create Azure Key Vault

Create a key vault resource using Azure CLI or Portal

You need to specify Access policies. Select principal as Application you created in previous step and grant permissions e.g. I have granted Read and List Secret permissions.

Create a Secret and specify Name and Value. The value for this secret is going to be loaded in App Configuration.

Keep note of the Key Vault's DNS name which is needed to be specified for Vault in Core Web API appsettings.json. Core Web API

Add Azure Key Vault Configuration provider nuget package Microsoft.Extensions.Configuration.AzureKeyVault.

In ConfigureAppConfiguration method snippet displayed below, App configuration is loaded from Azure Key Vault. As described above, you need to specify values for Vault, ClientId and ClientSecret in appsettings.json. Along with this you need to set IsVaultEnabled to true in appsettings.json.

if (Convert.ToBoolean(configInProgress["AppConfiguration:IsVaultEnabled"])){  config.AddAzureKeyVault(configInProgress["AppConfiguration:Vault"],                          configInProgress["AppConfiguration:ClientId"],                          configInProgress["AppConfiguration:ClientSecret"]);  config.Build(); }

In ConfigureServices method of Startup class, Configuration instance is registered with AppConfiguration class

services.Configure<AppConfiguration>(Configuration.GetSection("AppConfiguration"));services.Configure<AppConfiguration>(Configuration); public class AppConfiguration{    public string DatabaseConnectionString    {        get;        set;    }    public bool IsVaultEnabled    {        get;        set;    }    public string Vault    {        get;        set;    }    public string ClientId    {        get;        set;    }    public string ClientSecret    {        get;        set;    }    public string DatabaseConnectionStringFromAppsettings    {        get;        set;    }    public string DatabaseConnectionStringFromDockerEnvVariable    {        get;        set;    }    public string DatabaseConnectionStringFromKubernetesEnvVariable    {        get;        set;    }    public string DatabaseConnectionStringFromKubernetesMountedFile    {        get;        set;    }    public string DatabaseConnectionStringFromAzureKeyVault    {        get;        set;    }}

The code snippet from ConfigController displays App Configuration values from all the sources

[HttpGet]public IEnumerable<string> Get(){     return new List<string>()            {                $"DatabaseConnectionStringFromAppsettings: {_appSettings.Value.DatabaseConnectionStringFromAppsettings}",                $"DatabaseConnectionStringFromDockerEnvVariable: {_appSettings.Value.DatabaseConnectionStringFromDockerEnvVariable}",                $"DatabaseConnectionStringFromKubernetesEnvVariable: {_appSettings.Value.DatabaseConnectionStringFromKubernetesEnvVariable}",                $"DatabaseConnectionStringFromKubernetesMountedFile: {_appSettings.Value.DatabaseConnectionStringFromKubernetesMountedFile}",                $"DatabaseConnectionStringFromAzureKeyVault: {_appSettings.Value.DatabaseConnectionStringFromAzureKeyVault}"            };}

Build the docker image and deploy the resources to Azure Kubernetes Service. Browse to "http://YOUR_HOST/api/Config" to see list of App Configurations with Values populated from these sources.

The source code for this article can be downloaded from  GitHub repository