dotnet / aspire

An opinionated, cloud ready stack for building observable, production ready, distributed applications in .NET
https://learn.microsoft.com/dotnet/aspire
MIT License
3.55k stars 389 forks source link

Allow BicepSecretOutput to target a specific keyvault instance #4061

Open davidfowl opened 3 months ago

davidfowl commented 3 months ago

Right now if an azure resource declares a BicepSecretOutputReference the system will create a keyvault per azure resource to isolate the secrets within that keyvault for that specific resource. We want to allow a model where keyvault is shared so that means being able to carve out a unique prefix for secrets instead of a keyvault. To make this more generic, an AzureBicepResource should expose a method to enable specifying a keyvault to store secrets.

var builder = DistributedApplication.CreateBuilder(args);

var redis = builder.AddRedis("cache").PublishAsAzureRedis();

builder.AddProject<Projects.WebApplication1>("webapplication1")
       .WithExternalHttpEndpoints()
       .WithReference(redis);

builder.Build().Run();
{
    "cache": {
      "type": "azure.bicep.v0",
      "connectionString": "{cache.secretOutputs.connectionString}",
      "path": "cache.module.bicep",
      "params": {
        "keyVaultName": ""
      }
    }
}

The empty keyvault name tells azd and the azure provisioning logic to create a keyvault for the cache resource and store the connection string (which contains secrets) in key vault.

The other important part is that the bicep generated references the specified keyvault, and assumes that it can use the secret name connectionString which also maps to the secret output name.

AzureRedisResource ```C# // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; namespace Aspire.Hosting.Azure; /// /// Represents an Azure Redis resource. /// /// The inner resource. /// Callback to populate the construct with Azure resources. public class AzureRedisResource(RedisResource innerResource, Action configureConstruct) : AzureConstructResource(innerResource.Name, configureConstruct), IResourceWithConnectionString { /// /// Gets the "connectionString" output reference from the bicep template for the Azure Redis resource. /// public BicepSecretOutputReference ConnectionString => new("connectionString", this); /// /// Gets the connection string template for the manifest for the Azure Redis resource. /// public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"{ConnectionString}"); /// public override string Name => innerResource.Name; /// public override ResourceAnnotationCollection Annotations => innerResource.Annotations; } ```
Bicep ```bicep targetScope = 'resourceGroup' @description('') param location string = resourceGroup().location @description('') param keyVaultName string resource keyVault_IeF8jZvXV 'Microsoft.KeyVault/vaults@2022-07-01' existing = { name: keyVaultName } resource redisCache_enclX3umP 'Microsoft.Cache/Redis@2020-06-01' = { name: toLower(take('cache${uniqueString(resourceGroup().id)}', 24)) location: location tags: { 'aspire-resource-name': 'cache' } properties: { enableNonSslPort: false minimumTlsVersion: '1.2' sku: { name: 'Basic' family: 'C' capacity: 1 } } } resource keyVaultSecret_Ddsc3HjrA 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { parent: keyVault_IeF8jZvXV name: 'connectionString' location: location properties: { value: '${redisCache_enclX3umP.properties.hostName},ssl=true,password=${redisCache_enclX3umP.listKeys(redisCache_enclX3umP.apiVersion).primaryKey}' } } ```

We want to allow the user to specify their own keyvault. To make this work, we should provide either a global way, or a per resource way for that to be specified e.g:

var builder = DistributedApplication.CreateBuilder(args);

var kv = builder.AddAzureKeyVault("kv");

var redis = builder.AddRedis("cache").PublishAsAzureRedis((az, _, _) =>
{
    az.UseSecretOutputKeyVault(kv);
});

builder.AddProject<Projects.WebApplication1>("webapplication1")
       .WithExternalHttpEndpoints()
       .WithReference(redis);

builder.Build().Run();

This method would set the keyvault name and it would generate a unique prefix for this resource to isolate secrets from other resources.

mitchdenny commented 3 months ago

For this I think we would need to change when the callback is evaluated. At the moment the callback is evaluated AFTER the internal one, at which point the KeyVaultSecret is already created. What we are really trying to accomplish here is replacing the default KeyVaultSecret creation behavior instead of adding onto the overall resource construction.