microsoft / microsoft/aspire

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

Open
#7,069 7 comments 0 reactions 0 assignees View on GitHub
area-app-model area-deployment
Dominant language
C#
Stars
6.3k
Forks
991
Avg merge
2d 15h
Merged PRs (30d)
196

Description

## 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

```diff
namespace Aspire.Hosting;

public static class ProjectResourceBuilderExtensions
{
+ public static IResourceBuilder WithBuildProperty(this IResourceBuilder builder, string propertyName, string propertyValue) where T: ProjectResource
+ public static IResourceBuilder WithBuildProperty(this IResourceBuilder builder, string propertyName, IResourceBuilder propertyValue) where T: ProjectResource
+ public static IResourceBuilder WithBuildProperty(this IResourceBuilder builder, string propertyName, ReferenceExpression propertyValue) where T: ProjectResource
}
```

```C#
namespace Aspire.Hosting.ApplicationModel;

public class ProjectBuildAnnotation
{
public Dictionary BuildArguments { get; } = [];
}
```

Manifest

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

## Usage Examples

```csharp
var builder = DistributedApplication.CreateBuilder(args);

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

builder.AddProject("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?

- [x] 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()}.
4. Reference the deployed container image from another Aspire project
```csharp
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.

```csharp
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.

```csharp
public static IResourceBuilder WithContainerPublishingOptions(
this IResourceBuilder 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:**
```csharp
private async Task WriteProjectAsync(ProjectResource project)
{
if (!project.TryGetLastAnnotation(out var metadata))
{
throw new DistributedApplicationException("Project metadata not found.");
}

var relativePathToProjectFile = GetManifestRelativePath(metadata.ProjectPath);

if (project.TryGetLastAnnotation(out var deploymentTarget))
{
Writer.WriteString("type", "project.v1");
}
else
{
Writer.WriteString("type", "project.v0");
}

if (project.TryGetLastAnnotation(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**
```json
"apiservice": {
"type": "project.v1",
"path": "ApiService/ApiService.csproj",
"deployment": {
"type": "azure.bicep.v0",
"path": "apiservice.module.bicep",
"params": {
"apiservice_containerimage": "{apiservice.containerImage}"
}
}
}
```
**Updated**
```json
"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:
```csharp
builder.AddProject("apiservice")
.WithContainerPublishingOptions("v1.2.0;latest", "my-microservices/apiservice")
```
and then reference the deployed container image in a separate project as
```csharp
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:

```go
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:

```go
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:

```go
// 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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.