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.71k stars 425 forks source link

Infrastructure versioning with CDK callbacks #3121

Open mitchdenny opened 6 months ago

mitchdenny commented 6 months ago

Starting a bit of an API design conversation for us to consider prior to GA, but probably to implement post GA in a servicing release. At the moment when you add an Azure resource like the following:

builder.AddSqlServer("sql").AsAzureSqlDatabase();

... you get our interpretation of what the ideal (but basic) Azure SQL deployment should be for use with Aspire. However what is ideal will change over time based on the capabilities in Aspire itself, but also how the resource changes over time.

So how can we account for this drift? We can't just simply update the default CDK calls we make especially if they are incompatible with the previous infrastructure. We need a way to capture snapshots of the various versions of the infrastructure that we've used over time as well as mappings between them.

My proposal is lean into CDKs ability to create a single class that defines the structure of some infrastructure and use it as the argument for the CDK callback. For example we might have a pattern that looks like this:

public abstract class AzureSqlResourceModuleConstruct : ResourceModuleConstruct
{
  // Expose properties for things this resource type needs.
}

public class TheAspireGAAzureSqlresourceModuleConstruct : AzureSqlResourceModuleConstruct
{
  // Move code construct code that defines base infrastructure from PublishAs method to here.
}

public static IResourceBuilder<SqlServerServerResource> AsAzureSqlDatabase(this IResourceBuilder<SqlServerServiceResource> builder){
  // The Aspire 8.0 version of the infra is the default.
  return builder.AsAzureSqlDatabase<TheAspireGAAzureSqlresourceModuleConstruct>();
}

public static IResourceBuilder<SqlServerServerResource> AsAzureSqlDatabase<T>(this IResourceBuilder<SqlServerServerResource> builder, Action<T> configureResource) where T: AzureSqlResourceModuleConstruct, new()
{
   var construct = new T();
   configureResource(construct);
}

I think this neatly solves a few things. It gives us a way of describing flavors of infrastructure (so its possible for people to define way more complex variations of the infrastructure as long as they inherit from our base resourcemoduleconstruct for that particular resource type. It also dramatically simplifies the callback for each resource ... the construct itself would act kind of like a resource specific callback context which could be extended over time (and because its generic, its shape can change dramatically).

Finally I think that this could also be used to handle breaking changes in infrastructure. Lets say that the way we defined SQL Resources in .NET Aspire 8.0 GA turns out to be something that we want to encourage people to change. We could introduce a code analysis rules that tells people they should use the overload with a type parameter and what variant of the construct they should use.

We could even have "upgrade constructs" that could potentially handle inplace upgrades.

mitchdenny commented 6 months ago

/cc @davidfowl @tg-msft @JoshLove-msft

tg-msft commented 6 months ago

Tracking this for Azure.Provisioning at https://github.com/Azure/azure-sdk-for-net-pr/issues/2091

davidfowl commented 3 weeks ago

This feels important to consider for 9 as part of https://github.com/dotnet/aspire/issues/5583 @eerhardt