Reputation: 381
Setup
I'm running a .NET 6 web app inside an Azure Container App and accessing Azure Key Vault for secrets and connection strings. Dependency Injection works fine for injecting my custom Key Vault client (wrapper of Azure.Security.KeyVault.Secrets.SecretClient), and I can successfully retrieve secrets after Build() (even from within Program.cs).
Problem
I need to fetch a connection string from Key Vault before Build() so I can pass it to a service constructor when registering dependencies. The SecretClient.GetSecret(keyName) call fails at this point.
I don't face the same problem when I run locally.
Logs
When the Container app is trying to activate the revision (and runs the Program.cs), I see this error from the Container app Log stream:
What I've Tried
Question
How can I securely retrieve a secret before calling Build() in a .NET 6 containerized web app? Or is there a better pattern for handling this scenario?
Any insights would be greatly appreciated!
Code
public class KeyVaultService : IKeyVaultService
{
private readonly SecretClient _secretClient;
public KeyVaultService(string keyVaultName)
{
_secretClient = new SecretClient(new Uri($"https://{keyVaultName}.vault.azure.net"), new DefaultAzureCredential());
}
public string GetSecret(string keyName)
{
KeyVaultSecret secret = _secretClient.GetSecret(keyName);
return secret.Value;
}
}
public class Program
{
public static void Main(string[] args)
{
WebApplicationBuilder? builder = WebApplication.CreateBuilder(args);
// ... configuring logging, etc ...
var keyVault = new KeyVaultService(keyVaultName);
// this is where it fails, but only in the Container app
// locally works fine.
string conn = keyVault.GetSecret("ConnectionString");
// registering a service that needs connectionString in its constructor.
var app = builder.Build();
// after Build() the Key Vault works fine.
keyVault = new KeyVaultService(keyVaultName);
conn = keyVault.GetSecret("ConnectionString");
// omitting unrelated code, like app.Run()
}
}
UPDATE
I tried adding the Key Vault to Configuration and does not work in my Azure Container App (works locally though).
From logs I noticed that before Build() the builder.Configuration["ConnectionString"] returns null or empty string, so I guess that Key Vault was not loaded as a provider.
WebApplicationBuilder? builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddAzureKeyVault(XXXXX, new DefaultAzureCredential());
builder.ConfigureDatabase(builder.Configuration["ConnectionString"]);
var app = builder.Build();
Upvotes: 0
Views: 110
Reputation: 11163
The overall design of loading configuration, registering services, and using IOptions
to expose that configuration to services has gone through a number of revisions during the development of dotnet core. So there are many ways to solve this design problem.
Personally I would implement something like the following. Adding all secrets in the vault as configuration values, then follow the "options pattern" to inject those options into services.
builder.Configuration.AddAzureKeyVault(
new Uri(builder.Configuration["AzureKeyVaultConnectionString"]),
new DefaultAzureCredential()
);
builder.Services.Configure<OptionsType>(
builder.Configuration.GetSection("SectionName")
);
However that will depend on exactly how your connection string is being configured. When calling WebApplication.CreateBuilder
, "only configuration that is necessary for the host" will be loaded immediately. This certainly includes any ASPNETCORE_
environment variables.
The remaining "application configuration providers" will be registered and only loaded while building the web application. You can follow this pattern yourself to register additional application configuration;
builder.ConfigureAppConfiguration((context, builder) => {
builder.AddAzureKeyVault(
new Uri(context.Configuration["AzureKeyVaultConnectionString"]),
new DefaultAzureCredential()
);
});
Similarly you can delay registering services;
builder.ConfigureServices((context, services) => {
// etc
});
Though I would probably just tweak how you are passing in your connection string to ensure it is considered "host configuration".
You should never need to create any instance of your services manually. If you think you need to create a service before calling .Build
, then you are mistaken. If you explain what you are actually trying to do, then I could explain how you are supposed to achieve what you want. Either during the process of building the host, during startup of the host, or lazily when your service dependency is first required.
Upvotes: 1