Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying MSBuild arguments to build project resources for in publish mode #7069

Open
1 task done
FullStackChef opened this issue Jan 11, 2025 · 6 comments
Open
1 task done
Labels
area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication
Milestone

Comments

@FullStackChef
Copy link

FullStackChef commented Jan 11, 2025

Background and Motivation

There should be a way to control how the container for a project gets produced in publish mode. Right now the deployment tool (such as azd) uses the manifest sees the project resource and has to infer how that project gets turned into a container image. Docker files are similar, but containers are able to expose build arguments that instruct tools on what values to pass to docker build. We need to same capability for projects.

Proposed API

namespace Aspire.Hosting;

public static class ProjectResourceBuilderExtensions
{
+    public static IResourceBuilder<T> WithBuildProperty<T>(this IResourceBuilder<T> builder, string propertyName, string propertyValue) where T: ProjectResource
+    public static IResourceBuilder<T> WithBuildProperty<T>(this IResourceBuilder<T> builder, string propertyName,  IResourceBuilder<ParameterResource> propertyValue) where T: ProjectResource
+    public static IResourceBuilder<T> WithBuildProperty<T>(this IResourceBuilder<T> builder, string propertyName,  ReferenceExpression propertyValue) where T: ProjectResource
}
namespace Aspire.Hosting.ApplicationModel;

public class ProjectBuildAnnotation
{
    public Dictionary<string, object?> BuildArguments { get; } = [];
}

Manifest

{
  "api": {
      "type": "project.v1",
      "build": {
         "args": {
            "ContainerImage": "{image.value}"
         }
      }
    }
}

Usage Examples

var builder = DistributedApplication.CreateBuilder(args);

var imageName = builder.AddParameter("api-image");

builder.AddProject<Projects.MyApi>("api")
       .WithBuildProperty("ContainerImage", imageName);

builder.Build().Run();

Risks

We need to make sure that build arguments are applied in all of the places that make sense. Today, the apphost gets built, along with referenced projects before we execute. That might cause some confusion.

original issue

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

We deploy microservices via Aspire and Azure Developer CLI (azd). Currently, Aspire does not specify container repository naming conventions or image tags in the service manifests, and Azure Developer CLI (azd) applies default naming conventions:

Repository name: {projectName}/{serviceName}-{env}
Image tag: azd-deploy-{clock.now().unix()}

Steps to Reproduce

  1. Create a new Aspire project with a project reference.
  2. Run azd deploy.
  3. Observe that the deployed container image includes:
  • A repository name in the format {projectName}/{serviceName}-{env}.
  • An image tag in the format azd-deploy-{clock.now().unix()}.
  1. Reference the deployed container image from another Aspire project
builder.AddContainer("deployed-image", "{projectName}/{serviceName}-{env}", "azd-deploy-{clock.now().unix()}")
       .WithImageRegistry("myregistry.azurecr.io");

This behaviour limits our ability to use semantic versioning and custom repository names for container images.

Describe the solution you'd like

I propose adding support in Aspire to allow developers to specify container repository names and image tags directly in the manifest. Aspire should generate the manifest with these options so that azd can use the specified values instead of its defaults.

Proposed API Changes in Aspire

  1. Add ContainerPublishingOptionsAnnotation
    This annotation will store the repository and image tag information.

    namespace Aspire.Hosting.ApplicationModel;
    internal class ContainerPublishingOptionsAnnotation : IResourceAnnotation
    {
        public string? Repository { get; set; }
        public string? ImageTag { get; set; }
    }
  2. Add Extension Method to Project Resource Builder
    The extension method enables developers to configure these values.

    public static IResourceBuilder<ProjectResource> WithContainerPublishingOptions(
        this IResourceBuilder<ProjectResource> builder, 
        string? imageTag, 
        string? repository)
    {
        ArgumentNullException.ThrowIfNull(builder);
    
        ContainerPublishingOptionsAnnotation annotation = new()
        {
            ImageTag = imageTag,
            Repository = repository
        };
        builder.Resource.Annotations.Add(annotation);
        return builder;
    }
  3. Modify WriteToProjectAsync in ManifestPublishingContext
    Update the manifest generation logic to include the config section if ContainerPublishingOptionsAnnotation is present.

    Updated Code:

    private async Task WriteProjectAsync(ProjectResource project)
    {
        if (!project.TryGetLastAnnotation<IProjectMetadata>(out var metadata))
        {
            throw new DistributedApplicationException("Project metadata not found.");
        }
    
        var relativePathToProjectFile = GetManifestRelativePath(metadata.ProjectPath);
    
        if (project.TryGetLastAnnotation<DeploymentTargetAnnotation>(out var deploymentTarget))
        {
            Writer.WriteString("type", "project.v1");
        }
        else
        {
            Writer.WriteString("type", "project.v0");
        }
    
        if (project.TryGetLastAnnotation<ContainerPublishingOptionsAnnotation>(out var containerImageAnnotation))
        {
            Writer.WriteStartObject("config");
            if (containerImageAnnotation.ImageTag is not null)
            {
                Writer.WriteString("containerImageTag", containerImageAnnotation.ImageTag);
            }
            if (containerImageAnnotation.Repository is not null)
            {
                Writer.WriteString("containerRepository", containerImageAnnotation.Repository);
            }
            Writer.WriteEndObject();
        }
    
        Writer.WriteString("path", relativePathToProjectFile);
    
        if (deploymentTarget is not null)
        {
            await WriteDeploymentTarget(deploymentTarget).ConfigureAwait(false);
        }
    
        await WriteCommandLineArgumentsAsync(project).ConfigureAwait(false);
    
        await WriteEnvironmentVariablesAsync(project).ConfigureAwait(false);
    
        WriteBindings(project);
    }

Manifest changes:

Current

"apiservice": {
  "type": "project.v1",
  "path": "ApiService/ApiService.csproj",
  "deployment": {
    "type": "azure.bicep.v0",
    "path": "apiservice.module.bicep",
    "params": {
      "apiservice_containerimage": "{apiservice.containerImage}"
    }
  }
}

Updated

"apiservice": {
  "type": "project.v1",
  "path": "ApiService/ApiService.csproj",
  "config": {
    "containerRepository": "my-microservices/apiservice",
    "containerImageTag": "v1.2.0;latest"
  },
  "deployment": {
    "type": "azure.bicep.v0",
    "path": "apiservice.module.bicep",
    "params": {
      "apiservice_containerimage": "{apiservice.containerImage}"
    }
  }
}

This would permit developers to do the following:

builder.AddProject<Projects.ApiService>("apiservice")
       .WithContainerPublishingOptions("v1.2.0;latest", "my-microservices/apiservice")

and then reference the deployed container image in a separate project as

builder.AddContainer("deployed-image", "my-microservices/apiservice", "v1.2.0")
       .WithImageRegistry("myregistry.azurecr.io");

Additional context

Currently, azd generates the container image name using the following logic:

imageName := fmt.Sprintf("%s:%s",
    at.containerHelper.DefaultImageName(serviceConfig),
    at.containerHelper.DefaultImageTag())

This applies default values for the image repository and tag. To support configurable options for containerImageTags and containerRepository, azd would need a corresponding change such as:

containerImageTags := at.containerHelper.DefaultImageTag()
if serviceConfig.Config["containerImageTags"] != nil {
    containerImageTags = serviceConfig.Config["containerImageTags"].(string)
}

containerRepository := at.containerHelper.DefaultImageName(serviceConfig)
if serviceConfig.Config["containerRepository"] != nil {
    containerRepository = serviceConfig.Config["containerRepository"].(string)
}

imageName := fmt.Sprintf("%s:%s", containerRepository, containerImageTags)

This change would allow azd to read the custom values defined in the Aspire-generated manifest under the config section, enabling developers to override the defaults with their desired repository names and semantic version tags.

The selection of "config" as the manifest property is driven by the existing azd ServiceConfig type which defines:

// Custom configuration for the service target
	Config map[string]any `yaml:"config,omitempty"`

Final Notes:
These changes will:

  • Enable semantic versioning for microservices, improving maintainability.
  • Provide developers with control over repository naming conventions to align with organizational standards.
  • Allow seamless reuse of container images across multiple projects by supporting explicit repository and tag references.

These updates align with azd's existing ServiceConfig structure, leveraging its config property for container customization. I am happy to contribute the necessary code changes.

@davidfowl davidfowl added area-deployment area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication labels Jan 11, 2025
@davidfowl
Copy link
Member

What is WithContainer? Do you mean AddDockerFile or WithDockerFile?

@davidfowl davidfowl added this to the Backlog milestone Jan 11, 2025
@FullStackChef
Copy link
Author

sorry - meant builder.AddContainer("image","tag")

@davidfowl
Copy link
Member

davidfowl commented Jan 11, 2025

I'm confused. Are you building a container image using AddContainer(...).WithDockerFile?

@FullStackChef
Copy link
Author

FullStackChef commented Jan 11, 2025

Here's an example of what I'm talking about

================================

Microservice Repo: FileService

AppHost:

var storageResource = builder.AddAzureStorage("storageAccount")
                             .RunAsEmulator(opt => opt.WithBlobPort(10000).WithLifetime(ContainerLifetime.Persistent))
                             .AddBlobs("files");

var apiService = builder.AddProject<Projects.FileServiceApi>("apiservice")
                        .WithReference(storageResource);

========================

Mircorservice Repo: RulesEngine

AppHost:

var cosmosDb = builder.AddAzureCosmosDB("rulesData")
                  .WithExternalHttpEndpoints()
                  .ConfigureInfrastructure(infr =>
                  {
                      var account = infr.GetProvisionableResources().OfType<CosmosDBAccount>().First();
                      account.Capabilities.Add(new CosmosDBAccountCapability { Name = "EnableServerless" });
                  })
                  .AddDatabase("rules");

var rulesEngine = builder.AddNpmApp("rules-engine", "../RulesEngine")
                        .PublishAsDockerFile();

var apiService = builder.AddProject<Projects.RulesApi>("apiservice")
                        .WithReference(rulesEngine)
                        .WithReference(cosmosDb);

===========================
Product Repo: e.g SaasProduct1

// File Service
var storageResource = builder.AddAzureStorage("storageAccount")
                             .RunAsEmulator(opt => opt.WithBlobPort(10000).WithLifetime(ContainerLifetime.Persistent))
                             .AddBlobs("files");

var fileService = builder.AddContainer("file-service","fileserviceapi/apiservice","azd-deploy-124215")
                        .WithReference(storageResource);

// Rules Engine
var cosmosDb = builder.AddAzureCosmosDB("rulesData")
                  .WithExternalHttpEndpoints()
                  .ConfigureInfrastructure(infr =>
                  {
                      var account = infr.GetProvisionableResources().OfType<CosmosDBAccount>().First();
                      account.Capabilities.Add(new CosmosDBAccountCapability { Name = "EnableServerless" });
                  })
                  .AddDatabase("rules");

var rulesEngine = builder.AddContainer("rules-engine","rulesengine/rulesengine","azd-deploy-523532")

var rulesApi = builder.AddContainer("rules-engine","rulesengine/apiservice","azd-deploy-5235243")
                        .WithReference(rulesEngine)
                        .WithReference(cosmosDb);

var application = builder.AddProject<Projects.ContentApp>("frontend")
                        .WithReference(rulesApi)
                        .WithReference(fileService)

In this way - we can build and test all of our microservice in isolation - we then have a product repo that pulls it all together and (would ideally) reference semantically versioned container images from azure container registry for our microservices

@davidfowl
Copy link
Member

In the samples I dint see why azd would build any container. The image in this situation is baked into the bicep as a literal.

@davidfowl
Copy link
Member

I think I understand what you are asking for, but I think what you want is to be able to control msbuild properties when building projects. Today we have no way of setting build properties for projects.

@davidfowl davidfowl changed the title Allow customisation of container image tags (and container repository name) Allow specifying MSBuild arguments to build project resources for in publish mode Jan 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication
Projects
None yet
Development

No branches or pull requests

2 participants